MediaWiki REL1_39
ApiUpload.php
Go to the documentation of this file.
1<?php
28
32class ApiUpload extends ApiBase {
33
35
37 protected $mUpload = null;
38
39 protected $mParams;
40
42 private $jobQueueGroup;
43
51 public function __construct(
52 ApiMain $mainModule,
53 $moduleName,
54 JobQueueGroup $jobQueueGroup,
55 WatchlistManager $watchlistManager,
56 UserOptionsLookup $userOptionsLookup
57 ) {
58 parent::__construct( $mainModule, $moduleName );
59 $this->jobQueueGroup = $jobQueueGroup;
60
61 // Variables needed in ApiWatchlistTrait trait
62 $this->watchlistExpiryEnabled = $this->getConfig()->get( MainConfigNames::WatchlistExpiry );
63 $this->watchlistMaxDuration =
64 $this->getConfig()->get( MainConfigNames::WatchlistExpiryMaxDuration );
65 $this->watchlistManager = $watchlistManager;
66 $this->userOptionsLookup = $userOptionsLookup;
67 }
68
69 public function execute() {
70 // Check whether upload is enabled
71 if ( !UploadBase::isEnabled() ) {
72 $this->dieWithError( 'uploaddisabled' );
73 }
74
75 $user = $this->getUser();
76
77 // Parameter handling
78 $this->mParams = $this->extractRequestParams();
79 $request = $this->getMain()->getRequest();
80 // Check if async mode is actually supported (jobs done in cli mode)
81 $this->mParams['async'] = ( $this->mParams['async'] &&
82 $this->getConfig()->get( MainConfigNames::EnableAsyncUploads ) );
83 // Add the uploaded file to the params array
84 $this->mParams['file'] = $request->getFileName( 'file' );
85 $this->mParams['chunk'] = $request->getFileName( 'chunk' );
86
87 // Copy the session key to the file key, for backward compatibility.
88 if ( !$this->mParams['filekey'] && $this->mParams['sessionkey'] ) {
89 $this->mParams['filekey'] = $this->mParams['sessionkey'];
90 }
91
92 // Select an upload module
93 try {
94 if ( !$this->selectUploadModule() ) {
95 return; // not a true upload, but a status request or similar
96 } elseif ( !isset( $this->mUpload ) ) {
97 $this->dieDebug( __METHOD__, 'No upload module set' );
98 }
99 } catch ( UploadStashException $e ) { // XXX: don't spam exception log
100 $this->dieStatus( $this->handleStashException( $e ) );
101 }
102
103 // First check permission to upload
104 $this->checkPermissions( $user );
105
106 // Fetch the file (usually a no-op)
108 $status = $this->mUpload->fetchFile();
109 if ( !$status->isGood() ) {
110 $this->dieStatus( $status );
111 }
112
113 // Check the uploaded file
114 $this->verifyUpload();
115
116 // Check if the user has the rights to modify or overwrite the requested title
117 // (This check is irrelevant if stashing is already requested, since the errors
118 // can always be fixed by changing the title)
119 if ( !$this->mParams['stash'] ) {
120 $permErrors = $this->mUpload->verifyTitlePermissions( $user );
121 if ( $permErrors !== true ) {
122 $this->dieRecoverableError( $permErrors, 'filename' );
123 }
124 }
125
126 // Get the result based on the current upload context:
127 try {
128 $result = $this->getContextResult();
129 } catch ( UploadStashException $e ) { // XXX: don't spam exception log
130 $this->dieStatus( $this->handleStashException( $e ) );
131 }
132 $this->getResult()->addValue( null, $this->getModuleName(), $result );
133
134 // Add 'imageinfo' in a separate addValue() call. File metadata can be unreasonably large,
135 // so otherwise when it exceeded $wgAPIMaxResultSize, no result would be returned (T143993).
136 // @phan-suppress-next-line PhanTypeArraySuspiciousNullable False positive
137 if ( $result['result'] === 'Success' ) {
138 $imageinfo = $this->mUpload->getImageInfo( $this->getResult() );
139 $this->getResult()->addValue( $this->getModuleName(), 'imageinfo', $imageinfo );
140 }
141
142 // Cleanup any temporary mess
143 $this->mUpload->cleanupTempFile();
144 }
145
150 private function getContextResult() {
151 $warnings = $this->getApiWarnings();
152 if ( $warnings && !$this->mParams['ignorewarnings'] ) {
153 // Get warnings formatted in result array format
154 return $this->getWarningsResult( $warnings );
155 } elseif ( $this->mParams['chunk'] ) {
156 // Add chunk, and get result
157 return $this->getChunkResult( $warnings );
158 } elseif ( $this->mParams['stash'] ) {
159 // Stash the file and get stash result
160 return $this->getStashResult( $warnings );
161 }
162
163 // Check throttle after we've handled warnings
164 if ( UploadBase::isThrottled( $this->getUser() )
165 ) {
166 $this->dieWithError( 'apierror-ratelimited' );
167 }
168
169 // This is the most common case -- a normal upload with no warnings
170 // performUpload will return a formatted properly for the API with status
171 return $this->performUpload( $warnings );
172 }
173
179 private function getStashResult( $warnings ) {
180 $result = [];
181 $result['result'] = 'Success';
182 if ( $warnings && count( $warnings ) > 0 ) {
183 $result['warnings'] = $warnings;
184 }
185 // Some uploads can request they be stashed, so as not to publish them immediately.
186 // In this case, a failure to stash ought to be fatal
187 $this->performStash( 'critical', $result );
188
189 return $result;
190 }
191
197 private function getWarningsResult( $warnings ) {
198 $result = [];
199 $result['result'] = 'Warning';
200 $result['warnings'] = $warnings;
201 // in case the warnings can be fixed with some further user action, let's stash this upload
202 // and return a key they can use to restart it
203 $this->performStash( 'optional', $result );
204
205 return $result;
206 }
207
214 public static function getMinUploadChunkSize( Config $config ) {
215 $configured = $config->get( MainConfigNames::MinUploadChunkSize );
216
217 // Leave some room for other POST parameters
218 $postMax = (
220 ini_get( 'post_max_size' ),
221 PHP_INT_MAX
222 ) ?: PHP_INT_MAX
223 ) - 1024;
224
225 // Ensure the minimum chunk size is less than PHP upload limits
226 // or the maximum upload size.
227 return min(
228 $configured,
229 UploadBase::getMaxUploadSize( 'file' ),
230 UploadBase::getMaxPhpUploadSize(),
231 $postMax
232 );
233 }
234
240 private function getChunkResult( $warnings ) {
241 $result = [];
242
243 if ( $warnings && count( $warnings ) > 0 ) {
244 $result['warnings'] = $warnings;
245 }
246
247 $request = $this->getMain()->getRequest();
248 $chunkPath = $request->getFileTempname( 'chunk' );
249 $chunkSize = $request->getUpload( 'chunk' )->getSize();
250 $totalSoFar = $this->mParams['offset'] + $chunkSize;
251 $minChunkSize = self::getMinUploadChunkSize( $this->getConfig() );
252
253 // Double check sizing
254 if ( $totalSoFar > $this->mParams['filesize'] ) {
255 $this->dieWithError( 'apierror-invalid-chunk' );
256 }
257
258 // Enforce minimum chunk size
259 if ( $totalSoFar != $this->mParams['filesize'] && $chunkSize < $minChunkSize ) {
260 $this->dieWithError( [ 'apierror-chunk-too-small', Message::numParam( $minChunkSize ) ] );
261 }
262
263 if ( $this->mParams['offset'] == 0 ) {
264 $filekey = $this->performStash( 'critical' );
265 } else {
266 $filekey = $this->mParams['filekey'];
267
268 // Don't allow further uploads to an already-completed session
269 $progress = UploadBase::getSessionStatus( $this->getUser(), $filekey );
270 if ( !$progress ) {
271 // Probably can't get here, but check anyway just in case
272 $this->dieWithError( 'apierror-stashfailed-nosession', 'stashfailed' );
273 } elseif ( $progress['result'] !== 'Continue' || $progress['stage'] !== 'uploading' ) {
274 $this->dieWithError( 'apierror-stashfailed-complete', 'stashfailed' );
275 }
276
277 $status = $this->mUpload->addChunk(
278 $chunkPath, $chunkSize, $this->mParams['offset'] );
279 if ( !$status->isGood() ) {
280 $extradata = [
281 'offset' => $this->mUpload->getOffset(),
282 ];
283
284 $this->dieStatusWithCode( $status, 'stashfailed', $extradata );
285 }
286 }
287
288 // Check we added the last chunk:
289 if ( $totalSoFar == $this->mParams['filesize'] ) {
290 if ( $this->mParams['async'] ) {
292 $this->getUser(),
293 $filekey,
294 [ 'result' => 'Poll',
295 'stage' => 'queued', 'status' => Status::newGood() ]
296 );
297 $this->jobQueueGroup->push( new AssembleUploadChunksJob(
298 Title::makeTitle( NS_FILE, $filekey ),
299 [
300 'filename' => $this->mParams['filename'],
301 'filekey' => $filekey,
302 'session' => $this->getContext()->exportSession()
303 ]
304 ) );
305 $result['result'] = 'Poll';
306 $result['stage'] = 'queued';
307 } else {
308 $status = $this->mUpload->concatenateChunks();
309 if ( !$status->isGood() ) {
311 $this->getUser(),
312 $filekey,
313 [ 'result' => 'Failure', 'stage' => 'assembling', 'status' => $status ]
314 );
315 $this->dieStatusWithCode( $status, 'stashfailed' );
316 }
317
318 // We can only get warnings like 'duplicate' after concatenating the chunks
319 $warnings = $this->getApiWarnings();
320 if ( $warnings ) {
321 $result['warnings'] = $warnings;
322 }
323
324 // The fully concatenated file has a new filekey. So remove
325 // the old filekey and fetch the new one.
326 UploadBase::setSessionStatus( $this->getUser(), $filekey, false );
327 $this->mUpload->stash->removeFile( $filekey );
328 $filekey = $this->mUpload->getStashFile()->getFileKey();
329
330 $result['result'] = 'Success';
331 }
332 } else {
334 $this->getUser(),
335 $filekey,
336 [
337 'result' => 'Continue',
338 'stage' => 'uploading',
339 'offset' => $totalSoFar,
340 'status' => Status::newGood(),
341 ]
342 );
343 $result['result'] = 'Continue';
344 $result['offset'] = $totalSoFar;
345 }
346
347 $result['filekey'] = $filekey;
348
349 return $result;
350 }
351
364 private function performStash( $failureMode, &$data = null ) {
365 $isPartial = (bool)$this->mParams['chunk'];
366 try {
367 $status = $this->mUpload->tryStashFile( $this->getUser(), $isPartial );
368
369 if ( $status->isGood() && !$status->getValue() ) {
370 // Not actually a 'good' status...
371 $status->fatal( new ApiMessage( 'apierror-stashinvalidfile', 'stashfailed' ) );
372 }
373 } catch ( Exception $e ) {
374 $debugMessage = 'Stashing temporary file failed: ' . get_class( $e ) . ' ' . $e->getMessage();
375 wfDebug( __METHOD__ . ' ' . $debugMessage );
376 $status = Status::newFatal( $this->getErrorFormatter()->getMessageFromException(
377 $e, [ 'wrap' => new ApiMessage( 'apierror-stashexception', 'stashfailed' ) ]
378 ) );
379 }
380
381 if ( $status->isGood() ) {
382 $stashFile = $status->getValue();
383 $data['filekey'] = $stashFile->getFileKey();
384 // Backwards compatibility
385 $data['sessionkey'] = $data['filekey'];
386 return $data['filekey'];
387 }
388
389 if ( $status->getMessage()->getKey() === 'uploadstash-exception' ) {
390 // The exceptions thrown by upload stash code and pretty silly and UploadBase returns poor
391 // Statuses for it. Just extract the exception details and parse them ourselves.
392 list( $exceptionType, $message ) = $status->getMessage()->getParams();
393 $debugMessage = 'Stashing temporary file failed: ' . $exceptionType . ' ' . $message;
394 wfDebug( __METHOD__ . ' ' . $debugMessage );
395 }
396
397 // Bad status
398 if ( $failureMode !== 'optional' ) {
399 $this->dieStatus( $status );
400 } else {
401 $data['stasherrors'] = $this->getErrorFormatter()->arrayFromStatus( $status );
402 return null;
403 }
404 }
405
416 private function dieRecoverableError( $errors, $parameter = null ) {
417 $this->performStash( 'optional', $data );
418
419 if ( $parameter ) {
420 $data['invalidparameter'] = $parameter;
421 }
422
423 $sv = StatusValue::newGood();
424 foreach ( $errors as $error ) {
425 $msg = ApiMessage::create( $error );
426 $msg->setApiData( $msg->getApiData() + $data );
427 $sv->fatal( $msg );
428 }
429 $this->dieStatus( $sv );
430 }
431
442 public function dieStatusWithCode( $status, $overrideCode, $moreExtraData = null ) {
443 $sv = StatusValue::newGood();
444 foreach ( $status->getErrors() as $error ) {
445 $msg = ApiMessage::create( $error, $overrideCode );
446 if ( $moreExtraData ) {
447 $msg->setApiData( $msg->getApiData() + $moreExtraData );
448 }
449 $sv->fatal( $msg );
450 }
451 $this->dieStatus( $sv );
452 }
453
462 protected function selectUploadModule() {
463 $request = $this->getMain()->getRequest();
464
465 // chunk or one and only one of the following parameters is needed
466 if ( !$this->mParams['chunk'] ) {
467 $this->requireOnlyOneParameter( $this->mParams,
468 'filekey', 'file', 'url' );
469 }
470
471 // Status report for "upload to stash"/"upload from stash"
472 if ( $this->mParams['filekey'] && $this->mParams['checkstatus'] ) {
473 $progress = UploadBase::getSessionStatus( $this->getUser(), $this->mParams['filekey'] );
474 if ( !$progress ) {
475 $this->dieWithError( 'apierror-upload-missingresult', 'missingresult' );
476 } elseif ( !$progress['status']->isGood() ) {
477 $this->dieStatusWithCode( $progress['status'], 'stashfailed' );
478 }
479 if ( isset( $progress['status']->value['verification'] ) ) {
480 $this->checkVerification( $progress['status']->value['verification'] );
481 }
482 if ( isset( $progress['status']->value['warnings'] ) ) {
483 $warnings = $this->transformWarnings( $progress['status']->value['warnings'] );
484 if ( $warnings ) {
485 $progress['warnings'] = $warnings;
486 }
487 }
488 unset( $progress['status'] ); // remove Status object
489 $imageinfo = null;
490 if ( isset( $progress['imageinfo'] ) ) {
491 $imageinfo = $progress['imageinfo'];
492 unset( $progress['imageinfo'] );
493 }
494
495 $this->getResult()->addValue( null, $this->getModuleName(), $progress );
496 // Add 'imageinfo' in a separate addValue() call. File metadata can be unreasonably large,
497 // so otherwise when it exceeded $wgAPIMaxResultSize, no result would be returned (T143993).
498 if ( $imageinfo ) {
499 $this->getResult()->addValue( $this->getModuleName(), 'imageinfo', $imageinfo );
500 }
501
502 return false;
503 }
504
505 // The following modules all require the filename parameter to be set
506 if ( $this->mParams['filename'] === null ) {
507 $this->dieWithError( [ 'apierror-missingparam', 'filename' ] );
508 }
509
510 if ( $this->mParams['chunk'] ) {
511 // Chunk upload
512 $this->mUpload = new UploadFromChunks( $this->getUser() );
513 if ( isset( $this->mParams['filekey'] ) ) {
514 if ( $this->mParams['offset'] === 0 ) {
515 $this->dieWithError( 'apierror-upload-filekeynotallowed', 'filekeynotallowed' );
516 }
517
518 // handle new chunk
519 $this->mUpload->continueChunks(
520 $this->mParams['filename'],
521 $this->mParams['filekey'],
522 $request->getUpload( 'chunk' )
523 );
524 } else {
525 if ( $this->mParams['offset'] !== 0 ) {
526 $this->dieWithError( 'apierror-upload-filekeyneeded', 'filekeyneeded' );
527 }
528
529 // handle first chunk
530 $this->mUpload->initialize(
531 $this->mParams['filename'],
532 $request->getUpload( 'chunk' )
533 );
534 }
535 } elseif ( isset( $this->mParams['filekey'] ) ) {
536 // Upload stashed in a previous request
537 if ( !UploadFromStash::isValidKey( $this->mParams['filekey'] ) ) {
538 $this->dieWithError( 'apierror-invalid-file-key' );
539 }
540
541 $this->mUpload = new UploadFromStash( $this->getUser() );
542 // This will not download the temp file in initialize() in async mode.
543 // We still have enough information to call checkWarnings() and such.
544 $this->mUpload->initialize(
545 $this->mParams['filekey'], $this->mParams['filename'], !$this->mParams['async']
546 );
547 } elseif ( isset( $this->mParams['file'] ) ) {
548 // Can't async upload directly from a POSTed file, we'd have to
549 // stash the file and then queue the publish job. The user should
550 // just submit the two API queries to perform those two steps.
551 if ( $this->mParams['async'] ) {
552 $this->dieWithError( 'apierror-cannot-async-upload-file' );
553 }
554
555 $this->mUpload = new UploadFromFile();
556 $this->mUpload->initialize(
557 $this->mParams['filename'],
558 $request->getUpload( 'file' )
559 );
560 } elseif ( isset( $this->mParams['url'] ) ) {
561 // Make sure upload by URL is enabled:
562 if ( !UploadFromUrl::isEnabled() ) {
563 $this->dieWithError( 'copyuploaddisabled' );
564 }
565
566 if ( !UploadFromUrl::isAllowedHost( $this->mParams['url'] ) ) {
567 $this->dieWithError( 'apierror-copyuploadbaddomain' );
568 }
569
570 if ( !UploadFromUrl::isAllowedUrl( $this->mParams['url'] ) ) {
571 $this->dieWithError( 'apierror-copyuploadbadurl' );
572 }
573
574 $this->mUpload = new UploadFromUrl;
575 $this->mUpload->initialize( $this->mParams['filename'],
576 $this->mParams['url'] );
577 }
578
579 return true;
580 }
581
587 protected function checkPermissions( $user ) {
588 // Check whether the user has the appropriate permissions to upload anyway
589 $permission = $this->mUpload->isAllowed( $user );
590
591 if ( $permission !== true ) {
592 if ( !$user->isRegistered() ) {
593 $this->dieWithError( [ 'apierror-mustbeloggedin', $this->msg( 'action-upload' ) ] );
594 }
595
596 $this->dieStatus( User::newFatalPermissionDeniedStatus( $permission ) );
597 }
598
599 // Check blocks
600 if ( $user->isBlockedFromUpload() ) {
601 // @phan-suppress-next-line PhanTypeMismatchArgumentNullable Block is checked and not null
602 $this->dieBlocked( $user->getBlock() );
603 }
604
605 // Global blocks
606 if ( $user->isBlockedGlobally() ) {
607 $this->dieBlocked( $user->getGlobalBlock() );
608 }
609 }
610
614 protected function verifyUpload() {
615 if ( $this->mParams['chunk'] ) {
616 $maxSize = UploadBase::getMaxUploadSize();
617 if ( $this->mParams['filesize'] > $maxSize ) {
618 $this->dieWithError( 'file-too-large' );
619 }
620 if ( !$this->mUpload->getTitle() ) {
621 $this->dieWithError( 'illegal-filename' );
622 }
623 // file will be assembled after having uploaded the last chunk,
624 // so we can only validate the name at this point
625 $verification = $this->mUpload->validateName();
626 if ( $verification === true ) {
627 return;
628 }
629 } elseif ( $this->mParams['async'] && $this->mParams['filekey'] ) {
630 // file will be assembled in a background process, so we
631 // can only validate the name at this point
632 // file verification will happen in background process
633 $verification = $this->mUpload->validateName();
634 if ( $verification === true ) {
635 return;
636 }
637 } else {
638 wfDebug( __METHOD__ . " about to verify" );
639
640 $verification = $this->mUpload->verifyUpload();
641 if ( $verification['status'] === UploadBase::OK ) {
642 return;
643 }
644 }
645
646 $this->checkVerification( $verification );
647 }
648
654 protected function checkVerification( array $verification ) {
655 switch ( $verification['status'] ) {
656 // Recoverable errors
657 case UploadBase::MIN_LENGTH_PARTNAME:
658 $this->dieRecoverableError( [ 'filename-tooshort' ], 'filename' );
659 // dieRecoverableError prevents continuation
660 case UploadBase::ILLEGAL_FILENAME:
661 $this->dieRecoverableError(
662 [ ApiMessage::create(
663 'illegal-filename', null, [ 'filename' => $verification['filtered'] ]
664 ) ], 'filename'
665 );
666 // dieRecoverableError prevents continuation
667 case UploadBase::FILENAME_TOO_LONG:
668 $this->dieRecoverableError( [ 'filename-toolong' ], 'filename' );
669 // dieRecoverableError prevents continuation
670 case UploadBase::FILETYPE_MISSING:
671 $this->dieRecoverableError( [ 'filetype-missing' ], 'filename' );
672 // dieRecoverableError prevents continuation
673 case UploadBase::WINDOWS_NONASCII_FILENAME:
674 $this->dieRecoverableError( [ 'windows-nonascii-filename' ], 'filename' );
675
676 // Unrecoverable errors
677 case UploadBase::EMPTY_FILE:
678 $this->dieWithError( 'empty-file' );
679 // dieWithError prevents continuation
680 case UploadBase::FILE_TOO_LARGE:
681 $this->dieWithError( 'file-too-large' );
682 // dieWithError prevents continuation
683
684 case UploadBase::FILETYPE_BADTYPE:
685 $extradata = [
686 'filetype' => $verification['finalExt'],
687 'allowed' => array_values( array_unique(
688 $this->getConfig()->get( MainConfigNames::FileExtensions ) ) )
689 ];
690 $extensions =
691 array_unique( $this->getConfig()->get( MainConfigNames::FileExtensions ) );
692 $msg = [
693 'filetype-banned-type',
694 null, // filled in below
695 Message::listParam( $extensions, 'comma' ),
696 count( $extensions ),
697 null, // filled in below
698 ];
699 ApiResult::setIndexedTagName( $extradata['allowed'], 'ext' );
700
701 if ( isset( $verification['blacklistedExt'] ) ) {
702 $msg[1] = Message::listParam( $verification['blacklistedExt'], 'comma' );
703 $msg[4] = count( $verification['blacklistedExt'] );
704 $extradata['blacklisted'] = array_values( $verification['blacklistedExt'] );
705 ApiResult::setIndexedTagName( $extradata['blacklisted'], 'ext' );
706 } else {
707 $msg[1] = $verification['finalExt'];
708 $msg[4] = 1;
709 }
710
711 $this->dieWithError( $msg, 'filetype-banned', $extradata );
712 // dieWithError prevents continuation
713
714 case UploadBase::VERIFICATION_ERROR:
715 $msg = ApiMessage::create( $verification['details'], 'verification-error' );
716 if ( $verification['details'][0] instanceof MessageSpecifier ) {
717 $details = array_merge( [ $msg->getKey() ], $msg->getParams() );
718 } else {
719 $details = $verification['details'];
720 }
721 ApiResult::setIndexedTagName( $details, 'detail' );
722 $msg->setApiData( $msg->getApiData() + [ 'details' => $details ] );
723 // @phan-suppress-next-line PhanTypeMismatchArgument
724 $this->dieWithError( $msg );
725 // dieWithError prevents continuation
726
727 case UploadBase::HOOK_ABORTED:
728 $msg = $verification['error'] === '' ? 'hookaborted' : $verification['error'];
729 $this->dieWithError( $msg, 'hookaborted', [ 'details' => $verification['error'] ] );
730 // dieWithError prevents continuation
731 default:
732 $this->dieWithError( 'apierror-unknownerror-nocode', 'unknown-error',
733 [ 'details' => [ 'code' => $verification['status'] ] ] );
734 }
735 }
736
744 protected function getApiWarnings() {
745 $warnings = UploadBase::makeWarningsSerializable(
746 $this->mUpload->checkWarnings( $this->getUser() )
747 );
748
749 return $this->transformWarnings( $warnings );
750 }
751
752 protected function transformWarnings( $warnings ) {
753 if ( $warnings ) {
754 // Add indices
755 ApiResult::setIndexedTagName( $warnings, 'warning' );
756
757 if ( isset( $warnings['duplicate'] ) ) {
758 $dupes = array_column( $warnings['duplicate'], 'fileName' );
759 ApiResult::setIndexedTagName( $dupes, 'duplicate' );
760 $warnings['duplicate'] = $dupes;
761 }
762
763 if ( isset( $warnings['exists'] ) ) {
764 $warning = $warnings['exists'];
765 unset( $warnings['exists'] );
766 $localFile = $warning['normalizedFile'] ?? $warning['file'];
767 $warnings[$warning['warning']] = $localFile['fileName'];
768 }
769
770 if ( isset( $warnings['no-change'] ) ) {
771 $file = $warnings['no-change'];
772 unset( $warnings['no-change'] );
773
774 $warnings['nochange'] = [
775 'timestamp' => wfTimestamp( TS_ISO_8601, $file['timestamp'] )
776 ];
777 }
778
779 if ( isset( $warnings['duplicate-version'] ) ) {
780 $dupes = [];
781 foreach ( $warnings['duplicate-version'] as $dupe ) {
782 $dupes[] = [
783 'timestamp' => wfTimestamp( TS_ISO_8601, $dupe['timestamp'] )
784 ];
785 }
786 unset( $warnings['duplicate-version'] );
787
788 ApiResult::setIndexedTagName( $dupes, 'ver' );
789 $warnings['duplicateversions'] = $dupes;
790 }
791 }
792
793 return $warnings;
794 }
795
802 protected function handleStashException( $e ) {
803 switch ( get_class( $e ) ) {
804 case UploadStashFileNotFoundException::class:
805 $wrap = 'apierror-stashedfilenotfound';
806 break;
807 case UploadStashBadPathException::class:
808 $wrap = 'apierror-stashpathinvalid';
809 break;
810 case UploadStashFileException::class:
811 $wrap = 'apierror-stashfilestorage';
812 break;
813 case UploadStashZeroLengthFileException::class:
814 $wrap = 'apierror-stashzerolength';
815 break;
816 case UploadStashNotLoggedInException::class:
817 return StatusValue::newFatal( ApiMessage::create(
818 [ 'apierror-mustbeloggedin', $this->msg( 'action-upload' ) ], 'stashnotloggedin'
819 ) );
820 case UploadStashWrongOwnerException::class:
821 $wrap = 'apierror-stashwrongowner';
822 break;
823 case UploadStashNoSuchKeyException::class:
824 $wrap = 'apierror-stashnosuchfilekey';
825 break;
826 default:
827 $wrap = [ 'uploadstash-exception', get_class( $e ) ];
828 break;
829 }
830 return StatusValue::newFatal(
831 $this->getErrorFormatter()->getMessageFromException( $e, [ 'wrap' => $wrap ] )
832 );
833 }
834
842 protected function performUpload( $warnings ) {
843 // Use comment as initial page text by default
844 if ( $this->mParams['text'] === null ) {
845 $this->mParams['text'] = $this->mParams['comment'];
846 }
847
849 $file = $this->mUpload->getLocalFile();
850 $user = $this->getUser();
851 $title = $file->getTitle();
852
853 // for preferences mode, we want to watch if 'watchdefault' is set,
854 // or if the *file* doesn't exist, and either 'watchuploads' or
855 // 'watchcreations' is set. But getWatchlistValue()'s automatic
856 // handling checks if the *title* exists or not, so we need to check
857 // all three preferences manually.
858 $watch = $this->getWatchlistValue(
859 $this->mParams['watchlist'], $title, $user, 'watchdefault'
860 );
861
862 if ( !$watch && $this->mParams['watchlist'] == 'preferences' && !$file->exists() ) {
863 $watch = (
864 $this->getWatchlistValue( 'preferences', $title, $user, 'watchuploads' ) ||
865 $this->getWatchlistValue( 'preferences', $title, $user, 'watchcreations' )
866 );
867 }
868 $watchlistExpiry = $this->getExpiryFromParams( $this->mParams );
869
870 // Deprecated parameters
871 if ( $this->mParams['watch'] ) {
872 $watch = true;
873 }
874
875 if ( $this->mParams['tags'] ) {
876 $status = ChangeTags::canAddTagsAccompanyingChange( $this->mParams['tags'], $this->getAuthority() );
877 if ( !$status->isOK() ) {
878 $this->dieStatus( $status );
879 }
880 }
881
882 // No errors, no warnings: do the upload
883 $result = [];
884 if ( $this->mParams['async'] ) {
885 $progress = UploadBase::getSessionStatus( $this->getUser(), $this->mParams['filekey'] );
886 if ( $progress && $progress['result'] === 'Poll' ) {
887 $this->dieWithError( 'apierror-upload-inprogress', 'publishfailed' );
888 }
889 UploadBase::setSessionStatus(
890 $this->getUser(),
891 $this->mParams['filekey'],
892 [ 'result' => 'Poll', 'stage' => 'queued', 'status' => Status::newGood() ]
893 );
894 $this->jobQueueGroup->push( new PublishStashedFileJob(
895 Title::makeTitle( NS_FILE, $this->mParams['filename'] ),
896 [
897 'filename' => $this->mParams['filename'],
898 'filekey' => $this->mParams['filekey'],
899 'comment' => $this->mParams['comment'],
900 'tags' => $this->mParams['tags'] ?? [],
901 'text' => $this->mParams['text'],
902 'watch' => $watch,
903 'watchlistexpiry' => $watchlistExpiry,
904 'session' => $this->getContext()->exportSession()
905 ]
906 ) );
907 $result['result'] = 'Poll';
908 $result['stage'] = 'queued';
909 } else {
911 $status = $this->mUpload->performUpload(
912 $this->mParams['comment'],
913 $this->mParams['text'],
914 $watch,
915 $this->getUser(),
916 $this->mParams['tags'] ?? [],
917 $watchlistExpiry
918 );
919
920 if ( !$status->isGood() ) {
921 $this->dieRecoverableError( $status->getErrors() );
922 }
923 $result['result'] = 'Success';
924 }
925
926 $result['filename'] = $file->getName();
927 if ( $warnings && count( $warnings ) > 0 ) {
928 $result['warnings'] = $warnings;
929 }
930
931 return $result;
932 }
933
934 public function mustBePosted() {
935 return true;
936 }
937
938 public function isWriteMode() {
939 return true;
940 }
941
942 public function getAllowedParams() {
943 $params = [
944 'filename' => [
945 ParamValidator::PARAM_TYPE => 'string',
946 ],
947 'comment' => [
948 ParamValidator::PARAM_DEFAULT => ''
949 ],
950 'tags' => [
951 ParamValidator::PARAM_TYPE => 'tags',
952 ParamValidator::PARAM_ISMULTI => true,
953 ],
954 'text' => [
955 ParamValidator::PARAM_TYPE => 'text',
956 ],
957 'watch' => [
958 ParamValidator::PARAM_DEFAULT => false,
959 ParamValidator::PARAM_DEPRECATED => true,
960 ],
961 ];
962
963 // Params appear in the docs in the order they are defined,
964 // which is why this is here and not at the bottom.
965 $params += $this->getWatchlistParams( [
966 'watch',
967 'preferences',
968 'nochange',
969 ] );
970
971 $params += [
972 'ignorewarnings' => false,
973 'file' => [
974 ParamValidator::PARAM_TYPE => 'upload',
975 ],
976 'url' => null,
977 'filekey' => null,
978 'sessionkey' => [
979 ParamValidator::PARAM_DEPRECATED => true,
980 ],
981 'stash' => false,
982
983 'filesize' => [
984 ParamValidator::PARAM_TYPE => 'integer',
985 IntegerDef::PARAM_MIN => 0,
986 IntegerDef::PARAM_MAX => UploadBase::getMaxUploadSize(),
987 ],
988 'offset' => [
989 ParamValidator::PARAM_TYPE => 'integer',
990 IntegerDef::PARAM_MIN => 0,
991 ],
992 'chunk' => [
993 ParamValidator::PARAM_TYPE => 'upload',
994 ],
995
996 'async' => false,
997 'checkstatus' => false,
998 ];
999
1000 return $params;
1001 }
1002
1003 public function needsToken() {
1004 return 'csrf';
1005 }
1006
1007 protected function getExamplesMessages() {
1008 return [
1009 'action=upload&filename=Wiki.png' .
1010 '&url=http%3A//upload.wikimedia.org/wikipedia/en/b/bc/Wiki.png&token=123ABC'
1011 => 'apihelp-upload-example-url',
1012 'action=upload&filename=Wiki.png&filekey=filekey&ignorewarnings=1&token=123ABC'
1013 => 'apihelp-upload-example-filekey',
1014 ];
1015 }
1016
1017 public function getHelpUrls() {
1018 return 'https://www.mediawiki.org/wiki/Special:MyLanguage/API:Upload';
1019 }
1020}
getWatchlistValue(string $watchlist, Title $title, User $user, ?string $userOption=null)
Return true if we're to watch the page, false if not.
getExpiryFromParams(array $params)
Get formatted expiry from the given parameters, or null if no expiry was provided.
getWatchlistParams(array $watchOptions=[])
Get additional allow params specific to watchlisting.
const NS_FILE
Definition Defines.php:70
wfDebug( $text, $dest='all', array $context=[])
Sends a line to the debug log if enabled or, optionally, to a comment in output.
wfShorthandToInteger(?string $string='', int $default=-1)
Converts shorthand byte notation to integer form.
wfTimestamp( $outputtype=TS_UNIX, $ts=0)
Get a timestamp string in one of various formats.
This abstract class implements many basic API functions, and is the base of all API classes.
Definition ApiBase.php:56
dieWithError( $msg, $code=null, $data=null, $httpCode=0)
Abort execution with an error.
Definition ApiBase.php:1454
static dieDebug( $method, $message)
Internal code errors should be reported with this method.
Definition ApiBase.php:1656
getMain()
Get the main module.
Definition ApiBase.php:514
getErrorFormatter()
Definition ApiBase.php:640
requireOnlyOneParameter( $params,... $required)
Die if none or more than one of a certain set of parameters is set and not false.
Definition ApiBase.php:903
getResult()
Get the result object.
Definition ApiBase.php:629
extractRequestParams( $options=[])
Using getAllowedParams(), this function makes an array of the values provided by the user,...
Definition ApiBase.php:765
getModuleName()
Get the name of the module being executed by this instance.
Definition ApiBase.php:498
dieStatus(StatusValue $status)
Throw an ApiUsageException based on the Status object.
Definition ApiBase.php:1515
dieBlocked(Block $block)
Throw an ApiUsageException, which will (if uncaught) call the main module's error handler and die wit...
Definition ApiBase.php:1483
This is the main API class, used for both external and internal processing.
Definition ApiMain.php:52
Extension of Message implementing IApiMessage.
static create( $msg, $code=null, array $data=null)
Create an IApiMessage for the message.
execute()
Evaluates the parameters, performs the requested query, and sets up the result.
Definition ApiUpload.php:69
checkPermissions( $user)
Checks that the user has permissions to perform this upload.
verifyUpload()
Performs file verification, dies on error.
UploadBase UploadFromChunks $mUpload
Definition ApiUpload.php:37
getAllowedParams()
Returns an array of allowed parameters (parameter name) => (default value) or (parameter name) => (ar...
transformWarnings( $warnings)
handleStashException( $e)
Handles a stash exception, giving a useful error to the user.
getHelpUrls()
Return links to more detailed help pages about the module.
dieStatusWithCode( $status, $overrideCode, $moreExtraData=null)
Like dieStatus(), but always uses $overrideCode for the error code, unless the code comes from IApiMe...
isWriteMode()
Indicates whether this module requires write mode.
static getMinUploadChunkSize(Config $config)
__construct(ApiMain $mainModule, $moduleName, JobQueueGroup $jobQueueGroup, WatchlistManager $watchlistManager, UserOptionsLookup $userOptionsLookup)
Definition ApiUpload.php:51
getExamplesMessages()
Returns usage examples for this module.
selectUploadModule()
Select an upload module and set it to mUpload.
needsToken()
Returns the token type this module requires in order to execute.
performUpload( $warnings)
Perform the actual upload.
checkVerification(array $verification)
Performs file verification, dies on error.
mustBePosted()
Indicates whether this module must be called with a POST request.
getApiWarnings()
Check warnings.
Assemble the segments of a chunked upload.
static canAddTagsAccompanyingChange(array $tags, Authority $performer=null, $checkBlock=true)
Is it OK to allow the user to apply all the specified tags at the same time as they edit/make the cha...
msg( $key,... $params)
Get a Message object with context set Parameters are the same as wfMessage()
exportSession()
Export the resolved user IP, HTTP headers, user ID, and session ID.
getContext()
Get the base IContextSource object.
Class to handle enqueueing of background jobs.
get( $type)
Get the job queue object for a given queue type.
A class containing constants representing the names of configuration variables.
Provides access to user options.
static listParam(array $list, $type='text')
Definition Message.php:1277
static numParam( $num)
Definition Message.php:1145
Upload a file from the upload stash into the local file repo.
static newFatal( $message,... $parameters)
Factory function for fatal errors.
static newGood( $value=null)
Factory function for good results.
UploadBase and subclasses are the backend of MediaWiki's file uploads.
static setSessionStatus(UserIdentity $user, $statusKey, $value)
Set the current status of a chunked upload (used for polling)
static getSessionStatus(UserIdentity $user, $statusKey)
Get the current status of a chunked upload (used for polling)
Implements uploading from chunks.
Implements regular file uploads.
Implements uploading from previously stored file.
static isValidKey( $key)
Implements uploading from a HTTP resource.
initialize( $name, $url)
Entry point for API upload.
static isAllowedHost( $url)
Checks whether the URL is for an allowed host The domains in the allowlist can include wildcard chara...
static isAllowedUrl( $url)
Checks whether the URL is not allowed.
static isEnabled()
Checks if the upload from URL feature is enabled.
static newFatalPermissionDeniedStatus( $permission)
Factory function for fatal permission-denied errors.
Definition User.php:3402
Service for formatting and validating API parameters.
Type definition for integer types.
trait ApiWatchlistTrait
An ApiWatchlistTrait adds class properties and convenience methods for APIs that allow you to watch a...
Interface for configuration instances.
Definition Config.php:30
get( $name)
Get a configuration variable such as "Sitename" or "UploadMaintenance.".
if(PHP_SAPI !='cli-server') if(!isset( $_SERVER['SCRIPT_FILENAME'])) $file
Item class for a filearchive table row.
Definition router.php:42