MediaWiki REL1_35
ApiUpload.php
Go to the documentation of this file.
1<?php
26class ApiUpload extends ApiBase {
27
29
31 protected $mUpload = null;
32
33 protected $mParams;
34
35 public function __construct( ApiMain $mainModule, $moduleName, $modulePrefix = '' ) {
36 parent::__construct( $mainModule, $moduleName, $modulePrefix );
37
38 $this->watchlistExpiryEnabled = $this->getConfig()->get( 'WatchlistExpiry' );
39 $this->watchlistMaxDuration = $this->getConfig()->get( 'WatchlistExpiryMaxDuration' );
40 }
41
42 public function execute() {
43 // Check whether upload is enabled
44 if ( !UploadBase::isEnabled() ) {
45 $this->dieWithError( 'uploaddisabled' );
46 }
47
48 $user = $this->getUser();
49
50 // Parameter handling
51 $this->mParams = $this->extractRequestParams();
52 $request = $this->getMain()->getRequest();
53 // Check if async mode is actually supported (jobs done in cli mode)
54 $this->mParams['async'] = ( $this->mParams['async'] &&
55 $this->getConfig()->get( 'EnableAsyncUploads' ) );
56 // Add the uploaded file to the params array
57 $this->mParams['file'] = $request->getFileName( 'file' );
58 $this->mParams['chunk'] = $request->getFileName( 'chunk' );
59
60 // Copy the session key to the file key, for backward compatibility.
61 if ( !$this->mParams['filekey'] && $this->mParams['sessionkey'] ) {
62 $this->mParams['filekey'] = $this->mParams['sessionkey'];
63 }
64
65 // Select an upload module
66 try {
67 if ( !$this->selectUploadModule() ) {
68 return; // not a true upload, but a status request or similar
69 } elseif ( !isset( $this->mUpload ) ) {
70 $this->dieDebug( __METHOD__, 'No upload module set' );
71 }
72 } catch ( UploadStashException $e ) { // XXX: don't spam exception log
73 $this->dieStatus( $this->handleStashException( $e ) );
74 }
75
76 // First check permission to upload
77 $this->checkPermissions( $user );
78
79 // Fetch the file (usually a no-op)
81 $status = $this->mUpload->fetchFile();
82 if ( !$status->isGood() ) {
83 $this->dieStatus( $status );
84 }
85
86 // Check if the uploaded file is sane
87 $this->verifyUpload();
88
89 // Check if the user has the rights to modify or overwrite the requested title
90 // (This check is irrelevant if stashing is already requested, since the errors
91 // can always be fixed by changing the title)
92 if ( !$this->mParams['stash'] ) {
93 $permErrors = $this->mUpload->verifyTitlePermissions( $user );
94 if ( $permErrors !== true ) {
95 $this->dieRecoverableError( $permErrors, 'filename' );
96 }
97 }
98
99 // Get the result based on the current upload context:
100 try {
101 $result = $this->getContextResult();
102 } catch ( UploadStashException $e ) { // XXX: don't spam exception log
103 $this->dieStatus( $this->handleStashException( $e ) );
104 }
105 $this->getResult()->addValue( null, $this->getModuleName(), $result );
106
107 // Add 'imageinfo' in a separate addValue() call. File metadata can be unreasonably large,
108 // so otherwise when it exceeded $wgAPIMaxResultSize, no result would be returned (T143993).
109 // @phan-suppress-next-line PhanTypeArraySuspiciousNullable False positive
110 if ( $result['result'] === 'Success' ) {
111 $imageinfo = $this->mUpload->getImageInfo( $this->getResult() );
112 $this->getResult()->addValue( $this->getModuleName(), 'imageinfo', $imageinfo );
113 }
114
115 // Cleanup any temporary mess
116 $this->mUpload->cleanupTempFile();
117 }
118
123 private function getContextResult() {
124 $warnings = $this->getApiWarnings();
125 if ( $warnings && !$this->mParams['ignorewarnings'] ) {
126 // Get warnings formatted in result array format
127 return $this->getWarningsResult( $warnings );
128 } elseif ( $this->mParams['chunk'] ) {
129 // Add chunk, and get result
130 return $this->getChunkResult( $warnings );
131 } elseif ( $this->mParams['stash'] ) {
132 // Stash the file and get stash result
133 return $this->getStashResult( $warnings );
134 }
135
136 // Check throttle after we've handled warnings
137 if ( UploadBase::isThrottled( $this->getUser() )
138 ) {
139 $this->dieWithError( 'apierror-ratelimited' );
140 }
141
142 // This is the most common case -- a normal upload with no warnings
143 // performUpload will return a formatted properly for the API with status
144 return $this->performUpload( $warnings );
145 }
146
152 private function getStashResult( $warnings ) {
153 $result = [];
154 $result['result'] = 'Success';
155 if ( $warnings && count( $warnings ) > 0 ) {
156 $result['warnings'] = $warnings;
157 }
158 // Some uploads can request they be stashed, so as not to publish them immediately.
159 // In this case, a failure to stash ought to be fatal
160 $this->performStash( 'critical', $result );
161
162 return $result;
163 }
164
170 private function getWarningsResult( $warnings ) {
171 $result = [];
172 $result['result'] = 'Warning';
173 $result['warnings'] = $warnings;
174 // in case the warnings can be fixed with some further user action, let's stash this upload
175 // and return a key they can use to restart it
176 $this->performStash( 'optional', $result );
177
178 return $result;
179 }
180
187 public static function getMinUploadChunkSize( Config $config ) {
188 $configured = $config->get( 'MinUploadChunkSize' );
189
190 // Leave some room for other POST parameters
191 $postMax = (
193 ini_get( 'post_max_size' ),
194 PHP_INT_MAX
195 ) ?: PHP_INT_MAX
196 ) - 1024;
197
198 // Ensure the minimum chunk size is less than PHP upload limits
199 // or the maximum upload size.
200 return min(
201 $configured,
202 UploadBase::getMaxUploadSize( 'file' ),
203 UploadBase::getMaxPhpUploadSize(),
204 $postMax
205 );
206 }
207
213 private function getChunkResult( $warnings ) {
214 $result = [];
215
216 if ( $warnings && count( $warnings ) > 0 ) {
217 $result['warnings'] = $warnings;
218 }
219
220 $request = $this->getMain()->getRequest();
221 $chunkPath = $request->getFileTempname( 'chunk' );
222 $chunkSize = $request->getUpload( 'chunk' )->getSize();
223 $totalSoFar = $this->mParams['offset'] + $chunkSize;
224 $minChunkSize = self::getMinUploadChunkSize( $this->getConfig() );
225
226 // Sanity check sizing
227 if ( $totalSoFar > $this->mParams['filesize'] ) {
228 $this->dieWithError( 'apierror-invalid-chunk' );
229 }
230
231 // Enforce minimum chunk size
232 if ( $totalSoFar != $this->mParams['filesize'] && $chunkSize < $minChunkSize ) {
233 $this->dieWithError( [ 'apierror-chunk-too-small', Message::numParam( $minChunkSize ) ] );
234 }
235
236 if ( $this->mParams['offset'] == 0 ) {
237 $filekey = $this->performStash( 'critical' );
238 } else {
239 $filekey = $this->mParams['filekey'];
240
241 // Don't allow further uploads to an already-completed session
242 $progress = UploadBase::getSessionStatus( $this->getUser(), $filekey );
243 if ( !$progress ) {
244 // Probably can't get here, but check anyway just in case
245 $this->dieWithError( 'apierror-stashfailed-nosession', 'stashfailed' );
246 } elseif ( $progress['result'] !== 'Continue' || $progress['stage'] !== 'uploading' ) {
247 $this->dieWithError( 'apierror-stashfailed-complete', 'stashfailed' );
248 }
249
250 $status = $this->mUpload->addChunk(
251 $chunkPath, $chunkSize, $this->mParams['offset'] );
252 if ( !$status->isGood() ) {
253 $extradata = [
254 'offset' => $this->mUpload->getOffset(),
255 ];
256
257 $this->dieStatusWithCode( $status, 'stashfailed', $extradata );
258 }
259 }
260
261 // Check we added the last chunk:
262 if ( $totalSoFar == $this->mParams['filesize'] ) {
263 if ( $this->mParams['async'] ) {
264 UploadBase::setSessionStatus(
265 $this->getUser(),
266 $filekey,
267 [ 'result' => 'Poll',
268 'stage' => 'queued', 'status' => Status::newGood() ]
269 );
270 JobQueueGroup::singleton()->push( new AssembleUploadChunksJob(
271 Title::makeTitle( NS_FILE, $filekey ),
272 [
273 'filename' => $this->mParams['filename'],
274 'filekey' => $filekey,
275 'session' => $this->getContext()->exportSession()
276 ]
277 ) );
278 $result['result'] = 'Poll';
279 $result['stage'] = 'queued';
280 } else {
281 $status = $this->mUpload->concatenateChunks();
282 if ( !$status->isGood() ) {
283 UploadBase::setSessionStatus(
284 $this->getUser(),
285 $filekey,
286 [ 'result' => 'Failure', 'stage' => 'assembling', 'status' => $status ]
287 );
288 $this->dieStatusWithCode( $status, 'stashfailed' );
289 }
290
291 // We can only get warnings like 'duplicate' after concatenating the chunks
292 $warnings = $this->getApiWarnings();
293 if ( $warnings ) {
294 $result['warnings'] = $warnings;
295 }
296
297 // The fully concatenated file has a new filekey. So remove
298 // the old filekey and fetch the new one.
299 UploadBase::setSessionStatus( $this->getUser(), $filekey, false );
300 $this->mUpload->stash->removeFile( $filekey );
301 $filekey = $this->mUpload->getStashFile()->getFileKey();
302
303 $result['result'] = 'Success';
304 }
305 } else {
306 UploadBase::setSessionStatus(
307 $this->getUser(),
308 $filekey,
309 [
310 'result' => 'Continue',
311 'stage' => 'uploading',
312 'offset' => $totalSoFar,
313 'status' => Status::newGood(),
314 ]
315 );
316 $result['result'] = 'Continue';
317 $result['offset'] = $totalSoFar;
318 }
319
320 $result['filekey'] = $filekey;
321
322 return $result;
323 }
324
337 private function performStash( $failureMode, &$data = null ) {
338 $isPartial = (bool)$this->mParams['chunk'];
339 try {
340 $status = $this->mUpload->tryStashFile( $this->getUser(), $isPartial );
341
342 if ( $status->isGood() && !$status->getValue() ) {
343 // Not actually a 'good' status...
344 $status->fatal( new ApiMessage( 'apierror-stashinvalidfile', 'stashfailed' ) );
345 }
346 } catch ( Exception $e ) {
347 $debugMessage = 'Stashing temporary file failed: ' . get_class( $e ) . ' ' . $e->getMessage();
348 wfDebug( __METHOD__ . ' ' . $debugMessage );
349 $status = Status::newFatal( $this->getErrorFormatter()->getMessageFromException(
350 $e, [ 'wrap' => new ApiMessage( 'apierror-stashexception', 'stashfailed' ) ]
351 ) );
352 }
353
354 if ( $status->isGood() ) {
355 $stashFile = $status->getValue();
356 $data['filekey'] = $stashFile->getFileKey();
357 // Backwards compatibility
358 $data['sessionkey'] = $data['filekey'];
359 return $data['filekey'];
360 }
361
362 if ( $status->getMessage()->getKey() === 'uploadstash-exception' ) {
363 // The exceptions thrown by upload stash code and pretty silly and UploadBase returns poor
364 // Statuses for it. Just extract the exception details and parse them ourselves.
365 list( $exceptionType, $message ) = $status->getMessage()->getParams();
366 $debugMessage = 'Stashing temporary file failed: ' . $exceptionType . ' ' . $message;
367 wfDebug( __METHOD__ . ' ' . $debugMessage );
368 }
369
370 // Bad status
371 if ( $failureMode !== 'optional' ) {
372 $this->dieStatus( $status );
373 } else {
374 $data['stasherrors'] = $this->getErrorFormatter()->arrayFromStatus( $status );
375 return null;
376 }
377 }
378
388 private function dieRecoverableError( $errors, $parameter = null ) {
389 $this->performStash( 'optional', $data );
390
391 if ( $parameter ) {
392 $data['invalidparameter'] = $parameter;
393 }
394
395 $sv = StatusValue::newGood();
396 foreach ( $errors as $error ) {
397 $msg = ApiMessage::create( $error );
398 $msg->setApiData( $msg->getApiData() + $data );
399 $sv->fatal( $msg );
400 }
401 $this->dieStatus( $sv );
402 }
403
413 public function dieStatusWithCode( $status, $overrideCode, $moreExtraData = null ) {
414 $sv = StatusValue::newGood();
415 foreach ( $status->getErrors() as $error ) {
416 $msg = ApiMessage::create( $error, $overrideCode );
417 if ( $moreExtraData ) {
418 $msg->setApiData( $msg->getApiData() + $moreExtraData );
419 }
420 $sv->fatal( $msg );
421 }
422 $this->dieStatus( $sv );
423 }
424
433 protected function selectUploadModule() {
434 $request = $this->getMain()->getRequest();
435
436 // chunk or one and only one of the following parameters is needed
437 if ( !$this->mParams['chunk'] ) {
438 $this->requireOnlyOneParameter( $this->mParams,
439 'filekey', 'file', 'url' );
440 }
441
442 // Status report for "upload to stash"/"upload from stash"
443 if ( $this->mParams['filekey'] && $this->mParams['checkstatus'] ) {
444 $progress = UploadBase::getSessionStatus( $this->getUser(), $this->mParams['filekey'] );
445 if ( !$progress ) {
446 $this->dieWithError( 'apierror-upload-missingresult', 'missingresult' );
447 } elseif ( !$progress['status']->isGood() ) {
448 $this->dieStatusWithCode( $progress['status'], 'stashfailed' );
449 }
450 if ( isset( $progress['status']->value['verification'] ) ) {
451 $this->checkVerification( $progress['status']->value['verification'] );
452 }
453 if ( isset( $progress['status']->value['warnings'] ) ) {
454 $warnings = $this->transformWarnings( $progress['status']->value['warnings'] );
455 if ( $warnings ) {
456 $progress['warnings'] = $warnings;
457 }
458 }
459 unset( $progress['status'] ); // remove Status object
460 $imageinfo = null;
461 if ( isset( $progress['imageinfo'] ) ) {
462 $imageinfo = $progress['imageinfo'];
463 unset( $progress['imageinfo'] );
464 }
465
466 $this->getResult()->addValue( null, $this->getModuleName(), $progress );
467 // Add 'imageinfo' in a separate addValue() call. File metadata can be unreasonably large,
468 // so otherwise when it exceeded $wgAPIMaxResultSize, no result would be returned (T143993).
469 if ( $imageinfo ) {
470 $this->getResult()->addValue( $this->getModuleName(), 'imageinfo', $imageinfo );
471 }
472
473 return false;
474 }
475
476 // The following modules all require the filename parameter to be set
477 if ( $this->mParams['filename'] === null ) {
478 $this->dieWithError( [ 'apierror-missingparam', 'filename' ] );
479 }
480
481 if ( $this->mParams['chunk'] ) {
482 // Chunk upload
483 $this->mUpload = new UploadFromChunks( $this->getUser() );
484 if ( isset( $this->mParams['filekey'] ) ) {
485 if ( $this->mParams['offset'] === 0 ) {
486 $this->dieWithError( 'apierror-upload-filekeynotallowed', 'filekeynotallowed' );
487 }
488
489 // handle new chunk
490 $this->mUpload->continueChunks(
491 $this->mParams['filename'],
492 $this->mParams['filekey'],
493 $request->getUpload( 'chunk' )
494 );
495 } else {
496 if ( $this->mParams['offset'] !== 0 ) {
497 $this->dieWithError( 'apierror-upload-filekeyneeded', 'filekeyneeded' );
498 }
499
500 // handle first chunk
501 $this->mUpload->initialize(
502 $this->mParams['filename'],
503 $request->getUpload( 'chunk' )
504 );
505 }
506 } elseif ( isset( $this->mParams['filekey'] ) ) {
507 // Upload stashed in a previous request
508 if ( !UploadFromStash::isValidKey( $this->mParams['filekey'] ) ) {
509 $this->dieWithError( 'apierror-invalid-file-key' );
510 }
511
512 $this->mUpload = new UploadFromStash( $this->getUser() );
513 // This will not download the temp file in initialize() in async mode.
514 // We still have enough information to call checkWarnings() and such.
515 $this->mUpload->initialize(
516 $this->mParams['filekey'], $this->mParams['filename'], !$this->mParams['async']
517 );
518 } elseif ( isset( $this->mParams['file'] ) ) {
519 // Can't async upload directly from a POSTed file, we'd have to
520 // stash the file and then queue the publish job. The user should
521 // just submit the two API queries to perform those two steps.
522 if ( $this->mParams['async'] ) {
523 $this->dieWithError( 'apierror-cannot-async-upload-file' );
524 }
525
526 $this->mUpload = new UploadFromFile();
527 $this->mUpload->initialize(
528 $this->mParams['filename'],
529 $request->getUpload( 'file' )
530 );
531 } elseif ( isset( $this->mParams['url'] ) ) {
532 // Make sure upload by URL is enabled:
533 if ( !UploadFromUrl::isEnabled() ) {
534 $this->dieWithError( 'copyuploaddisabled' );
535 }
536
537 if ( !UploadFromUrl::isAllowedHost( $this->mParams['url'] ) ) {
538 $this->dieWithError( 'apierror-copyuploadbaddomain' );
539 }
540
541 if ( !UploadFromUrl::isAllowedUrl( $this->mParams['url'] ) ) {
542 $this->dieWithError( 'apierror-copyuploadbadurl' );
543 }
544
545 $this->mUpload = new UploadFromUrl;
546 $this->mUpload->initialize( $this->mParams['filename'],
547 $this->mParams['url'] );
548 }
549
550 return true;
551 }
552
558 protected function checkPermissions( $user ) {
559 // Check whether the user has the appropriate permissions to upload anyway
560 $permission = $this->mUpload->isAllowed( $user );
561
562 if ( $permission !== true ) {
563 if ( !$user->isLoggedIn() ) {
564 $this->dieWithError( [ 'apierror-mustbeloggedin', $this->msg( 'action-upload' ) ] );
565 }
566
567 $this->dieStatus( User::newFatalPermissionDeniedStatus( $permission ) );
568 }
569
570 // Check blocks
571 if ( $user->isBlockedFromUpload() ) {
572 $this->dieBlocked( $user->getBlock() );
573 }
574
575 // Global blocks
576 if ( $user->isBlockedGlobally() ) {
577 $this->dieBlocked( $user->getGlobalBlock() );
578 }
579 }
580
584 protected function verifyUpload() {
585 if ( $this->mParams['chunk'] ) {
586 $maxSize = UploadBase::getMaxUploadSize();
587 if ( $this->mParams['filesize'] > $maxSize ) {
588 $this->dieWithError( 'file-too-large' );
589 }
590 if ( !$this->mUpload->getTitle() ) {
591 $this->dieWithError( 'illegal-filename' );
592 }
593 // file will be assembled after having uploaded the last chunk,
594 // so we can only validate the name at this point
595 $verification = $this->mUpload->validateName();
596 if ( $verification === true ) {
597 return;
598 }
599 } elseif ( $this->mParams['async'] && $this->mParams['filekey'] ) {
600 // file will be assembled in a background process, so we
601 // can only validate the name at this point
602 // file verification will happen in background process
603 $verification = $this->mUpload->validateName();
604 if ( $verification === true ) {
605 return;
606 }
607 } else {
608 wfDebug( __METHOD__ . " about to verify" );
609
610 $verification = $this->mUpload->verifyUpload();
611 if ( $verification['status'] === UploadBase::OK ) {
612 return;
613 }
614 }
615
616 $this->checkVerification( $verification );
617 }
618
623 protected function checkVerification( array $verification ) {
624 switch ( $verification['status'] ) {
625 // Recoverable errors
626 case UploadBase::MIN_LENGTH_PARTNAME:
627 $this->dieRecoverableError( [ 'filename-tooshort' ], 'filename' );
628 break;
629 case UploadBase::ILLEGAL_FILENAME:
630 $this->dieRecoverableError(
631 [ ApiMessage::create(
632 'illegal-filename', null, [ 'filename' => $verification['filtered'] ]
633 ) ], 'filename'
634 );
635 break;
636 case UploadBase::FILENAME_TOO_LONG:
637 $this->dieRecoverableError( [ 'filename-toolong' ], 'filename' );
638 break;
639 case UploadBase::FILETYPE_MISSING:
640 $this->dieRecoverableError( [ 'filetype-missing' ], 'filename' );
641 break;
642 case UploadBase::WINDOWS_NONASCII_FILENAME:
643 $this->dieRecoverableError( [ 'windows-nonascii-filename' ], 'filename' );
644 break;
645
646 // Unrecoverable errors
647 case UploadBase::EMPTY_FILE:
648 $this->dieWithError( 'empty-file' );
649 break;
650 case UploadBase::FILE_TOO_LARGE:
651 $this->dieWithError( 'file-too-large' );
652 break;
653
654 case UploadBase::FILETYPE_BADTYPE:
655 $extradata = [
656 'filetype' => $verification['finalExt'],
657 'allowed' => array_values( array_unique( $this->getConfig()->get( 'FileExtensions' ) ) )
658 ];
659 $extensions = array_unique( $this->getConfig()->get( 'FileExtensions' ) );
660 $msg = [
661 'filetype-banned-type',
662 null, // filled in below
663 Message::listParam( $extensions, 'comma' ),
664 count( $extensions ),
665 null, // filled in below
666 ];
667 ApiResult::setIndexedTagName( $extradata['allowed'], 'ext' );
668
669 if ( isset( $verification['blacklistedExt'] ) ) {
670 $msg[1] = Message::listParam( $verification['blacklistedExt'], 'comma' );
671 $msg[4] = count( $verification['blacklistedExt'] );
672 $extradata['blacklisted'] = array_values( $verification['blacklistedExt'] );
673 ApiResult::setIndexedTagName( $extradata['blacklisted'], 'ext' );
674 } else {
675 $msg[1] = $verification['finalExt'];
676 $msg[4] = 1;
677 }
678
679 $this->dieWithError( $msg, 'filetype-banned', $extradata );
680 break;
681
682 case UploadBase::VERIFICATION_ERROR:
683 $msg = ApiMessage::create( $verification['details'], 'verification-error' );
684 if ( $verification['details'][0] instanceof MessageSpecifier ) {
685 $details = array_merge( [ $msg->getKey() ], $msg->getParams() );
686 } else {
687 $details = $verification['details'];
688 }
689 ApiResult::setIndexedTagName( $details, 'detail' );
690 $msg->setApiData( $msg->getApiData() + [ 'details' => $details ] );
691 // @phan-suppress-next-line PhanTypeMismatchArgument
692 $this->dieWithError( $msg );
693 break;
694
695 case UploadBase::HOOK_ABORTED:
696 $msg = $verification['error'] === '' ? 'hookaborted' : $verification['error'];
697 $this->dieWithError( $msg, 'hookaborted', [ 'details' => $verification['error'] ] );
698 break;
699 default:
700 $this->dieWithError( 'apierror-unknownerror-nocode', 'unknown-error',
701 [ 'details' => [ 'code' => $verification['status'] ] ] );
702 break;
703 }
704 }
705
713 protected function getApiWarnings() {
714 $warnings = UploadBase::makeWarningsSerializable(
715 $this->mUpload->checkWarnings( $this->getUser() )
716 );
717
718 return $this->transformWarnings( $warnings );
719 }
720
721 protected function transformWarnings( $warnings ) {
722 if ( $warnings ) {
723 // Add indices
724 ApiResult::setIndexedTagName( $warnings, 'warning' );
725
726 if ( isset( $warnings['duplicate'] ) ) {
727 $dupes = [];
728 foreach ( $warnings['duplicate'] as $dupe ) {
729 $dupes[] = $dupe['fileName'];
730 }
731 ApiResult::setIndexedTagName( $dupes, 'duplicate' );
732 $warnings['duplicate'] = $dupes;
733 }
734
735 if ( isset( $warnings['exists'] ) ) {
736 $warning = $warnings['exists'];
737 unset( $warnings['exists'] );
738 $localFile = $warning['normalizedFile'] ?? $warning['file'];
739 $warnings[$warning['warning']] = $localFile['fileName'];
740 }
741
742 if ( isset( $warnings['no-change'] ) ) {
743 $file = $warnings['no-change'];
744 unset( $warnings['no-change'] );
745
746 $warnings['nochange'] = [
747 'timestamp' => wfTimestamp( TS_ISO_8601, $file['timestamp'] )
748 ];
749 }
750
751 if ( isset( $warnings['duplicate-version'] ) ) {
752 $dupes = [];
753 foreach ( $warnings['duplicate-version'] as $dupe ) {
754 $dupes[] = [
755 'timestamp' => wfTimestamp( TS_ISO_8601, $dupe['timestamp'] )
756 ];
757 }
758 unset( $warnings['duplicate-version'] );
759
760 ApiResult::setIndexedTagName( $dupes, 'ver' );
761 $warnings['duplicateversions'] = $dupes;
762 }
763 }
764
765 return $warnings;
766 }
767
774 protected function handleStashException( $e ) {
775 switch ( get_class( $e ) ) {
776 case UploadStashFileNotFoundException::class:
777 $wrap = 'apierror-stashedfilenotfound';
778 break;
779 case UploadStashBadPathException::class:
780 $wrap = 'apierror-stashpathinvalid';
781 break;
782 case UploadStashFileException::class:
783 $wrap = 'apierror-stashfilestorage';
784 break;
785 case UploadStashZeroLengthFileException::class:
786 $wrap = 'apierror-stashzerolength';
787 break;
788 case UploadStashNotLoggedInException::class:
789 return StatusValue::newFatal( ApiMessage::create(
790 [ 'apierror-mustbeloggedin', $this->msg( 'action-upload' ) ], 'stashnotloggedin'
791 ) );
792 case UploadStashWrongOwnerException::class:
793 $wrap = 'apierror-stashwrongowner';
794 break;
795 case UploadStashNoSuchKeyException::class:
796 $wrap = 'apierror-stashnosuchfilekey';
797 break;
798 default:
799 $wrap = [ 'uploadstash-exception', get_class( $e ) ];
800 break;
801 }
802 return StatusValue::newFatal(
803 $this->getErrorFormatter()->getMessageFromException( $e, [ 'wrap' => $wrap ] )
804 );
805 }
806
814 protected function performUpload( $warnings ) {
815 // Use comment as initial page text by default
816 if ( $this->mParams['text'] === null ) {
817 $this->mParams['text'] = $this->mParams['comment'];
818 }
819
821 $file = $this->mUpload->getLocalFile();
822 $user = $this->getUser();
823 $title = $file->getTitle();
824
825 // for preferences mode, we want to watch if 'watchdefault' is set,
826 // or if the *file* doesn't exist, and either 'watchuploads' or
827 // 'watchcreations' is set. But getWatchlistValue()'s automatic
828 // handling checks if the *title* exists or not, so we need to check
829 // all three preferences manually.
830 $watch = $this->getWatchlistValue(
831 $this->mParams['watchlist'], $title, $user, 'watchdefault'
832 );
833
834 if ( !$watch && $this->mParams['watchlist'] == 'preferences' && !$file->exists() ) {
835 $watch = (
836 $this->getWatchlistValue( 'preferences', $title, $user, 'watchuploads' ) ||
837 $this->getWatchlistValue( 'preferences', $title, $user, 'watchcreations' )
838 );
839 }
840 $watchlistExpiry = $this->getExpiryFromParams( $this->mParams );
841
842 // Deprecated parameters
843 if ( $this->mParams['watch'] ) {
844 $watch = true;
845 }
846
847 if ( $this->mParams['tags'] ) {
848 $status = ChangeTags::canAddTagsAccompanyingChange( $this->mParams['tags'], $this->getUser() );
849 if ( !$status->isOK() ) {
850 $this->dieStatus( $status );
851 }
852 }
853
854 // No errors, no warnings: do the upload
855 $result = [];
856 if ( $this->mParams['async'] ) {
857 $progress = UploadBase::getSessionStatus( $this->getUser(), $this->mParams['filekey'] );
858 if ( $progress && $progress['result'] === 'Poll' ) {
859 $this->dieWithError( 'apierror-upload-inprogress', 'publishfailed' );
860 }
861 UploadBase::setSessionStatus(
862 $this->getUser(),
863 $this->mParams['filekey'],
864 [ 'result' => 'Poll', 'stage' => 'queued', 'status' => Status::newGood() ]
865 );
866 JobQueueGroup::singleton()->push( new PublishStashedFileJob(
867 Title::makeTitle( NS_FILE, $this->mParams['filename'] ),
868 [
869 'filename' => $this->mParams['filename'],
870 'filekey' => $this->mParams['filekey'],
871 'comment' => $this->mParams['comment'],
872 'tags' => $this->mParams['tags'],
873 'text' => $this->mParams['text'],
874 'watch' => $watch,
875 'watchlistexpiry' => $watchlistExpiry,
876 'session' => $this->getContext()->exportSession()
877 ]
878 ) );
879 $result['result'] = 'Poll';
880 $result['stage'] = 'queued';
881 } else {
883 $status = $this->mUpload->performUpload(
884 $this->mParams['comment'],
885 $this->mParams['text'],
886 $watch,
887 $this->getUser(),
888 $this->mParams['tags'],
889 $watchlistExpiry
890 );
891
892 if ( !$status->isGood() ) {
893 $this->dieRecoverableError( $status->getErrors() );
894 }
895 $result['result'] = 'Success';
896 }
897
898 $result['filename'] = $file->getName();
899 if ( $warnings && count( $warnings ) > 0 ) {
900 $result['warnings'] = $warnings;
901 }
902
903 return $result;
904 }
905
906 public function mustBePosted() {
907 return true;
908 }
909
910 public function isWriteMode() {
911 return true;
912 }
913
914 public function getAllowedParams() {
915 $params = [
916 'filename' => [
917 ApiBase::PARAM_TYPE => 'string',
918 ],
919 'comment' => [
921 ],
922 'tags' => [
923 ApiBase::PARAM_TYPE => 'tags',
925 ],
926 'text' => [
927 ApiBase::PARAM_TYPE => 'text',
928 ],
929 'watch' => [
930 ApiBase::PARAM_DFLT => false,
932 ],
933 ];
934
935 // Params appear in the docs in the order they are defined,
936 // which is why this is here and not at the bottom.
937 $params += $this->getWatchlistParams( [
938 'watch',
939 'preferences',
940 'nochange',
941 ] );
942
943 $params += [
944 'ignorewarnings' => false,
945 'file' => [
946 ApiBase::PARAM_TYPE => 'upload',
947 ],
948 'url' => null,
949 'filekey' => null,
950 'sessionkey' => [
952 ],
953 'stash' => false,
954
955 'filesize' => [
956 ApiBase::PARAM_TYPE => 'integer',
958 ApiBase::PARAM_MAX => UploadBase::getMaxUploadSize(),
959 ],
960 'offset' => [
961 ApiBase::PARAM_TYPE => 'integer',
963 ],
964 'chunk' => [
965 ApiBase::PARAM_TYPE => 'upload',
966 ],
967
968 'async' => false,
969 'checkstatus' => false,
970 ];
971
972 return $params;
973 }
974
975 public function needsToken() {
976 return 'csrf';
977 }
978
979 protected function getExamplesMessages() {
980 return [
981 'action=upload&filename=Wiki.png' .
982 '&url=http%3A//upload.wikimedia.org/wikipedia/en/b/bc/Wiki.png&token=123ABC'
983 => 'apihelp-upload-example-url',
984 'action=upload&filename=Wiki.png&filekey=filekey&ignorewarnings=1&token=123ABC'
985 => 'apihelp-upload-example-filekey',
986 ];
987 }
988
989 public function getHelpUrls() {
990 return 'https://www.mediawiki.org/wiki/Special:MyLanguage/API:Upload';
991 }
992}
getWatchlistValue(string $watchlist, Title $title, User $user, ?string $userOption=null)
Return true if we're to watch the page, false if not.
getExpiryFromParams(array $params)
Get formatted expiry from the given parameters, or null if no expiry was provided.
getWatchlistParams(array $watchOptions=[])
Get additional allow params specific to watchlisting.
wfDebug( $text, $dest='all', array $context=[])
Sends a line to the debug log if enabled or, optionally, to a comment in output.
wfShorthandToInteger(?string $string='', int $default=-1)
Converts shorthand byte notation to integer form.
wfTimestamp( $outputtype=TS_UNIX, $ts=0)
Get a timestamp string in one of various formats.
This abstract class implements many basic API functions, and is the base of all API classes.
Definition ApiBase.php:52
dieWithError( $msg, $code=null, $data=null, $httpCode=0)
Abort execution with an error.
Definition ApiBase.php:1437
const PARAM_DEPRECATED
Definition ApiBase.php:98
const PARAM_MAX
Definition ApiBase.php:82
static dieDebug( $method, $message)
Internal code errors should be reported with this method.
Definition ApiBase.php:1629
getMain()
Get the main module.
Definition ApiBase.php:515
const PARAM_TYPE
Definition ApiBase.php:78
getErrorFormatter()
Get the error formatter Stable to override.
Definition ApiBase.php:635
const PARAM_DFLT
Definition ApiBase.php:70
requireOnlyOneParameter( $params,... $required)
Die if none or more than one of a certain set of parameters is set and not false.
Definition ApiBase.php:909
const PARAM_MIN
Definition ApiBase.php:90
dieBlocked(AbstractBlock $block)
Throw an ApiUsageException, which will (if uncaught) call the main module's error handler and die wit...
Definition ApiBase.php:1464
getResult()
Get the result object.
Definition ApiBase.php:620
extractRequestParams( $options=[])
Using getAllowedParams(), this function makes an array of the values provided by the user,...
Definition ApiBase.php:772
getModuleName()
Get the name of the module being executed by this instance.
Definition ApiBase.php:499
dieStatus(StatusValue $status)
Throw an ApiUsageException based on the Status object.
Definition ApiBase.php:1495
const PARAM_ISMULTI
Definition ApiBase.php:74
This is the main API class, used for both external and internal processing.
Definition ApiMain.php:47
Extension of Message implementing IApiMessage @newable.
__construct(ApiMain $mainModule, $moduleName, $modulePrefix='')
Stable to call.
Definition ApiUpload.php:35
performStash( $failureMode, &$data=null)
Stash the file and add the file key, or error information if it fails, to the data.
execute()
Evaluates the parameters, performs the requested query, and sets up the result.
Definition ApiUpload.php:42
checkPermissions( $user)
Checks that the user has permissions to perform this upload.
dieRecoverableError( $errors, $parameter=null)
Throw an error that the user can recover from by providing a better value for $parameter.
verifyUpload()
Performs file verification, dies on error.
UploadBase UploadFromChunks $mUpload
Definition ApiUpload.php:31
getAllowedParams()
Returns an array of allowed parameters (parameter name) => (default value) or (parameter name) => (ar...
transformWarnings( $warnings)
handleStashException( $e)
Handles a stash exception, giving a useful error to the user.
getHelpUrls()
Return links to more detailed help pages about the module.
dieStatusWithCode( $status, $overrideCode, $moreExtraData=null)
Like dieStatus(), but always uses $overrideCode for the error code, unless the code comes from IApiMe...
isWriteMode()
Indicates whether this module requires write mode.
static getMinUploadChunkSize(Config $config)
getWarningsResult( $warnings)
Get Warnings Result.
getContextResult()
Get an upload result based on upload context.
getChunkResult( $warnings)
Get the result of a chunk upload.
getExamplesMessages()
Returns usage examples for this module.
selectUploadModule()
Select an upload module and set it to mUpload.
getStashResult( $warnings)
Get Stash Result, throws an exception if the file could not be stashed.
needsToken()
Returns the token type this module requires in order to execute.
performUpload( $warnings)
Perform the actual upload.
checkVerification(array $verification)
Performs file verification, dies on error.
mustBePosted()
Indicates whether this module must be called with a POST request Stable to override.
getApiWarnings()
Check warnings.
Assemble the segments of a chunked upload.
static canAddTagsAccompanyingChange(array $tags, User $user=null)
Is it OK to allow the user to apply all the specified tags at the same time as they edit/make the cha...
getUser()
Stable to override.
msg( $key,... $params)
Get a Message object with context set Parameters are the same as wfMessage()
exportSession()
Export the resolved user IP, HTTP headers, user ID, and session ID.
getContext()
Get the base IContextSource object.
static listParam(array $list, $type='text')
Definition Message.php:1141
static numParam( $num)
Definition Message.php:1064
Upload a file from the upload stash into the local file repo.
UploadBase and subclasses are the backend of MediaWiki's file uploads.
Implements uploading from chunks.
Implements regular file uploads.
Implements uploading from previously stored file.
static isValidKey( $key)
Implements uploading from a HTTP resource.
initialize( $name, $url)
Entry point for API upload.
static isAllowedHost( $url)
Checks whether the URL is for an allowed host The domains in the whitelist can include wildcard chara...
static isAllowedUrl( $url)
Checks whether the URL is not allowed.
static isEnabled()
Checks if the upload from URL feature is enabled.
static newFatalPermissionDeniedStatus( $permission)
Factory function for fatal permission-denied errors.
Definition User.php:4537
trait ApiWatchlistTrait
An ApiWatchlistTrait adds class properties and convenience methods for APIs that allow you to watch a...
const NS_FILE
Definition Defines.php:76
Interface for configuration instances.
Definition Config.php:30
get( $name)
Get a configuration variable such as "Sitename" or "UploadMaintenance.".
Stable for implementing.
if(PHP_SAPI !='cli-server') if(!isset( $_SERVER['SCRIPT_FILENAME'])) $file
Item class for a filearchive table row.
Definition router.php:42