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