Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
53.95% |
41 / 76 |
|
33.33% |
2 / 6 |
CRAP | |
0.00% |
0 / 1 |
FileRevisionFromRemoteUrl | |
53.95% |
41 / 76 |
|
33.33% |
2 / 6 |
36.98 | |
0.00% |
0 / 1 |
__construct | |
100.00% |
11 / 11 |
|
100.00% |
1 / 1 |
1 | |||
prepare | |
80.00% |
16 / 20 |
|
0.00% |
0 / 1 |
4.13 | |||
validate | |
52.94% |
9 / 17 |
|
0.00% |
0 / 1 |
5.67 | |||
commit | |
44.44% |
4 / 9 |
|
0.00% |
0 / 1 |
4.54 | |||
createUploadLog | |
0.00% |
0 / 18 |
|
0.00% |
0 / 1 |
6 | |||
getWikiRevision | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 |
1 | <?php |
2 | |
3 | namespace FileImporter\Operations; |
4 | |
5 | use FileImporter\Data\FileRevision; |
6 | use FileImporter\Data\TextRevision; |
7 | use FileImporter\Exceptions\HttpRequestException; |
8 | use FileImporter\Exceptions\ImportException; |
9 | use FileImporter\Exceptions\LocalizedImportException; |
10 | use FileImporter\Interfaces\ImportOperation; |
11 | use FileImporter\Services\Http\HttpRequestExecutor; |
12 | use FileImporter\Services\UploadBase\UploadBaseFactory; |
13 | use FileImporter\Services\UploadBase\ValidatingUploadBase; |
14 | use FileImporter\Services\WikiRevisionFactory; |
15 | use ManualLogEntry; |
16 | use MediaWiki\FileBackend\FSFile\TempFSFileFactory; |
17 | use MediaWiki\Permissions\RestrictionStore; |
18 | use MediaWiki\Status\Status; |
19 | use MediaWiki\Title\Title; |
20 | use MediaWiki\User\User; |
21 | use MediaWiki\User\UserIdentityLookup; |
22 | use MWHttpRequest; |
23 | use Psr\Log\LoggerInterface; |
24 | use Psr\Log\NullLogger; |
25 | use StatusValue; |
26 | use UnexpectedValueException; |
27 | use UploadBase; |
28 | use UploadRevisionImporter; |
29 | use WikiRevision; |
30 | |
31 | /** |
32 | * @license GPL-2.0-or-later |
33 | * @author Addshore |
34 | */ |
35 | class FileRevisionFromRemoteUrl implements ImportOperation { |
36 | |
37 | private Title $plannedTitle; |
38 | private User $user; |
39 | private FileRevision $fileRevision; |
40 | private ?TextRevision $textRevision; |
41 | private UserIdentityLookup $userLookup; |
42 | private HttpRequestExecutor $httpRequestExecutor; |
43 | private WikiRevisionFactory $wikiRevisionFactory; |
44 | private UploadBaseFactory $uploadBaseFactory; |
45 | private UploadRevisionImporter $importer; |
46 | private RestrictionStore $restrictionStore; |
47 | private LoggerInterface $logger; |
48 | |
49 | /** @var WikiRevision|null */ |
50 | private $wikiRevision = null; |
51 | /** @var ValidatingUploadBase|null */ |
52 | private $uploadBase = null; |
53 | |
54 | /** |
55 | * @param Title $plannedTitle |
56 | * @param User $user user performing the import |
57 | * @param FileRevision $fileRevision |
58 | * @param TextRevision|null $textRevision |
59 | * @param UserIdentityLookup $userLookup |
60 | * @param HttpRequestExecutor $httpRequestExecutor |
61 | * @param WikiRevisionFactory $wikiRevisionFactory |
62 | * @param UploadBaseFactory $uploadBaseFactory |
63 | * @param UploadRevisionImporter $importer |
64 | * @param RestrictionStore $restrictionStore |
65 | * @param LoggerInterface|null $logger |
66 | */ |
67 | public function __construct( |
68 | Title $plannedTitle, |
69 | User $user, |
70 | FileRevision $fileRevision, |
71 | ?TextRevision $textRevision, |
72 | UserIdentityLookup $userLookup, |
73 | HttpRequestExecutor $httpRequestExecutor, |
74 | WikiRevisionFactory $wikiRevisionFactory, |
75 | UploadBaseFactory $uploadBaseFactory, |
76 | UploadRevisionImporter $importer, |
77 | RestrictionStore $restrictionStore, |
78 | ?LoggerInterface $logger = null |
79 | ) { |
80 | $this->plannedTitle = $plannedTitle; |
81 | $this->user = $user; |
82 | $this->fileRevision = $fileRevision; |
83 | $this->textRevision = $textRevision; |
84 | $this->userLookup = $userLookup; |
85 | $this->httpRequestExecutor = $httpRequestExecutor; |
86 | $this->wikiRevisionFactory = $wikiRevisionFactory; |
87 | $this->uploadBaseFactory = $uploadBaseFactory; |
88 | $this->importer = $importer; |
89 | $this->restrictionStore = $restrictionStore; |
90 | $this->logger = $logger ?? new NullLogger(); |
91 | } |
92 | |
93 | /** |
94 | * Method to prepare an operation. This will not commit anything to any persistent storage. |
95 | * @return StatusValue isOK on success |
96 | */ |
97 | public function prepare(): StatusValue { |
98 | $fileUrl = $this->fileRevision->getField( 'url' ); |
99 | if ( !MWHttpRequest::isValidURI( $fileUrl ) ) { |
100 | // invalid URL detected |
101 | return StatusValue::newFatal( 'fileimporter-cantparseurl' ); |
102 | } |
103 | |
104 | $tmpFile = ( new TempFSFileFactory( wfTempDir() ) )->newTempFSFile( 'fileimporter_' ); |
105 | $tmpFile->bind( $this ); |
106 | |
107 | try { |
108 | $this->httpRequestExecutor->executeAndSave( $fileUrl, $tmpFile->getPath() ); |
109 | } catch ( HttpRequestException $ex ) { |
110 | if ( $ex->getCode() === 404 ) { |
111 | throw new LocalizedImportException( 'fileimporter-filemissinginrevision', $ex ); |
112 | } |
113 | throw $ex; |
114 | } |
115 | |
116 | $this->wikiRevision = $this->wikiRevisionFactory->newFromFileRevision( |
117 | $this->fileRevision, |
118 | $tmpFile->getPath() |
119 | ); |
120 | |
121 | $this->wikiRevision->setTitle( $this->plannedTitle ); |
122 | |
123 | $this->uploadBase = $this->uploadBaseFactory->newValidatingUploadBase( |
124 | $this->plannedTitle, |
125 | $this->wikiRevision->getFileSrc() |
126 | ); |
127 | |
128 | return StatusValue::newGood(); |
129 | } |
130 | |
131 | /** |
132 | * Method to validate prepared data that should be committed. |
133 | * |
134 | * @return StatusValue isOK on success |
135 | * @throws ImportException when critical validations fail |
136 | */ |
137 | public function validate(): StatusValue { |
138 | $errorCode = $this->uploadBase->validateTitle(); |
139 | if ( $errorCode !== UploadBase::OK ) { |
140 | $this->logger->error( |
141 | __METHOD__ . " failed to validate title, error code {$errorCode}", |
142 | [ 'fileRevision-getFields' => $this->fileRevision->getFields() ] |
143 | ); |
144 | return StatusValue::newFatal( 'fileimporter-filenameerror-illegal' ); |
145 | } |
146 | |
147 | // Even administrators should not (accidentially) move a file to a protected file name |
148 | if ( $this->restrictionStore->isProtected( $this->plannedTitle ) ) { |
149 | return StatusValue::newFatal( 'fileimporter-filenameerror-protected' ); |
150 | } |
151 | |
152 | $fileValidationStatus = $this->uploadBase->validateFile(); |
153 | if ( !$fileValidationStatus->isOK() ) { |
154 | $message = Status::wrap( $fileValidationStatus )->getMessage(); |
155 | return StatusValue::newFatal( 'fileimporter-cantimportfileinvalid', $message ); |
156 | } |
157 | |
158 | return $this->uploadBase->validateUpload( |
159 | $this->user, |
160 | $this->textRevision |
161 | ); |
162 | } |
163 | |
164 | /** |
165 | * Commit this operation to persistent storage. |
166 | * @return StatusValue isOK on success |
167 | */ |
168 | public function commit(): StatusValue { |
169 | $status = $this->importer->import( $this->wikiRevision ); |
170 | |
171 | if ( !$status->isGood() ) { |
172 | $this->logger->error( __METHOD__ . ' failed to commit.', [ |
173 | 'fileRevision-getFields' => $this->fileRevision->getFields(), |
174 | 'status' => $status->__toString(), |
175 | ] ); |
176 | } |
177 | |
178 | /** |
179 | * Core only creates log entries for the latest revision. This results in a complete upload |
180 | * log only when the revisions are uploaded in chronological order, and all using the same |
181 | * file name. |
182 | * |
183 | * Here we are not only working in reverse chronological order, but also with archive file |
184 | * names that are all different. Core can't know if it needs to create historical log |
185 | * entries for these. |
186 | * |
187 | * According to {@see \LocalFile::publishTo} the {@see \StatusValue::$value} contains the |
188 | * archive file name. |
189 | */ |
190 | if ( $status->value !== '' ) { |
191 | $this->createUploadLog(); |
192 | } |
193 | |
194 | return $status; |
195 | } |
196 | |
197 | /** |
198 | * @see \LocalFile::recordUpload2 |
199 | */ |
200 | private function createUploadLog() { |
201 | $username = $this->wikiRevision->getUser(); |
202 | $performer = $this->userLookup->getUserIdentityByName( $username ); |
203 | if ( !$performer ) { |
204 | throw new UnexpectedValueException( "Unexpected non-normalized username \"$username\"" ); |
205 | } |
206 | |
207 | $logEntry = new ManualLogEntry( 'upload', 'upload' ); |
208 | $logEntry->setTimestamp( $this->wikiRevision->getTimestamp() ); |
209 | $logEntry->setPerformer( $performer ); |
210 | $logEntry->setComment( $this->wikiRevision->getComment() ); |
211 | $logEntry->setAssociatedRevId( $this->wikiRevision->getID() ); |
212 | $logEntry->setTarget( $this->wikiRevision->getTitle() ); |
213 | $logEntry->setParameters( |
214 | [ |
215 | 'img_sha1' => $this->wikiRevision->getSha1(), |
216 | 'img_timestamp' => $this->wikiRevision->getTimestamp() |
217 | ] |
218 | ); |
219 | |
220 | $logId = $logEntry->insert(); |
221 | $logEntry->publish( $logId ); |
222 | } |
223 | |
224 | /** |
225 | * @return WikiRevision|null |
226 | */ |
227 | public function getWikiRevision() { |
228 | return $this->wikiRevision; |
229 | } |
230 | |
231 | } |