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