MediaWiki  master
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  // @phan-suppress-next-line PhanTypeArraySuspiciousNullable False positive
113  if ( $result['result'] === 'Success' ) {
114  $imageinfo = $this->mUpload->getImageInfo( $this->getResult() );
115  $this->getResult()->addValue( $this->getModuleName(), 'imageinfo', $imageinfo );
116  }
117 
118  // Cleanup any temporary mess
119  $this->mUpload->cleanupTempFile();
120  }
121 
126  private function getContextResult() {
127  $warnings = $this->getApiWarnings();
128  if ( $warnings && !$this->mParams['ignorewarnings'] ) {
129  // Get warnings formatted in result array format
130  return $this->getWarningsResult( $warnings );
131  } elseif ( $this->mParams['chunk'] ) {
132  // Add chunk, and get result
133  return $this->getChunkResult( $warnings );
134  } elseif ( $this->mParams['stash'] ) {
135  // Stash the file and get stash result
136  return $this->getStashResult( $warnings );
137  }
138 
139  // Check throttle after we've handled warnings
140  if ( UploadBase::isThrottled( $this->getUser() )
141  ) {
142  $this->dieWithError( 'apierror-ratelimited' );
143  }
144 
145  // This is the most common case -- a normal upload with no warnings
146  // performUpload will return a formatted properly for the API with status
147  return $this->performUpload( $warnings );
148  }
149 
155  private function getStashResult( $warnings ) {
156  $result = [];
157  $result['result'] = 'Success';
158  if ( $warnings && count( $warnings ) > 0 ) {
159  $result['warnings'] = $warnings;
160  }
161  // Some uploads can request they be stashed, so as not to publish them immediately.
162  // In this case, a failure to stash ought to be fatal
163  $this->performStash( 'critical', $result );
164 
165  return $result;
166  }
167 
173  private function getWarningsResult( $warnings ) {
174  $result = [];
175  $result['result'] = 'Warning';
176  $result['warnings'] = $warnings;
177  // in case the warnings can be fixed with some further user action, let's stash this upload
178  // and return a key they can use to restart it
179  $this->performStash( 'optional', $result );
180 
181  return $result;
182  }
183 
190  public static function getMinUploadChunkSize( Config $config ) {
191  $configured = $config->get( 'MinUploadChunkSize' );
192 
193  // Leave some room for other POST parameters
194  $postMax = (
196  ini_get( 'post_max_size' ),
197  PHP_INT_MAX
198  ) ?: PHP_INT_MAX
199  ) - 1024;
200 
201  // Ensure the minimum chunk size is less than PHP upload limits
202  // or the maximum upload size.
203  return min(
204  $configured,
207  $postMax
208  );
209  }
210 
216  private function getChunkResult( $warnings ) {
217  $result = [];
218 
219  if ( $warnings && count( $warnings ) > 0 ) {
220  $result['warnings'] = $warnings;
221  }
222 
223  $request = $this->getMain()->getRequest();
224  $chunkPath = $request->getFileTempname( 'chunk' );
225  $chunkSize = $request->getUpload( 'chunk' )->getSize();
226  $totalSoFar = $this->mParams['offset'] + $chunkSize;
227  $minChunkSize = self::getMinUploadChunkSize( $this->getConfig() );
228 
229  // Sanity check sizing
230  if ( $totalSoFar > $this->mParams['filesize'] ) {
231  $this->dieWithError( 'apierror-invalid-chunk' );
232  }
233 
234  // Enforce minimum chunk size
235  if ( $totalSoFar != $this->mParams['filesize'] && $chunkSize < $minChunkSize ) {
236  $this->dieWithError( [ 'apierror-chunk-too-small', Message::numParam( $minChunkSize ) ] );
237  }
238 
239  if ( $this->mParams['offset'] == 0 ) {
240  $filekey = $this->performStash( 'critical' );
241  } else {
242  $filekey = $this->mParams['filekey'];
243 
244  // Don't allow further uploads to an already-completed session
245  $progress = UploadBase::getSessionStatus( $this->getUser(), $filekey );
246  if ( !$progress ) {
247  // Probably can't get here, but check anyway just in case
248  $this->dieWithError( 'apierror-stashfailed-nosession', 'stashfailed' );
249  } elseif ( $progress['result'] !== 'Continue' || $progress['stage'] !== 'uploading' ) {
250  $this->dieWithError( 'apierror-stashfailed-complete', 'stashfailed' );
251  }
252 
253  $status = $this->mUpload->addChunk(
254  $chunkPath, $chunkSize, $this->mParams['offset'] );
255  if ( !$status->isGood() ) {
256  $extradata = [
257  'offset' => $this->mUpload->getOffset(),
258  ];
259 
260  $this->dieStatusWithCode( $status, 'stashfailed', $extradata );
261  }
262  }
263 
264  // Check we added the last chunk:
265  if ( $totalSoFar == $this->mParams['filesize'] ) {
266  if ( $this->mParams['async'] ) {
268  $this->getUser(),
269  $filekey,
270  [ 'result' => 'Poll',
271  'stage' => 'queued', 'status' => Status::newGood() ]
272  );
274  Title::makeTitle( NS_FILE, $filekey ),
275  [
276  'filename' => $this->mParams['filename'],
277  'filekey' => $filekey,
278  'session' => $this->getContext()->exportSession()
279  ]
280  ) );
281  $result['result'] = 'Poll';
282  $result['stage'] = 'queued';
283  } else {
284  $status = $this->mUpload->concatenateChunks();
285  if ( !$status->isGood() ) {
287  $this->getUser(),
288  $filekey,
289  [ 'result' => 'Failure', 'stage' => 'assembling', 'status' => $status ]
290  );
291  $this->dieStatusWithCode( $status, 'stashfailed' );
292  }
293 
294  // We can only get warnings like 'duplicate' after concatenating the chunks
295  $warnings = $this->getApiWarnings();
296  if ( $warnings ) {
297  $result['warnings'] = $warnings;
298  }
299 
300  // The fully concatenated file has a new filekey. So remove
301  // the old filekey and fetch the new one.
302  UploadBase::setSessionStatus( $this->getUser(), $filekey, false );
303  $this->mUpload->stash->removeFile( $filekey );
304  $filekey = $this->mUpload->getStashFile()->getFileKey();
305 
306  $result['result'] = 'Success';
307  }
308  } else {
310  $this->getUser(),
311  $filekey,
312  [
313  'result' => 'Continue',
314  'stage' => 'uploading',
315  'offset' => $totalSoFar,
316  'status' => Status::newGood(),
317  ]
318  );
319  $result['result'] = 'Continue';
320  $result['offset'] = $totalSoFar;
321  }
322 
323  $result['filekey'] = $filekey;
324 
325  return $result;
326  }
327 
340  private function performStash( $failureMode, &$data = null ) {
341  $isPartial = (bool)$this->mParams['chunk'];
342  try {
343  $status = $this->mUpload->tryStashFile( $this->getUser(), $isPartial );
344 
345  if ( $status->isGood() && !$status->getValue() ) {
346  // Not actually a 'good' status...
347  $status->fatal( new ApiMessage( 'apierror-stashinvalidfile', 'stashfailed' ) );
348  }
349  } catch ( Exception $e ) {
350  $debugMessage = 'Stashing temporary file failed: ' . get_class( $e ) . ' ' . $e->getMessage();
351  wfDebug( __METHOD__ . ' ' . $debugMessage . "\n" );
352  $status = Status::newFatal( $this->getErrorFormatter()->getMessageFromException(
353  $e, [ 'wrap' => new ApiMessage( 'apierror-stashexception', 'stashfailed' ) ]
354  ) );
355  }
356 
357  if ( $status->isGood() ) {
358  $stashFile = $status->getValue();
359  $data['filekey'] = $stashFile->getFileKey();
360  // Backwards compatibility
361  $data['sessionkey'] = $data['filekey'];
362  return $data['filekey'];
363  }
364 
365  if ( $status->getMessage()->getKey() === 'uploadstash-exception' ) {
366  // The exceptions thrown by upload stash code and pretty silly and UploadBase returns poor
367  // Statuses for it. Just extract the exception details and parse them ourselves.
368  list( $exceptionType, $message ) = $status->getMessage()->getParams();
369  $debugMessage = 'Stashing temporary file failed: ' . $exceptionType . ' ' . $message;
370  wfDebug( __METHOD__ . ' ' . $debugMessage . "\n" );
371  }
372 
373  // Bad status
374  if ( $failureMode !== 'optional' ) {
375  $this->dieStatus( $status );
376  } else {
377  $data['stasherrors'] = $this->getErrorFormatter()->arrayFromStatus( $status );
378  return null;
379  }
380  }
381 
391  private function dieRecoverableError( $errors, $parameter = null ) {
392  $this->performStash( 'optional', $data );
393 
394  if ( $parameter ) {
395  $data['invalidparameter'] = $parameter;
396  }
397 
398  $sv = StatusValue::newGood();
399  foreach ( $errors as $error ) {
400  $msg = ApiMessage::create( $error );
401  $msg->setApiData( $msg->getApiData() + $data );
402  $sv->fatal( $msg );
403  }
404  $this->dieStatus( $sv );
405  }
406 
416  public function dieStatusWithCode( $status, $overrideCode, $moreExtraData = null ) {
417  $sv = StatusValue::newGood();
418  foreach ( $status->getErrors() as $error ) {
419  $msg = ApiMessage::create( $error, $overrideCode );
420  if ( $moreExtraData ) {
421  $msg->setApiData( $msg->getApiData() + $moreExtraData );
422  }
423  $sv->fatal( $msg );
424  }
425  $this->dieStatus( $sv );
426  }
427 
436  protected function selectUploadModule() {
437  $request = $this->getMain()->getRequest();
438 
439  // chunk or one and only one of the following parameters is needed
440  if ( !$this->mParams['chunk'] ) {
441  $this->requireOnlyOneParameter( $this->mParams,
442  'filekey', 'file', 'url' );
443  }
444 
445  // Status report for "upload to stash"/"upload from stash"
446  if ( $this->mParams['filekey'] && $this->mParams['checkstatus'] ) {
447  $progress = UploadBase::getSessionStatus( $this->getUser(), $this->mParams['filekey'] );
448  if ( !$progress ) {
449  $this->dieWithError( 'apierror-upload-missingresult', 'missingresult' );
450  } elseif ( !$progress['status']->isGood() ) {
451  $this->dieStatusWithCode( $progress['status'], 'stashfailed' );
452  }
453  if ( isset( $progress['status']->value['verification'] ) ) {
454  $this->checkVerification( $progress['status']->value['verification'] );
455  }
456  if ( isset( $progress['status']->value['warnings'] ) ) {
457  $warnings = $this->transformWarnings( $progress['status']->value['warnings'] );
458  if ( $warnings ) {
459  $progress['warnings'] = $warnings;
460  }
461  }
462  unset( $progress['status'] ); // remove Status object
463  $imageinfo = null;
464  if ( isset( $progress['imageinfo'] ) ) {
465  $imageinfo = $progress['imageinfo'];
466  unset( $progress['imageinfo'] );
467  }
468 
469  $this->getResult()->addValue( null, $this->getModuleName(), $progress );
470  // Add 'imageinfo' in a separate addValue() call. File metadata can be unreasonably large,
471  // so otherwise when it exceeded $wgAPIMaxResultSize, no result would be returned (T143993).
472  if ( $imageinfo ) {
473  $this->getResult()->addValue( $this->getModuleName(), 'imageinfo', $imageinfo );
474  }
475 
476  return false;
477  }
478 
479  // The following modules all require the filename parameter to be set
480  if ( $this->mParams['filename'] === null ) {
481  $this->dieWithError( [ 'apierror-missingparam', 'filename' ] );
482  }
483 
484  if ( $this->mParams['chunk'] ) {
485  // Chunk upload
486  $this->mUpload = new UploadFromChunks( $this->getUser() );
487  if ( isset( $this->mParams['filekey'] ) ) {
488  if ( $this->mParams['offset'] === 0 ) {
489  $this->dieWithError( 'apierror-upload-filekeynotallowed', 'filekeynotallowed' );
490  }
491 
492  // handle new chunk
493  $this->mUpload->continueChunks(
494  $this->mParams['filename'],
495  $this->mParams['filekey'],
496  $request->getUpload( 'chunk' )
497  );
498  } else {
499  if ( $this->mParams['offset'] !== 0 ) {
500  $this->dieWithError( 'apierror-upload-filekeyneeded', 'filekeyneeded' );
501  }
502 
503  // handle first chunk
504  $this->mUpload->initialize(
505  $this->mParams['filename'],
506  $request->getUpload( 'chunk' )
507  );
508  }
509  } elseif ( isset( $this->mParams['filekey'] ) ) {
510  // Upload stashed in a previous request
511  if ( !UploadFromStash::isValidKey( $this->mParams['filekey'] ) ) {
512  $this->dieWithError( 'apierror-invalid-file-key' );
513  }
514 
515  $this->mUpload = new UploadFromStash( $this->getUser() );
516  // This will not download the temp file in initialize() in async mode.
517  // We still have enough information to call checkWarnings() and such.
518  $this->mUpload->initialize(
519  $this->mParams['filekey'], $this->mParams['filename'], !$this->mParams['async']
520  );
521  } elseif ( isset( $this->mParams['file'] ) ) {
522  // Can't async upload directly from a POSTed file, we'd have to
523  // stash the file and then queue the publish job. The user should
524  // just submit the two API queries to perform those two steps.
525  if ( $this->mParams['async'] ) {
526  $this->dieWithError( 'apierror-cannot-async-upload-file' );
527  }
528 
529  $this->mUpload = new UploadFromFile();
530  $this->mUpload->initialize(
531  $this->mParams['filename'],
532  $request->getUpload( 'file' )
533  );
534  } elseif ( isset( $this->mParams['url'] ) ) {
535  // Make sure upload by URL is enabled:
536  if ( !UploadFromUrl::isEnabled() ) {
537  $this->dieWithError( 'copyuploaddisabled' );
538  }
539 
540  if ( !UploadFromUrl::isAllowedHost( $this->mParams['url'] ) ) {
541  $this->dieWithError( 'apierror-copyuploadbaddomain' );
542  }
543 
544  if ( !UploadFromUrl::isAllowedUrl( $this->mParams['url'] ) ) {
545  $this->dieWithError( 'apierror-copyuploadbadurl' );
546  }
547 
548  $this->mUpload = new UploadFromUrl;
549  $this->mUpload->initialize( $this->mParams['filename'],
550  $this->mParams['url'] );
551  }
552 
553  return true;
554  }
555 
561  protected function checkPermissions( $user ) {
562  // Check whether the user has the appropriate permissions to upload anyway
563  $permission = $this->mUpload->isAllowed( $user );
564 
565  if ( $permission !== true ) {
566  if ( !$user->isLoggedIn() ) {
567  $this->dieWithError( [ 'apierror-mustbeloggedin', $this->msg( 'action-upload' ) ] );
568  }
569 
570  $this->dieStatus( User::newFatalPermissionDeniedStatus( $permission ) );
571  }
572 
573  // Check blocks
574  if ( $user->isBlockedFromUpload() ) {
575  $this->dieBlocked( $user->getBlock() );
576  }
577 
578  // Global blocks
579  if ( $user->isBlockedGlobally() ) {
580  $this->dieBlocked( $user->getGlobalBlock() );
581  }
582  }
583 
587  protected function verifyUpload() {
588  $verification = $this->mUpload->verifyUpload();
589  if ( $verification['status'] === UploadBase::OK ) {
590  return;
591  }
592 
593  $this->checkVerification( $verification );
594  }
595 
600  protected function checkVerification( array $verification ) {
601  switch ( $verification['status'] ) {
602  // Recoverable errors
604  $this->dieRecoverableError( [ 'filename-tooshort' ], 'filename' );
605  break;
607  $this->dieRecoverableError(
609  'illegal-filename', null, [ 'filename' => $verification['filtered'] ]
610  ) ], 'filename'
611  );
612  break;
614  $this->dieRecoverableError( [ 'filename-toolong' ], 'filename' );
615  break;
617  $this->dieRecoverableError( [ 'filetype-missing' ], 'filename' );
618  break;
620  $this->dieRecoverableError( [ 'windows-nonascii-filename' ], 'filename' );
621  break;
622 
623  // Unrecoverable errors
625  $this->dieWithError( 'empty-file' );
626  break;
628  $this->dieWithError( 'file-too-large' );
629  break;
630 
632  $extradata = [
633  'filetype' => $verification['finalExt'],
634  'allowed' => array_values( array_unique( $this->getConfig()->get( 'FileExtensions' ) ) )
635  ];
636  $extensions = array_unique( $this->getConfig()->get( 'FileExtensions' ) );
637  $msg = [
638  'filetype-banned-type',
639  null, // filled in below
640  Message::listParam( $extensions, 'comma' ),
641  count( $extensions ),
642  null, // filled in below
643  ];
644  ApiResult::setIndexedTagName( $extradata['allowed'], 'ext' );
645 
646  if ( isset( $verification['blacklistedExt'] ) ) {
647  $msg[1] = Message::listParam( $verification['blacklistedExt'], 'comma' );
648  $msg[4] = count( $verification['blacklistedExt'] );
649  $extradata['blacklisted'] = array_values( $verification['blacklistedExt'] );
650  ApiResult::setIndexedTagName( $extradata['blacklisted'], 'ext' );
651  } else {
652  $msg[1] = $verification['finalExt'];
653  $msg[4] = 1;
654  }
655 
656  $this->dieWithError( $msg, 'filetype-banned', $extradata );
657  break;
658 
660  $msg = ApiMessage::create( $verification['details'], 'verification-error' );
661  if ( $verification['details'][0] instanceof MessageSpecifier ) {
662  $details = array_merge( [ $msg->getKey() ], $msg->getParams() );
663  } else {
664  $details = $verification['details'];
665  }
666  ApiResult::setIndexedTagName( $details, 'detail' );
667  $msg->setApiData( $msg->getApiData() + [ 'details' => $details ] );
668  // @phan-suppress-next-line PhanTypeMismatchArgument
669  $this->dieWithError( $msg );
670  break;
671 
673  $msg = $verification['error'] === '' ? 'hookaborted' : $verification['error'];
674  $this->dieWithError( $msg, 'hookaborted', [ 'details' => $verification['error'] ] );
675  break;
676  default:
677  $this->dieWithError( 'apierror-unknownerror-nocode', 'unknown-error',
678  [ 'details' => [ 'code' => $verification['status'] ] ] );
679  break;
680  }
681  }
682 
690  protected function getApiWarnings() {
692  $this->mUpload->checkWarnings( $this->getUser() )
693  );
694 
695  return $this->transformWarnings( $warnings );
696  }
697 
698  protected function transformWarnings( $warnings ) {
699  if ( $warnings ) {
700  // Add indices
701  ApiResult::setIndexedTagName( $warnings, 'warning' );
702 
703  if ( isset( $warnings['duplicate'] ) ) {
704  $dupes = [];
705  foreach ( $warnings['duplicate'] as $dupe ) {
706  $dupes[] = $dupe['fileName'];
707  }
708  ApiResult::setIndexedTagName( $dupes, 'duplicate' );
709  $warnings['duplicate'] = $dupes;
710  }
711 
712  if ( isset( $warnings['exists'] ) ) {
713  $warning = $warnings['exists'];
714  unset( $warnings['exists'] );
715  $localFile = $warning['normalizedFile'] ?? $warning['file'];
716  $warnings[$warning['warning']] = $localFile['fileName'];
717  }
718 
719  if ( isset( $warnings['no-change'] ) ) {
720  $file = $warnings['no-change'];
721  unset( $warnings['no-change'] );
722 
723  $warnings['nochange'] = [
724  'timestamp' => wfTimestamp( TS_ISO_8601, $file['timestamp'] )
725  ];
726  }
727 
728  if ( isset( $warnings['duplicate-version'] ) ) {
729  $dupes = [];
730  foreach ( $warnings['duplicate-version'] as $dupe ) {
731  $dupes[] = [
732  'timestamp' => wfTimestamp( TS_ISO_8601, $dupe['timestamp'] )
733  ];
734  }
735  unset( $warnings['duplicate-version'] );
736 
737  ApiResult::setIndexedTagName( $dupes, 'ver' );
738  $warnings['duplicateversions'] = $dupes;
739  }
740  }
741 
742  return $warnings;
743  }
744 
751  protected function handleStashException( $e ) {
752  switch ( get_class( $e ) ) {
753  case UploadStashFileNotFoundException::class:
754  $wrap = 'apierror-stashedfilenotfound';
755  break;
756  case UploadStashBadPathException::class:
757  $wrap = 'apierror-stashpathinvalid';
758  break;
759  case UploadStashFileException::class:
760  $wrap = 'apierror-stashfilestorage';
761  break;
762  case UploadStashZeroLengthFileException::class:
763  $wrap = 'apierror-stashzerolength';
764  break;
765  case UploadStashNotLoggedInException::class:
767  [ 'apierror-mustbeloggedin', $this->msg( 'action-upload' ) ], 'stashnotloggedin'
768  ) );
769  case UploadStashWrongOwnerException::class:
770  $wrap = 'apierror-stashwrongowner';
771  break;
772  case UploadStashNoSuchKeyException::class:
773  $wrap = 'apierror-stashnosuchfilekey';
774  break;
775  default:
776  $wrap = [ 'uploadstash-exception', get_class( $e ) ];
777  break;
778  }
779  return StatusValue::newFatal(
780  $this->getErrorFormatter()->getMessageFromException( $e, [ 'wrap' => $wrap ] )
781  );
782  }
783 
791  protected function performUpload( $warnings ) {
792  // Use comment as initial page text by default
793  if ( $this->mParams['text'] === null ) {
794  $this->mParams['text'] = $this->mParams['comment'];
795  }
796 
798  $file = $this->mUpload->getLocalFile();
799 
800  // For preferences mode, we want to watch if 'watchdefault' is set,
801  // or if the *file* doesn't exist, and either 'watchuploads' or
802  // 'watchcreations' is set. But getWatchlistValue()'s automatic
803  // handling checks if the *title* exists or not, so we need to check
804  // all three preferences manually.
805  $watch = $this->getWatchlistValue(
806  $this->mParams['watchlist'], $file->getTitle(), 'watchdefault'
807  );
808 
809  if ( !$watch && $this->mParams['watchlist'] == 'preferences' && !$file->exists() ) {
810  $watch = (
811  $this->getWatchlistValue( 'preferences', $file->getTitle(), 'watchuploads' ) ||
812  $this->getWatchlistValue( 'preferences', $file->getTitle(), 'watchcreations' )
813  );
814  }
815 
816  // Deprecated parameters
817  if ( $this->mParams['watch'] ) {
818  $watch = true;
819  }
820 
821  if ( $this->mParams['tags'] ) {
822  $status = ChangeTags::canAddTagsAccompanyingChange( $this->mParams['tags'], $this->getUser() );
823  if ( !$status->isOK() ) {
824  $this->dieStatus( $status );
825  }
826  }
827 
828  // No errors, no warnings: do the upload
829  $result = [];
830  if ( $this->mParams['async'] ) {
831  $progress = UploadBase::getSessionStatus( $this->getUser(), $this->mParams['filekey'] );
832  if ( $progress && $progress['result'] === 'Poll' ) {
833  $this->dieWithError( 'apierror-upload-inprogress', 'publishfailed' );
834  }
836  $this->getUser(),
837  $this->mParams['filekey'],
838  [ 'result' => 'Poll', 'stage' => 'queued', 'status' => Status::newGood() ]
839  );
841  Title::makeTitle( NS_FILE, $this->mParams['filename'] ),
842  [
843  'filename' => $this->mParams['filename'],
844  'filekey' => $this->mParams['filekey'],
845  'comment' => $this->mParams['comment'],
846  'tags' => $this->mParams['tags'],
847  'text' => $this->mParams['text'],
848  'watch' => $watch,
849  'session' => $this->getContext()->exportSession()
850  ]
851  ) );
852  $result['result'] = 'Poll';
853  $result['stage'] = 'queued';
854  } else {
856  $status = $this->mUpload->performUpload( $this->mParams['comment'],
857  $this->mParams['text'], $watch, $this->getUser(), $this->mParams['tags'] );
858 
859  if ( !$status->isGood() ) {
860  $this->dieRecoverableError( $status->getErrors() );
861  }
862  $result['result'] = 'Success';
863  }
864 
865  $result['filename'] = $file->getName();
866  if ( $warnings && count( $warnings ) > 0 ) {
867  $result['warnings'] = $warnings;
868  }
869 
870  return $result;
871  }
872 
873  public function mustBePosted() {
874  return true;
875  }
876 
877  public function isWriteMode() {
878  return true;
879  }
880 
881  public function getAllowedParams() {
882  $params = [
883  'filename' => [
884  ApiBase::PARAM_TYPE => 'string',
885  ],
886  'comment' => [
887  ApiBase::PARAM_DFLT => ''
888  ],
889  'tags' => [
890  ApiBase::PARAM_TYPE => 'tags',
891  ApiBase::PARAM_ISMULTI => true,
892  ],
893  'text' => [
894  ApiBase::PARAM_TYPE => 'text',
895  ],
896  'watch' => [
897  ApiBase::PARAM_DFLT => false,
899  ],
900  'watchlist' => [
901  ApiBase::PARAM_DFLT => 'preferences',
903  'watch',
904  'preferences',
905  'nochange'
906  ],
907  ],
908  'ignorewarnings' => false,
909  'file' => [
910  ApiBase::PARAM_TYPE => 'upload',
911  ],
912  'url' => null,
913  'filekey' => null,
914  'sessionkey' => [
916  ],
917  'stash' => false,
918 
919  'filesize' => [
920  ApiBase::PARAM_TYPE => 'integer',
921  ApiBase::PARAM_MIN => 0,
923  ],
924  'offset' => [
925  ApiBase::PARAM_TYPE => 'integer',
926  ApiBase::PARAM_MIN => 0,
927  ],
928  'chunk' => [
929  ApiBase::PARAM_TYPE => 'upload',
930  ],
931 
932  'async' => false,
933  'checkstatus' => false,
934  ];
935 
936  return $params;
937  }
938 
939  public function needsToken() {
940  return 'csrf';
941  }
942 
943  protected function getExamplesMessages() {
944  return [
945  'action=upload&filename=Wiki.png' .
946  '&url=http%3A//upload.wikimedia.org/wikipedia/en/b/bc/Wiki.png&token=123ABC'
947  => 'apihelp-upload-example-url',
948  'action=upload&filename=Wiki.png&filekey=filekey&ignorewarnings=1&token=123ABC'
949  => 'apihelp-upload-example-filekey',
950  ];
951  }
952 
953  public function getHelpUrls() {
954  return 'https://www.mediawiki.org/wiki/Special:MyLanguage/API:Upload';
955  }
956 }
UploadBase
Definition: UploadBase.php:42
ApiUpload\getChunkResult
getChunkResult( $warnings)
Get the result of a chunk upload.
Definition: ApiUpload.php:216
ContextSource\getConfig
getConfig()
Definition: ContextSource.php:63
ApiUpload\selectUploadModule
selectUploadModule()
Select an upload module and set it to mUpload.
Definition: ApiUpload.php:436
Message\numParam
static numParam( $num)
Definition: Message.php:1046
ContextSource\getContext
getContext()
Get the base IContextSource object.
Definition: ContextSource.php:40
UploadBase\VERIFICATION_ERROR
const VERIFICATION_ERROR
Definition: UploadBase.php:103
ApiUpload\isWriteMode
isWriteMode()
Indicates whether this module requires write mode.
Definition: ApiUpload.php:877
StatusValue\newFatal
static newFatal( $message,... $parameters)
Factory function for fatal errors.
Definition: StatusValue.php:69
UploadBase\isThrottled
static isThrottled( $user)
Returns true if the user has surpassed the upload rate limit, false otherwise.
Definition: UploadBase.php:166
ApiUpload\getStashResult
getStashResult( $warnings)
Get Stash Result, throws an exception if the file could not be stashed.
Definition: ApiUpload.php:155
UploadBase\makeWarningsSerializable
static makeWarningsSerializable( $warnings)
Convert the warnings array returned by checkWarnings() to something that can be serialized.
Definition: UploadBase.php:736
UploadBase\FILE_TOO_LARGE
const FILE_TOO_LARGE
Definition: UploadBase.php:105
UploadBase\MIN_LENGTH_PARTNAME
const MIN_LENGTH_PARTNAME
Definition: UploadBase.php:98
User\newFatalPermissionDeniedStatus
static newFatalPermissionDeniedStatus( $permission)
Factory function for fatal permission-denied errors.
Definition: User.php:4874
ApiUpload\verifyUpload
verifyUpload()
Performs file verification, dies on error.
Definition: ApiUpload.php:587
ApiBase\dieWithError
dieWithError( $msg, $code=null, $data=null, $httpCode=null)
Abort execution with an error.
Definition: ApiBase.php:1379
wfTimestamp
wfTimestamp( $outputtype=TS_UNIX, $ts=0)
Get a timestamp string in one of various formats.
Definition: GlobalFunctions.php:1806
UploadBase\isEnabled
static isEnabled()
Returns true if uploads are enabled.
Definition: UploadBase.php:135
ApiBase\PARAM_TYPE
const PARAM_TYPE
(boolean) Inverse of IntegerDef::PARAM_IGNORE_RANGE
Definition: ApiBase.php:60
ApiBase\getResult
getResult()
Get the result object.
Definition: ApiBase.php:538
ApiUpload\getWarningsResult
getWarningsResult( $warnings)
Get Warnings Result.
Definition: ApiUpload.php:173
MessageSpecifier
Definition: MessageSpecifier.php:21
UploadBase\getMaxPhpUploadSize
static getMaxPhpUploadSize()
Get the PHP maximum uploaded file size, based on ini settings.
Definition: UploadBase.php:2220
NS_FILE
const NS_FILE
Definition: Defines.php:75
$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
UploadBase\setSessionStatus
static setSessionStatus(User $user, $statusKey, $value)
Set the current status of a chunked upload (used for polling)
Definition: UploadBase.php:2260
ApiUpload\$mUpload
UploadBase UploadFromChunks $mUpload
Definition: ApiUpload.php:28
ApiUpload\getApiWarnings
getApiWarnings()
Check warnings.
Definition: ApiUpload.php:690
ApiUpload\getAllowedParams
getAllowedParams()
Returns an array of allowed parameters (parameter name) => (default value) or (parameter name) => (ar...
Definition: ApiUpload.php:881
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:1406
UploadFromStash
Implements uploading from previously stored file.
Definition: UploadFromStash.php:32
UploadBase\HOOK_ABORTED
const HOOK_ABORTED
Definition: UploadBase.php:104
UploadBase\OK
const OK
Definition: UploadBase.php:96
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:943
UploadBase\EMPTY_FILE
const EMPTY_FILE
Definition: UploadBase.php:97
Message\listParam
static listParam(array $list, $type='text')
Definition: Message.php:1123
ApiBase
This abstract class implements many basic API functions, and is the base of all API classes.
Definition: ApiBase.php:48
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) Inverse of IntegerDef::PARAM_IGNORE_RANGE
Definition: ApiBase.php:65
Config
Interface for configuration instances.
Definition: Config.php:28
ApiBase\PARAM_MIN
const PARAM_MIN
(boolean) Inverse of IntegerDef::PARAM_IGNORE_RANGE
Definition: ApiBase.php:63
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:416
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:340
Config\get
get( $name)
Get a configuration variable such as "Sitename" or "UploadMaintenance.".
UploadFromUrl\isEnabled
static isEnabled()
Checks if the upload from URL feature is enabled.
Definition: UploadFromUrl.php:65
UploadBase\WINDOWS_NONASCII_FILENAME
const WINDOWS_NONASCII_FILENAME
Definition: UploadBase.php:106
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:391
ApiUpload\getMinUploadChunkSize
static getMinUploadChunkSize(Config $config)
Definition: ApiUpload.php:190
ApiBase\PARAM_MAX
const PARAM_MAX
(boolean) Inverse of IntegerDef::PARAM_IGNORE_RANGE
Definition: ApiBase.php:61
ApiBase\extractRequestParams
extractRequestParams( $options=[])
Using getAllowedParams(), this function makes an array of the values provided by the user,...
Definition: ApiBase.php:659
ApiUpload\getContextResult
getContextResult()
Get an upload result based on upload context.
Definition: ApiUpload.php:126
ApiUpload\handleStashException
handleStashException( $e)
Handles a stash exception, giving a useful error to the user.
Definition: ApiUpload.php:751
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:595
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:993
ApiUpload\needsToken
needsToken()
Returns the token type this module requires in order to execute.
Definition: ApiUpload.php:939
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:604
UploadFromStash\isValidKey
static isValidKey( $key)
Definition: UploadFromStash.php:73
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:796
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:953
ApiUpload\mustBePosted
mustBePosted()
Indicates whether this module must be called with a POST request.
Definition: ApiUpload.php:873
ApiUpload\performUpload
performUpload( $warnings)
Perform the actual upload.
Definition: ApiUpload.php:791
UploadBase\FILENAME_TOO_LONG
const FILENAME_TOO_LONG
Definition: UploadBase.php:107
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
UploadBase\getSessionStatus
static getSessionStatus(User $user, $statusKey)
Get the current status of a chunked upload (used for polling)
Definition: UploadBase.php:2241
UploadBase\ILLEGAL_FILENAME
const ILLEGAL_FILENAME
Definition: UploadBase.php:99
UploadBase\getMaxUploadSize
static getMaxUploadSize( $forType=null)
Get MediaWiki's maximum uploaded file size for given type of upload, based on $wgMaxUploadSize.
Definition: UploadBase.php:2199
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:528
wfShorthandToInteger
wfShorthandToInteger( $string='', $default=-1)
Converts shorthand byte notation to integer form.
Definition: GlobalFunctions.php:2708
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
(boolean) Inverse of IntegerDef::PARAM_IGNORE_RANGE
Definition: ApiBase.php:58
ApiUpload\checkPermissions
checkPermissions( $user)
Checks that the user has permissions to perform this upload.
Definition: ApiUpload.php:561
ApiBase\dieStatus
dieStatus(StatusValue $status)
Throw an ApiUsageException based on the Status object.
Definition: ApiBase.php:1437
ApiBase\getModuleName
getModuleName()
Get the name of the module being executed by this instance.
Definition: ApiBase.php:418
ApiBase\PARAM_ISMULTI
const PARAM_ISMULTI
(boolean) Inverse of IntegerDef::PARAM_IGNORE_RANGE
Definition: ApiBase.php:59
ApiBase\getMain
getMain()
Get the main module.
Definition: ApiBase.php:434
UploadFromChunks
Implements uploading from chunks.
Definition: UploadFromChunks.php:33
ApiUpload\transformWarnings
transformWarnings( $warnings)
Definition: ApiUpload.php:698
ApiBase\dieDebug
static dieDebug( $method, $message)
Internal code errors should be reported with this method.
Definition: ApiBase.php:1571
UploadFromFile
Implements regular file uploads.
Definition: UploadFromFile.php:30
ApiBase\getErrorFormatter
getErrorFormatter()
Get the error formatter.
Definition: ApiBase.php:552
UploadStashException
Definition: UploadStashException.php:26
ApiUpload\checkVerification
checkVerification(array $verification)
Performs file verification, dies on error.
Definition: ApiUpload.php:600
UploadBase\FILETYPE_MISSING
const FILETYPE_MISSING
Definition: UploadBase.php:101
UploadBase\FILETYPE_BADTYPE
const FILETYPE_BADTYPE
Definition: UploadBase.php:102