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  [ $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  $this->performStash( 'optional', $data );
418 
419  if ( $parameter ) {
420  $data['invalidparameter'] = $parameter;
421  }
422 
423  $sv = StatusValue::newGood();
424  foreach ( $errors as $error ) {
425  $msg = ApiMessage::create( $error );
426  $msg->setApiData( $msg->getApiData() + $data );
427  $sv->fatal( $msg );
428  }
429  $this->dieStatus( $sv );
430  }
431 
442  public function dieStatusWithCode( $status, $overrideCode, $moreExtraData = null ) {
443  $sv = StatusValue::newGood();
444  foreach ( $status->getErrors() as $error ) {
445  $msg = ApiMessage::create( $error, $overrideCode );
446  if ( $moreExtraData ) {
447  $msg->setApiData( $msg->getApiData() + $moreExtraData );
448  }
449  $sv->fatal( $msg );
450  }
451  $this->dieStatus( $sv );
452  }
453 
462  protected function selectUploadModule() {
463  $request = $this->getMain()->getRequest();
464 
465  // chunk or one and only one of the following parameters is needed
466  if ( !$this->mParams['chunk'] ) {
467  $this->requireOnlyOneParameter( $this->mParams,
468  'filekey', 'file', 'url' );
469  }
470 
471  // Status report for "upload to stash"/"upload from stash"
472  if ( $this->mParams['filekey'] && $this->mParams['checkstatus'] ) {
473  $progress = UploadBase::getSessionStatus( $this->getUser(), $this->mParams['filekey'] );
474  if ( !$progress ) {
475  $this->dieWithError( 'apierror-upload-missingresult', 'missingresult' );
476  } elseif ( !$progress['status']->isGood() ) {
477  $this->dieStatusWithCode( $progress['status'], 'stashfailed' );
478  }
479  if ( isset( $progress['status']->value['verification'] ) ) {
480  $this->checkVerification( $progress['status']->value['verification'] );
481  }
482  if ( isset( $progress['status']->value['warnings'] ) ) {
483  $warnings = $this->transformWarnings( $progress['status']->value['warnings'] );
484  if ( $warnings ) {
485  $progress['warnings'] = $warnings;
486  }
487  }
488  unset( $progress['status'] ); // remove Status object
489  $imageinfo = null;
490  if ( isset( $progress['imageinfo'] ) ) {
491  $imageinfo = $progress['imageinfo'];
492  unset( $progress['imageinfo'] );
493  }
494 
495  $this->getResult()->addValue( null, $this->getModuleName(), $progress );
496  // Add 'imageinfo' in a separate addValue() call. File metadata can be unreasonably large,
497  // so otherwise when it exceeded $wgAPIMaxResultSize, no result would be returned (T143993).
498  if ( $imageinfo ) {
499  $this->getResult()->addValue( $this->getModuleName(), 'imageinfo', $imageinfo );
500  }
501 
502  return false;
503  }
504 
505  // The following modules all require the filename parameter to be set
506  if ( $this->mParams['filename'] === null ) {
507  $this->dieWithError( [ 'apierror-missingparam', 'filename' ] );
508  }
509 
510  if ( $this->mParams['chunk'] ) {
511  // Chunk upload
512  $this->mUpload = new UploadFromChunks( $this->getUser() );
513  if ( isset( $this->mParams['filekey'] ) ) {
514  if ( $this->mParams['offset'] === 0 ) {
515  $this->dieWithError( 'apierror-upload-filekeynotallowed', 'filekeynotallowed' );
516  }
517 
518  // handle new chunk
519  $this->mUpload->continueChunks(
520  $this->mParams['filename'],
521  $this->mParams['filekey'],
522  $request->getUpload( 'chunk' )
523  );
524  } else {
525  if ( $this->mParams['offset'] !== 0 ) {
526  $this->dieWithError( 'apierror-upload-filekeyneeded', 'filekeyneeded' );
527  }
528 
529  // handle first chunk
530  $this->mUpload->initialize(
531  $this->mParams['filename'],
532  $request->getUpload( 'chunk' )
533  );
534  }
535  } elseif ( isset( $this->mParams['filekey'] ) ) {
536  // Upload stashed in a previous request
537  if ( !UploadFromStash::isValidKey( $this->mParams['filekey'] ) ) {
538  $this->dieWithError( 'apierror-invalid-file-key' );
539  }
540 
541  $this->mUpload = new UploadFromStash( $this->getUser() );
542  // This will not download the temp file in initialize() in async mode.
543  // We still have enough information to call checkWarnings() and such.
544  $this->mUpload->initialize(
545  $this->mParams['filekey'], $this->mParams['filename'], !$this->mParams['async']
546  );
547  } elseif ( isset( $this->mParams['file'] ) ) {
548  // Can't async upload directly from a POSTed file, we'd have to
549  // stash the file and then queue the publish job. The user should
550  // just submit the two API queries to perform those two steps.
551  if ( $this->mParams['async'] ) {
552  $this->dieWithError( 'apierror-cannot-async-upload-file' );
553  }
554 
555  $this->mUpload = new UploadFromFile();
556  $this->mUpload->initialize(
557  $this->mParams['filename'],
558  $request->getUpload( 'file' )
559  );
560  } elseif ( isset( $this->mParams['url'] ) ) {
561  // Make sure upload by URL is enabled:
562  if ( !UploadFromUrl::isEnabled() ) {
563  $this->dieWithError( 'copyuploaddisabled' );
564  }
565 
566  if ( !UploadFromUrl::isAllowedHost( $this->mParams['url'] ) ) {
567  $this->dieWithError( 'apierror-copyuploadbaddomain' );
568  }
569 
570  if ( !UploadFromUrl::isAllowedUrl( $this->mParams['url'] ) ) {
571  $this->dieWithError( 'apierror-copyuploadbadurl' );
572  }
573 
574  $this->mUpload = new UploadFromUrl;
575  $this->mUpload->initialize( $this->mParams['filename'],
576  $this->mParams['url'] );
577  }
578 
579  return true;
580  }
581 
587  protected function checkPermissions( $user ) {
588  // Check whether the user has the appropriate permissions to upload anyway
589  $permission = $this->mUpload->isAllowed( $user );
590 
591  if ( $permission !== true ) {
592  if ( !$user->isRegistered() ) {
593  $this->dieWithError( [ 'apierror-mustbeloggedin', $this->msg( 'action-upload' ) ] );
594  }
595 
596  $this->dieStatus( User::newFatalPermissionDeniedStatus( $permission ) );
597  }
598 
599  // Check blocks
600  if ( $user->isBlockedFromUpload() ) {
601  // @phan-suppress-next-line PhanTypeMismatchArgumentNullable Block is checked and not null
602  $this->dieBlocked( $user->getBlock() );
603  }
604  }
605 
609  protected function verifyUpload() {
610  if ( $this->mParams['chunk'] ) {
611  $maxSize = UploadBase::getMaxUploadSize();
612  if ( $this->mParams['filesize'] > $maxSize ) {
613  $this->dieWithError( 'file-too-large' );
614  }
615  if ( !$this->mUpload->getTitle() ) {
616  $this->dieWithError( 'illegal-filename' );
617  }
618  // file will be assembled after having uploaded the last chunk,
619  // so we can only validate the name at this point
620  $verification = $this->mUpload->validateName();
621  if ( $verification === true ) {
622  return;
623  }
624  } elseif ( $this->mParams['async'] && $this->mParams['filekey'] ) {
625  // file will be assembled in a background process, so we
626  // can only validate the name at this point
627  // file verification will happen in background process
628  $verification = $this->mUpload->validateName();
629  if ( $verification === true ) {
630  return;
631  }
632  } else {
633  wfDebug( __METHOD__ . " about to verify" );
634 
635  $verification = $this->mUpload->verifyUpload();
636  if ( $verification['status'] === UploadBase::OK ) {
637  return;
638  }
639  }
640 
641  $this->checkVerification( $verification );
642  }
643 
649  protected function checkVerification( array $verification ) {
650  switch ( $verification['status'] ) {
651  // Recoverable errors
653  $this->dieRecoverableError( [ 'filename-tooshort' ], 'filename' );
654  // dieRecoverableError prevents continuation
656  $this->dieRecoverableError(
658  'illegal-filename', null, [ 'filename' => $verification['filtered'] ]
659  ) ], 'filename'
660  );
661  // dieRecoverableError prevents continuation
663  $this->dieRecoverableError( [ 'filename-toolong' ], 'filename' );
664  // dieRecoverableError prevents continuation
666  $this->dieRecoverableError( [ 'filetype-missing' ], 'filename' );
667  // dieRecoverableError prevents continuation
669  $this->dieRecoverableError( [ 'windows-nonascii-filename' ], 'filename' );
670 
671  // Unrecoverable errors
673  $this->dieWithError( 'empty-file' );
674  // dieWithError prevents continuation
676  $this->dieWithError( 'file-too-large' );
677  // dieWithError prevents continuation
678 
680  $extradata = [
681  'filetype' => $verification['finalExt'],
682  'allowed' => array_values( array_unique(
683  $this->getConfig()->get( MainConfigNames::FileExtensions ) ) )
684  ];
685  $extensions =
686  array_unique( $this->getConfig()->get( MainConfigNames::FileExtensions ) );
687  $msg = [
688  'filetype-banned-type',
689  null, // filled in below
690  Message::listParam( $extensions, 'comma' ),
691  count( $extensions ),
692  null, // filled in below
693  ];
694  ApiResult::setIndexedTagName( $extradata['allowed'], 'ext' );
695 
696  if ( isset( $verification['blacklistedExt'] ) ) {
697  $msg[1] = Message::listParam( $verification['blacklistedExt'], 'comma' );
698  $msg[4] = count( $verification['blacklistedExt'] );
699  $extradata['blacklisted'] = array_values( $verification['blacklistedExt'] );
700  ApiResult::setIndexedTagName( $extradata['blacklisted'], 'ext' );
701  } else {
702  $msg[1] = $verification['finalExt'];
703  $msg[4] = 1;
704  }
705 
706  $this->dieWithError( $msg, 'filetype-banned', $extradata );
707  // dieWithError prevents continuation
708 
710  $msg = ApiMessage::create( $verification['details'], 'verification-error' );
711  if ( $verification['details'][0] instanceof MessageSpecifier ) {
712  $details = array_merge( [ $msg->getKey() ], $msg->getParams() );
713  } else {
714  $details = $verification['details'];
715  }
716  ApiResult::setIndexedTagName( $details, 'detail' );
717  $msg->setApiData( $msg->getApiData() + [ 'details' => $details ] );
718  // @phan-suppress-next-line PhanTypeMismatchArgument
719  $this->dieWithError( $msg );
720  // dieWithError prevents continuation
721 
723  $msg = $verification['error'] === '' ? 'hookaborted' : $verification['error'];
724  $this->dieWithError( $msg, 'hookaborted', [ 'details' => $verification['error'] ] );
725  // dieWithError prevents continuation
726  default:
727  $this->dieWithError( 'apierror-unknownerror-nocode', 'unknown-error',
728  [ 'details' => [ 'code' => $verification['status'] ] ] );
729  }
730  }
731 
739  protected function getApiWarnings() {
741  $this->mUpload->checkWarnings( $this->getUser() )
742  );
743 
744  return $this->transformWarnings( $warnings );
745  }
746 
747  protected function transformWarnings( $warnings ) {
748  if ( $warnings ) {
749  // Add indices
750  ApiResult::setIndexedTagName( $warnings, 'warning' );
751 
752  if ( isset( $warnings['duplicate'] ) ) {
753  $dupes = array_column( $warnings['duplicate'], 'fileName' );
754  ApiResult::setIndexedTagName( $dupes, 'duplicate' );
755  $warnings['duplicate'] = $dupes;
756  }
757 
758  if ( isset( $warnings['exists'] ) ) {
759  $warning = $warnings['exists'];
760  unset( $warnings['exists'] );
761  $localFile = $warning['normalizedFile'] ?? $warning['file'];
762  $warnings[$warning['warning']] = $localFile['fileName'];
763  }
764 
765  if ( isset( $warnings['no-change'] ) ) {
766  $file = $warnings['no-change'];
767  unset( $warnings['no-change'] );
768 
769  $warnings['nochange'] = [
770  'timestamp' => wfTimestamp( TS_ISO_8601, $file['timestamp'] )
771  ];
772  }
773 
774  if ( isset( $warnings['duplicate-version'] ) ) {
775  $dupes = [];
776  foreach ( $warnings['duplicate-version'] as $dupe ) {
777  $dupes[] = [
778  'timestamp' => wfTimestamp( TS_ISO_8601, $dupe['timestamp'] )
779  ];
780  }
781  unset( $warnings['duplicate-version'] );
782 
783  ApiResult::setIndexedTagName( $dupes, 'ver' );
784  $warnings['duplicateversions'] = $dupes;
785  }
786  }
787 
788  return $warnings;
789  }
790 
797  protected function handleStashException( $e ) {
798  switch ( get_class( $e ) ) {
799  case UploadStashFileNotFoundException::class:
800  $wrap = 'apierror-stashedfilenotfound';
801  break;
802  case UploadStashBadPathException::class:
803  $wrap = 'apierror-stashpathinvalid';
804  break;
805  case UploadStashFileException::class:
806  $wrap = 'apierror-stashfilestorage';
807  break;
808  case UploadStashZeroLengthFileException::class:
809  $wrap = 'apierror-stashzerolength';
810  break;
811  case UploadStashNotLoggedInException::class:
813  [ 'apierror-mustbeloggedin', $this->msg( 'action-upload' ) ], 'stashnotloggedin'
814  ) );
815  case UploadStashWrongOwnerException::class:
816  $wrap = 'apierror-stashwrongowner';
817  break;
818  case UploadStashNoSuchKeyException::class:
819  $wrap = 'apierror-stashnosuchfilekey';
820  break;
821  default:
822  $wrap = [ 'uploadstash-exception', get_class( $e ) ];
823  break;
824  }
825  return StatusValue::newFatal(
826  $this->getErrorFormatter()->getMessageFromException( $e, [ 'wrap' => $wrap ] )
827  );
828  }
829 
837  protected function performUpload( $warnings ) {
838  // Use comment as initial page text by default
839  if ( $this->mParams['text'] === null ) {
840  $this->mParams['text'] = $this->mParams['comment'];
841  }
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:57
dieWithError( $msg, $code=null, $data=null, $httpCode=0)
Abort execution with an error.
Definition: ApiBase.php:1455
static dieDebug( $method, $message)
Internal code errors should be reported with this method.
Definition: ApiBase.php:1696
getMain()
Get the main module.
Definition: ApiBase.php:515
getErrorFormatter()
Definition: ApiBase.php:641
requireOnlyOneParameter( $params,... $required)
Die if none or more than one of a certain set of parameters is set and not false.
Definition: ApiBase.php:904
getResult()
Get the result object.
Definition: ApiBase.php:630
extractRequestParams( $options=[])
Using getAllowedParams(), this function makes an array of the values provided by the user,...
Definition: ApiBase.php:766
getModuleName()
Get the name of the module being executed by this instance.
Definition: ApiBase.php:499
dieStatus(StatusValue $status)
Throw an ApiUsageException based on the Status object.
Definition: ApiBase.php:1516
dieBlocked(Block $block)
Throw an ApiUsageException, which will (if uncaught) call the main module's error handler and die wit...
Definition: ApiBase.php:1484
This is the main API class, used for both external and internal processing.
Definition: ApiMain.php:55
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:587
verifyUpload()
Performs file verification, dies on error.
Definition: ApiUpload.php:609
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:937
transformWarnings( $warnings)
Definition: ApiUpload.php:747
handleStashException( $e)
Handles a stash exception, giving a useful error to the user.
Definition: ApiUpload.php:797
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:442
isWriteMode()
Indicates whether this module requires write mode.
Definition: ApiUpload.php:933
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:1002
selectUploadModule()
Select an upload module and set it to mUpload.
Definition: ApiUpload.php:462
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:837
checkVerification(array $verification)
Performs file verification, dies on error.
Definition: ApiUpload.php:649
mustBePosted()
Indicates whether this module must be called with a POST request.
Definition: ApiUpload.php:929
getApiWarnings()
Check warnings.
Definition: ApiUpload.php:739
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:1278
static numParam( $num)
Definition: Message.php:1146
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 makeTitle( $ns, $title, $fragment='', $interwiki='')
Create a new Title from a namespace index and a DB key.
Definition: Title.php:641
static makeWarningsSerializable( $warnings)
Convert the warnings array returned by checkWarnings() to something that can be serialized.
Definition: UploadBase.php:738
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:3389
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