These results are based on our legacy PHP analysis, consider migrating to our new PHP analysis engine instead. Learn more
1 | <?php |
||
2 | /** |
||
3 | * |
||
4 | * |
||
5 | * Created on Aug 21, 2008 |
||
6 | * |
||
7 | * Copyright © 2008 - 2010 Bryan Tong Minh <[email protected]> |
||
8 | * |
||
9 | * This program is free software; you can redistribute it and/or modify |
||
10 | * it under the terms of the GNU General Public License as published by |
||
11 | * the Free Software Foundation; either version 2 of the License, or |
||
12 | * (at your option) any later version. |
||
13 | * |
||
14 | * This program is distributed in the hope that it will be useful, |
||
15 | * but WITHOUT ANY WARRANTY; without even the implied warranty of |
||
16 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
||
17 | * GNU General Public License for more details. |
||
18 | * |
||
19 | * You should have received a copy of the GNU General Public License along |
||
20 | * with this program; if not, write to the Free Software Foundation, Inc., |
||
21 | * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. |
||
22 | * http://www.gnu.org/copyleft/gpl.html |
||
23 | * |
||
24 | * @file |
||
25 | */ |
||
26 | |||
27 | /** |
||
28 | * @ingroup API |
||
29 | */ |
||
30 | class ApiUpload extends ApiBase { |
||
31 | /** @var UploadBase|UploadFromChunks */ |
||
32 | protected $mUpload = null; |
||
33 | |||
34 | protected $mParams; |
||
35 | |||
36 | public function execute() { |
||
37 | // Check whether upload is enabled |
||
38 | if ( !UploadBase::isEnabled() ) { |
||
39 | $this->dieUsageMsg( 'uploaddisabled' ); |
||
40 | } |
||
41 | |||
42 | $user = $this->getUser(); |
||
43 | |||
44 | // Parameter handling |
||
45 | $this->mParams = $this->extractRequestParams(); |
||
46 | $request = $this->getMain()->getRequest(); |
||
47 | // Check if async mode is actually supported (jobs done in cli mode) |
||
48 | $this->mParams['async'] = ( $this->mParams['async'] && |
||
49 | $this->getConfig()->get( 'EnableAsyncUploads' ) ); |
||
50 | // Add the uploaded file to the params array |
||
51 | $this->mParams['file'] = $request->getFileName( 'file' ); |
||
52 | $this->mParams['chunk'] = $request->getFileName( 'chunk' ); |
||
53 | |||
54 | // Copy the session key to the file key, for backward compatibility. |
||
55 | if ( !$this->mParams['filekey'] && $this->mParams['sessionkey'] ) { |
||
56 | $this->mParams['filekey'] = $this->mParams['sessionkey']; |
||
57 | } |
||
58 | |||
59 | // Select an upload module |
||
60 | try { |
||
61 | if ( !$this->selectUploadModule() ) { |
||
62 | return; // not a true upload, but a status request or similar |
||
63 | } elseif ( !isset( $this->mUpload ) ) { |
||
64 | $this->dieUsage( 'No upload module set', 'nomodule' ); |
||
65 | } |
||
66 | } catch ( UploadStashException $e ) { // XXX: don't spam exception log |
||
67 | list( $msg, $code ) = $this->handleStashException( get_class( $e ), $e->getMessage() ); |
||
68 | $this->dieUsage( $msg, $code ); |
||
69 | } |
||
70 | |||
71 | // First check permission to upload |
||
72 | $this->checkPermissions( $user ); |
||
73 | |||
74 | // Fetch the file (usually a no-op) |
||
75 | /** @var $status Status */ |
||
76 | $status = $this->mUpload->fetchFile(); |
||
77 | if ( !$status->isGood() ) { |
||
78 | $errors = $status->getErrorsArray(); |
||
79 | $error = array_shift( $errors[0] ); |
||
80 | $this->dieUsage( 'Error fetching file from remote source', $error, 0, $errors[0] ); |
||
81 | } |
||
82 | |||
83 | // Check if the uploaded file is sane |
||
84 | if ( $this->mParams['chunk'] ) { |
||
85 | $maxSize = UploadBase::getMaxUploadSize(); |
||
86 | if ( $this->mParams['filesize'] > $maxSize ) { |
||
87 | $this->dieUsage( 'The file you submitted was too large', 'file-too-large' ); |
||
88 | } |
||
89 | if ( !$this->mUpload->getTitle() ) { |
||
90 | $this->dieUsage( 'Invalid file title supplied', 'internal-error' ); |
||
91 | } |
||
92 | } elseif ( $this->mParams['async'] && $this->mParams['filekey'] ) { |
||
93 | // defer verification to background process |
||
94 | } else { |
||
95 | wfDebug( __METHOD__ . " about to verify\n" ); |
||
96 | $this->verifyUpload(); |
||
97 | } |
||
98 | |||
99 | // Check if the user has the rights to modify or overwrite the requested title |
||
100 | // (This check is irrelevant if stashing is already requested, since the errors |
||
101 | // can always be fixed by changing the title) |
||
102 | if ( !$this->mParams['stash'] ) { |
||
103 | $permErrors = $this->mUpload->verifyTitlePermissions( $user ); |
||
104 | if ( $permErrors !== true ) { |
||
105 | $this->dieRecoverableError( $permErrors[0], 'filename' ); |
||
106 | } |
||
107 | } |
||
108 | |||
109 | // Get the result based on the current upload context: |
||
110 | try { |
||
111 | $result = $this->getContextResult(); |
||
112 | } catch ( UploadStashException $e ) { // XXX: don't spam exception log |
||
113 | list( $msg, $code ) = $this->handleStashException( get_class( $e ), $e->getMessage() ); |
||
114 | $this->dieUsage( $msg, $code ); |
||
115 | } |
||
116 | $this->getResult()->addValue( null, $this->getModuleName(), $result ); |
||
117 | |||
118 | // Add 'imageinfo' in a separate addValue() call. File metadata can be unreasonably large, |
||
119 | // so otherwise when it exceeded $wgAPIMaxResultSize, no result would be returned (T143993). |
||
120 | if ( $result['result'] === 'Success' ) { |
||
121 | $imageinfo = $this->mUpload->getImageInfo( $this->getResult() ); |
||
122 | $this->getResult()->addValue( $this->getModuleName(), 'imageinfo', $imageinfo ); |
||
123 | } |
||
124 | |||
125 | // Cleanup any temporary mess |
||
126 | $this->mUpload->cleanupTempFile(); |
||
127 | } |
||
128 | |||
129 | /** |
||
130 | * Get an upload result based on upload context |
||
131 | * @return array |
||
132 | */ |
||
133 | private function getContextResult() { |
||
134 | $warnings = $this->getApiWarnings(); |
||
135 | if ( $warnings && !$this->mParams['ignorewarnings'] ) { |
||
136 | // Get warnings formatted in result array format |
||
137 | return $this->getWarningsResult( $warnings ); |
||
138 | } elseif ( $this->mParams['chunk'] ) { |
||
139 | // Add chunk, and get result |
||
140 | return $this->getChunkResult( $warnings ); |
||
141 | } elseif ( $this->mParams['stash'] ) { |
||
142 | // Stash the file and get stash result |
||
143 | return $this->getStashResult( $warnings ); |
||
144 | } |
||
145 | |||
146 | // Check throttle after we've handled warnings |
||
147 | if ( UploadBase::isThrottled( $this->getUser() ) |
||
148 | ) { |
||
149 | $this->dieUsageMsg( 'actionthrottledtext' ); |
||
150 | } |
||
151 | |||
152 | // This is the most common case -- a normal upload with no warnings |
||
153 | // performUpload will return a formatted properly for the API with status |
||
154 | return $this->performUpload( $warnings ); |
||
155 | } |
||
156 | |||
157 | /** |
||
158 | * Get Stash Result, throws an exception if the file could not be stashed. |
||
159 | * @param array $warnings Array of Api upload warnings |
||
160 | * @return array |
||
161 | */ |
||
162 | private function getStashResult( $warnings ) { |
||
163 | $result = []; |
||
164 | $result['result'] = 'Success'; |
||
165 | if ( $warnings && count( $warnings ) > 0 ) { |
||
166 | $result['warnings'] = $warnings; |
||
167 | } |
||
168 | // Some uploads can request they be stashed, so as not to publish them immediately. |
||
169 | // In this case, a failure to stash ought to be fatal |
||
170 | $this->performStash( 'critical', $result ); |
||
171 | |||
172 | return $result; |
||
173 | } |
||
174 | |||
175 | /** |
||
176 | * Get Warnings Result |
||
177 | * @param array $warnings Array of Api upload warnings |
||
178 | * @return array |
||
179 | */ |
||
180 | private function getWarningsResult( $warnings ) { |
||
181 | $result = []; |
||
182 | $result['result'] = 'Warning'; |
||
183 | $result['warnings'] = $warnings; |
||
184 | // in case the warnings can be fixed with some further user action, let's stash this upload |
||
185 | // and return a key they can use to restart it |
||
186 | $this->performStash( 'optional', $result ); |
||
187 | |||
188 | return $result; |
||
189 | } |
||
190 | |||
191 | /** |
||
192 | * Get the result of a chunk upload. |
||
193 | * @param array $warnings Array of Api upload warnings |
||
194 | * @return array |
||
195 | */ |
||
196 | private function getChunkResult( $warnings ) { |
||
197 | $result = []; |
||
198 | |||
199 | if ( $warnings && count( $warnings ) > 0 ) { |
||
200 | $result['warnings'] = $warnings; |
||
201 | } |
||
202 | |||
203 | $request = $this->getMain()->getRequest(); |
||
204 | $chunkPath = $request->getFileTempname( 'chunk' ); |
||
205 | $chunkSize = $request->getUpload( 'chunk' )->getSize(); |
||
206 | $totalSoFar = $this->mParams['offset'] + $chunkSize; |
||
207 | $minChunkSize = $this->getConfig()->get( 'MinUploadChunkSize' ); |
||
208 | |||
209 | // Sanity check sizing |
||
210 | if ( $totalSoFar > $this->mParams['filesize'] ) { |
||
211 | $this->dieUsage( |
||
212 | 'Offset plus current chunk is greater than claimed file size', 'invalid-chunk' |
||
213 | ); |
||
214 | } |
||
215 | |||
216 | // Enforce minimum chunk size |
||
217 | if ( $totalSoFar != $this->mParams['filesize'] && $chunkSize < $minChunkSize ) { |
||
218 | $this->dieUsage( |
||
219 | "Minimum chunk size is $minChunkSize bytes for non-final chunks", 'chunk-too-small' |
||
220 | ); |
||
221 | } |
||
222 | |||
223 | if ( $this->mParams['offset'] == 0 ) { |
||
224 | $filekey = $this->performStash( 'critical' ); |
||
225 | } else { |
||
226 | $filekey = $this->mParams['filekey']; |
||
227 | |||
228 | // Don't allow further uploads to an already-completed session |
||
229 | $progress = UploadBase::getSessionStatus( $this->getUser(), $filekey ); |
||
230 | if ( !$progress ) { |
||
231 | // Probably can't get here, but check anyway just in case |
||
232 | $this->dieUsage( 'No chunked upload session with this key', 'stashfailed' ); |
||
233 | } elseif ( $progress['result'] !== 'Continue' || $progress['stage'] !== 'uploading' ) { |
||
234 | $this->dieUsage( |
||
235 | 'Chunked upload is already completed, check status for details', 'stashfailed' |
||
236 | ); |
||
237 | } |
||
238 | |||
239 | $status = $this->mUpload->addChunk( |
||
240 | $chunkPath, $chunkSize, $this->mParams['offset'] ); |
||
241 | if ( !$status->isGood() ) { |
||
242 | $extradata = [ |
||
243 | 'offset' => $this->mUpload->getOffset(), |
||
244 | ]; |
||
245 | |||
246 | $this->dieStatusWithCode( $status, 'stashfailed', $extradata ); |
||
247 | } |
||
248 | } |
||
249 | |||
250 | // Check we added the last chunk: |
||
251 | if ( $totalSoFar == $this->mParams['filesize'] ) { |
||
252 | if ( $this->mParams['async'] ) { |
||
253 | UploadBase::setSessionStatus( |
||
254 | $this->getUser(), |
||
255 | $filekey, |
||
256 | [ 'result' => 'Poll', |
||
257 | 'stage' => 'queued', 'status' => Status::newGood() ] |
||
258 | ); |
||
259 | JobQueueGroup::singleton()->push( new AssembleUploadChunksJob( |
||
260 | Title::makeTitle( NS_FILE, $filekey ), |
||
261 | [ |
||
262 | 'filename' => $this->mParams['filename'], |
||
263 | 'filekey' => $filekey, |
||
264 | 'session' => $this->getContext()->exportSession() |
||
265 | ] |
||
266 | ) ); |
||
267 | $result['result'] = 'Poll'; |
||
268 | $result['stage'] = 'queued'; |
||
269 | } else { |
||
270 | $status = $this->mUpload->concatenateChunks(); |
||
271 | if ( !$status->isGood() ) { |
||
272 | UploadBase::setSessionStatus( |
||
273 | $this->getUser(), |
||
274 | $filekey, |
||
275 | [ 'result' => 'Failure', 'stage' => 'assembling', 'status' => $status ] |
||
276 | ); |
||
277 | $this->dieStatusWithCode( $status, 'stashfailed' ); |
||
278 | } |
||
279 | |||
280 | // We can only get warnings like 'duplicate' after concatenating the chunks |
||
281 | $warnings = $this->getApiWarnings(); |
||
282 | if ( $warnings ) { |
||
283 | $result['warnings'] = $warnings; |
||
284 | } |
||
285 | |||
286 | // The fully concatenated file has a new filekey. So remove |
||
287 | // the old filekey and fetch the new one. |
||
288 | UploadBase::setSessionStatus( $this->getUser(), $filekey, false ); |
||
289 | $this->mUpload->stash->removeFile( $filekey ); |
||
290 | $filekey = $this->mUpload->getStashFile()->getFileKey(); |
||
291 | |||
292 | $result['result'] = 'Success'; |
||
293 | } |
||
294 | } else { |
||
295 | UploadBase::setSessionStatus( |
||
296 | $this->getUser(), |
||
297 | $filekey, |
||
298 | [ |
||
299 | 'result' => 'Continue', |
||
300 | 'stage' => 'uploading', |
||
301 | 'offset' => $totalSoFar, |
||
302 | 'status' => Status::newGood(), |
||
303 | ] |
||
304 | ); |
||
305 | $result['result'] = 'Continue'; |
||
306 | $result['offset'] = $totalSoFar; |
||
307 | } |
||
308 | |||
309 | $result['filekey'] = $filekey; |
||
310 | |||
311 | return $result; |
||
312 | } |
||
313 | |||
314 | /** |
||
315 | * Stash the file and add the file key, or error information if it fails, to the data. |
||
316 | * |
||
317 | * @param string $failureMode What to do on failure to stash: |
||
318 | * - When 'critical', use dieStatus() to produce an error response and throw an exception. |
||
319 | * Use this when stashing the file was the primary purpose of the API request. |
||
320 | * - When 'optional', only add a 'stashfailed' key to the data and return null. |
||
321 | * Use this when some error happened for a non-stash upload and we're stashing the file |
||
322 | * only to save the client the trouble of re-uploading it. |
||
323 | * @param array &$data API result to which to add the information |
||
324 | * @return string|null File key |
||
325 | */ |
||
326 | private function performStash( $failureMode, &$data = null ) { |
||
327 | $isPartial = (bool)$this->mParams['chunk']; |
||
328 | try { |
||
329 | $status = $this->mUpload->tryStashFile( $this->getUser(), $isPartial ); |
||
330 | |||
331 | if ( $status->isGood() && !$status->getValue() ) { |
||
332 | // Not actually a 'good' status... |
||
333 | $status->fatal( new ApiRawMessage( 'Invalid stashed file', 'stashfailed' ) ); |
||
334 | } |
||
335 | } catch ( Exception $e ) { |
||
336 | $debugMessage = 'Stashing temporary file failed: ' . get_class( $e ) . ' ' . $e->getMessage(); |
||
337 | wfDebug( __METHOD__ . ' ' . $debugMessage . "\n" ); |
||
338 | $status = Status::newFatal( new ApiRawMessage( $e->getMessage(), 'stashfailed' ) ); |
||
339 | } |
||
340 | |||
341 | if ( $status->isGood() ) { |
||
342 | $stashFile = $status->getValue(); |
||
343 | $data['filekey'] = $stashFile->getFileKey(); |
||
344 | // Backwards compatibility |
||
345 | $data['sessionkey'] = $data['filekey']; |
||
346 | return $data['filekey']; |
||
347 | } |
||
348 | |||
349 | if ( $status->getMessage()->getKey() === 'uploadstash-exception' ) { |
||
350 | // The exceptions thrown by upload stash code and pretty silly and UploadBase returns poor |
||
351 | // Statuses for it. Just extract the exception details and parse them ourselves. |
||
352 | list( $exceptionType, $message ) = $status->getMessage()->getParams(); |
||
353 | $debugMessage = 'Stashing temporary file failed: ' . $exceptionType . ' ' . $message; |
||
354 | wfDebug( __METHOD__ . ' ' . $debugMessage . "\n" ); |
||
355 | list( $msg, $code ) = $this->handleStashException( $exceptionType, $message ); |
||
356 | $status = Status::newFatal( new ApiRawMessage( $msg, $code ) ); |
||
357 | } |
||
358 | |||
359 | // Bad status |
||
360 | if ( $failureMode !== 'optional' ) { |
||
361 | $this->dieStatus( $status ); |
||
362 | } else { |
||
363 | list( $code, $msg ) = $this->getErrorFromStatus( $status ); |
||
364 | $data['stashfailed'] = $msg; |
||
365 | return null; |
||
366 | } |
||
367 | } |
||
368 | |||
369 | /** |
||
370 | * Throw an error that the user can recover from by providing a better |
||
371 | * value for $parameter |
||
372 | * |
||
373 | * @param array|string|MessageSpecifier $error Error suitable for passing to dieUsageMsg() |
||
374 | * @param string $parameter Parameter that needs revising |
||
375 | * @param array $data Optional extra data to pass to the user |
||
376 | * @param string $code Error code to use if the error is unknown |
||
377 | * @throws UsageException |
||
378 | */ |
||
379 | private function dieRecoverableError( $error, $parameter, $data = [], $code = 'unknownerror' ) { |
||
380 | $this->performStash( 'optional', $data ); |
||
381 | $data['invalidparameter'] = $parameter; |
||
382 | |||
383 | $parsed = $this->parseMsg( $error ); |
||
384 | if ( isset( $parsed['data'] ) ) { |
||
385 | $data = array_merge( $data, $parsed['data'] ); |
||
386 | } |
||
387 | if ( $parsed['code'] === 'unknownerror' ) { |
||
388 | $parsed['code'] = $code; |
||
389 | } |
||
390 | |||
391 | $this->dieUsage( $parsed['info'], $parsed['code'], 0, $data ); |
||
392 | } |
||
393 | |||
394 | /** |
||
395 | * Like dieStatus(), but always uses $overrideCode for the error code, unless the code comes from |
||
396 | * IApiMessage. |
||
397 | * |
||
398 | * @param Status $status |
||
399 | * @param string $overrideCode Error code to use if there isn't one from IApiMessage |
||
400 | * @param array|null $moreExtraData |
||
401 | * @throws UsageException |
||
402 | */ |
||
403 | public function dieStatusWithCode( $status, $overrideCode, $moreExtraData = null ) { |
||
404 | $extraData = null; |
||
405 | list( $code, $msg ) = $this->getErrorFromStatus( $status, $extraData ); |
||
406 | $errors = $status->getErrorsByType( 'error' ) ?: $status->getErrorsByType( 'warning' ); |
||
407 | if ( !( $errors[0]['message'] instanceof IApiMessage ) ) { |
||
408 | $code = $overrideCode; |
||
409 | } |
||
410 | if ( $moreExtraData ) { |
||
411 | $extraData = $extraData ?: []; |
||
412 | $extraData += $moreExtraData; |
||
413 | } |
||
414 | $this->dieUsage( $msg, $code, 0, $extraData ); |
||
415 | } |
||
416 | |||
417 | /** |
||
418 | * Select an upload module and set it to mUpload. Dies on failure. If the |
||
419 | * request was a status request and not a true upload, returns false; |
||
420 | * otherwise true |
||
421 | * |
||
422 | * @return bool |
||
423 | */ |
||
424 | protected function selectUploadModule() { |
||
425 | $request = $this->getMain()->getRequest(); |
||
426 | |||
427 | // chunk or one and only one of the following parameters is needed |
||
428 | if ( !$this->mParams['chunk'] ) { |
||
429 | $this->requireOnlyOneParameter( $this->mParams, |
||
430 | 'filekey', 'file', 'url' ); |
||
431 | } |
||
432 | |||
433 | // Status report for "upload to stash"/"upload from stash" |
||
434 | if ( $this->mParams['filekey'] && $this->mParams['checkstatus'] ) { |
||
435 | $progress = UploadBase::getSessionStatus( $this->getUser(), $this->mParams['filekey'] ); |
||
436 | if ( !$progress ) { |
||
437 | $this->dieUsage( 'No result in status data', 'missingresult' ); |
||
438 | } elseif ( !$progress['status']->isGood() ) { |
||
439 | $this->dieStatusWithCode( $progress['status'], 'stashfailed' ); |
||
440 | } |
||
441 | if ( isset( $progress['status']->value['verification'] ) ) { |
||
442 | $this->checkVerification( $progress['status']->value['verification'] ); |
||
443 | } |
||
444 | if ( isset( $progress['status']->value['warnings'] ) ) { |
||
445 | $warnings = $this->transformWarnings( $progress['status']->value['warnings'] ); |
||
446 | if ( $warnings ) { |
||
447 | $progress['warnings'] = $warnings; |
||
448 | } |
||
449 | } |
||
450 | unset( $progress['status'] ); // remove Status object |
||
451 | $imageinfo = null; |
||
452 | if ( isset( $progress['imageinfo'] ) ) { |
||
453 | $imageinfo = $progress['imageinfo']; |
||
454 | unset( $progress['imageinfo'] ); |
||
455 | } |
||
456 | |||
457 | $this->getResult()->addValue( null, $this->getModuleName(), $progress ); |
||
458 | // Add 'imageinfo' in a separate addValue() call. File metadata can be unreasonably large, |
||
459 | // so otherwise when it exceeded $wgAPIMaxResultSize, no result would be returned (T143993). |
||
460 | if ( $imageinfo ) { |
||
461 | $this->getResult()->addValue( $this->getModuleName(), 'imageinfo', $imageinfo ); |
||
462 | } |
||
463 | |||
464 | return false; |
||
465 | } |
||
466 | |||
467 | // The following modules all require the filename parameter to be set |
||
468 | if ( is_null( $this->mParams['filename'] ) ) { |
||
469 | $this->dieUsageMsg( [ 'missingparam', 'filename' ] ); |
||
470 | } |
||
471 | |||
472 | if ( $this->mParams['chunk'] ) { |
||
473 | // Chunk upload |
||
474 | $this->mUpload = new UploadFromChunks( $this->getUser() ); |
||
475 | if ( isset( $this->mParams['filekey'] ) ) { |
||
476 | if ( $this->mParams['offset'] === 0 ) { |
||
477 | $this->dieUsage( 'Cannot supply a filekey when offset is 0', 'badparams' ); |
||
478 | } |
||
479 | |||
480 | // handle new chunk |
||
481 | $this->mUpload->continueChunks( |
||
482 | $this->mParams['filename'], |
||
483 | $this->mParams['filekey'], |
||
484 | $request->getUpload( 'chunk' ) |
||
485 | ); |
||
486 | } else { |
||
487 | if ( $this->mParams['offset'] !== 0 ) { |
||
488 | $this->dieUsage( 'Must supply a filekey when offset is non-zero', 'badparams' ); |
||
489 | } |
||
490 | |||
491 | // handle first chunk |
||
492 | $this->mUpload->initialize( |
||
493 | $this->mParams['filename'], |
||
494 | $request->getUpload( 'chunk' ) |
||
495 | ); |
||
496 | } |
||
497 | } elseif ( isset( $this->mParams['filekey'] ) ) { |
||
498 | // Upload stashed in a previous request |
||
499 | if ( !UploadFromStash::isValidKey( $this->mParams['filekey'] ) ) { |
||
500 | $this->dieUsageMsg( 'invalid-file-key' ); |
||
501 | } |
||
502 | |||
503 | $this->mUpload = new UploadFromStash( $this->getUser() ); |
||
504 | // This will not download the temp file in initialize() in async mode. |
||
505 | // We still have enough information to call checkWarnings() and such. |
||
506 | $this->mUpload->initialize( |
||
507 | $this->mParams['filekey'], $this->mParams['filename'], !$this->mParams['async'] |
||
508 | ); |
||
509 | } elseif ( isset( $this->mParams['file'] ) ) { |
||
510 | $this->mUpload = new UploadFromFile(); |
||
511 | $this->mUpload->initialize( |
||
512 | $this->mParams['filename'], |
||
513 | $request->getUpload( 'file' ) |
||
514 | ); |
||
515 | } elseif ( isset( $this->mParams['url'] ) ) { |
||
516 | // Make sure upload by URL is enabled: |
||
517 | if ( !UploadFromUrl::isEnabled() ) { |
||
518 | $this->dieUsageMsg( 'copyuploaddisabled' ); |
||
519 | } |
||
520 | |||
521 | if ( !UploadFromUrl::isAllowedHost( $this->mParams['url'] ) ) { |
||
522 | $this->dieUsageMsg( 'copyuploadbaddomain' ); |
||
523 | } |
||
524 | |||
525 | if ( !UploadFromUrl::isAllowedUrl( $this->mParams['url'] ) ) { |
||
526 | $this->dieUsageMsg( 'copyuploadbadurl' ); |
||
527 | } |
||
528 | |||
529 | $this->mUpload = new UploadFromUrl; |
||
530 | $this->mUpload->initialize( $this->mParams['filename'], |
||
531 | $this->mParams['url'] ); |
||
532 | } |
||
533 | |||
534 | return true; |
||
535 | } |
||
536 | |||
537 | /** |
||
538 | * Checks that the user has permissions to perform this upload. |
||
539 | * Dies with usage message on inadequate permissions. |
||
540 | * @param User $user The user to check. |
||
541 | */ |
||
542 | protected function checkPermissions( $user ) { |
||
543 | // Check whether the user has the appropriate permissions to upload anyway |
||
544 | $permission = $this->mUpload->isAllowed( $user ); |
||
545 | |||
546 | if ( $permission !== true ) { |
||
547 | if ( !$user->isLoggedIn() ) { |
||
548 | $this->dieUsageMsg( [ 'mustbeloggedin', 'upload' ] ); |
||
549 | } |
||
550 | |||
551 | $this->dieUsageMsg( 'badaccess-groups' ); |
||
552 | } |
||
553 | |||
554 | // Check blocks |
||
555 | if ( $user->isBlocked() ) { |
||
556 | $this->dieBlocked( $user->getBlock() ); |
||
557 | } |
||
558 | |||
559 | // Global blocks |
||
560 | if ( $user->isBlockedGlobally() ) { |
||
561 | $this->dieBlocked( $user->getGlobalBlock() ); |
||
562 | } |
||
563 | } |
||
564 | |||
565 | /** |
||
566 | * Performs file verification, dies on error. |
||
567 | */ |
||
568 | protected function verifyUpload() { |
||
569 | $verification = $this->mUpload->verifyUpload(); |
||
570 | if ( $verification['status'] === UploadBase::OK ) { |
||
571 | return; |
||
572 | } |
||
573 | |||
574 | $this->checkVerification( $verification ); |
||
575 | } |
||
576 | |||
577 | /** |
||
578 | * Performs file verification, dies on error. |
||
579 | * @param array $verification |
||
580 | */ |
||
581 | protected function checkVerification( array $verification ) { |
||
582 | // @todo Move them to ApiBase's message map |
||
583 | switch ( $verification['status'] ) { |
||
584 | // Recoverable errors |
||
585 | case UploadBase::MIN_LENGTH_PARTNAME: |
||
586 | $this->dieRecoverableError( 'filename-tooshort', 'filename' ); |
||
587 | break; |
||
588 | case UploadBase::ILLEGAL_FILENAME: |
||
589 | $this->dieRecoverableError( 'illegal-filename', 'filename', |
||
590 | [ 'filename' => $verification['filtered'] ] ); |
||
591 | break; |
||
592 | case UploadBase::FILENAME_TOO_LONG: |
||
593 | $this->dieRecoverableError( 'filename-toolong', 'filename' ); |
||
594 | break; |
||
595 | case UploadBase::FILETYPE_MISSING: |
||
596 | $this->dieRecoverableError( 'filetype-missing', 'filename' ); |
||
597 | break; |
||
598 | case UploadBase::WINDOWS_NONASCII_FILENAME: |
||
599 | $this->dieRecoverableError( 'windows-nonascii-filename', 'filename' ); |
||
600 | break; |
||
601 | |||
602 | // Unrecoverable errors |
||
603 | case UploadBase::EMPTY_FILE: |
||
604 | $this->dieUsage( 'The file you submitted was empty', 'empty-file' ); |
||
605 | break; |
||
606 | case UploadBase::FILE_TOO_LARGE: |
||
607 | $this->dieUsage( 'The file you submitted was too large', 'file-too-large' ); |
||
608 | break; |
||
609 | |||
610 | case UploadBase::FILETYPE_BADTYPE: |
||
611 | $extradata = [ |
||
612 | 'filetype' => $verification['finalExt'], |
||
613 | 'allowed' => array_values( array_unique( $this->getConfig()->get( 'FileExtensions' ) ) ) |
||
614 | ]; |
||
615 | ApiResult::setIndexedTagName( $extradata['allowed'], 'ext' ); |
||
616 | |||
617 | $msg = 'Filetype not permitted: '; |
||
618 | if ( isset( $verification['blacklistedExt'] ) ) { |
||
619 | $msg .= implode( ', ', $verification['blacklistedExt'] ); |
||
620 | $extradata['blacklisted'] = array_values( $verification['blacklistedExt'] ); |
||
621 | ApiResult::setIndexedTagName( $extradata['blacklisted'], 'ext' ); |
||
622 | } else { |
||
623 | $msg .= $verification['finalExt']; |
||
624 | } |
||
625 | $this->dieUsage( $msg, 'filetype-banned', 0, $extradata ); |
||
626 | break; |
||
627 | case UploadBase::VERIFICATION_ERROR: |
||
628 | $parsed = $this->parseMsg( $verification['details'] ); |
||
629 | $info = "This file did not pass file verification: {$parsed['info']}"; |
||
630 | if ( $verification['details'][0] instanceof IApiMessage ) { |
||
631 | $code = $parsed['code']; |
||
632 | } else { |
||
633 | // For backwards-compatibility, all of the errors from UploadBase::verifyFile() are |
||
634 | // reported as 'verification-error', and the real error code is reported in 'details'. |
||
635 | $code = 'verification-error'; |
||
636 | } |
||
637 | if ( $verification['details'][0] instanceof IApiMessage ) { |
||
638 | $msg = $verification['details'][0]; |
||
639 | $details = array_merge( [ $msg->getKey() ], $msg->getParams() ); |
||
640 | } else { |
||
641 | $details = $verification['details']; |
||
642 | } |
||
643 | ApiResult::setIndexedTagName( $details, 'detail' ); |
||
644 | $data = [ 'details' => $details ]; |
||
645 | if ( isset( $parsed['data'] ) ) { |
||
646 | $data = array_merge( $data, $parsed['data'] ); |
||
647 | } |
||
648 | |||
649 | $this->dieUsage( $info, $code, 0, $data ); |
||
650 | break; |
||
651 | case UploadBase::HOOK_ABORTED: |
||
652 | if ( is_array( $verification['error'] ) ) { |
||
653 | $params = $verification['error']; |
||
654 | } elseif ( $verification['error'] !== '' ) { |
||
655 | $params = [ $verification['error'] ]; |
||
656 | } else { |
||
657 | $params = [ 'hookaborted' ]; |
||
658 | } |
||
659 | $key = array_shift( $params ); |
||
660 | $msg = $this->msg( $key, $params )->inLanguage( 'en' )->useDatabase( false )->text(); |
||
661 | $this->dieUsage( $msg, 'hookaborted', 0, [ 'details' => $verification['error'] ] ); |
||
662 | break; |
||
663 | default: |
||
664 | $this->dieUsage( 'An unknown error occurred', 'unknown-error', |
||
665 | 0, [ 'details' => [ 'code' => $verification['status'] ] ] ); |
||
666 | break; |
||
667 | } |
||
668 | } |
||
669 | |||
670 | /** |
||
671 | * Check warnings. |
||
672 | * Returns a suitable array for inclusion into API results if there were warnings |
||
673 | * Returns the empty array if there were no warnings |
||
674 | * |
||
675 | * @return array |
||
676 | */ |
||
677 | protected function getApiWarnings() { |
||
678 | $warnings = $this->mUpload->checkWarnings(); |
||
679 | |||
680 | return $this->transformWarnings( $warnings ); |
||
681 | } |
||
682 | |||
683 | protected function transformWarnings( $warnings ) { |
||
684 | if ( $warnings ) { |
||
685 | // Add indices |
||
686 | ApiResult::setIndexedTagName( $warnings, 'warning' ); |
||
687 | |||
688 | if ( isset( $warnings['duplicate'] ) ) { |
||
689 | $dupes = []; |
||
690 | /** @var File $dupe */ |
||
691 | foreach ( $warnings['duplicate'] as $dupe ) { |
||
692 | $dupes[] = $dupe->getName(); |
||
693 | } |
||
694 | ApiResult::setIndexedTagName( $dupes, 'duplicate' ); |
||
695 | $warnings['duplicate'] = $dupes; |
||
696 | } |
||
697 | |||
698 | if ( isset( $warnings['exists'] ) ) { |
||
699 | $warning = $warnings['exists']; |
||
700 | unset( $warnings['exists'] ); |
||
701 | /** @var LocalFile $localFile */ |
||
702 | $localFile = isset( $warning['normalizedFile'] ) |
||
703 | ? $warning['normalizedFile'] |
||
704 | : $warning['file']; |
||
705 | $warnings[$warning['warning']] = $localFile->getName(); |
||
706 | } |
||
707 | |||
708 | if ( isset( $warnings['no-change'] ) ) { |
||
709 | /** @var File $file */ |
||
710 | $file = $warnings['no-change']; |
||
711 | unset( $warnings['no-change'] ); |
||
712 | |||
713 | $warnings['nochange'] = [ |
||
714 | 'timestamp' => wfTimestamp( TS_ISO_8601, $file->getTimestamp() ) |
||
715 | ]; |
||
716 | } |
||
717 | |||
718 | if ( isset( $warnings['duplicate-version'] ) ) { |
||
719 | $dupes = []; |
||
720 | /** @var File $dupe */ |
||
721 | foreach ( $warnings['duplicate-version'] as $dupe ) { |
||
722 | $dupes[] = [ |
||
723 | 'timestamp' => wfTimestamp( TS_ISO_8601, $dupe->getTimestamp() ) |
||
724 | ]; |
||
725 | } |
||
726 | unset( $warnings['duplicate-version'] ); |
||
727 | |||
728 | ApiResult::setIndexedTagName( $dupes, 'ver' ); |
||
729 | $warnings['duplicateversions'] = $dupes; |
||
730 | } |
||
731 | } |
||
732 | |||
733 | return $warnings; |
||
734 | } |
||
735 | |||
736 | /** |
||
737 | * Handles a stash exception, giving a useful error to the user. |
||
738 | * @param string $exceptionType Class name of the exception we encountered. |
||
739 | * @param string $message Message of the exception we encountered. |
||
740 | * @return array Array of message and code, suitable for passing to dieUsage() |
||
741 | */ |
||
742 | protected function handleStashException( $exceptionType, $message ) { |
||
743 | switch ( $exceptionType ) { |
||
744 | case 'UploadStashFileNotFoundException': |
||
745 | return [ |
||
746 | 'Could not find the file in the stash: ' . $message, |
||
747 | 'stashedfilenotfound' |
||
748 | ]; |
||
749 | case 'UploadStashBadPathException': |
||
750 | return [ |
||
751 | 'File key of improper format or otherwise invalid: ' . $message, |
||
752 | 'stashpathinvalid' |
||
753 | ]; |
||
754 | case 'UploadStashFileException': |
||
755 | return [ |
||
756 | 'Could not store upload in the stash: ' . $message, |
||
757 | 'stashfilestorage' |
||
758 | ]; |
||
759 | case 'UploadStashZeroLengthFileException': |
||
760 | return [ |
||
761 | 'File is of zero length, and could not be stored in the stash: ' . |
||
762 | $message, |
||
763 | 'stashzerolength' |
||
764 | ]; |
||
765 | case 'UploadStashNotLoggedInException': |
||
766 | return [ 'Not logged in: ' . $message, 'stashnotloggedin' ]; |
||
767 | case 'UploadStashWrongOwnerException': |
||
768 | return [ 'Wrong owner: ' . $message, 'stashwrongowner' ]; |
||
769 | case 'UploadStashNoSuchKeyException': |
||
770 | return [ 'No such filekey: ' . $message, 'stashnosuchfilekey' ]; |
||
771 | default: |
||
772 | return [ $exceptionType . ': ' . $message, 'stasherror' ]; |
||
773 | } |
||
774 | } |
||
775 | |||
776 | /** |
||
777 | * Perform the actual upload. Returns a suitable result array on success; |
||
778 | * dies on failure. |
||
779 | * |
||
780 | * @param array $warnings Array of Api upload warnings |
||
781 | * @return array |
||
782 | */ |
||
783 | protected function performUpload( $warnings ) { |
||
784 | // Use comment as initial page text by default |
||
785 | if ( is_null( $this->mParams['text'] ) ) { |
||
786 | $this->mParams['text'] = $this->mParams['comment']; |
||
787 | } |
||
788 | |||
789 | /** @var $file LocalFile */ |
||
790 | $file = $this->mUpload->getLocalFile(); |
||
791 | |||
792 | // For preferences mode, we want to watch if 'watchdefault' is set, |
||
793 | // or if the *file* doesn't exist, and either 'watchuploads' or |
||
794 | // 'watchcreations' is set. But getWatchlistValue()'s automatic |
||
795 | // handling checks if the *title* exists or not, so we need to check |
||
796 | // all three preferences manually. |
||
797 | $watch = $this->getWatchlistValue( |
||
798 | $this->mParams['watchlist'], $file->getTitle(), 'watchdefault' |
||
799 | ); |
||
800 | |||
801 | if ( !$watch && $this->mParams['watchlist'] == 'preferences' && !$file->exists() ) { |
||
802 | $watch = ( |
||
803 | $this->getWatchlistValue( 'preferences', $file->getTitle(), 'watchuploads' ) || |
||
804 | $this->getWatchlistValue( 'preferences', $file->getTitle(), 'watchcreations' ) |
||
805 | ); |
||
806 | } |
||
807 | |||
808 | // Deprecated parameters |
||
809 | if ( $this->mParams['watch'] ) { |
||
810 | $watch = true; |
||
811 | } |
||
812 | |||
813 | if ( $this->mParams['tags'] ) { |
||
814 | $status = ChangeTags::canAddTagsAccompanyingChange( $this->mParams['tags'], $this->getUser() ); |
||
815 | if ( !$status->isOK() ) { |
||
816 | $this->dieStatus( $status ); |
||
817 | } |
||
818 | } |
||
819 | |||
820 | // No errors, no warnings: do the upload |
||
821 | if ( $this->mParams['async'] ) { |
||
822 | $progress = UploadBase::getSessionStatus( $this->getUser(), $this->mParams['filekey'] ); |
||
823 | if ( $progress && $progress['result'] === 'Poll' ) { |
||
824 | $this->dieUsage( 'Upload from stash already in progress.', 'publishfailed' ); |
||
825 | } |
||
826 | UploadBase::setSessionStatus( |
||
827 | $this->getUser(), |
||
828 | $this->mParams['filekey'], |
||
829 | [ 'result' => 'Poll', 'stage' => 'queued', 'status' => Status::newGood() ] |
||
830 | ); |
||
831 | JobQueueGroup::singleton()->push( new PublishStashedFileJob( |
||
832 | Title::makeTitle( NS_FILE, $this->mParams['filename'] ), |
||
833 | [ |
||
834 | 'filename' => $this->mParams['filename'], |
||
835 | 'filekey' => $this->mParams['filekey'], |
||
836 | 'comment' => $this->mParams['comment'], |
||
837 | 'tags' => $this->mParams['tags'], |
||
838 | 'text' => $this->mParams['text'], |
||
839 | 'watch' => $watch, |
||
840 | 'session' => $this->getContext()->exportSession() |
||
841 | ] |
||
842 | ) ); |
||
843 | $result['result'] = 'Poll'; |
||
0 ignored issues
–
show
|
|||
844 | $result['stage'] = 'queued'; |
||
845 | } else { |
||
846 | /** @var $status Status */ |
||
847 | $status = $this->mUpload->performUpload( $this->mParams['comment'], |
||
848 | $this->mParams['text'], $watch, $this->getUser(), $this->mParams['tags'] ); |
||
849 | |||
850 | if ( !$status->isGood() ) { |
||
851 | // Is there really no better way to do this? |
||
852 | $errors = $status->getErrorsByType( 'error' ); |
||
853 | $msg = array_merge( [ $errors[0]['message'] ], $errors[0]['params'] ); |
||
854 | $data = $status->getErrorsArray(); |
||
855 | ApiResult::setIndexedTagName( $data, 'error' ); |
||
856 | // For backwards-compatibility, we use the 'internal-error' fallback key and merge $data |
||
857 | // into the root of the response (rather than something sane like [ 'details' => $data ]). |
||
858 | $this->dieRecoverableError( $msg, null, $data, 'internal-error' ); |
||
859 | } |
||
860 | $result['result'] = 'Success'; |
||
861 | } |
||
862 | |||
863 | $result['filename'] = $file->getName(); |
||
864 | if ( $warnings && count( $warnings ) > 0 ) { |
||
865 | $result['warnings'] = $warnings; |
||
866 | } |
||
867 | |||
868 | return $result; |
||
869 | } |
||
870 | |||
871 | public function mustBePosted() { |
||
872 | return true; |
||
873 | } |
||
874 | |||
875 | public function isWriteMode() { |
||
876 | return true; |
||
877 | } |
||
878 | |||
879 | public function getAllowedParams() { |
||
880 | $params = [ |
||
881 | 'filename' => [ |
||
882 | ApiBase::PARAM_TYPE => 'string', |
||
883 | ], |
||
884 | 'comment' => [ |
||
885 | ApiBase::PARAM_DFLT => '' |
||
886 | ], |
||
887 | 'tags' => [ |
||
888 | ApiBase::PARAM_TYPE => 'tags', |
||
889 | ApiBase::PARAM_ISMULTI => true, |
||
890 | ], |
||
891 | 'text' => [ |
||
892 | ApiBase::PARAM_TYPE => 'text', |
||
893 | ], |
||
894 | 'watch' => [ |
||
895 | ApiBase::PARAM_DFLT => false, |
||
896 | ApiBase::PARAM_DEPRECATED => true, |
||
897 | ], |
||
898 | 'watchlist' => [ |
||
899 | ApiBase::PARAM_DFLT => 'preferences', |
||
900 | ApiBase::PARAM_TYPE => [ |
||
901 | 'watch', |
||
902 | 'preferences', |
||
903 | 'nochange' |
||
904 | ], |
||
905 | ], |
||
906 | 'ignorewarnings' => false, |
||
907 | 'file' => [ |
||
908 | ApiBase::PARAM_TYPE => 'upload', |
||
909 | ], |
||
910 | 'url' => null, |
||
911 | 'filekey' => null, |
||
912 | 'sessionkey' => [ |
||
913 | ApiBase::PARAM_DEPRECATED => true, |
||
914 | ], |
||
915 | 'stash' => false, |
||
916 | |||
917 | 'filesize' => [ |
||
918 | ApiBase::PARAM_TYPE => 'integer', |
||
919 | ApiBase::PARAM_MIN => 0, |
||
920 | ApiBase::PARAM_MAX => UploadBase::getMaxUploadSize(), |
||
921 | ], |
||
922 | 'offset' => [ |
||
923 | ApiBase::PARAM_TYPE => 'integer', |
||
924 | ApiBase::PARAM_MIN => 0, |
||
925 | ], |
||
926 | 'chunk' => [ |
||
927 | ApiBase::PARAM_TYPE => 'upload', |
||
928 | ], |
||
929 | |||
930 | 'async' => false, |
||
931 | 'checkstatus' => false, |
||
932 | ]; |
||
933 | |||
934 | return $params; |
||
935 | } |
||
936 | |||
937 | public function needsToken() { |
||
938 | return 'csrf'; |
||
939 | } |
||
940 | |||
941 | protected function getExamplesMessages() { |
||
942 | return [ |
||
943 | 'action=upload&filename=Wiki.png' . |
||
944 | '&url=http%3A//upload.wikimedia.org/wikipedia/en/b/bc/Wiki.png&token=123ABC' |
||
945 | => 'apihelp-upload-example-url', |
||
946 | 'action=upload&filename=Wiki.png&filekey=filekey&ignorewarnings=1&token=123ABC' |
||
947 | => 'apihelp-upload-example-filekey', |
||
948 | ]; |
||
949 | } |
||
950 | |||
951 | public function getHelpUrls() { |
||
952 | return 'https://www.mediawiki.org/wiki/API:Upload'; |
||
953 | } |
||
954 | } |
||
955 |
Adding an explicit array definition is generally preferable to implicit array definition as it guarantees a stable state of the code.
Let’s take a look at an example:
As you can see in this example, the array
$myArray
is initialized the first time when the foreach loop is entered. You can also see that the value of thebar
key is only written conditionally; thus, its value might result from a previous iteration.This might or might not be intended. To make your intention clear, your code more readible and to avoid accidental bugs, we recommend to add an explicit initialization $myArray = array() either outside or inside the foreach loop.