MediaWiki  master
ApiUpload.php
Go to the documentation of this file.
1 <?php
28 
32 class ApiUpload extends ApiBase {
33 
35 
37  protected $mUpload = null;
38 
39  protected $mParams;
40 
42  private $jobQueueGroup;
43 
51  public function __construct(
52  ApiMain $mainModule,
53  $moduleName,
54  JobQueueGroup $jobQueueGroup,
55  WatchlistManager $watchlistManager,
56  UserOptionsLookup $userOptionsLookup
57  ) {
58  parent::__construct( $mainModule, $moduleName );
59  $this->jobQueueGroup = $jobQueueGroup;
60 
61  // Variables needed in ApiWatchlistTrait trait
62  $this->watchlistExpiryEnabled = $this->getConfig()->get( MainConfigNames::WatchlistExpiry );
63  $this->watchlistMaxDuration =
64  $this->getConfig()->get( MainConfigNames::WatchlistExpiryMaxDuration );
65  $this->watchlistManager = $watchlistManager;
66  $this->userOptionsLookup = $userOptionsLookup;
67  }
68 
69  public function execute() {
70  // Check whether upload is enabled
71  if ( !UploadBase::isEnabled() ) {
72  $this->dieWithError( 'uploaddisabled' );
73  }
74 
75  $user = $this->getUser();
76 
77  // Parameter handling
78  $this->mParams = $this->extractRequestParams();
79  $request = $this->getMain()->getRequest();
80  // Check if async mode is actually supported (jobs done in cli mode)
81  $this->mParams['async'] = ( $this->mParams['async'] &&
82  $this->getConfig()->get( MainConfigNames::EnableAsyncUploads ) );
83  // Add the uploaded file to the params array
84  $this->mParams['file'] = $request->getFileName( 'file' );
85  $this->mParams['chunk'] = $request->getFileName( 'chunk' );
86 
87  // Copy the session key to the file key, for backward compatibility.
88  if ( !$this->mParams['filekey'] && $this->mParams['sessionkey'] ) {
89  $this->mParams['filekey'] = $this->mParams['sessionkey'];
90  }
91 
92  // Select an upload module
93  try {
94  if ( !$this->selectUploadModule() ) {
95  return; // not a true upload, but a status request or similar
96  } elseif ( !isset( $this->mUpload ) ) {
97  $this->dieDebug( __METHOD__, 'No upload module set' );
98  }
99  } catch ( UploadStashException $e ) { // XXX: don't spam exception log
100  $this->dieStatus( $this->handleStashException( $e ) );
101  }
102 
103  // First check permission to upload
104  $this->checkPermissions( $user );
105 
106  // Fetch the file (usually a no-op)
108  $status = $this->mUpload->fetchFile();
109  if ( !$status->isGood() ) {
110  $this->dieStatus( $status );
111  }
112 
113  // Check the uploaded file
114  $this->verifyUpload();
115 
116  // Check if the user has the rights to modify or overwrite the requested title
117  // (This check is irrelevant if stashing is already requested, since the errors
118  // can always be fixed by changing the title)
119  if ( !$this->mParams['stash'] ) {
120  $permErrors = $this->mUpload->verifyTitlePermissions( $user );
121  if ( $permErrors !== true ) {
122  $this->dieRecoverableError( $permErrors, 'filename' );
123  }
124  }
125 
126  // Get the result based on the current upload context:
127  try {
128  $result = $this->getContextResult();
129  } catch ( UploadStashException $e ) { // XXX: don't spam exception log
130  $this->dieStatus( $this->handleStashException( $e ) );
131  }
132  $this->getResult()->addValue( null, $this->getModuleName(), $result );
133 
134  // Add 'imageinfo' in a separate addValue() call. File metadata can be unreasonably large,
135  // so otherwise when it exceeded $wgAPIMaxResultSize, no result would be returned (T143993).
136  // @phan-suppress-next-line PhanTypeArraySuspiciousNullable False positive
137  if ( $result['result'] === 'Success' ) {
138  $imageinfo = $this->mUpload->getImageInfo( $this->getResult() );
139  $this->getResult()->addValue( $this->getModuleName(), 'imageinfo', $imageinfo );
140  }
141 
142  // Cleanup any temporary mess
143  $this->mUpload->cleanupTempFile();
144  }
145 
150  private function getContextResult() {
151  $warnings = $this->getApiWarnings();
152  if ( $warnings && !$this->mParams['ignorewarnings'] ) {
153  // Get warnings formatted in result array format
154  return $this->getWarningsResult( $warnings );
155  } elseif ( $this->mParams['chunk'] ) {
156  // Add chunk, and get result
157  return $this->getChunkResult( $warnings );
158  } elseif ( $this->mParams['stash'] ) {
159  // Stash the file and get stash result
160  return $this->getStashResult( $warnings );
161  }
162 
163  // Check throttle after we've handled warnings
164  if ( UploadBase::isThrottled( $this->getUser() )
165  ) {
166  $this->dieWithError( 'apierror-ratelimited' );
167  }
168 
169  // This is the most common case -- a normal upload with no warnings
170  // performUpload will return a formatted properly for the API with status
171  return $this->performUpload( $warnings );
172  }
173 
179  private function getStashResult( $warnings ) {
180  $result = [];
181  $result['result'] = 'Success';
182  if ( $warnings && count( $warnings ) > 0 ) {
183  $result['warnings'] = $warnings;
184  }
185  // Some uploads can request they be stashed, so as not to publish them immediately.
186  // In this case, a failure to stash ought to be fatal
187  $this->performStash( 'critical', $result );
188 
189  return $result;
190  }
191 
197  private function getWarningsResult( $warnings ) {
198  $result = [];
199  $result['result'] = 'Warning';
200  $result['warnings'] = $warnings;
201  // in case the warnings can be fixed with some further user action, let's stash this upload
202  // and return a key they can use to restart it
203  $this->performStash( 'optional', $result );
204 
205  return $result;
206  }
207 
214  public static function getMinUploadChunkSize( Config $config ) {
215  $configured = $config->get( MainConfigNames::MinUploadChunkSize );
216 
217  // Leave some room for other POST parameters
218  $postMax = (
220  ini_get( 'post_max_size' ),
221  PHP_INT_MAX
222  ) ?: PHP_INT_MAX
223  ) - 1024;
224 
225  // Ensure the minimum chunk size is less than PHP upload limits
226  // or the maximum upload size.
227  return min(
228  $configured,
231  $postMax
232  );
233  }
234 
240  private function getChunkResult( $warnings ) {
241  $result = [];
242 
243  if ( $warnings && count( $warnings ) > 0 ) {
244  $result['warnings'] = $warnings;
245  }
246 
247  $request = $this->getMain()->getRequest();
248  $chunkPath = $request->getFileTempname( 'chunk' );
249  $chunkSize = $request->getUpload( 'chunk' )->getSize();
250  $totalSoFar = $this->mParams['offset'] + $chunkSize;
251  $minChunkSize = self::getMinUploadChunkSize( $this->getConfig() );
252 
253  // Double check sizing
254  if ( $totalSoFar > $this->mParams['filesize'] ) {
255  $this->dieWithError( 'apierror-invalid-chunk' );
256  }
257 
258  // Enforce minimum chunk size
259  if ( $totalSoFar != $this->mParams['filesize'] && $chunkSize < $minChunkSize ) {
260  $this->dieWithError( [ 'apierror-chunk-too-small', Message::numParam( $minChunkSize ) ] );
261  }
262 
263  if ( $this->mParams['offset'] == 0 ) {
264  $filekey = $this->performStash( 'critical' );
265  } else {
266  $filekey = $this->mParams['filekey'];
267 
268  // Don't allow further uploads to an already-completed session
269  $progress = UploadBase::getSessionStatus( $this->getUser(), $filekey );
270  if ( !$progress ) {
271  // Probably can't get here, but check anyway just in case
272  $this->dieWithError( 'apierror-stashfailed-nosession', 'stashfailed' );
273  } elseif ( $progress['result'] !== 'Continue' || $progress['stage'] !== 'uploading' ) {
274  $this->dieWithError( 'apierror-stashfailed-complete', 'stashfailed' );
275  }
276 
277  $status = $this->mUpload->addChunk(
278  $chunkPath, $chunkSize, $this->mParams['offset'] );
279  if ( !$status->isGood() ) {
280  $extradata = [
281  'offset' => $this->mUpload->getOffset(),
282  ];
283 
284  $this->dieStatusWithCode( $status, 'stashfailed', $extradata );
285  }
286  }
287 
288  // Check we added the last chunk:
289  if ( $totalSoFar == $this->mParams['filesize'] ) {
290  if ( $this->mParams['async'] ) {
292  $this->getUser(),
293  $filekey,
294  [ 'result' => 'Poll',
295  'stage' => 'queued', 'status' => Status::newGood() ]
296  );
297  $this->jobQueueGroup->push( new AssembleUploadChunksJob(
298  Title::makeTitle( NS_FILE, $filekey ),
299  [
300  'filename' => $this->mParams['filename'],
301  'filekey' => $filekey,
302  'session' => $this->getContext()->exportSession()
303  ]
304  ) );
305  $result['result'] = 'Poll';
306  $result['stage'] = 'queued';
307  } else {
308  $status = $this->mUpload->concatenateChunks();
309  if ( !$status->isGood() ) {
311  $this->getUser(),
312  $filekey,
313  [ 'result' => 'Failure', 'stage' => 'assembling', 'status' => $status ]
314  );
315  $this->dieStatusWithCode( $status, 'stashfailed' );
316  }
317 
318  // We can only get warnings like 'duplicate' after concatenating the chunks
319  $warnings = $this->getApiWarnings();
320  if ( $warnings ) {
321  $result['warnings'] = $warnings;
322  }
323 
324  // The fully concatenated file has a new filekey. So remove
325  // the old filekey and fetch the new one.
326  UploadBase::setSessionStatus( $this->getUser(), $filekey, false );
327  $this->mUpload->stash->removeFile( $filekey );
328  $filekey = $this->mUpload->getStashFile()->getFileKey();
329 
330  $result['result'] = 'Success';
331  }
332  } else {
334  $this->getUser(),
335  $filekey,
336  [
337  'result' => 'Continue',
338  'stage' => 'uploading',
339  'offset' => $totalSoFar,
340  'status' => Status::newGood(),
341  ]
342  );
343  $result['result'] = 'Continue';
344  $result['offset'] = $totalSoFar;
345  }
346 
347  $result['filekey'] = $filekey;
348 
349  return $result;
350  }
351 
364  private function performStash( $failureMode, &$data = null ) {
365  $isPartial = (bool)$this->mParams['chunk'];
366  try {
367  $status = $this->mUpload->tryStashFile( $this->getUser(), $isPartial );
368 
369  if ( $status->isGood() && !$status->getValue() ) {
370  // Not actually a 'good' status...
371  $status->fatal( new ApiMessage( 'apierror-stashinvalidfile', 'stashfailed' ) );
372  }
373  } catch ( Exception $e ) {
374  $debugMessage = 'Stashing temporary file failed: ' . get_class( $e ) . ' ' . $e->getMessage();
375  wfDebug( __METHOD__ . ' ' . $debugMessage );
376  $status = Status::newFatal( $this->getErrorFormatter()->getMessageFromException(
377  $e, [ 'wrap' => new ApiMessage( 'apierror-stashexception', 'stashfailed' ) ]
378  ) );
379  }
380 
381  if ( $status->isGood() ) {
382  $stashFile = $status->getValue();
383  $data['filekey'] = $stashFile->getFileKey();
384  // Backwards compatibility
385  $data['sessionkey'] = $data['filekey'];
386  return $data['filekey'];
387  }
388 
389  if ( $status->getMessage()->getKey() === 'uploadstash-exception' ) {
390  // The exceptions thrown by upload stash code and pretty silly and UploadBase returns poor
391  // Statuses for it. Just extract the exception details and parse them ourselves.
392  list( $exceptionType, $message ) = $status->getMessage()->getParams();
393  $debugMessage = 'Stashing temporary file failed: ' . $exceptionType . ' ' . $message;
394  wfDebug( __METHOD__ . ' ' . $debugMessage );
395  }
396 
397  // Bad status
398  if ( $failureMode !== 'optional' ) {
399  $this->dieStatus( $status );
400  } else {
401  $data['stasherrors'] = $this->getErrorFormatter()->arrayFromStatus( $status );
402  return null;
403  }
404  }
405 
416  private function dieRecoverableError( $errors, $parameter = null ) {
417  // @phan-suppress-previous-line PhanTypeMissingReturn
418  $this->performStash( 'optional', $data );
419 
420  if ( $parameter ) {
421  $data['invalidparameter'] = $parameter;
422  }
423 
424  $sv = StatusValue::newGood();
425  foreach ( $errors as $error ) {
426  $msg = ApiMessage::create( $error );
427  $msg->setApiData( $msg->getApiData() + $data );
428  $sv->fatal( $msg );
429  }
430  $this->dieStatus( $sv );
431  }
432 
443  public function dieStatusWithCode( $status, $overrideCode, $moreExtraData = null ) {
444  // @phan-suppress-previous-line PhanTypeMissingReturn
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->isRegistered() ) {
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  // Global blocks
608  if ( $user->isBlockedGlobally() ) {
609  $this->dieBlocked( $user->getGlobalBlock() );
610  }
611  }
612 
616  protected function verifyUpload() {
617  if ( $this->mParams['chunk'] ) {
618  $maxSize = UploadBase::getMaxUploadSize();
619  if ( $this->mParams['filesize'] > $maxSize ) {
620  $this->dieWithError( 'file-too-large' );
621  }
622  if ( !$this->mUpload->getTitle() ) {
623  $this->dieWithError( 'illegal-filename' );
624  }
625  // file will be assembled after having uploaded the last chunk,
626  // so we can only validate the name at this point
627  $verification = $this->mUpload->validateName();
628  if ( $verification === true ) {
629  return;
630  }
631  } elseif ( $this->mParams['async'] && $this->mParams['filekey'] ) {
632  // file will be assembled in a background process, so we
633  // can only validate the name at this point
634  // file verification will happen in background process
635  $verification = $this->mUpload->validateName();
636  if ( $verification === true ) {
637  return;
638  }
639  } else {
640  wfDebug( __METHOD__ . " about to verify" );
641 
642  $verification = $this->mUpload->verifyUpload();
643  if ( $verification['status'] === UploadBase::OK ) {
644  return;
645  }
646  }
647 
648  $this->checkVerification( $verification );
649  }
650 
656  protected function checkVerification( array $verification ) {
657  // @phan-suppress-previous-line PhanTypeMissingReturn
658  switch ( $verification['status'] ) {
659  // Recoverable errors
661  $this->dieRecoverableError( [ 'filename-tooshort' ], 'filename' );
662  // dieRecoverableError prevents continuation
664  $this->dieRecoverableError(
666  'illegal-filename', null, [ 'filename' => $verification['filtered'] ]
667  ) ], 'filename'
668  );
669  // dieRecoverableError prevents continuation
671  $this->dieRecoverableError( [ 'filename-toolong' ], 'filename' );
672  // dieRecoverableError prevents continuation
674  $this->dieRecoverableError( [ 'filetype-missing' ], 'filename' );
675  // dieRecoverableError prevents continuation
677  $this->dieRecoverableError( [ 'windows-nonascii-filename' ], 'filename' );
678 
679  // Unrecoverable errors
681  $this->dieWithError( 'empty-file' );
682  // dieWithError prevents continuation
684  $this->dieWithError( 'file-too-large' );
685  // dieWithError prevents continuation
686 
688  $extradata = [
689  'filetype' => $verification['finalExt'],
690  'allowed' => array_values( array_unique(
691  $this->getConfig()->get( MainConfigNames::FileExtensions ) ) )
692  ];
693  $extensions =
694  array_unique( $this->getConfig()->get( MainConfigNames::FileExtensions ) );
695  $msg = [
696  'filetype-banned-type',
697  null, // filled in below
698  Message::listParam( $extensions, 'comma' ),
699  count( $extensions ),
700  null, // filled in below
701  ];
702  ApiResult::setIndexedTagName( $extradata['allowed'], 'ext' );
703 
704  if ( isset( $verification['blacklistedExt'] ) ) {
705  $msg[1] = Message::listParam( $verification['blacklistedExt'], 'comma' );
706  $msg[4] = count( $verification['blacklistedExt'] );
707  $extradata['blacklisted'] = array_values( $verification['blacklistedExt'] );
708  ApiResult::setIndexedTagName( $extradata['blacklisted'], 'ext' );
709  } else {
710  $msg[1] = $verification['finalExt'];
711  $msg[4] = 1;
712  }
713 
714  $this->dieWithError( $msg, 'filetype-banned', $extradata );
715  // dieWithError prevents continuation
716 
718  $msg = ApiMessage::create( $verification['details'], 'verification-error' );
719  if ( $verification['details'][0] instanceof MessageSpecifier ) {
720  $details = array_merge( [ $msg->getKey() ], $msg->getParams() );
721  } else {
722  $details = $verification['details'];
723  }
724  ApiResult::setIndexedTagName( $details, 'detail' );
725  $msg->setApiData( $msg->getApiData() + [ 'details' => $details ] );
726  // @phan-suppress-next-line PhanTypeMismatchArgument
727  $this->dieWithError( $msg );
728  // dieWithError prevents continuation
729 
731  $msg = $verification['error'] === '' ? 'hookaborted' : $verification['error'];
732  $this->dieWithError( $msg, 'hookaborted', [ 'details' => $verification['error'] ] );
733  // dieWithError prevents continuation
734  default:
735  $this->dieWithError( 'apierror-unknownerror-nocode', 'unknown-error',
736  [ 'details' => [ 'code' => $verification['status'] ] ] );
737  }
738  }
739 
747  protected function getApiWarnings() {
749  $this->mUpload->checkWarnings( $this->getUser() )
750  );
751 
752  return $this->transformWarnings( $warnings );
753  }
754 
755  protected function transformWarnings( $warnings ) {
756  if ( $warnings ) {
757  // Add indices
758  ApiResult::setIndexedTagName( $warnings, 'warning' );
759 
760  if ( isset( $warnings['duplicate'] ) ) {
761  $dupes = array_column( $warnings['duplicate'], 'fileName' );
762  ApiResult::setIndexedTagName( $dupes, 'duplicate' );
763  $warnings['duplicate'] = $dupes;
764  }
765 
766  if ( isset( $warnings['exists'] ) ) {
767  $warning = $warnings['exists'];
768  unset( $warnings['exists'] );
769  $localFile = $warning['normalizedFile'] ?? $warning['file'];
770  $warnings[$warning['warning']] = $localFile['fileName'];
771  }
772 
773  if ( isset( $warnings['no-change'] ) ) {
774  $file = $warnings['no-change'];
775  unset( $warnings['no-change'] );
776 
777  $warnings['nochange'] = [
778  'timestamp' => wfTimestamp( TS_ISO_8601, $file['timestamp'] )
779  ];
780  }
781 
782  if ( isset( $warnings['duplicate-version'] ) ) {
783  $dupes = [];
784  foreach ( $warnings['duplicate-version'] as $dupe ) {
785  $dupes[] = [
786  'timestamp' => wfTimestamp( TS_ISO_8601, $dupe['timestamp'] )
787  ];
788  }
789  unset( $warnings['duplicate-version'] );
790 
791  ApiResult::setIndexedTagName( $dupes, 'ver' );
792  $warnings['duplicateversions'] = $dupes;
793  }
794  }
795 
796  return $warnings;
797  }
798 
805  protected function handleStashException( $e ) {
806  switch ( get_class( $e ) ) {
807  case UploadStashFileNotFoundException::class:
808  $wrap = 'apierror-stashedfilenotfound';
809  break;
810  case UploadStashBadPathException::class:
811  $wrap = 'apierror-stashpathinvalid';
812  break;
813  case UploadStashFileException::class:
814  $wrap = 'apierror-stashfilestorage';
815  break;
816  case UploadStashZeroLengthFileException::class:
817  $wrap = 'apierror-stashzerolength';
818  break;
819  case UploadStashNotLoggedInException::class:
821  [ 'apierror-mustbeloggedin', $this->msg( 'action-upload' ) ], 'stashnotloggedin'
822  ) );
823  case UploadStashWrongOwnerException::class:
824  $wrap = 'apierror-stashwrongowner';
825  break;
826  case UploadStashNoSuchKeyException::class:
827  $wrap = 'apierror-stashnosuchfilekey';
828  break;
829  default:
830  $wrap = [ 'uploadstash-exception', get_class( $e ) ];
831  break;
832  }
833  return StatusValue::newFatal(
834  $this->getErrorFormatter()->getMessageFromException( $e, [ 'wrap' => $wrap ] )
835  );
836  }
837 
845  protected function performUpload( $warnings ) {
846  // Use comment as initial page text by default
847  if ( $this->mParams['text'] === null ) {
848  $this->mParams['text'] = $this->mParams['comment'];
849  }
850 
852  $file = $this->mUpload->getLocalFile();
853  $user = $this->getUser();
854  $title = $file->getTitle();
855 
856  // for preferences mode, we want to watch if 'watchdefault' is set,
857  // or if the *file* doesn't exist, and either 'watchuploads' or
858  // 'watchcreations' is set. But getWatchlistValue()'s automatic
859  // handling checks if the *title* exists or not, so we need to check
860  // all three preferences manually.
861  $watch = $this->getWatchlistValue(
862  $this->mParams['watchlist'], $title, $user, 'watchdefault'
863  );
864 
865  if ( !$watch && $this->mParams['watchlist'] == 'preferences' && !$file->exists() ) {
866  $watch = (
867  $this->getWatchlistValue( 'preferences', $title, $user, 'watchuploads' ) ||
868  $this->getWatchlistValue( 'preferences', $title, $user, 'watchcreations' )
869  );
870  }
871  $watchlistExpiry = $this->getExpiryFromParams( $this->mParams );
872 
873  // Deprecated parameters
874  if ( $this->mParams['watch'] ) {
875  $watch = true;
876  }
877 
878  if ( $this->mParams['tags'] ) {
879  $status = ChangeTags::canAddTagsAccompanyingChange( $this->mParams['tags'], $this->getAuthority() );
880  if ( !$status->isOK() ) {
881  $this->dieStatus( $status );
882  }
883  }
884 
885  // No errors, no warnings: do the upload
886  $result = [];
887  if ( $this->mParams['async'] ) {
888  $progress = UploadBase::getSessionStatus( $this->getUser(), $this->mParams['filekey'] );
889  if ( $progress && $progress['result'] === 'Poll' ) {
890  $this->dieWithError( 'apierror-upload-inprogress', 'publishfailed' );
891  }
893  $this->getUser(),
894  $this->mParams['filekey'],
895  [ 'result' => 'Poll', 'stage' => 'queued', 'status' => Status::newGood() ]
896  );
897  $this->jobQueueGroup->push( new PublishStashedFileJob(
898  Title::makeTitle( NS_FILE, $this->mParams['filename'] ),
899  [
900  'filename' => $this->mParams['filename'],
901  'filekey' => $this->mParams['filekey'],
902  'comment' => $this->mParams['comment'],
903  'tags' => $this->mParams['tags'] ?? [],
904  'text' => $this->mParams['text'],
905  'watch' => $watch,
906  'watchlistexpiry' => $watchlistExpiry,
907  'session' => $this->getContext()->exportSession()
908  ]
909  ) );
910  $result['result'] = 'Poll';
911  $result['stage'] = 'queued';
912  } else {
914  $status = $this->mUpload->performUpload(
915  $this->mParams['comment'],
916  $this->mParams['text'],
917  $watch,
918  $this->getUser(),
919  $this->mParams['tags'] ?? [],
920  $watchlistExpiry
921  );
922 
923  if ( !$status->isGood() ) {
924  $this->dieRecoverableError( $status->getErrors() );
925  }
926  $result['result'] = 'Success';
927  }
928 
929  $result['filename'] = $file->getName();
930  if ( $warnings && count( $warnings ) > 0 ) {
931  $result['warnings'] = $warnings;
932  }
933 
934  return $result;
935  }
936 
937  public function mustBePosted() {
938  return true;
939  }
940 
941  public function isWriteMode() {
942  return true;
943  }
944 
945  public function getAllowedParams() {
946  $params = [
947  'filename' => [
948  ParamValidator::PARAM_TYPE => 'string',
949  ],
950  'comment' => [
951  ParamValidator::PARAM_DEFAULT => ''
952  ],
953  'tags' => [
954  ParamValidator::PARAM_TYPE => 'tags',
955  ParamValidator::PARAM_ISMULTI => true,
956  ],
957  'text' => [
958  ParamValidator::PARAM_TYPE => 'text',
959  ],
960  'watch' => [
961  ParamValidator::PARAM_DEFAULT => false,
962  ParamValidator::PARAM_DEPRECATED => true,
963  ],
964  ];
965 
966  // Params appear in the docs in the order they are defined,
967  // which is why this is here and not at the bottom.
968  $params += $this->getWatchlistParams( [
969  'watch',
970  'preferences',
971  'nochange',
972  ] );
973 
974  $params += [
975  'ignorewarnings' => false,
976  'file' => [
977  ParamValidator::PARAM_TYPE => 'upload',
978  ],
979  'url' => null,
980  'filekey' => null,
981  'sessionkey' => [
982  ParamValidator::PARAM_DEPRECATED => true,
983  ],
984  'stash' => false,
985 
986  'filesize' => [
987  ParamValidator::PARAM_TYPE => 'integer',
988  IntegerDef::PARAM_MIN => 0,
989  IntegerDef::PARAM_MAX => UploadBase::getMaxUploadSize(),
990  ],
991  'offset' => [
992  ParamValidator::PARAM_TYPE => 'integer',
993  IntegerDef::PARAM_MIN => 0,
994  ],
995  'chunk' => [
996  ParamValidator::PARAM_TYPE => 'upload',
997  ],
998 
999  'async' => false,
1000  'checkstatus' => false,
1001  ];
1002 
1003  return $params;
1004  }
1005 
1006  public function needsToken() {
1007  return 'csrf';
1008  }
1009 
1010  protected function getExamplesMessages() {
1011  return [
1012  'action=upload&filename=Wiki.png' .
1013  '&url=http%3A//upload.wikimedia.org/wikipedia/en/b/bc/Wiki.png&token=123ABC'
1014  => 'apihelp-upload-example-url',
1015  'action=upload&filename=Wiki.png&filekey=filekey&ignorewarnings=1&token=123ABC'
1016  => 'apihelp-upload-example-filekey',
1017  ];
1018  }
1019 
1020  public function getHelpUrls() {
1021  return 'https://www.mediawiki.org/wiki/Special:MyLanguage/API:Upload';
1022  }
1023 }
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:56
dieWithError( $msg, $code=null, $data=null, $httpCode=0)
Abort execution with an error.
Definition: ApiBase.php:1453
static dieDebug( $method, $message)
Internal code errors should be reported with this method.
Definition: ApiBase.php:1658
getMain()
Get the main module.
Definition: ApiBase.php:514
getErrorFormatter()
Definition: ApiBase.php:640
requireOnlyOneParameter( $params,... $required)
Die if none or more than one of a certain set of parameters is set and not false.
Definition: ApiBase.php:903
getResult()
Get the result object.
Definition: ApiBase.php:629
extractRequestParams( $options=[])
Using getAllowedParams(), this function makes an array of the values provided by the user,...
Definition: ApiBase.php:765
getModuleName()
Get the name of the module being executed by this instance.
Definition: ApiBase.php:498
dieStatus(StatusValue $status)
Throw an ApiUsageException based on the Status object.
Definition: ApiBase.php:1516
dieBlocked(Block $block)
Throw an ApiUsageException, which will (if uncaught) call the main module's error handler and die wit...
Definition: ApiBase.php:1483
This is the main API class, used for both external and internal processing.
Definition: ApiMain.php:52
Extension of Message implementing IApiMessage.
Definition: ApiMessage.php:27
static create( $msg, $code=null, array $data=null)
Create an IApiMessage for the message.
Definition: ApiMessage.php:43
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:69
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:616
UploadBase UploadFromChunks $mUpload
Definition: ApiUpload.php:37
getAllowedParams()
Returns an array of allowed parameters (parameter name) => (default value) or (parameter name) => (ar...
Definition: ApiUpload.php:945
transformWarnings( $warnings)
Definition: ApiUpload.php:755
handleStashException( $e)
Handles a stash exception, giving a useful error to the user.
Definition: ApiUpload.php:805
getHelpUrls()
Return links to more detailed help pages about the module.
Definition: ApiUpload.php:1020
dieStatusWithCode( $status, $overrideCode, $moreExtraData=null)
Like dieStatus(), but always uses $overrideCode for the error code, unless the code comes from IApiMe...
Definition: ApiUpload.php:443
isWriteMode()
Indicates whether this module requires write mode.
Definition: ApiUpload.php:941
static getMinUploadChunkSize(Config $config)
Definition: ApiUpload.php:214
__construct(ApiMain $mainModule, $moduleName, JobQueueGroup $jobQueueGroup, WatchlistManager $watchlistManager, UserOptionsLookup $userOptionsLookup)
Definition: ApiUpload.php:51
getExamplesMessages()
Returns usage examples for this module.
Definition: ApiUpload.php:1010
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:1006
performUpload( $warnings)
Perform the actual upload.
Definition: ApiUpload.php:845
checkVerification(array $verification)
Performs file verification, dies on error.
Definition: ApiUpload.php:656
mustBePosted()
Indicates whether this module must be called with a POST request.
Definition: ApiUpload.php:937
getApiWarnings()
Check warnings.
Definition: ApiUpload.php:747
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:635
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.
A class containing constants representing the names of configuration variables.
Provides access to user options.
static listParam(array $list, $type='text')
Definition: Message.php:1298
static numParam( $num)
Definition: Message.php:1166
Upload a file from the upload stash into the local file repo.
static newFatal( $message,... $parameters)
Factory function for fatal errors.
Definition: StatusValue.php:70
static newGood( $value=null)
Factory function for good results.
Definition: StatusValue.php:82
static makeTitle( $ns, $title, $fragment='', $interwiki='')
Create a new Title from a namespace index and a DB key.
Definition: Title.php:638
static makeWarningsSerializable( $warnings)
Convert the warnings array returned by checkWarnings() to something that can be serialized.
Definition: UploadBase.php:737
static setSessionStatus(UserIdentity $user, $statusKey, $value)
Set the current status of a chunked upload (used for polling)
const EMPTY_FILE
Definition: UploadBase.php:107
const FILETYPE_MISSING
Definition: UploadBase.php:111
static isEnabled()
Returns true if uploads are enabled.
Definition: UploadBase.php:146
static getSessionStatus(UserIdentity $user, $statusKey)
Get the current status of a chunked upload (used for polling)
const HOOK_ABORTED
Definition: UploadBase.php:114
const VERIFICATION_ERROR
Definition: UploadBase.php:113
const WINDOWS_NONASCII_FILENAME
Definition: UploadBase.php:116
const FILETYPE_BADTYPE
Definition: UploadBase.php:112
static getMaxUploadSize( $forType=null)
Get MediaWiki's maximum uploaded file size for given type of upload, based on $wgMaxUploadSize.
const FILE_TOO_LARGE
Definition: UploadBase.php:115
static isThrottled( $user)
Returns true if the user has surpassed the upload rate limit, false otherwise.
Definition: UploadBase.php:176
const ILLEGAL_FILENAME
Definition: UploadBase.php:109
const MIN_LENGTH_PARTNAME
Definition: UploadBase.php:108
const FILENAME_TOO_LONG
Definition: UploadBase.php:117
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.
static newFatalPermissionDeniedStatus( $permission)
Factory function for fatal permission-denied errors.
Definition: User.php:3417
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: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