MediaWiki  master
ApiUpload.php
Go to the documentation of this file.
1 <?php
32 
36 class ApiUpload extends ApiBase {
37 
39 
41  protected $mUpload = null;
42 
43  protected $mParams;
44 
45  private JobQueueGroup $jobQueueGroup;
46 
54  public function __construct(
55  ApiMain $mainModule,
56  $moduleName,
57  JobQueueGroup $jobQueueGroup,
58  WatchlistManager $watchlistManager,
59  UserOptionsLookup $userOptionsLookup
60  ) {
61  parent::__construct( $mainModule, $moduleName );
62  $this->jobQueueGroup = $jobQueueGroup;
63 
64  // Variables needed in ApiWatchlistTrait trait
65  $this->watchlistExpiryEnabled = $this->getConfig()->get( MainConfigNames::WatchlistExpiry );
66  $this->watchlistMaxDuration =
67  $this->getConfig()->get( MainConfigNames::WatchlistExpiryMaxDuration );
68  $this->watchlistManager = $watchlistManager;
69  $this->userOptionsLookup = $userOptionsLookup;
70  }
71 
72  public function execute() {
73  // Check whether upload is enabled
74  if ( !UploadBase::isEnabled() ) {
75  $this->dieWithError( 'uploaddisabled' );
76  }
77 
78  $user = $this->getUser();
79 
80  // Parameter handling
81  $this->mParams = $this->extractRequestParams();
82  $request = $this->getMain()->getRequest();
83  // Check if async mode is actually supported (jobs done in cli mode)
84  $this->mParams['async'] = ( $this->mParams['async'] &&
85  $this->getConfig()->get( MainConfigNames::EnableAsyncUploads ) );
86  // Add the uploaded file to the params array
87  $this->mParams['file'] = $request->getFileName( 'file' );
88  $this->mParams['chunk'] = $request->getFileName( 'chunk' );
89 
90  // Copy the session key to the file key, for backward compatibility.
91  if ( !$this->mParams['filekey'] && $this->mParams['sessionkey'] ) {
92  $this->mParams['filekey'] = $this->mParams['sessionkey'];
93  }
94 
95  // Select an upload module
96  try {
97  if ( !$this->selectUploadModule() ) {
98  return; // not a true upload, but a status request or similar
99  } elseif ( !isset( $this->mUpload ) ) {
100  $this->dieDebug( __METHOD__, 'No upload module set' );
101  }
102  } catch ( UploadStashException $e ) { // XXX: don't spam exception log
103  $this->dieStatus( $this->handleStashException( $e ) );
104  }
105 
106  // First check permission to upload
107  $this->checkPermissions( $user );
108 
109  // Fetch the file (usually a no-op)
111  $status = $this->mUpload->fetchFile();
112  if ( !$status->isGood() ) {
113  $this->dieStatus( $status );
114  }
115 
116  // Check the uploaded file
117  $this->verifyUpload();
118 
119  // Check if the user has the rights to modify or overwrite the requested title
120  // (This check is irrelevant if stashing is already requested, since the errors
121  // can always be fixed by changing the title)
122  if ( !$this->mParams['stash'] ) {
123  $permErrors = $this->mUpload->verifyTitlePermissions( $user );
124  if ( $permErrors !== true ) {
125  $this->dieRecoverableError( $permErrors, 'filename' );
126  }
127  }
128 
129  // Get the result based on the current upload context:
130  try {
131  $result = $this->getContextResult();
132  } catch ( UploadStashException $e ) { // XXX: don't spam exception log
133  $this->dieStatus( $this->handleStashException( $e ) );
134  }
135  $this->getResult()->addValue( null, $this->getModuleName(), $result );
136 
137  // Add 'imageinfo' in a separate addValue() call. File metadata can be unreasonably large,
138  // so otherwise when it exceeded $wgAPIMaxResultSize, no result would be returned (T143993).
139  // @phan-suppress-next-line PhanTypeArraySuspiciousNullable False positive
140  if ( $result['result'] === 'Success' ) {
141  $imageinfo = $this->mUpload->getImageInfo( $this->getResult() );
142  $this->getResult()->addValue( $this->getModuleName(), 'imageinfo', $imageinfo );
143  }
144 
145  // Cleanup any temporary mess
146  $this->mUpload->cleanupTempFile();
147  }
148 
153  private function getContextResult() {
154  $warnings = $this->getApiWarnings();
155  if ( $warnings && !$this->mParams['ignorewarnings'] ) {
156  // Get warnings formatted in result array format
157  return $this->getWarningsResult( $warnings );
158  } elseif ( $this->mParams['chunk'] ) {
159  // Add chunk, and get result
160  return $this->getChunkResult( $warnings );
161  } elseif ( $this->mParams['stash'] ) {
162  // Stash the file and get stash result
163  return $this->getStashResult( $warnings );
164  }
165 
166  // Check throttle after we've handled warnings
167  if ( UploadBase::isThrottled( $this->getUser() ) ) {
168  $this->dieWithError( 'apierror-ratelimited' );
169  }
170 
171  // This is the most common case -- a normal upload with no warnings
172  // performUpload will return a formatted properly for the API with status
173  return $this->performUpload( $warnings );
174  }
175 
181  private function getStashResult( $warnings ) {
182  $result = [];
183  $result['result'] = 'Success';
184  if ( $warnings && count( $warnings ) > 0 ) {
185  $result['warnings'] = $warnings;
186  }
187  // Some uploads can request they be stashed, so as not to publish them immediately.
188  // In this case, a failure to stash ought to be fatal
189  $this->performStash( 'critical', $result );
190 
191  return $result;
192  }
193 
199  private function getWarningsResult( $warnings ) {
200  $result = [];
201  $result['result'] = 'Warning';
202  $result['warnings'] = $warnings;
203  // in case the warnings can be fixed with some further user action, let's stash this upload
204  // and return a key they can use to restart it
205  $this->performStash( 'optional', $result );
206 
207  return $result;
208  }
209 
216  public static function getMinUploadChunkSize( Config $config ) {
217  $configured = $config->get( MainConfigNames::MinUploadChunkSize );
218 
219  // Leave some room for other POST parameters
220  $postMax = (
222  ini_get( 'post_max_size' ),
223  PHP_INT_MAX
224  ) ?: PHP_INT_MAX
225  ) - 1024;
226 
227  // Ensure the minimum chunk size is less than PHP upload limits
228  // or the maximum upload size.
229  return min(
230  $configured,
233  $postMax
234  );
235  }
236 
242  private function getChunkResult( $warnings ) {
243  $result = [];
244 
245  if ( $warnings && count( $warnings ) > 0 ) {
246  $result['warnings'] = $warnings;
247  }
248 
249  $request = $this->getMain()->getRequest();
250  $chunkPath = $request->getFileTempname( 'chunk' );
251  $chunkSize = $request->getUpload( 'chunk' )->getSize();
252  $totalSoFar = $this->mParams['offset'] + $chunkSize;
253  $minChunkSize = self::getMinUploadChunkSize( $this->getConfig() );
254 
255  // Double check sizing
256  if ( $totalSoFar > $this->mParams['filesize'] ) {
257  $this->dieWithError( 'apierror-invalid-chunk' );
258  }
259 
260  // Enforce minimum chunk size
261  if ( $totalSoFar != $this->mParams['filesize'] && $chunkSize < $minChunkSize ) {
262  $this->dieWithError( [ 'apierror-chunk-too-small', Message::numParam( $minChunkSize ) ] );
263  }
264 
265  if ( $this->mParams['offset'] == 0 ) {
266  $filekey = $this->performStash( 'critical' );
267  } else {
268  $filekey = $this->mParams['filekey'];
269 
270  // Don't allow further uploads to an already-completed session
271  $progress = UploadBase::getSessionStatus( $this->getUser(), $filekey );
272  if ( !$progress ) {
273  // Probably can't get here, but check anyway just in case
274  $this->dieWithError( 'apierror-stashfailed-nosession', 'stashfailed' );
275  } elseif ( $progress['result'] !== 'Continue' || $progress['stage'] !== 'uploading' ) {
276  $this->dieWithError( 'apierror-stashfailed-complete', 'stashfailed' );
277  }
278 
279  $status = $this->mUpload->addChunk(
280  $chunkPath, $chunkSize, $this->mParams['offset'] );
281  if ( !$status->isGood() ) {
282  $extradata = [
283  'offset' => $this->mUpload->getOffset(),
284  ];
285 
286  $this->dieStatusWithCode( $status, 'stashfailed', $extradata );
287  }
288  }
289 
290  // Check we added the last chunk:
291  if ( $totalSoFar == $this->mParams['filesize'] ) {
292  if ( $this->mParams['async'] ) {
294  $this->getUser(),
295  $filekey,
296  [ 'result' => 'Poll',
297  'stage' => 'queued', 'status' => Status::newGood() ]
298  );
299  $this->jobQueueGroup->push( new AssembleUploadChunksJob(
300  Title::makeTitle( NS_FILE, $filekey ),
301  [
302  'filename' => $this->mParams['filename'],
303  'filekey' => $filekey,
304  'session' => $this->getContext()->exportSession()
305  ]
306  ) );
307  $result['result'] = 'Poll';
308  $result['stage'] = 'queued';
309  } else {
310  $status = $this->mUpload->concatenateChunks();
311  if ( !$status->isGood() ) {
313  $this->getUser(),
314  $filekey,
315  [ 'result' => 'Failure', 'stage' => 'assembling', 'status' => $status ]
316  );
317  $this->dieStatusWithCode( $status, 'stashfailed' );
318  }
319 
320  // We can only get warnings like 'duplicate' after concatenating the chunks
321  $warnings = $this->getApiWarnings();
322  if ( $warnings ) {
323  $result['warnings'] = $warnings;
324  }
325 
326  // The fully concatenated file has a new filekey. So remove
327  // the old filekey and fetch the new one.
328  UploadBase::setSessionStatus( $this->getUser(), $filekey, false );
329  $this->mUpload->stash->removeFile( $filekey );
330  $filekey = $this->mUpload->getStashFile()->getFileKey();
331 
332  $result['result'] = 'Success';
333  }
334  } else {
336  $this->getUser(),
337  $filekey,
338  [
339  'result' => 'Continue',
340  'stage' => 'uploading',
341  'offset' => $totalSoFar,
342  'status' => Status::newGood(),
343  ]
344  );
345  $result['result'] = 'Continue';
346  $result['offset'] = $totalSoFar;
347  }
348 
349  $result['filekey'] = $filekey;
350 
351  return $result;
352  }
353 
366  private function performStash( $failureMode, &$data = null ) {
367  $isPartial = (bool)$this->mParams['chunk'];
368  try {
369  $status = $this->mUpload->tryStashFile( $this->getUser(), $isPartial );
370 
371  if ( $status->isGood() && !$status->getValue() ) {
372  // Not actually a 'good' status...
373  $status->fatal( new ApiMessage( 'apierror-stashinvalidfile', 'stashfailed' ) );
374  }
375  } catch ( Exception $e ) {
376  $debugMessage = 'Stashing temporary file failed: ' . get_class( $e ) . ' ' . $e->getMessage();
377  wfDebug( __METHOD__ . ' ' . $debugMessage );
378  $status = Status::newFatal( $this->getErrorFormatter()->getMessageFromException(
379  $e, [ 'wrap' => new ApiMessage( 'apierror-stashexception', 'stashfailed' ) ]
380  ) );
381  }
382 
383  if ( $status->isGood() ) {
384  $stashFile = $status->getValue();
385  $data['filekey'] = $stashFile->getFileKey();
386  // Backwards compatibility
387  $data['sessionkey'] = $data['filekey'];
388  return $data['filekey'];
389  }
390 
391  if ( $status->getMessage()->getKey() === 'uploadstash-exception' ) {
392  // The exceptions thrown by upload stash code and pretty silly and UploadBase returns poor
393  // Statuses for it. Just extract the exception details and parse them ourselves.
394  [ $exceptionType, $message ] = $status->getMessage()->getParams();
395  $debugMessage = 'Stashing temporary file failed: ' . $exceptionType . ' ' . $message;
396  wfDebug( __METHOD__ . ' ' . $debugMessage );
397  }
398 
399  // Bad status
400  if ( $failureMode !== 'optional' ) {
401  $this->dieStatus( $status );
402  } else {
403  $data['stasherrors'] = $this->getErrorFormatter()->arrayFromStatus( $status );
404  return null;
405  }
406  }
407 
418  private function dieRecoverableError( $errors, $parameter = null ) {
419  $this->performStash( 'optional', $data );
420 
421  if ( $parameter ) {
422  $data['invalidparameter'] = $parameter;
423  }
424 
425  $sv = StatusValue::newGood();
426  foreach ( $errors as $error ) {
427  $msg = ApiMessage::create( $error );
428  $msg->setApiData( $msg->getApiData() + $data );
429  $sv->fatal( $msg );
430  }
431  $this->dieStatus( $sv );
432  }
433 
444  public function dieStatusWithCode( $status, $overrideCode, $moreExtraData = null ) {
445  $sv = StatusValue::newGood();
446  foreach ( $status->getErrors() as $error ) {
447  $msg = ApiMessage::create( $error, $overrideCode );
448  if ( $moreExtraData ) {
449  $msg->setApiData( $msg->getApiData() + $moreExtraData );
450  }
451  $sv->fatal( $msg );
452  }
453  $this->dieStatus( $sv );
454  }
455 
464  protected function selectUploadModule() {
465  $request = $this->getMain()->getRequest();
466 
467  // chunk or one and only one of the following parameters is needed
468  if ( !$this->mParams['chunk'] ) {
469  $this->requireOnlyOneParameter( $this->mParams,
470  'filekey', 'file', 'url' );
471  }
472 
473  // Status report for "upload to stash"/"upload from stash"
474  if ( $this->mParams['filekey'] && $this->mParams['checkstatus'] ) {
475  $progress = UploadBase::getSessionStatus( $this->getUser(), $this->mParams['filekey'] );
476  if ( !$progress ) {
477  $this->dieWithError( 'apierror-upload-missingresult', 'missingresult' );
478  } elseif ( !$progress['status']->isGood() ) {
479  $this->dieStatusWithCode( $progress['status'], 'stashfailed' );
480  }
481  if ( isset( $progress['status']->value['verification'] ) ) {
482  $this->checkVerification( $progress['status']->value['verification'] );
483  }
484  if ( isset( $progress['status']->value['warnings'] ) ) {
485  $warnings = $this->transformWarnings( $progress['status']->value['warnings'] );
486  if ( $warnings ) {
487  $progress['warnings'] = $warnings;
488  }
489  }
490  unset( $progress['status'] ); // remove Status object
491  $imageinfo = null;
492  if ( isset( $progress['imageinfo'] ) ) {
493  $imageinfo = $progress['imageinfo'];
494  unset( $progress['imageinfo'] );
495  }
496 
497  $this->getResult()->addValue( null, $this->getModuleName(), $progress );
498  // Add 'imageinfo' in a separate addValue() call. File metadata can be unreasonably large,
499  // so otherwise when it exceeded $wgAPIMaxResultSize, no result would be returned (T143993).
500  if ( $imageinfo ) {
501  $this->getResult()->addValue( $this->getModuleName(), 'imageinfo', $imageinfo );
502  }
503 
504  return false;
505  }
506 
507  // The following modules all require the filename parameter to be set
508  if ( $this->mParams['filename'] === null ) {
509  $this->dieWithError( [ 'apierror-missingparam', 'filename' ] );
510  }
511 
512  if ( $this->mParams['chunk'] ) {
513  // Chunk upload
514  $this->mUpload = new UploadFromChunks( $this->getUser() );
515  if ( isset( $this->mParams['filekey'] ) ) {
516  if ( $this->mParams['offset'] === 0 ) {
517  $this->dieWithError( 'apierror-upload-filekeynotallowed', 'filekeynotallowed' );
518  }
519 
520  // handle new chunk
521  $this->mUpload->continueChunks(
522  $this->mParams['filename'],
523  $this->mParams['filekey'],
524  $request->getUpload( 'chunk' )
525  );
526  } else {
527  if ( $this->mParams['offset'] !== 0 ) {
528  $this->dieWithError( 'apierror-upload-filekeyneeded', 'filekeyneeded' );
529  }
530 
531  // handle first chunk
532  $this->mUpload->initialize(
533  $this->mParams['filename'],
534  $request->getUpload( 'chunk' )
535  );
536  }
537  } elseif ( isset( $this->mParams['filekey'] ) ) {
538  // Upload stashed in a previous request
539  if ( !UploadFromStash::isValidKey( $this->mParams['filekey'] ) ) {
540  $this->dieWithError( 'apierror-invalid-file-key' );
541  }
542 
543  $this->mUpload = new UploadFromStash( $this->getUser() );
544  // This will not download the temp file in initialize() in async mode.
545  // We still have enough information to call checkWarnings() and such.
546  $this->mUpload->initialize(
547  $this->mParams['filekey'], $this->mParams['filename'], !$this->mParams['async']
548  );
549  } elseif ( isset( $this->mParams['file'] ) ) {
550  // Can't async upload directly from a POSTed file, we'd have to
551  // stash the file and then queue the publish job. The user should
552  // just submit the two API queries to perform those two steps.
553  if ( $this->mParams['async'] ) {
554  $this->dieWithError( 'apierror-cannot-async-upload-file' );
555  }
556 
557  $this->mUpload = new UploadFromFile();
558  $this->mUpload->initialize(
559  $this->mParams['filename'],
560  $request->getUpload( 'file' )
561  );
562  } elseif ( isset( $this->mParams['url'] ) ) {
563  // Make sure upload by URL is enabled:
564  if ( !UploadFromUrl::isEnabled() ) {
565  $this->dieWithError( 'copyuploaddisabled' );
566  }
567 
568  if ( !UploadFromUrl::isAllowedHost( $this->mParams['url'] ) ) {
569  $this->dieWithError( 'apierror-copyuploadbaddomain' );
570  }
571 
572  if ( !UploadFromUrl::isAllowedUrl( $this->mParams['url'] ) ) {
573  $this->dieWithError( 'apierror-copyuploadbadurl' );
574  }
575 
576  $this->mUpload = new UploadFromUrl;
577  $this->mUpload->initialize( $this->mParams['filename'],
578  $this->mParams['url'] );
579  }
580 
581  return true;
582  }
583 
589  protected function checkPermissions( $user ) {
590  // Check whether the user has the appropriate permissions to upload anyway
591  $permission = $this->mUpload->isAllowed( $user );
592 
593  if ( $permission !== true ) {
594  if ( !$user->isNamed() ) {
595  $this->dieWithError( [ 'apierror-mustbeloggedin', $this->msg( 'action-upload' ) ] );
596  }
597 
598  $this->dieStatus( User::newFatalPermissionDeniedStatus( $permission ) );
599  }
600 
601  // Check blocks
602  if ( $user->isBlockedFromUpload() ) {
603  // @phan-suppress-next-line PhanTypeMismatchArgumentNullable Block is checked and not null
604  $this->dieBlocked( $user->getBlock() );
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  switch ( $verification['status'] ) {
653  // Recoverable errors
655  $this->dieRecoverableError( [ 'filename-tooshort' ], 'filename' );
656  // dieRecoverableError prevents continuation
658  $this->dieRecoverableError(
660  'illegal-filename', null, [ 'filename' => $verification['filtered'] ]
661  ) ], 'filename'
662  );
663  // dieRecoverableError prevents continuation
665  $this->dieRecoverableError( [ 'filename-toolong' ], 'filename' );
666  // dieRecoverableError prevents continuation
668  $this->dieRecoverableError( [ 'filetype-missing' ], 'filename' );
669  // dieRecoverableError prevents continuation
671  $this->dieRecoverableError( [ 'windows-nonascii-filename' ], 'filename' );
672 
673  // Unrecoverable errors
675  $this->dieWithError( 'empty-file' );
676  // dieWithError prevents continuation
678  $this->dieWithError( 'file-too-large' );
679  // dieWithError prevents continuation
680 
682  $extradata = [
683  'filetype' => $verification['finalExt'],
684  'allowed' => array_values( array_unique(
685  $this->getConfig()->get( MainConfigNames::FileExtensions ) ) )
686  ];
687  $extensions =
688  array_unique( $this->getConfig()->get( MainConfigNames::FileExtensions ) );
689  $msg = [
690  'filetype-banned-type',
691  null, // filled in below
692  Message::listParam( $extensions, 'comma' ),
693  count( $extensions ),
694  null, // filled in below
695  ];
696  ApiResult::setIndexedTagName( $extradata['allowed'], 'ext' );
697 
698  if ( isset( $verification['blacklistedExt'] ) ) {
699  $msg[1] = Message::listParam( $verification['blacklistedExt'], 'comma' );
700  $msg[4] = count( $verification['blacklistedExt'] );
701  $extradata['blacklisted'] = array_values( $verification['blacklistedExt'] );
702  ApiResult::setIndexedTagName( $extradata['blacklisted'], 'ext' );
703  } else {
704  $msg[1] = $verification['finalExt'];
705  $msg[4] = 1;
706  }
707 
708  $this->dieWithError( $msg, 'filetype-banned', $extradata );
709  // dieWithError prevents continuation
710 
712  $msg = ApiMessage::create( $verification['details'], 'verification-error' );
713  if ( $verification['details'][0] instanceof MessageSpecifier ) {
714  $details = array_merge( [ $msg->getKey() ], $msg->getParams() );
715  } else {
716  $details = $verification['details'];
717  }
718  ApiResult::setIndexedTagName( $details, 'detail' );
719  $msg->setApiData( $msg->getApiData() + [ 'details' => $details ] );
720  // @phan-suppress-next-line PhanTypeMismatchArgument
721  $this->dieWithError( $msg );
722  // dieWithError prevents continuation
723 
725  $msg = $verification['error'] === '' ? 'hookaborted' : $verification['error'];
726  $this->dieWithError( $msg, 'hookaborted', [ 'details' => $verification['error'] ] );
727  // dieWithError prevents continuation
728  default:
729  $this->dieWithError( 'apierror-unknownerror-nocode', 'unknown-error',
730  [ 'details' => [ 'code' => $verification['status'] ] ] );
731  }
732  }
733 
741  protected function getApiWarnings() {
743  $this->mUpload->checkWarnings( $this->getUser() )
744  );
745 
746  return $this->transformWarnings( $warnings );
747  }
748 
749  protected function transformWarnings( $warnings ) {
750  if ( $warnings ) {
751  // Add indices
752  ApiResult::setIndexedTagName( $warnings, 'warning' );
753 
754  if ( isset( $warnings['duplicate'] ) ) {
755  $dupes = array_column( $warnings['duplicate'], 'fileName' );
756  ApiResult::setIndexedTagName( $dupes, 'duplicate' );
757  $warnings['duplicate'] = $dupes;
758  }
759 
760  if ( isset( $warnings['exists'] ) ) {
761  $warning = $warnings['exists'];
762  unset( $warnings['exists'] );
763  $localFile = $warning['normalizedFile'] ?? $warning['file'];
764  $warnings[$warning['warning']] = $localFile['fileName'];
765  }
766 
767  if ( isset( $warnings['no-change'] ) ) {
768  $file = $warnings['no-change'];
769  unset( $warnings['no-change'] );
770 
771  $warnings['nochange'] = [
772  'timestamp' => wfTimestamp( TS_ISO_8601, $file['timestamp'] )
773  ];
774  }
775 
776  if ( isset( $warnings['duplicate-version'] ) ) {
777  $dupes = [];
778  foreach ( $warnings['duplicate-version'] as $dupe ) {
779  $dupes[] = [
780  'timestamp' => wfTimestamp( TS_ISO_8601, $dupe['timestamp'] )
781  ];
782  }
783  unset( $warnings['duplicate-version'] );
784 
785  ApiResult::setIndexedTagName( $dupes, 'ver' );
786  $warnings['duplicateversions'] = $dupes;
787  }
788  }
789 
790  return $warnings;
791  }
792 
799  protected function handleStashException( $e ) {
800  switch ( get_class( $e ) ) {
801  case UploadStashFileNotFoundException::class:
802  $wrap = 'apierror-stashedfilenotfound';
803  break;
804  case UploadStashBadPathException::class:
805  $wrap = 'apierror-stashpathinvalid';
806  break;
807  case UploadStashFileException::class:
808  $wrap = 'apierror-stashfilestorage';
809  break;
810  case UploadStashZeroLengthFileException::class:
811  $wrap = 'apierror-stashzerolength';
812  break;
813  case UploadStashNotLoggedInException::class:
815  [ 'apierror-mustbeloggedin', $this->msg( 'action-upload' ) ], 'stashnotloggedin'
816  ) );
817  case UploadStashWrongOwnerException::class:
818  $wrap = 'apierror-stashwrongowner';
819  break;
820  case UploadStashNoSuchKeyException::class:
821  $wrap = 'apierror-stashnosuchfilekey';
822  break;
823  default:
824  $wrap = [ 'uploadstash-exception', get_class( $e ) ];
825  break;
826  }
827  return StatusValue::newFatal(
828  $this->getErrorFormatter()->getMessageFromException( $e, [ 'wrap' => $wrap ] )
829  );
830  }
831 
839  protected function performUpload( $warnings ) {
840  // Use comment as initial page text by default
841  $this->mParams['text'] ??= $this->mParams['comment'];
842 
844  $file = $this->mUpload->getLocalFile();
845  $user = $this->getUser();
846  $title = $file->getTitle();
847 
848  // for preferences mode, we want to watch if 'watchdefault' is set,
849  // or if the *file* doesn't exist, and either 'watchuploads' or
850  // 'watchcreations' is set. But getWatchlistValue()'s automatic
851  // handling checks if the *title* exists or not, so we need to check
852  // all three preferences manually.
853  $watch = $this->getWatchlistValue(
854  $this->mParams['watchlist'], $title, $user, 'watchdefault'
855  );
856 
857  if ( !$watch && $this->mParams['watchlist'] == 'preferences' && !$file->exists() ) {
858  $watch = (
859  $this->getWatchlistValue( 'preferences', $title, $user, 'watchuploads' ) ||
860  $this->getWatchlistValue( 'preferences', $title, $user, 'watchcreations' )
861  );
862  }
863  $watchlistExpiry = $this->getExpiryFromParams( $this->mParams );
864 
865  // Deprecated parameters
866  if ( $this->mParams['watch'] ) {
867  $watch = true;
868  }
869 
870  if ( $this->mParams['tags'] ) {
871  $status = ChangeTags::canAddTagsAccompanyingChange( $this->mParams['tags'], $this->getAuthority() );
872  if ( !$status->isOK() ) {
873  $this->dieStatus( $status );
874  }
875  }
876 
877  // No errors, no warnings: do the upload
878  $result = [];
879  if ( $this->mParams['async'] ) {
880  $progress = UploadBase::getSessionStatus( $this->getUser(), $this->mParams['filekey'] );
881  if ( $progress && $progress['result'] === 'Poll' ) {
882  $this->dieWithError( 'apierror-upload-inprogress', 'publishfailed' );
883  }
885  $this->getUser(),
886  $this->mParams['filekey'],
887  [ 'result' => 'Poll', 'stage' => 'queued', 'status' => Status::newGood() ]
888  );
889  $this->jobQueueGroup->push( new PublishStashedFileJob(
890  Title::makeTitle( NS_FILE, $this->mParams['filename'] ),
891  [
892  'filename' => $this->mParams['filename'],
893  'filekey' => $this->mParams['filekey'],
894  'comment' => $this->mParams['comment'],
895  'tags' => $this->mParams['tags'] ?? [],
896  'text' => $this->mParams['text'],
897  'watch' => $watch,
898  'watchlistexpiry' => $watchlistExpiry,
899  'session' => $this->getContext()->exportSession()
900  ]
901  ) );
902  $result['result'] = 'Poll';
903  $result['stage'] = 'queued';
904  } else {
906  $status = $this->mUpload->performUpload(
907  $this->mParams['comment'],
908  $this->mParams['text'],
909  $watch,
910  $this->getUser(),
911  $this->mParams['tags'] ?? [],
912  $watchlistExpiry
913  );
914 
915  if ( !$status->isGood() ) {
916  $this->dieRecoverableError( $status->getErrors() );
917  }
918  $result['result'] = 'Success';
919  }
920 
921  $result['filename'] = $file->getName();
922  if ( $warnings && count( $warnings ) > 0 ) {
923  $result['warnings'] = $warnings;
924  }
925 
926  return $result;
927  }
928 
929  public function mustBePosted() {
930  return true;
931  }
932 
933  public function isWriteMode() {
934  return true;
935  }
936 
937  public function getAllowedParams() {
938  $params = [
939  'filename' => [
940  ParamValidator::PARAM_TYPE => 'string',
941  ],
942  'comment' => [
943  ParamValidator::PARAM_DEFAULT => ''
944  ],
945  'tags' => [
946  ParamValidator::PARAM_TYPE => 'tags',
947  ParamValidator::PARAM_ISMULTI => true,
948  ],
949  'text' => [
950  ParamValidator::PARAM_TYPE => 'text',
951  ],
952  'watch' => [
953  ParamValidator::PARAM_DEFAULT => false,
954  ParamValidator::PARAM_DEPRECATED => true,
955  ],
956  ];
957 
958  // Params appear in the docs in the order they are defined,
959  // which is why this is here and not at the bottom.
960  $params += $this->getWatchlistParams( [
961  'watch',
962  'preferences',
963  'nochange',
964  ] );
965 
966  $params += [
967  'ignorewarnings' => false,
968  'file' => [
969  ParamValidator::PARAM_TYPE => 'upload',
970  ],
971  'url' => null,
972  'filekey' => null,
973  'sessionkey' => [
974  ParamValidator::PARAM_DEPRECATED => true,
975  ],
976  'stash' => false,
977 
978  'filesize' => [
979  ParamValidator::PARAM_TYPE => 'integer',
980  IntegerDef::PARAM_MIN => 0,
981  IntegerDef::PARAM_MAX => UploadBase::getMaxUploadSize(),
982  ],
983  'offset' => [
984  ParamValidator::PARAM_TYPE => 'integer',
985  IntegerDef::PARAM_MIN => 0,
986  ],
987  'chunk' => [
988  ParamValidator::PARAM_TYPE => 'upload',
989  ],
990 
991  'async' => false,
992  'checkstatus' => false,
993  ];
994 
995  return $params;
996  }
997 
998  public function needsToken() {
999  return 'csrf';
1000  }
1001 
1002  protected function getExamplesMessages() {
1003  return [
1004  'action=upload&filename=Wiki.png' .
1005  '&url=http%3A//upload.wikimedia.org/wikipedia/en/b/bc/Wiki.png&token=123ABC'
1006  => 'apihelp-upload-example-url',
1007  'action=upload&filename=Wiki.png&filekey=filekey&ignorewarnings=1&token=123ABC'
1008  => 'apihelp-upload-example-filekey',
1009  ];
1010  }
1011 
1012  public function getHelpUrls() {
1013  return 'https://www.mediawiki.org/wiki/Special:MyLanguage/API:Upload';
1014  }
1015 }
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.
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:62
dieWithError( $msg, $code=null, $data=null, $httpCode=0)
Abort execution with an error.
Definition: ApiBase.php:1515
static dieDebug( $method, $message)
Internal code errors should be reported with this method.
Definition: ApiBase.php:1759
getMain()
Get the main module.
Definition: ApiBase.php:546
getErrorFormatter()
Definition: ApiBase.php:678
requireOnlyOneParameter( $params,... $required)
Die if 0 or more than one of a certain set of parameters is set and not false.
Definition: ApiBase.php:946
getResult()
Get the result object.
Definition: ApiBase.php:667
extractRequestParams( $options=[])
Using getAllowedParams(), this function makes an array of the values provided by the user,...
Definition: ApiBase.php:807
getModuleName()
Get the name of the module being executed by this instance.
Definition: ApiBase.php:528
dieStatus(StatusValue $status)
Throw an ApiUsageException based on the Status object.
Definition: ApiBase.php:1570
dieBlocked(Block $block)
Throw an ApiUsageException, which will (if uncaught) call the main module's error handler and die wit...
Definition: ApiBase.php:1544
This is the main API class, used for both external and internal processing.
Definition: ApiMain.php:64
Extension of Message implementing IApiMessage.
Definition: ApiMessage.php:29
static create( $msg, $code=null, array $data=null)
Create an IApiMessage for the message.
Definition: ApiMessage.php:45
static setIndexedTagName(array &$arr, $tag)
Set the tag name for numeric-keyed values in XML format.
Definition: ApiResult.php:604
execute()
Evaluates the parameters, performs the requested query, and sets up the result.
Definition: ApiUpload.php:72
checkPermissions( $user)
Checks that the user has permissions to perform this upload.
Definition: ApiUpload.php:589
verifyUpload()
Performs file verification, dies on error.
Definition: ApiUpload.php:611
UploadBase UploadFromChunks $mUpload
Definition: ApiUpload.php:41
getAllowedParams()
Returns an array of allowed parameters (parameter name) => (default value) or (parameter name) => (ar...
Definition: ApiUpload.php:937
transformWarnings( $warnings)
Definition: ApiUpload.php:749
handleStashException( $e)
Handles a stash exception, giving a useful error to the user.
Definition: ApiUpload.php:799
getHelpUrls()
Return links to more detailed help pages about the module.
Definition: ApiUpload.php:1012
dieStatusWithCode( $status, $overrideCode, $moreExtraData=null)
Like dieStatus(), but always uses $overrideCode for the error code, unless the code comes from IApiMe...
Definition: ApiUpload.php:444
isWriteMode()
Indicates whether this module requires write mode.
Definition: ApiUpload.php:933
static getMinUploadChunkSize(Config $config)
Definition: ApiUpload.php:216
__construct(ApiMain $mainModule, $moduleName, JobQueueGroup $jobQueueGroup, WatchlistManager $watchlistManager, UserOptionsLookup $userOptionsLookup)
Definition: ApiUpload.php:54
getExamplesMessages()
Returns usage examples for this module.
Definition: ApiUpload.php:1002
selectUploadModule()
Select an upload module and set it to mUpload.
Definition: ApiUpload.php:464
needsToken()
Returns the token type this module requires in order to execute.
Definition: ApiUpload.php:998
performUpload( $warnings)
Perform the actual upload.
Definition: ApiUpload.php:839
checkVerification(array $verification)
Performs file verification, dies on error.
Definition: ApiUpload.php:651
mustBePosted()
Indicates whether this module must be called with a POST request.
Definition: ApiUpload.php:929
getApiWarnings()
Check warnings.
Definition: ApiUpload.php:741
Assemble the segments of a chunked upload.
static canAddTagsAccompanyingChange(array $tags, Authority $performer=null, $checkBlock=true)
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:396
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.
Handle enqueueing of background jobs.
A class containing constants representing the names of configuration variables.
Generic operation result class Has warning/error list, boolean status and arbitrary value.
Definition: Status.php:58
Represents a title within MediaWiki.
Definition: Title.php:76
Provides access to user options.
internal since 1.36
Definition: User.php:98
static listParam(array $list, $type='text')
Definition: Message.php:1286
static numParam( $num)
Definition: Message.php:1154
Upload a file from the upload stash into the local file repo.
static newFatal( $message,... $parameters)
Factory function for fatal errors.
Definition: StatusValue.php:73
static newGood( $value=null)
Factory function for good results.
Definition: StatusValue.php:85
static makeWarningsSerializable( $warnings)
Convert the warnings array returned by checkWarnings() to something that can be serialized.
Definition: UploadBase.php:720
static setSessionStatus(UserIdentity $user, $statusKey, $value)
Set the current status of a chunked upload (used for polling).
const EMPTY_FILE
Definition: UploadBase.php:113
const FILETYPE_MISSING
Definition: UploadBase.php:117
static isEnabled()
Returns true if uploads are enabled.
Definition: UploadBase.php:153
static getSessionStatus(UserIdentity $user, $statusKey)
Get the current status of a chunked upload (used for polling).
const HOOK_ABORTED
Definition: UploadBase.php:120
const VERIFICATION_ERROR
Definition: UploadBase.php:119
const WINDOWS_NONASCII_FILENAME
Definition: UploadBase.php:122
const FILETYPE_BADTYPE
Definition: UploadBase.php:118
static getMaxUploadSize( $forType=null)
Get MediaWiki's maximum uploaded file size for a given type of upload, based on $wgMaxUploadSize.
const FILE_TOO_LARGE
Definition: UploadBase.php:121
static isThrottled( $user)
Returns true if the user has surpassed the upload rate limit, false otherwise.
Definition: UploadBase.php:183
const ILLEGAL_FILENAME
Definition: UploadBase.php:115
const MIN_LENGTH_PARTNAME
Definition: UploadBase.php:114
const FILENAME_TOO_LONG
Definition: UploadBase.php:123
static getMaxPhpUploadSize()
Get the PHP maximum uploaded file size, based on ini settings.
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.
Service for formatting and validating API parameters.
Type definition for integer types.
Definition: IntegerDef.php:23
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:32
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