MediaWiki  1.34.0
ApiUpload.php
Go to the documentation of this file.
1 <?php
26 class ApiUpload extends ApiBase {
28  protected $mUpload = null;
29 
30  protected $mParams;
31 
32  public function execute() {
33  // Check whether upload is enabled
34  if ( !UploadBase::isEnabled() ) {
35  $this->dieWithError( 'uploaddisabled' );
36  }
37 
38  $user = $this->getUser();
39 
40  // Parameter handling
41  $this->mParams = $this->extractRequestParams();
42  $request = $this->getMain()->getRequest();
43  // Check if async mode is actually supported (jobs done in cli mode)
44  $this->mParams['async'] = ( $this->mParams['async'] &&
45  $this->getConfig()->get( 'EnableAsyncUploads' ) );
46  // Add the uploaded file to the params array
47  $this->mParams['file'] = $request->getFileName( 'file' );
48  $this->mParams['chunk'] = $request->getFileName( 'chunk' );
49 
50  // Copy the session key to the file key, for backward compatibility.
51  if ( !$this->mParams['filekey'] && $this->mParams['sessionkey'] ) {
52  $this->mParams['filekey'] = $this->mParams['sessionkey'];
53  }
54 
55  // Select an upload module
56  try {
57  if ( !$this->selectUploadModule() ) {
58  return; // not a true upload, but a status request or similar
59  } elseif ( !isset( $this->mUpload ) ) {
60  $this->dieDebug( __METHOD__, 'No upload module set' );
61  }
62  } catch ( UploadStashException $e ) { // XXX: don't spam exception log
63  $this->dieStatus( $this->handleStashException( $e ) );
64  }
65 
66  // First check permission to upload
67  $this->checkPermissions( $user );
68 
69  // Fetch the file (usually a no-op)
71  $status = $this->mUpload->fetchFile();
72  if ( !$status->isGood() ) {
73  $this->dieStatus( $status );
74  }
75 
76  // Check if the uploaded file is sane
77  if ( $this->mParams['chunk'] ) {
78  $maxSize = UploadBase::getMaxUploadSize();
79  if ( $this->mParams['filesize'] > $maxSize ) {
80  $this->dieWithError( 'file-too-large' );
81  }
82  if ( !$this->mUpload->getTitle() ) {
83  $this->dieWithError( 'illegal-filename' );
84  }
85  } elseif ( $this->mParams['async'] && $this->mParams['filekey'] ) {
86  // defer verification to background process
87  } else {
88  wfDebug( __METHOD__ . " about to verify\n" );
89  $this->verifyUpload();
90  }
91 
92  // Check if the user has the rights to modify or overwrite the requested title
93  // (This check is irrelevant if stashing is already requested, since the errors
94  // can always be fixed by changing the title)
95  if ( !$this->mParams['stash'] ) {
96  $permErrors = $this->mUpload->verifyTitlePermissions( $user );
97  if ( $permErrors !== true ) {
98  $this->dieRecoverableError( $permErrors, 'filename' );
99  }
100  }
101 
102  // Get the result based on the current upload context:
103  try {
104  $result = $this->getContextResult();
105  } catch ( UploadStashException $e ) { // XXX: don't spam exception log
106  $this->dieStatus( $this->handleStashException( $e ) );
107  }
108  $this->getResult()->addValue( null, $this->getModuleName(), $result );
109 
110  // Add 'imageinfo' in a separate addValue() call. File metadata can be unreasonably large,
111  // so otherwise when it exceeded $wgAPIMaxResultSize, no result would be returned (T143993).
112  if ( $result['result'] === 'Success' ) {
113  $imageinfo = $this->mUpload->getImageInfo( $this->getResult() );
114  $this->getResult()->addValue( $this->getModuleName(), 'imageinfo', $imageinfo );
115  }
116 
117  // Cleanup any temporary mess
118  $this->mUpload->cleanupTempFile();
119  }
120 
125  private function getContextResult() {
126  $warnings = $this->getApiWarnings();
127  if ( $warnings && !$this->mParams['ignorewarnings'] ) {
128  // Get warnings formatted in result array format
129  return $this->getWarningsResult( $warnings );
130  } elseif ( $this->mParams['chunk'] ) {
131  // Add chunk, and get result
132  return $this->getChunkResult( $warnings );
133  } elseif ( $this->mParams['stash'] ) {
134  // Stash the file and get stash result
135  return $this->getStashResult( $warnings );
136  }
137 
138  // Check throttle after we've handled warnings
139  if ( UploadBase::isThrottled( $this->getUser() )
140  ) {
141  $this->dieWithError( 'apierror-ratelimited' );
142  }
143 
144  // This is the most common case -- a normal upload with no warnings
145  // performUpload will return a formatted properly for the API with status
146  return $this->performUpload( $warnings );
147  }
148 
154  private function getStashResult( $warnings ) {
155  $result = [];
156  $result['result'] = 'Success';
157  if ( $warnings && count( $warnings ) > 0 ) {
158  $result['warnings'] = $warnings;
159  }
160  // Some uploads can request they be stashed, so as not to publish them immediately.
161  // In this case, a failure to stash ought to be fatal
162  $this->performStash( 'critical', $result );
163 
164  return $result;
165  }
166 
172  private function getWarningsResult( $warnings ) {
173  $result = [];
174  $result['result'] = 'Warning';
175  $result['warnings'] = $warnings;
176  // in case the warnings can be fixed with some further user action, let's stash this upload
177  // and return a key they can use to restart it
178  $this->performStash( 'optional', $result );
179 
180  return $result;
181  }
182 
188  private function getChunkResult( $warnings ) {
189  $result = [];
190 
191  if ( $warnings && count( $warnings ) > 0 ) {
192  $result['warnings'] = $warnings;
193  }
194 
195  $request = $this->getMain()->getRequest();
196  $chunkPath = $request->getFileTempname( 'chunk' );
197  $chunkSize = $request->getUpload( 'chunk' )->getSize();
198  $totalSoFar = $this->mParams['offset'] + $chunkSize;
199  $minChunkSize = $this->getConfig()->get( 'MinUploadChunkSize' );
200 
201  // Sanity check sizing
202  if ( $totalSoFar > $this->mParams['filesize'] ) {
203  $this->dieWithError( 'apierror-invalid-chunk' );
204  }
205 
206  // Enforce minimum chunk size
207  if ( $totalSoFar != $this->mParams['filesize'] && $chunkSize < $minChunkSize ) {
208  $this->dieWithError( [ 'apierror-chunk-too-small', Message::numParam( $minChunkSize ) ] );
209  }
210 
211  if ( $this->mParams['offset'] == 0 ) {
212  $filekey = $this->performStash( 'critical' );
213  } else {
214  $filekey = $this->mParams['filekey'];
215 
216  // Don't allow further uploads to an already-completed session
217  $progress = UploadBase::getSessionStatus( $this->getUser(), $filekey );
218  if ( !$progress ) {
219  // Probably can't get here, but check anyway just in case
220  $this->dieWithError( 'apierror-stashfailed-nosession', 'stashfailed' );
221  } elseif ( $progress['result'] !== 'Continue' || $progress['stage'] !== 'uploading' ) {
222  $this->dieWithError( 'apierror-stashfailed-complete', 'stashfailed' );
223  }
224 
225  $status = $this->mUpload->addChunk(
226  $chunkPath, $chunkSize, $this->mParams['offset'] );
227  if ( !$status->isGood() ) {
228  $extradata = [
229  'offset' => $this->mUpload->getOffset(),
230  ];
231 
232  $this->dieStatusWithCode( $status, 'stashfailed', $extradata );
233  }
234  }
235 
236  // Check we added the last chunk:
237  if ( $totalSoFar == $this->mParams['filesize'] ) {
238  if ( $this->mParams['async'] ) {
239  UploadBase::setSessionStatus(
240  $this->getUser(),
241  $filekey,
242  [ 'result' => 'Poll',
243  'stage' => 'queued', 'status' => Status::newGood() ]
244  );
246  Title::makeTitle( NS_FILE, $filekey ),
247  [
248  'filename' => $this->mParams['filename'],
249  'filekey' => $filekey,
250  'session' => $this->getContext()->exportSession()
251  ]
252  ) );
253  $result['result'] = 'Poll';
254  $result['stage'] = 'queued';
255  } else {
256  $status = $this->mUpload->concatenateChunks();
257  if ( !$status->isGood() ) {
258  UploadBase::setSessionStatus(
259  $this->getUser(),
260  $filekey,
261  [ 'result' => 'Failure', 'stage' => 'assembling', 'status' => $status ]
262  );
263  $this->dieStatusWithCode( $status, 'stashfailed' );
264  }
265 
266  // We can only get warnings like 'duplicate' after concatenating the chunks
267  $warnings = $this->getApiWarnings();
268  if ( $warnings ) {
269  $result['warnings'] = $warnings;
270  }
271 
272  // The fully concatenated file has a new filekey. So remove
273  // the old filekey and fetch the new one.
274  UploadBase::setSessionStatus( $this->getUser(), $filekey, false );
275  $this->mUpload->stash->removeFile( $filekey );
276  $filekey = $this->mUpload->getStashFile()->getFileKey();
277 
278  $result['result'] = 'Success';
279  }
280  } else {
281  UploadBase::setSessionStatus(
282  $this->getUser(),
283  $filekey,
284  [
285  'result' => 'Continue',
286  'stage' => 'uploading',
287  'offset' => $totalSoFar,
288  'status' => Status::newGood(),
289  ]
290  );
291  $result['result'] = 'Continue';
292  $result['offset'] = $totalSoFar;
293  }
294 
295  $result['filekey'] = $filekey;
296 
297  return $result;
298  }
299 
312  private function performStash( $failureMode, &$data = null ) {
313  $isPartial = (bool)$this->mParams['chunk'];
314  try {
315  $status = $this->mUpload->tryStashFile( $this->getUser(), $isPartial );
316 
317  if ( $status->isGood() && !$status->getValue() ) {
318  // Not actually a 'good' status...
319  $status->fatal( new ApiMessage( 'apierror-stashinvalidfile', 'stashfailed' ) );
320  }
321  } catch ( Exception $e ) {
322  $debugMessage = 'Stashing temporary file failed: ' . get_class( $e ) . ' ' . $e->getMessage();
323  wfDebug( __METHOD__ . ' ' . $debugMessage . "\n" );
324  $status = Status::newFatal( $this->getErrorFormatter()->getMessageFromException(
325  $e, [ 'wrap' => new ApiMessage( 'apierror-stashexception', 'stashfailed' ) ]
326  ) );
327  }
328 
329  if ( $status->isGood() ) {
330  $stashFile = $status->getValue();
331  $data['filekey'] = $stashFile->getFileKey();
332  // Backwards compatibility
333  $data['sessionkey'] = $data['filekey'];
334  return $data['filekey'];
335  }
336 
337  if ( $status->getMessage()->getKey() === 'uploadstash-exception' ) {
338  // The exceptions thrown by upload stash code and pretty silly and UploadBase returns poor
339  // Statuses for it. Just extract the exception details and parse them ourselves.
340  list( $exceptionType, $message ) = $status->getMessage()->getParams();
341  $debugMessage = 'Stashing temporary file failed: ' . $exceptionType . ' ' . $message;
342  wfDebug( __METHOD__ . ' ' . $debugMessage . "\n" );
343  }
344 
345  // Bad status
346  if ( $failureMode !== 'optional' ) {
347  $this->dieStatus( $status );
348  } else {
349  $data['stasherrors'] = $this->getErrorFormatter()->arrayFromStatus( $status );
350  return null;
351  }
352  }
353 
363  private function dieRecoverableError( $errors, $parameter = null ) {
364  $this->performStash( 'optional', $data );
365 
366  if ( $parameter ) {
367  $data['invalidparameter'] = $parameter;
368  }
369 
370  $sv = StatusValue::newGood();
371  foreach ( $errors as $error ) {
372  $msg = ApiMessage::create( $error );
373  $msg->setApiData( $msg->getApiData() + $data );
374  $sv->fatal( $msg );
375  }
376  $this->dieStatus( $sv );
377  }
378 
388  public function dieStatusWithCode( $status, $overrideCode, $moreExtraData = null ) {
389  $sv = StatusValue::newGood();
390  foreach ( $status->getErrors() as $error ) {
391  $msg = ApiMessage::create( $error, $overrideCode );
392  if ( $moreExtraData ) {
393  $msg->setApiData( $msg->getApiData() + $moreExtraData );
394  }
395  $sv->fatal( $msg );
396  }
397  $this->dieStatus( $sv );
398  }
399 
407  protected function selectUploadModule() {
408  $request = $this->getMain()->getRequest();
409 
410  // chunk or one and only one of the following parameters is needed
411  if ( !$this->mParams['chunk'] ) {
412  $this->requireOnlyOneParameter( $this->mParams,
413  'filekey', 'file', 'url' );
414  }
415 
416  // Status report for "upload to stash"/"upload from stash"
417  if ( $this->mParams['filekey'] && $this->mParams['checkstatus'] ) {
418  $progress = UploadBase::getSessionStatus( $this->getUser(), $this->mParams['filekey'] );
419  if ( !$progress ) {
420  $this->dieWithError( 'apierror-upload-missingresult', 'missingresult' );
421  } elseif ( !$progress['status']->isGood() ) {
422  $this->dieStatusWithCode( $progress['status'], 'stashfailed' );
423  }
424  if ( isset( $progress['status']->value['verification'] ) ) {
425  $this->checkVerification( $progress['status']->value['verification'] );
426  }
427  if ( isset( $progress['status']->value['warnings'] ) ) {
428  $warnings = $this->transformWarnings( $progress['status']->value['warnings'] );
429  if ( $warnings ) {
430  $progress['warnings'] = $warnings;
431  }
432  }
433  unset( $progress['status'] ); // remove Status object
434  $imageinfo = null;
435  if ( isset( $progress['imageinfo'] ) ) {
436  $imageinfo = $progress['imageinfo'];
437  unset( $progress['imageinfo'] );
438  }
439 
440  $this->getResult()->addValue( null, $this->getModuleName(), $progress );
441  // Add 'imageinfo' in a separate addValue() call. File metadata can be unreasonably large,
442  // so otherwise when it exceeded $wgAPIMaxResultSize, no result would be returned (T143993).
443  if ( $imageinfo ) {
444  $this->getResult()->addValue( $this->getModuleName(), 'imageinfo', $imageinfo );
445  }
446 
447  return false;
448  }
449 
450  // The following modules all require the filename parameter to be set
451  if ( is_null( $this->mParams['filename'] ) ) {
452  $this->dieWithError( [ 'apierror-missingparam', 'filename' ] );
453  }
454 
455  if ( $this->mParams['chunk'] ) {
456  // Chunk upload
457  $this->mUpload = new UploadFromChunks( $this->getUser() );
458  if ( isset( $this->mParams['filekey'] ) ) {
459  if ( $this->mParams['offset'] === 0 ) {
460  $this->dieWithError( 'apierror-upload-filekeynotallowed', 'filekeynotallowed' );
461  }
462 
463  // handle new chunk
464  $this->mUpload->continueChunks(
465  $this->mParams['filename'],
466  $this->mParams['filekey'],
467  $request->getUpload( 'chunk' )
468  );
469  } else {
470  if ( $this->mParams['offset'] !== 0 ) {
471  $this->dieWithError( 'apierror-upload-filekeyneeded', 'filekeyneeded' );
472  }
473 
474  // handle first chunk
475  $this->mUpload->initialize(
476  $this->mParams['filename'],
477  $request->getUpload( 'chunk' )
478  );
479  }
480  } elseif ( isset( $this->mParams['filekey'] ) ) {
481  // Upload stashed in a previous request
482  if ( !UploadFromStash::isValidKey( $this->mParams['filekey'] ) ) {
483  $this->dieWithError( 'apierror-invalid-file-key' );
484  }
485 
486  $this->mUpload = new UploadFromStash( $this->getUser() );
487  // This will not download the temp file in initialize() in async mode.
488  // We still have enough information to call checkWarnings() and such.
489  $this->mUpload->initialize(
490  $this->mParams['filekey'], $this->mParams['filename'], !$this->mParams['async']
491  );
492  } elseif ( isset( $this->mParams['file'] ) ) {
493  // Can't async upload directly from a POSTed file, we'd have to
494  // stash the file and then queue the publish job. The user should
495  // just submit the two API queries to perform those two steps.
496  if ( $this->mParams['async'] ) {
497  $this->dieWithError( 'apierror-cannot-async-upload-file' );
498  }
499 
500  $this->mUpload = new UploadFromFile();
501  $this->mUpload->initialize(
502  $this->mParams['filename'],
503  $request->getUpload( 'file' )
504  );
505  } elseif ( isset( $this->mParams['url'] ) ) {
506  // Make sure upload by URL is enabled:
507  if ( !UploadFromUrl::isEnabled() ) {
508  $this->dieWithError( 'copyuploaddisabled' );
509  }
510 
511  if ( !UploadFromUrl::isAllowedHost( $this->mParams['url'] ) ) {
512  $this->dieWithError( 'apierror-copyuploadbaddomain' );
513  }
514 
515  if ( !UploadFromUrl::isAllowedUrl( $this->mParams['url'] ) ) {
516  $this->dieWithError( 'apierror-copyuploadbadurl' );
517  }
518 
519  $this->mUpload = new UploadFromUrl;
520  $this->mUpload->initialize( $this->mParams['filename'],
521  $this->mParams['url'] );
522  }
523 
524  return true;
525  }
526 
532  protected function checkPermissions( $user ) {
533  // Check whether the user has the appropriate permissions to upload anyway
534  $permission = $this->mUpload->isAllowed( $user );
535 
536  if ( $permission !== true ) {
537  if ( !$user->isLoggedIn() ) {
538  $this->dieWithError( [ 'apierror-mustbeloggedin', $this->msg( 'action-upload' ) ] );
539  }
540 
541  $this->dieStatus( User::newFatalPermissionDeniedStatus( $permission ) );
542  }
543 
544  // Check blocks
545  if ( $user->isBlockedFromUpload() ) {
546  $this->dieBlocked( $user->getBlock() );
547  }
548 
549  // Global blocks
550  if ( $user->isBlockedGlobally() ) {
551  $this->dieBlocked( $user->getGlobalBlock() );
552  }
553  }
554 
558  protected function verifyUpload() {
559  $verification = $this->mUpload->verifyUpload();
560  if ( $verification['status'] === UploadBase::OK ) {
561  return;
562  }
563 
564  $this->checkVerification( $verification );
565  }
566 
571  protected function checkVerification( array $verification ) {
572  switch ( $verification['status'] ) {
573  // Recoverable errors
574  case UploadBase::MIN_LENGTH_PARTNAME:
575  $this->dieRecoverableError( [ 'filename-tooshort' ], 'filename' );
576  break;
577  case UploadBase::ILLEGAL_FILENAME:
578  $this->dieRecoverableError(
580  'illegal-filename', null, [ 'filename' => $verification['filtered'] ]
581  ) ], 'filename'
582  );
583  break;
584  case UploadBase::FILENAME_TOO_LONG:
585  $this->dieRecoverableError( [ 'filename-toolong' ], 'filename' );
586  break;
587  case UploadBase::FILETYPE_MISSING:
588  $this->dieRecoverableError( [ 'filetype-missing' ], 'filename' );
589  break;
590  case UploadBase::WINDOWS_NONASCII_FILENAME:
591  $this->dieRecoverableError( [ 'windows-nonascii-filename' ], 'filename' );
592  break;
593 
594  // Unrecoverable errors
595  case UploadBase::EMPTY_FILE:
596  $this->dieWithError( 'empty-file' );
597  break;
598  case UploadBase::FILE_TOO_LARGE:
599  $this->dieWithError( 'file-too-large' );
600  break;
601 
602  case UploadBase::FILETYPE_BADTYPE:
603  $extradata = [
604  'filetype' => $verification['finalExt'],
605  'allowed' => array_values( array_unique( $this->getConfig()->get( 'FileExtensions' ) ) )
606  ];
607  $extensions = array_unique( $this->getConfig()->get( 'FileExtensions' ) );
608  $msg = [
609  'filetype-banned-type',
610  null, // filled in below
611  Message::listParam( $extensions, 'comma' ),
612  count( $extensions ),
613  null, // filled in below
614  ];
615  ApiResult::setIndexedTagName( $extradata['allowed'], 'ext' );
616 
617  if ( isset( $verification['blacklistedExt'] ) ) {
618  $msg[1] = Message::listParam( $verification['blacklistedExt'], 'comma' );
619  $msg[4] = count( $verification['blacklistedExt'] );
620  $extradata['blacklisted'] = array_values( $verification['blacklistedExt'] );
621  ApiResult::setIndexedTagName( $extradata['blacklisted'], 'ext' );
622  } else {
623  $msg[1] = $verification['finalExt'];
624  $msg[4] = 1;
625  }
626 
627  $this->dieWithError( $msg, 'filetype-banned', $extradata );
628  break;
629 
630  case UploadBase::VERIFICATION_ERROR:
631  $msg = ApiMessage::create( $verification['details'], 'verification-error' );
632  if ( $verification['details'][0] instanceof MessageSpecifier ) {
633  $details = array_merge( [ $msg->getKey() ], $msg->getParams() );
634  } else {
635  $details = $verification['details'];
636  }
637  ApiResult::setIndexedTagName( $details, 'detail' );
638  $msg->setApiData( $msg->getApiData() + [ 'details' => $details ] );
639  // @phan-suppress-next-line PhanTypeMismatchArgument
640  $this->dieWithError( $msg );
641  break;
642 
643  case UploadBase::HOOK_ABORTED:
644  $msg = $verification['error'] === '' ? 'hookaborted' : $verification['error'];
645  $this->dieWithError( $msg, 'hookaborted', [ 'details' => $verification['error'] ] );
646  break;
647  default:
648  $this->dieWithError( 'apierror-unknownerror-nocode', 'unknown-error',
649  [ 'details' => [ 'code' => $verification['status'] ] ] );
650  break;
651  }
652  }
653 
661  protected function getApiWarnings() {
662  $warnings = UploadBase::makeWarningsSerializable( $this->mUpload->checkWarnings() );
663 
664  return $this->transformWarnings( $warnings );
665  }
666 
667  protected function transformWarnings( $warnings ) {
668  if ( $warnings ) {
669  // Add indices
670  ApiResult::setIndexedTagName( $warnings, 'warning' );
671 
672  if ( isset( $warnings['duplicate'] ) ) {
673  $dupes = [];
674  foreach ( $warnings['duplicate'] as $dupe ) {
675  $dupes[] = $dupe['fileName'];
676  }
677  ApiResult::setIndexedTagName( $dupes, 'duplicate' );
678  $warnings['duplicate'] = $dupes;
679  }
680 
681  if ( isset( $warnings['exists'] ) ) {
682  $warning = $warnings['exists'];
683  unset( $warnings['exists'] );
684  $localFile = $warning['normalizedFile'] ?? $warning['file'];
685  $warnings[$warning['warning']] = $localFile['fileName'];
686  }
687 
688  if ( isset( $warnings['no-change'] ) ) {
689  $file = $warnings['no-change'];
690  unset( $warnings['no-change'] );
691 
692  $warnings['nochange'] = [
693  'timestamp' => wfTimestamp( TS_ISO_8601, $file['timestamp'] )
694  ];
695  }
696 
697  if ( isset( $warnings['duplicate-version'] ) ) {
698  $dupes = [];
699  foreach ( $warnings['duplicate-version'] as $dupe ) {
700  $dupes[] = [
701  'timestamp' => wfTimestamp( TS_ISO_8601, $dupe['timestamp'] )
702  ];
703  }
704  unset( $warnings['duplicate-version'] );
705 
706  ApiResult::setIndexedTagName( $dupes, 'ver' );
707  $warnings['duplicateversions'] = $dupes;
708  }
709  }
710 
711  return $warnings;
712  }
713 
720  protected function handleStashException( $e ) {
721  switch ( get_class( $e ) ) {
722  case UploadStashFileNotFoundException::class:
723  $wrap = 'apierror-stashedfilenotfound';
724  break;
725  case UploadStashBadPathException::class:
726  $wrap = 'apierror-stashpathinvalid';
727  break;
728  case UploadStashFileException::class:
729  $wrap = 'apierror-stashfilestorage';
730  break;
731  case UploadStashZeroLengthFileException::class:
732  $wrap = 'apierror-stashzerolength';
733  break;
734  case UploadStashNotLoggedInException::class:
736  [ 'apierror-mustbeloggedin', $this->msg( 'action-upload' ) ], 'stashnotloggedin'
737  ) );
738  case UploadStashWrongOwnerException::class:
739  $wrap = 'apierror-stashwrongowner';
740  break;
741  case UploadStashNoSuchKeyException::class:
742  $wrap = 'apierror-stashnosuchfilekey';
743  break;
744  default:
745  $wrap = [ 'uploadstash-exception', get_class( $e ) ];
746  break;
747  }
748  return StatusValue::newFatal(
749  $this->getErrorFormatter()->getMessageFromException( $e, [ 'wrap' => $wrap ] )
750  );
751  }
752 
760  protected function performUpload( $warnings ) {
761  // Use comment as initial page text by default
762  if ( is_null( $this->mParams['text'] ) ) {
763  $this->mParams['text'] = $this->mParams['comment'];
764  }
765 
767  $file = $this->mUpload->getLocalFile();
768 
769  // For preferences mode, we want to watch if 'watchdefault' is set,
770  // or if the *file* doesn't exist, and either 'watchuploads' or
771  // 'watchcreations' is set. But getWatchlistValue()'s automatic
772  // handling checks if the *title* exists or not, so we need to check
773  // all three preferences manually.
774  $watch = $this->getWatchlistValue(
775  $this->mParams['watchlist'], $file->getTitle(), 'watchdefault'
776  );
777 
778  if ( !$watch && $this->mParams['watchlist'] == 'preferences' && !$file->exists() ) {
779  $watch = (
780  $this->getWatchlistValue( 'preferences', $file->getTitle(), 'watchuploads' ) ||
781  $this->getWatchlistValue( 'preferences', $file->getTitle(), 'watchcreations' )
782  );
783  }
784 
785  // Deprecated parameters
786  if ( $this->mParams['watch'] ) {
787  $watch = true;
788  }
789 
790  if ( $this->mParams['tags'] ) {
791  $status = ChangeTags::canAddTagsAccompanyingChange( $this->mParams['tags'], $this->getUser() );
792  if ( !$status->isOK() ) {
793  $this->dieStatus( $status );
794  }
795  }
796 
797  // No errors, no warnings: do the upload
798  $result = [];
799  if ( $this->mParams['async'] ) {
800  $progress = UploadBase::getSessionStatus( $this->getUser(), $this->mParams['filekey'] );
801  if ( $progress && $progress['result'] === 'Poll' ) {
802  $this->dieWithError( 'apierror-upload-inprogress', 'publishfailed' );
803  }
804  UploadBase::setSessionStatus(
805  $this->getUser(),
806  $this->mParams['filekey'],
807  [ 'result' => 'Poll', 'stage' => 'queued', 'status' => Status::newGood() ]
808  );
810  Title::makeTitle( NS_FILE, $this->mParams['filename'] ),
811  [
812  'filename' => $this->mParams['filename'],
813  'filekey' => $this->mParams['filekey'],
814  'comment' => $this->mParams['comment'],
815  'tags' => $this->mParams['tags'],
816  'text' => $this->mParams['text'],
817  'watch' => $watch,
818  'session' => $this->getContext()->exportSession()
819  ]
820  ) );
821  $result['result'] = 'Poll';
822  $result['stage'] = 'queued';
823  } else {
825  $status = $this->mUpload->performUpload( $this->mParams['comment'],
826  $this->mParams['text'], $watch, $this->getUser(), $this->mParams['tags'] );
827 
828  if ( !$status->isGood() ) {
829  $this->dieRecoverableError( $status->getErrors() );
830  }
831  $result['result'] = 'Success';
832  }
833 
834  $result['filename'] = $file->getName();
835  if ( $warnings && count( $warnings ) > 0 ) {
836  $result['warnings'] = $warnings;
837  }
838 
839  return $result;
840  }
841 
842  public function mustBePosted() {
843  return true;
844  }
845 
846  public function isWriteMode() {
847  return true;
848  }
849 
850  public function getAllowedParams() {
851  $params = [
852  'filename' => [
853  ApiBase::PARAM_TYPE => 'string',
854  ],
855  'comment' => [
856  ApiBase::PARAM_DFLT => ''
857  ],
858  'tags' => [
859  ApiBase::PARAM_TYPE => 'tags',
860  ApiBase::PARAM_ISMULTI => true,
861  ],
862  'text' => [
863  ApiBase::PARAM_TYPE => 'text',
864  ],
865  'watch' => [
866  ApiBase::PARAM_DFLT => false,
868  ],
869  'watchlist' => [
870  ApiBase::PARAM_DFLT => 'preferences',
872  'watch',
873  'preferences',
874  'nochange'
875  ],
876  ],
877  'ignorewarnings' => false,
878  'file' => [
879  ApiBase::PARAM_TYPE => 'upload',
880  ],
881  'url' => null,
882  'filekey' => null,
883  'sessionkey' => [
885  ],
886  'stash' => false,
887 
888  'filesize' => [
889  ApiBase::PARAM_TYPE => 'integer',
890  ApiBase::PARAM_MIN => 0,
891  ApiBase::PARAM_MAX => UploadBase::getMaxUploadSize(),
892  ],
893  'offset' => [
894  ApiBase::PARAM_TYPE => 'integer',
895  ApiBase::PARAM_MIN => 0,
896  ],
897  'chunk' => [
898  ApiBase::PARAM_TYPE => 'upload',
899  ],
900 
901  'async' => false,
902  'checkstatus' => false,
903  ];
904 
905  return $params;
906  }
907 
908  public function needsToken() {
909  return 'csrf';
910  }
911 
912  protected function getExamplesMessages() {
913  return [
914  'action=upload&filename=Wiki.png' .
915  '&url=http%3A//upload.wikimedia.org/wikipedia/en/b/bc/Wiki.png&token=123ABC'
916  => 'apihelp-upload-example-url',
917  'action=upload&filename=Wiki.png&filekey=filekey&ignorewarnings=1&token=123ABC'
918  => 'apihelp-upload-example-filekey',
919  ];
920  }
921 
922  public function getHelpUrls() {
923  return 'https://www.mediawiki.org/wiki/Special:MyLanguage/API:Upload';
924  }
925 }
ApiUpload\getChunkResult
getChunkResult( $warnings)
Get the result of a chunk upload.
Definition: ApiUpload.php:188
ContextSource\getConfig
getConfig()
Definition: ContextSource.php:63
ApiUpload\selectUploadModule
selectUploadModule()
Select an upload module and set it to mUpload.
Definition: ApiUpload.php:407
ContextSource\getContext
getContext()
Get the base IContextSource object.
Definition: ContextSource.php:40
ApiUpload\isWriteMode
isWriteMode()
Indicates whether this module requires write mode.
Definition: ApiUpload.php:846
StatusValue\newFatal
static newFatal( $message,... $parameters)
Factory function for fatal errors.
Definition: StatusValue.php:69
ApiUpload\getStashResult
getStashResult( $warnings)
Get Stash Result, throws an exception if the file could not be stashed.
Definition: ApiUpload.php:154
User\newFatalPermissionDeniedStatus
static newFatalPermissionDeniedStatus( $permission)
Factory function for fatal permission-denied errors.
Definition: User.php:5284
ApiUpload\verifyUpload
verifyUpload()
Performs file verification, dies on error.
Definition: ApiUpload.php:558
ApiBase\dieWithError
dieWithError( $msg, $code=null, $data=null, $httpCode=null)
Abort execution with an error.
Definition: ApiBase.php:2014
wfTimestamp
wfTimestamp( $outputtype=TS_UNIX, $ts=0)
Get a timestamp string in one of various formats.
Definition: GlobalFunctions.php:1869
ApiBase\PARAM_TYPE
const PARAM_TYPE
(string|string[]) Either an array of allowed value strings, or a string type as described below.
Definition: ApiBase.php:94
ApiBase\getResult
getResult()
Get the result object.
Definition: ApiBase.php:640
ApiUpload\getWarningsResult
getWarningsResult( $warnings)
Get Warnings Result.
Definition: ApiUpload.php:172
MessageSpecifier
Definition: MessageSpecifier.php:21
NS_FILE
const NS_FILE
Definition: Defines.php:66
$file
if(PHP_SAPI !='cli-server') if(!isset( $_SERVER['SCRIPT_FILENAME'])) $file
Item class for a filearchive table row.
Definition: router.php:42
ApiUpload\execute
execute()
Evaluates the parameters, performs the requested query, and sets up the result.
Definition: ApiUpload.php:32
ApiUpload\$mUpload
UploadBase UploadFromChunks $mUpload
Definition: ApiUpload.php:28
ApiUpload\getApiWarnings
getApiWarnings()
Check warnings.
Definition: ApiUpload.php:661
ApiUpload\getAllowedParams
getAllowedParams()
Returns an array of allowed parameters (parameter name) => (default value) or (parameter name) => (ar...
Definition: ApiUpload.php:850
ApiBase\dieBlocked
dieBlocked(AbstractBlock $block)
Throw an ApiUsageException, which will (if uncaught) call the main module's error handler and die wit...
Definition: ApiBase.php:2055
UploadFromStash
Implements uploading from previously stored file.
Definition: UploadFromStash.php:30
value
if( $inline) $status value
Definition: SyntaxHighlight.php:346
ApiUpload\$mParams
$mParams
Definition: ApiUpload.php:30
ContextSource\getUser
getUser()
Definition: ContextSource.php:120
ApiUpload\getExamplesMessages
getExamplesMessages()
Returns usage examples for this module.
Definition: ApiUpload.php:912
ApiBase
This abstract class implements many basic API functions, and is the base of all API classes.
Definition: ApiBase.php:42
ApiMessage
Extension of Message implementing IApiMessage.
Definition: ApiMessage.php:26
UploadFromUrl\initialize
initialize( $name, $url)
Entry point for API upload.
Definition: UploadFromUrl.php:140
PublishStashedFileJob
Upload a file from the upload stash into the local file repo.
Definition: PublishStashedFileJob.php:32
ApiBase\PARAM_DEPRECATED
const PARAM_DEPRECATED
(boolean) Is the parameter deprecated (will show a warning)?
Definition: ApiBase.php:112
ApiBase\PARAM_MIN
const PARAM_MIN
(integer) Lowest value allowed for the parameter, for PARAM_TYPE 'integer' and 'limit'.
Definition: ApiBase.php:106
ApiUpload\dieStatusWithCode
dieStatusWithCode( $status, $overrideCode, $moreExtraData=null)
Like dieStatus(), but always uses $overrideCode for the error code, unless the code comes from IApiMe...
Definition: ApiUpload.php:388
UploadFromUrl
Implements uploading from a HTTP resource.
Definition: UploadFromUrl.php:34
ApiUpload\performStash
performStash( $failureMode, &$data=null)
Stash the file and add the file key, or error information if it fails, to the data.
Definition: ApiUpload.php:312
UploadFromUrl\isEnabled
static isEnabled()
Checks if the upload from URL feature is enabled.
Definition: UploadFromUrl.php:65
ApiUpload\dieRecoverableError
dieRecoverableError( $errors, $parameter=null)
Throw an error that the user can recover from by providing a better value for $parameter.
Definition: ApiUpload.php:363
ApiBase\PARAM_MAX
const PARAM_MAX
(integer) Max value allowed for the parameter, for PARAM_TYPE 'integer' and 'limit'.
Definition: ApiBase.php:97
ApiBase\extractRequestParams
extractRequestParams( $options=[])
Using getAllowedParams(), this function makes an array of the values provided by the user,...
Definition: ApiBase.php:761
ApiUpload\getContextResult
getContextResult()
Get an upload result based on upload context.
Definition: ApiUpload.php:125
ApiUpload\handleStashException
handleStashException( $e)
Handles a stash exception, giving a useful error to the user.
Definition: ApiUpload.php:720
ApiUpload
Definition: ApiUpload.php:26
Title\makeTitle
static makeTitle( $ns, $title, $fragment='', $interwiki='')
Create a new Title from a namespace index and a DB key.
Definition: Title.php:586
ApiMessage\create
static create( $msg, $code=null, array $data=null)
Create an IApiMessage for the message.
Definition: ApiMessage.php:40
wfDebug
wfDebug( $text, $dest='all', array $context=[])
Sends a line to the debug log if enabled or, optionally, to a comment in output.
Definition: GlobalFunctions.php:913
ApiBase\getWatchlistValue
getWatchlistValue( $watchlist, $titleObj, $userOption=null)
Return true if we're to watch the page, false if not, null if no change.
Definition: ApiBase.php:1099
ApiUpload\needsToken
needsToken()
Returns the token type this module requires in order to execute.
Definition: ApiUpload.php:908
ContextSource\msg
msg( $key,... $params)
Get a Message object with context set Parameters are the same as wfMessage()
Definition: ContextSource.php:168
ApiResult\setIndexedTagName
static setIndexedTagName(array &$arr, $tag)
Set the tag name for numeric-keyed values in XML format.
Definition: ApiResult.php:616
UploadFromStash\isValidKey
static isValidKey( $key)
Definition: UploadFromStash.php:71
UploadFromUrl\isAllowedUrl
static isAllowedUrl( $url)
Checks whether the URL is not allowed.
Definition: UploadFromUrl.php:123
StatusValue\newGood
static newGood( $value=null)
Factory function for good results.
Definition: StatusValue.php:81
AssembleUploadChunksJob
Assemble the segments of a chunked upload.
Definition: AssembleUploadChunksJob.php:30
ApiUpload\getHelpUrls
getHelpUrls()
Return links to more detailed help pages about the module.
Definition: ApiUpload.php:922
ApiUpload\mustBePosted
mustBePosted()
Indicates whether this module must be called with a POST request.
Definition: ApiUpload.php:842
ApiUpload\performUpload
performUpload( $warnings)
Perform the actual upload.
Definition: ApiUpload.php:760
ApiBase\requireOnlyOneParameter
requireOnlyOneParameter( $params, $required)
Die if none or more than one of a certain set of parameters is set and not false.
Definition: ApiBase.php:893
UploadFromUrl\isAllowedHost
static isAllowedHost( $url)
Checks whether the URL is for an allowed host The domains in the whitelist can include wildcard chara...
Definition: UploadFromUrl.php:79
ChangeTags\canAddTagsAccompanyingChange
static canAddTagsAccompanyingChange(array $tags, User $user=null)
Is it OK to allow the user to apply all the specified tags at the same time as they edit/make the cha...
Definition: ChangeTags.php:521
$status
return $status
Definition: SyntaxHighlight.php:347
JobQueueGroup\singleton
static singleton( $domain=false)
Definition: JobQueueGroup.php:70
ContextSource\exportSession
exportSession()
Export the resolved user IP, HTTP headers, user ID, and session ID.
Definition: ContextSource.php:179
ApiBase\PARAM_DFLT
const PARAM_DFLT
(null|boolean|integer|string) Default value of the parameter.
Definition: ApiBase.php:55
ApiUpload\checkPermissions
checkPermissions( $user)
Checks that the user has permissions to perform this upload.
Definition: ApiUpload.php:532
ApiBase\dieStatus
dieStatus(StatusValue $status)
Throw an ApiUsageException based on the Status object.
Definition: ApiBase.php:2086
ApiBase\getModuleName
getModuleName()
Get the name of the module being executed by this instance.
Definition: ApiBase.php:520
ApiBase\PARAM_ISMULTI
const PARAM_ISMULTI
(boolean) Accept multiple pipe-separated values for this parameter (e.g.
Definition: ApiBase.php:58
ApiBase\getMain
getMain()
Get the main module.
Definition: ApiBase.php:536
UploadFromChunks
Implements uploading from chunks.
Definition: UploadFromChunks.php:33
ApiUpload\transformWarnings
transformWarnings( $warnings)
Definition: ApiUpload.php:667
ApiBase\dieDebug
static dieDebug( $method, $message)
Internal code errors should be reported with this method.
Definition: ApiBase.php:2220
UploadFromFile
Implements regular file uploads.
Definition: UploadFromFile.php:30
ApiBase\getErrorFormatter
getErrorFormatter()
Get the error formatter.
Definition: ApiBase.php:654
UploadStashException
Definition: UploadStashException.php:26
ApiUpload\checkVerification
checkVerification(array $verification)
Performs file verification, dies on error.
Definition: ApiUpload.php:571