MediaWiki  master
ApiUpload.php
Go to the documentation of this file.
1 <?php
25 
29 class ApiUpload extends ApiBase {
30 
32 
34  protected $mUpload = null;
35 
36  protected $mParams;
37 
39  private $jobQueueGroup;
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,
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'] ) {
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() ) {
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 {
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 
412  private function dieRecoverableError( $errors, $parameter = null ) {
413  // @phan-suppress-previous-line PhanTypeMissingReturn
414  $this->performStash( 'optional', $data );
415 
416  if ( $parameter ) {
417  $data['invalidparameter'] = $parameter;
418  }
419 
420  $sv = StatusValue::newGood();
421  foreach ( $errors as $error ) {
422  $msg = ApiMessage::create( $error );
423  $msg->setApiData( $msg->getApiData() + $data );
424  $sv->fatal( $msg );
425  }
426  $this->dieStatus( $sv );
427  }
428 
439  public function dieStatusWithCode( $status, $overrideCode, $moreExtraData = null ) {
440  // @phan-suppress-previous-line PhanTypeMissingReturn
441  $sv = StatusValue::newGood();
442  foreach ( $status->getErrors() as $error ) {
443  $msg = ApiMessage::create( $error, $overrideCode );
444  if ( $moreExtraData ) {
445  $msg->setApiData( $msg->getApiData() + $moreExtraData );
446  }
447  $sv->fatal( $msg );
448  }
449  $this->dieStatus( $sv );
450  }
451 
460  protected function selectUploadModule() {
461  $request = $this->getMain()->getRequest();
462 
463  // chunk or one and only one of the following parameters is needed
464  if ( !$this->mParams['chunk'] ) {
465  $this->requireOnlyOneParameter( $this->mParams,
466  'filekey', 'file', 'url' );
467  }
468 
469  // Status report for "upload to stash"/"upload from stash"
470  if ( $this->mParams['filekey'] && $this->mParams['checkstatus'] ) {
471  $progress = UploadBase::getSessionStatus( $this->getUser(), $this->mParams['filekey'] );
472  if ( !$progress ) {
473  $this->dieWithError( 'apierror-upload-missingresult', 'missingresult' );
474  } elseif ( !$progress['status']->isGood() ) {
475  $this->dieStatusWithCode( $progress['status'], 'stashfailed' );
476  }
477  if ( isset( $progress['status']->value['verification'] ) ) {
478  $this->checkVerification( $progress['status']->value['verification'] );
479  }
480  if ( isset( $progress['status']->value['warnings'] ) ) {
481  $warnings = $this->transformWarnings( $progress['status']->value['warnings'] );
482  if ( $warnings ) {
483  $progress['warnings'] = $warnings;
484  }
485  }
486  unset( $progress['status'] ); // remove Status object
487  $imageinfo = null;
488  if ( isset( $progress['imageinfo'] ) ) {
489  $imageinfo = $progress['imageinfo'];
490  unset( $progress['imageinfo'] );
491  }
492 
493  $this->getResult()->addValue( null, $this->getModuleName(), $progress );
494  // Add 'imageinfo' in a separate addValue() call. File metadata can be unreasonably large,
495  // so otherwise when it exceeded $wgAPIMaxResultSize, no result would be returned (T143993).
496  if ( $imageinfo ) {
497  $this->getResult()->addValue( $this->getModuleName(), 'imageinfo', $imageinfo );
498  }
499 
500  return false;
501  }
502 
503  // The following modules all require the filename parameter to be set
504  if ( $this->mParams['filename'] === null ) {
505  $this->dieWithError( [ 'apierror-missingparam', 'filename' ] );
506  }
507 
508  if ( $this->mParams['chunk'] ) {
509  // Chunk upload
510  $this->mUpload = new UploadFromChunks( $this->getUser() );
511  if ( isset( $this->mParams['filekey'] ) ) {
512  if ( $this->mParams['offset'] === 0 ) {
513  $this->dieWithError( 'apierror-upload-filekeynotallowed', 'filekeynotallowed' );
514  }
515 
516  // handle new chunk
517  $this->mUpload->continueChunks(
518  $this->mParams['filename'],
519  $this->mParams['filekey'],
520  $request->getUpload( 'chunk' )
521  );
522  } else {
523  if ( $this->mParams['offset'] !== 0 ) {
524  $this->dieWithError( 'apierror-upload-filekeyneeded', 'filekeyneeded' );
525  }
526 
527  // handle first chunk
528  $this->mUpload->initialize(
529  $this->mParams['filename'],
530  $request->getUpload( 'chunk' )
531  );
532  }
533  } elseif ( isset( $this->mParams['filekey'] ) ) {
534  // Upload stashed in a previous request
535  if ( !UploadFromStash::isValidKey( $this->mParams['filekey'] ) ) {
536  $this->dieWithError( 'apierror-invalid-file-key' );
537  }
538 
539  $this->mUpload = new UploadFromStash( $this->getUser() );
540  // This will not download the temp file in initialize() in async mode.
541  // We still have enough information to call checkWarnings() and such.
542  $this->mUpload->initialize(
543  $this->mParams['filekey'], $this->mParams['filename'], !$this->mParams['async']
544  );
545  } elseif ( isset( $this->mParams['file'] ) ) {
546  // Can't async upload directly from a POSTed file, we'd have to
547  // stash the file and then queue the publish job. The user should
548  // just submit the two API queries to perform those two steps.
549  if ( $this->mParams['async'] ) {
550  $this->dieWithError( 'apierror-cannot-async-upload-file' );
551  }
552 
553  $this->mUpload = new UploadFromFile();
554  $this->mUpload->initialize(
555  $this->mParams['filename'],
556  $request->getUpload( 'file' )
557  );
558  } elseif ( isset( $this->mParams['url'] ) ) {
559  // Make sure upload by URL is enabled:
560  if ( !UploadFromUrl::isEnabled() ) {
561  $this->dieWithError( 'copyuploaddisabled' );
562  }
563 
564  if ( !UploadFromUrl::isAllowedHost( $this->mParams['url'] ) ) {
565  $this->dieWithError( 'apierror-copyuploadbaddomain' );
566  }
567 
568  if ( !UploadFromUrl::isAllowedUrl( $this->mParams['url'] ) ) {
569  $this->dieWithError( 'apierror-copyuploadbadurl' );
570  }
571 
572  $this->mUpload = new UploadFromUrl;
573  $this->mUpload->initialize( $this->mParams['filename'],
574  $this->mParams['url'] );
575  }
576 
577  return true;
578  }
579 
585  protected function checkPermissions( $user ) {
586  // Check whether the user has the appropriate permissions to upload anyway
587  $permission = $this->mUpload->isAllowed( $user );
588 
589  if ( $permission !== true ) {
590  if ( !$user->isRegistered() ) {
591  $this->dieWithError( [ 'apierror-mustbeloggedin', $this->msg( 'action-upload' ) ] );
592  }
593 
594  $this->dieStatus( User::newFatalPermissionDeniedStatus( $permission ) );
595  }
596 
597  // Check blocks
598  if ( $user->isBlockedFromUpload() ) {
599  $this->dieBlocked( $user->getBlock() );
600  }
601 
602  // Global blocks
603  if ( $user->isBlockedGlobally() ) {
604  $this->dieBlocked( $user->getGlobalBlock() );
605  }
606  }
607 
611  protected function verifyUpload() {
612  if ( $this->mParams['chunk'] ) {
613  $maxSize = UploadBase::getMaxUploadSize();
614  if ( $this->mParams['filesize'] > $maxSize ) {
615  $this->dieWithError( 'file-too-large' );
616  }
617  if ( !$this->mUpload->getTitle() ) {
618  $this->dieWithError( 'illegal-filename' );
619  }
620  // file will be assembled after having uploaded the last chunk,
621  // so we can only validate the name at this point
622  $verification = $this->mUpload->validateName();
623  if ( $verification === true ) {
624  return;
625  }
626  } elseif ( $this->mParams['async'] && $this->mParams['filekey'] ) {
627  // file will be assembled in a background process, so we
628  // can only validate the name at this point
629  // file verification will happen in background process
630  $verification = $this->mUpload->validateName();
631  if ( $verification === true ) {
632  return;
633  }
634  } else {
635  wfDebug( __METHOD__ . " about to verify" );
636 
637  $verification = $this->mUpload->verifyUpload();
638  if ( $verification['status'] === UploadBase::OK ) {
639  return;
640  }
641  }
642 
643  $this->checkVerification( $verification );
644  }
645 
651  protected function checkVerification( array $verification ) {
652  // @phan-suppress-previous-line PhanTypeMissingReturn
653  switch ( $verification['status'] ) {
654  // Recoverable errors
656  $this->dieRecoverableError( [ 'filename-tooshort' ], 'filename' );
657  // dieRecoverableError prevents continuation
659  $this->dieRecoverableError(
661  'illegal-filename', null, [ 'filename' => $verification['filtered'] ]
662  ) ], 'filename'
663  );
664  // dieRecoverableError prevents continuation
666  $this->dieRecoverableError( [ 'filename-toolong' ], 'filename' );
667  // dieRecoverableError prevents continuation
669  $this->dieRecoverableError( [ 'filetype-missing' ], 'filename' );
670  // dieRecoverableError prevents continuation
672  $this->dieRecoverableError( [ 'windows-nonascii-filename' ], 'filename' );
673 
674  // Unrecoverable errors
676  $this->dieWithError( 'empty-file' );
677  // dieWithError prevents continuation
679  $this->dieWithError( 'file-too-large' );
680  // dieWithError prevents continuation
681 
683  $extradata = [
684  'filetype' => $verification['finalExt'],
685  'allowed' => array_values( array_unique( $this->getConfig()->get( 'FileExtensions' ) ) )
686  ];
687  $extensions = array_unique( $this->getConfig()->get( 'FileExtensions' ) );
688  $msg = [
689  'filetype-banned-type',
690  null, // filled in below
691  Message::listParam( $extensions, 'comma' ),
692  count( $extensions ),
693  null, // filled in below
694  ];
695  ApiResult::setIndexedTagName( $extradata['allowed'], 'ext' );
696 
697  if ( isset( $verification['blacklistedExt'] ) ) {
698  $msg[1] = Message::listParam( $verification['blacklistedExt'], 'comma' );
699  $msg[4] = count( $verification['blacklistedExt'] );
700  $extradata['blacklisted'] = array_values( $verification['blacklistedExt'] );
701  ApiResult::setIndexedTagName( $extradata['blacklisted'], 'ext' );
702  } else {
703  $msg[1] = $verification['finalExt'];
704  $msg[4] = 1;
705  }
706 
707  $this->dieWithError( $msg, 'filetype-banned', $extradata );
708  // dieWithError prevents continuation
709 
711  $msg = ApiMessage::create( $verification['details'], 'verification-error' );
712  if ( $verification['details'][0] instanceof MessageSpecifier ) {
713  $details = array_merge( [ $msg->getKey() ], $msg->getParams() );
714  } else {
715  $details = $verification['details'];
716  }
717  ApiResult::setIndexedTagName( $details, 'detail' );
718  $msg->setApiData( $msg->getApiData() + [ 'details' => $details ] );
719  // @phan-suppress-next-line PhanTypeMismatchArgument
720  $this->dieWithError( $msg );
721  // dieWithError prevents continuation
722 
724  $msg = $verification['error'] === '' ? 'hookaborted' : $verification['error'];
725  $this->dieWithError( $msg, 'hookaborted', [ 'details' => $verification['error'] ] );
726  // dieWithError prevents continuation
727  default:
728  $this->dieWithError( 'apierror-unknownerror-nocode', 'unknown-error',
729  [ 'details' => [ 'code' => $verification['status'] ] ] );
730  }
731  }
732 
740  protected function getApiWarnings() {
742  $this->mUpload->checkWarnings( $this->getUser() )
743  );
744 
745  return $this->transformWarnings( $warnings );
746  }
747 
748  protected function transformWarnings( $warnings ) {
749  if ( $warnings ) {
750  // Add indices
751  ApiResult::setIndexedTagName( $warnings, 'warning' );
752 
753  if ( isset( $warnings['duplicate'] ) ) {
754  $dupes = array_column( $warnings['duplicate'], 'fileName' );
755  ApiResult::setIndexedTagName( $dupes, 'duplicate' );
756  $warnings['duplicate'] = $dupes;
757  }
758 
759  if ( isset( $warnings['exists'] ) ) {
760  $warning = $warnings['exists'];
761  unset( $warnings['exists'] );
762  $localFile = $warning['normalizedFile'] ?? $warning['file'];
763  $warnings[$warning['warning']] = $localFile['fileName'];
764  }
765 
766  if ( isset( $warnings['no-change'] ) ) {
767  $file = $warnings['no-change'];
768  unset( $warnings['no-change'] );
769 
770  $warnings['nochange'] = [
771  'timestamp' => wfTimestamp( TS_ISO_8601, $file['timestamp'] )
772  ];
773  }
774 
775  if ( isset( $warnings['duplicate-version'] ) ) {
776  $dupes = [];
777  foreach ( $warnings['duplicate-version'] as $dupe ) {
778  $dupes[] = [
779  'timestamp' => wfTimestamp( TS_ISO_8601, $dupe['timestamp'] )
780  ];
781  }
782  unset( $warnings['duplicate-version'] );
783 
784  ApiResult::setIndexedTagName( $dupes, 'ver' );
785  $warnings['duplicateversions'] = $dupes;
786  }
787  }
788 
789  return $warnings;
790  }
791 
798  protected function handleStashException( $e ) {
799  switch ( get_class( $e ) ) {
800  case UploadStashFileNotFoundException::class:
801  $wrap = 'apierror-stashedfilenotfound';
802  break;
803  case UploadStashBadPathException::class:
804  $wrap = 'apierror-stashpathinvalid';
805  break;
806  case UploadStashFileException::class:
807  $wrap = 'apierror-stashfilestorage';
808  break;
809  case UploadStashZeroLengthFileException::class:
810  $wrap = 'apierror-stashzerolength';
811  break;
812  case UploadStashNotLoggedInException::class:
814  [ 'apierror-mustbeloggedin', $this->msg( 'action-upload' ) ], 'stashnotloggedin'
815  ) );
816  case UploadStashWrongOwnerException::class:
817  $wrap = 'apierror-stashwrongowner';
818  break;
819  case UploadStashNoSuchKeyException::class:
820  $wrap = 'apierror-stashnosuchfilekey';
821  break;
822  default:
823  $wrap = [ 'uploadstash-exception', get_class( $e ) ];
824  break;
825  }
826  return StatusValue::newFatal(
827  $this->getErrorFormatter()->getMessageFromException( $e, [ 'wrap' => $wrap ] )
828  );
829  }
830 
838  protected function performUpload( $warnings ) {
839  // Use comment as initial page text by default
840  if ( $this->mParams['text'] === null ) {
841  $this->mParams['text'] = $this->mParams['comment'];
842  }
843 
845  $file = $this->mUpload->getLocalFile();
846  $user = $this->getUser();
847  $title = $file->getTitle();
848 
849  // for preferences mode, we want to watch if 'watchdefault' is set,
850  // or if the *file* doesn't exist, and either 'watchuploads' or
851  // 'watchcreations' is set. But getWatchlistValue()'s automatic
852  // handling checks if the *title* exists or not, so we need to check
853  // all three preferences manually.
854  $watch = $this->getWatchlistValue(
855  $this->mParams['watchlist'], $title, $user, 'watchdefault'
856  );
857 
858  if ( !$watch && $this->mParams['watchlist'] == 'preferences' && !$file->exists() ) {
859  $watch = (
860  $this->getWatchlistValue( 'preferences', $title, $user, 'watchuploads' ) ||
861  $this->getWatchlistValue( 'preferences', $title, $user, 'watchcreations' )
862  );
863  }
864  $watchlistExpiry = $this->getExpiryFromParams( $this->mParams );
865 
866  // Deprecated parameters
867  if ( $this->mParams['watch'] ) {
868  $watch = true;
869  }
870 
871  if ( $this->mParams['tags'] ) {
872  $status = ChangeTags::canAddTagsAccompanyingChange( $this->mParams['tags'], $this->getAuthority() );
873  if ( !$status->isOK() ) {
874  $this->dieStatus( $status );
875  }
876  }
877 
878  // No errors, no warnings: do the upload
879  $result = [];
880  if ( $this->mParams['async'] ) {
881  $progress = UploadBase::getSessionStatus( $this->getUser(), $this->mParams['filekey'] );
882  if ( $progress && $progress['result'] === 'Poll' ) {
883  $this->dieWithError( 'apierror-upload-inprogress', 'publishfailed' );
884  }
886  $this->getUser(),
887  $this->mParams['filekey'],
888  [ 'result' => 'Poll', 'stage' => 'queued', 'status' => Status::newGood() ]
889  );
890  $this->jobQueueGroup->push( new PublishStashedFileJob(
891  Title::makeTitle( NS_FILE, $this->mParams['filename'] ),
892  [
893  'filename' => $this->mParams['filename'],
894  'filekey' => $this->mParams['filekey'],
895  'comment' => $this->mParams['comment'],
896  'tags' => $this->mParams['tags'],
897  'text' => $this->mParams['text'],
898  'watch' => $watch,
899  'watchlistexpiry' => $watchlistExpiry,
900  'session' => $this->getContext()->exportSession()
901  ]
902  ) );
903  $result['result'] = 'Poll';
904  $result['stage'] = 'queued';
905  } else {
907  $status = $this->mUpload->performUpload(
908  $this->mParams['comment'],
909  $this->mParams['text'],
910  $watch,
911  $this->getUser(),
912  $this->mParams['tags'],
913  $watchlistExpiry
914  );
915 
916  if ( !$status->isGood() ) {
917  $this->dieRecoverableError( $status->getErrors() );
918  }
919  $result['result'] = 'Success';
920  }
921 
922  $result['filename'] = $file->getName();
923  if ( $warnings && count( $warnings ) > 0 ) {
924  $result['warnings'] = $warnings;
925  }
926 
927  return $result;
928  }
929 
930  public function mustBePosted() {
931  return true;
932  }
933 
934  public function isWriteMode() {
935  return true;
936  }
937 
938  public function getAllowedParams() {
939  $params = [
940  'filename' => [
941  ApiBase::PARAM_TYPE => 'string',
942  ],
943  'comment' => [
944  ApiBase::PARAM_DFLT => ''
945  ],
946  'tags' => [
947  ApiBase::PARAM_TYPE => 'tags',
948  ApiBase::PARAM_ISMULTI => true,
949  ],
950  'text' => [
951  ApiBase::PARAM_TYPE => 'text',
952  ],
953  'watch' => [
954  ApiBase::PARAM_DFLT => false,
956  ],
957  ];
958 
959  // Params appear in the docs in the order they are defined,
960  // which is why this is here and not at the bottom.
961  $params += $this->getWatchlistParams( [
962  'watch',
963  'preferences',
964  'nochange',
965  ] );
966 
967  $params += [
968  'ignorewarnings' => false,
969  'file' => [
970  ApiBase::PARAM_TYPE => 'upload',
971  ],
972  'url' => null,
973  'filekey' => null,
974  'sessionkey' => [
976  ],
977  'stash' => false,
978 
979  'filesize' => [
980  ApiBase::PARAM_TYPE => 'integer',
981  ApiBase::PARAM_MIN => 0,
983  ],
984  'offset' => [
985  ApiBase::PARAM_TYPE => 'integer',
986  ApiBase::PARAM_MIN => 0,
987  ],
988  'chunk' => [
989  ApiBase::PARAM_TYPE => 'upload',
990  ],
991 
992  'async' => false,
993  'checkstatus' => false,
994  ];
995 
996  return $params;
997  }
998 
999  public function needsToken() {
1000  return 'csrf';
1001  }
1002 
1003  protected function getExamplesMessages() {
1004  return [
1005  'action=upload&filename=Wiki.png' .
1006  '&url=http%3A//upload.wikimedia.org/wikipedia/en/b/bc/Wiki.png&token=123ABC'
1007  => 'apihelp-upload-example-url',
1008  'action=upload&filename=Wiki.png&filekey=filekey&ignorewarnings=1&token=123ABC'
1009  => 'apihelp-upload-example-filekey',
1010  ];
1011  }
1012 
1013  public function getHelpUrls() {
1014  return 'https://www.mediawiki.org/wiki/Special:MyLanguage/API:Upload';
1015  }
1016 }
UploadBase
UploadBase and subclasses are the backend of MediaWiki's file uploads.
Definition: UploadBase.php:47
ApiUpload\getChunkResult
getChunkResult( $warnings)
Get the result of a chunk upload.
Definition: ApiUpload.php:236
ApiUpload\__construct
__construct(ApiMain $mainModule, $moduleName, JobQueueGroup $jobQueueGroup, WatchlistManager $watchlistManager, UserOptionsLookup $userOptionsLookup)
Definition: ApiUpload.php:48
ApiMain
This is the main API class, used for both external and internal processing.
Definition: ApiMain.php:49
ContextSource\getConfig
getConfig()
Definition: ContextSource.php:72
ApiUpload\selectUploadModule
selectUploadModule()
Select an upload module and set it to mUpload.
Definition: ApiUpload.php:460
Message\numParam
static numParam( $num)
Definition: Message.php:1085
ContextSource\getContext
getContext()
Get the base IContextSource object.
Definition: ContextSource.php:47
UploadBase\VERIFICATION_ERROR
const VERIFICATION_ERROR
Definition: UploadBase.php:110
getExpiryFromParams
getExpiryFromParams(array $params)
Get formatted expiry from the given parameters, or null if no expiry was provided.
Definition: ApiWatchlistTrait.php:164
ApiUpload\isWriteMode
isWriteMode()
Indicates whether this module requires write mode.
Definition: ApiUpload.php:934
StatusValue\newFatal
static newFatal( $message,... $parameters)
Factory function for fatal errors.
Definition: StatusValue.php:70
UploadBase\isThrottled
static isThrottled( $user)
Returns true if the user has surpassed the upload rate limit, false otherwise.
Definition: UploadBase.php:173
ApiUpload\getStashResult
getStashResult( $warnings)
Get Stash Result, throws an exception if the file could not be stashed.
Definition: ApiUpload.php:175
UploadBase\makeWarningsSerializable
static makeWarningsSerializable( $warnings)
Convert the warnings array returned by checkWarnings() to something that can be serialized.
Definition: UploadBase.php:748
UploadBase\FILE_TOO_LARGE
const FILE_TOO_LARGE
Definition: UploadBase.php:112
UploadBase\MIN_LENGTH_PARTNAME
const MIN_LENGTH_PARTNAME
Definition: UploadBase.php:105
User\newFatalPermissionDeniedStatus
static newFatalPermissionDeniedStatus( $permission)
Factory function for fatal permission-denied errors.
Definition: User.php:4213
UploadBase\getSessionStatus
static getSessionStatus(UserIdentity $user, $statusKey)
Get the current status of a chunked upload (used for polling)
Definition: UploadBase.php:2248
ApiUpload\verifyUpload
verifyUpload()
Performs file verification, dies on error.
Definition: ApiUpload.php:611
ApiBase\dieWithError
dieWithError( $msg, $code=null, $data=null, $httpCode=null)
Abort execution with an error.
Definition: ApiBase.php:1379
wfTimestamp
wfTimestamp( $outputtype=TS_UNIX, $ts=0)
Get a timestamp string in one of various formats.
Definition: GlobalFunctions.php:1664
UploadBase\isEnabled
static isEnabled()
Returns true if uploads are enabled.
Definition: UploadBase.php:143
ApiBase\PARAM_TYPE
const PARAM_TYPE
Definition: ApiBase.php:72
ApiBase\getResult
getResult()
Get the result object.
Definition: ApiBase.php:571
ApiUpload\getWarningsResult
getWarningsResult( $warnings)
Get Warnings Result.
Definition: ApiUpload.php:193
MessageSpecifier
Definition: MessageSpecifier.php:24
UploadBase\getMaxPhpUploadSize
static getMaxPhpUploadSize()
Get the PHP maximum uploaded file size, based on ini settings.
Definition: UploadBase.php:2227
$file
if(PHP_SAPI !='cli-server') if(!isset( $_SERVER['SCRIPT_FILENAME'])) $file
Item class for a filearchive table row.
Definition: router.php:42
ApiUpload\execute
execute()
Evaluates the parameters, performs the requested query, and sets up the result.
Definition: ApiUpload.php:65
ApiUpload\$mUpload
UploadBase UploadFromChunks $mUpload
Definition: ApiUpload.php:34
ApiUpload\getApiWarnings
getApiWarnings()
Check warnings.
Definition: ApiUpload.php:740
ApiUpload\getAllowedParams
getAllowedParams()
Returns an array of allowed parameters (parameter name) => (default value) or (parameter name) => (ar...
Definition: ApiUpload.php:938
UploadFromStash
Implements uploading from previously stored file.
Definition: UploadFromStash.php:33
UploadBase\HOOK_ABORTED
const HOOK_ABORTED
Definition: UploadBase.php:111
UploadBase\OK
const OK
Definition: UploadBase.php:103
ApiUpload\$mParams
$mParams
Definition: ApiUpload.php:36
ContextSource\getUser
getUser()
Definition: ContextSource.php:136
ApiUpload\getExamplesMessages
getExamplesMessages()
Returns usage examples for this module.
Definition: ApiUpload.php:1003
UploadBase\EMPTY_FILE
const EMPTY_FILE
Definition: UploadBase.php:104
Message\listParam
static listParam(array $list, $type='text')
Definition: Message.php:1195
ApiBase
This abstract class implements many basic API functions, and is the base of all API classes.
Definition: ApiBase.php:55
ApiMessage
Extension of Message implementing IApiMessage @newable.
Definition: ApiMessage.php:27
ApiBase\dieBlocked
dieBlocked(Block $block)
Throw an ApiUsageException, which will (if uncaught) call the main module's error handler and die wit...
Definition: ApiBase.php:1409
UploadFromUrl\initialize
initialize( $name, $url)
Entry point for API upload.
Definition: UploadFromUrl.php:138
PublishStashedFileJob
Upload a file from the upload stash into the local file repo.
Definition: PublishStashedFileJob.php:32
ApiBase\PARAM_DEPRECATED
const PARAM_DEPRECATED
Definition: ApiBase.php:77
Config
Interface for configuration instances.
Definition: Config.php:30
ApiBase\PARAM_MIN
const PARAM_MIN
Definition: ApiBase.php:75
ApiUpload\dieStatusWithCode
dieStatusWithCode( $status, $overrideCode, $moreExtraData=null)
Like dieStatus(), but always uses $overrideCode for the error code, unless the code comes from IApiMe...
Definition: ApiUpload.php:439
UploadFromUrl
Implements uploading from a HTTP resource.
Definition: UploadFromUrl.php:34
MediaWiki\Watchlist\WatchlistManager
WatchlistManager service.
Definition: WatchlistManager.php:52
ApiUpload\performStash
performStash( $failureMode, &$data=null)
Stash the file and add the file key, or error information if it fails, to the data.
Definition: ApiUpload.php:360
Config\get
get( $name)
Get a configuration variable such as "Sitename" or "UploadMaintenance.".
UploadFromUrl\isEnabled
static isEnabled()
Checks if the upload from URL feature is enabled.
Definition: UploadFromUrl.php:63
UploadBase\WINDOWS_NONASCII_FILENAME
const WINDOWS_NONASCII_FILENAME
Definition: UploadBase.php:113
ApiUpload\dieRecoverableError
dieRecoverableError( $errors, $parameter=null)
Throw an error that the user can recover from by providing a better value for $parameter.
Definition: ApiUpload.php:412
ApiUpload\getMinUploadChunkSize
static getMinUploadChunkSize(Config $config)
Definition: ApiUpload.php:210
ApiBase\PARAM_MAX
const PARAM_MAX
Definition: ApiBase.php:73
ApiBase\extractRequestParams
extractRequestParams( $options=[])
Using getAllowedParams(), this function makes an array of the values provided by the user,...
Definition: ApiBase.php:707
ApiUpload\getContextResult
getContextResult()
Get an upload result based on upload context.
Definition: ApiUpload.php:146
ApiUpload\handleStashException
handleStashException( $e)
Handles a stash exception, giving a useful error to the user.
Definition: ApiUpload.php:798
ApiUpload
Definition: ApiUpload.php:29
$title
$title
Definition: testCompression.php:38
Title\makeTitle
static makeTitle( $ns, $title, $fragment='', $interwiki='')
Create a new Title from a namespace index and a DB key.
Definition: Title.php:651
ApiMessage\create
static create( $msg, $code=null, array $data=null)
Create an IApiMessage for the message.
Definition: ApiMessage.php:43
wfDebug
wfDebug( $text, $dest='all', array $context=[])
Sends a line to the debug log if enabled or, optionally, to a comment in output.
Definition: GlobalFunctions.php:894
ApiUpload\needsToken
needsToken()
Returns the token type this module requires in order to execute.
Definition: ApiUpload.php:999
UploadBase\setSessionStatus
static setSessionStatus(UserIdentity $user, $statusKey, $value)
Set the current status of a chunked upload (used for polling)
Definition: UploadBase.php:2267
ContextSource\msg
msg( $key,... $params)
Get a Message object with context set Parameters are the same as wfMessage()
Definition: ContextSource.php:197
ChangeTags\canAddTagsAccompanyingChange
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...
Definition: ChangeTags.php:625
getWatchlistValue
getWatchlistValue(string $watchlist, Title $title, User $user, ?string $userOption=null)
Return true if we're to watch the page, false if not.
Definition: ApiWatchlistTrait.php:116
ApiResult\setIndexedTagName
static setIndexedTagName(array &$arr, $tag)
Set the tag name for numeric-keyed values in XML format.
Definition: ApiResult.php:603
UploadFromStash\isValidKey
static isValidKey( $key)
Definition: UploadFromStash.php:74
ApiUpload\$jobQueueGroup
JobQueueGroup $jobQueueGroup
Definition: ApiUpload.php:39
ApiBase\requireOnlyOneParameter
requireOnlyOneParameter( $params,... $required)
Die if none or more than one of a certain set of parameters is set and not false.
Definition: ApiBase.php:844
UploadFromUrl\isAllowedUrl
static isAllowedUrl( $url)
Checks whether the URL is not allowed.
Definition: UploadFromUrl.php:121
ContextSource\getAuthority
getAuthority()
Definition: ContextSource.php:144
StatusValue\newGood
static newGood( $value=null)
Factory function for good results.
Definition: StatusValue.php:82
AssembleUploadChunksJob
Assemble the segments of a chunked upload.
Definition: AssembleUploadChunksJob.php:30
ApiUpload\getHelpUrls
getHelpUrls()
Return links to more detailed help pages about the module.
Definition: ApiUpload.php:1013
ApiUpload\mustBePosted
mustBePosted()
Indicates whether this module must be called with a POST request.
Definition: ApiUpload.php:930
ApiUpload\performUpload
performUpload( $warnings)
Perform the actual upload.
Definition: ApiUpload.php:838
UploadBase\FILENAME_TOO_LONG
const FILENAME_TOO_LONG
Definition: UploadBase.php:114
UploadFromUrl\isAllowedHost
static isAllowedHost( $url)
Checks whether the URL is for an allowed host The domains in the allowlist can include wildcard chara...
Definition: UploadFromUrl.php:77
MediaWiki\User\UserOptionsLookup
Provides access to user options.
Definition: UserOptionsLookup.php:29
UploadBase\ILLEGAL_FILENAME
const ILLEGAL_FILENAME
Definition: UploadBase.php:106
UploadBase\getMaxUploadSize
static getMaxUploadSize( $forType=null)
Get MediaWiki's maximum uploaded file size for given type of upload, based on $wgMaxUploadSize.
Definition: UploadBase.php:2206
ApiWatchlistTrait
trait ApiWatchlistTrait
An ApiWatchlistTrait adds class properties and convenience methods for APIs that allow you to watch a...
Definition: ApiWatchlistTrait.php:21
$watchlistManager
WatchlistManager $watchlistManager
Definition: ApiWatchlistTrait.php:30
$userOptionsLookup
UserOptionsLookup $userOptionsLookup
Definition: ApiWatchlistTrait.php:33
wfShorthandToInteger
wfShorthandToInteger( $string='', $default=-1)
Converts shorthand byte notation to integer form.
Definition: GlobalFunctions.php:2418
getWatchlistParams
getWatchlistParams(array $watchOptions=[])
Get additional allow params specific to watchlisting.
Definition: ApiWatchlistTrait.php:59
ContextSource\exportSession
exportSession()
Export the resolved user IP, HTTP headers, user ID, and session ID.
Definition: ContextSource.php:209
ApiBase\PARAM_DFLT
const PARAM_DFLT
Definition: ApiBase.php:70
ApiUpload\checkPermissions
checkPermissions( $user)
Checks that the user has permissions to perform this upload.
Definition: ApiUpload.php:585
ApiBase\dieStatus
dieStatus(StatusValue $status)
Throw an ApiUsageException based on the Status object.
Definition: ApiBase.php:1442
ApiBase\getModuleName
getModuleName()
Get the name of the module being executed by this instance.
Definition: ApiBase.php:440
ApiBase\PARAM_ISMULTI
const PARAM_ISMULTI
Definition: ApiBase.php:71
ApiBase\getMain
getMain()
Get the main module.
Definition: ApiBase.php:456
NS_FILE
const NS_FILE
Definition: Defines.php:70
UploadFromChunks
Implements uploading from chunks.
Definition: UploadFromChunks.php:33
ApiUpload\transformWarnings
transformWarnings( $warnings)
Definition: ApiUpload.php:748
ApiBase\dieDebug
static dieDebug( $method, $message)
Internal code errors should be reported with this method.
Definition: ApiBase.php:1582
UploadFromFile
Implements regular file uploads.
Definition: UploadFromFile.php:30
ApiBase\getErrorFormatter
getErrorFormatter()
Definition: ApiBase.php:582
JobQueueGroup
Class to handle enqueueing of background jobs.
Definition: JobQueueGroup.php:32
UploadStashException
@newable
Definition: UploadStashException.php:27
ApiUpload\checkVerification
checkVerification(array $verification)
Performs file verification, dies on error.
Definition: ApiUpload.php:651
UploadBase\FILETYPE_MISSING
const FILETYPE_MISSING
Definition: UploadBase.php:108
UploadBase\FILETYPE_BADTYPE
const FILETYPE_BADTYPE
Definition: UploadBase.php:109