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