42 private $jobQueueGroup;
58 parent::__construct( $mainModule, $moduleName );
59 $this->jobQueueGroup = $jobQueueGroup;
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;
71 if ( !UploadBase::isEnabled() ) {
79 $request = $this->
getMain()->getRequest();
81 $this->mParams[
'async'] = ( $this->mParams[
'async'] &&
82 $this->
getConfig()->get( MainConfigNames::EnableAsyncUploads ) );
84 $this->mParams[
'file'] = $request->getFileName(
'file' );
85 $this->mParams[
'chunk'] = $request->getFileName(
'chunk' );
88 if ( !$this->mParams[
'filekey'] && $this->mParams[
'sessionkey'] ) {
89 $this->mParams[
'filekey'] = $this->mParams[
'sessionkey'];
96 } elseif ( !isset( $this->mUpload ) ) {
97 $this->
dieDebug( __METHOD__,
'No upload module set' );
108 $status = $this->mUpload->fetchFile();
109 if ( !$status->isGood() ) {
119 if ( !$this->mParams[
'stash'] ) {
120 $permErrors = $this->mUpload->verifyTitlePermissions( $user );
121 if ( $permErrors !==
true ) {
122 $this->dieRecoverableError( $permErrors,
'filename' );
128 $result = $this->getContextResult();
137 if ( $result[
'result'] ===
'Success' ) {
138 $imageinfo = $this->mUpload->getImageInfo( $this->
getResult() );
143 $this->mUpload->cleanupTempFile();
150 private function getContextResult() {
152 if ( $warnings && !$this->mParams[
'ignorewarnings'] ) {
154 return $this->getWarningsResult( $warnings );
155 } elseif ( $this->mParams[
'chunk'] ) {
157 return $this->getChunkResult( $warnings );
158 } elseif ( $this->mParams[
'stash'] ) {
160 return $this->getStashResult( $warnings );
164 if ( UploadBase::isThrottled( $this->
getUser() )
179 private function getStashResult( $warnings ) {
181 $result[
'result'] =
'Success';
182 if ( $warnings && count( $warnings ) > 0 ) {
183 $result[
'warnings'] = $warnings;
187 $this->performStash(
'critical', $result );
197 private function getWarningsResult( $warnings ) {
199 $result[
'result'] =
'Warning';
200 $result[
'warnings'] = $warnings;
203 $this->performStash(
'optional', $result );
215 $configured = $config->
get( MainConfigNames::MinUploadChunkSize );
220 ini_get(
'post_max_size' ),
229 UploadBase::getMaxUploadSize(
'file' ),
230 UploadBase::getMaxPhpUploadSize(),
240 private function getChunkResult( $warnings ) {
243 if ( $warnings && count( $warnings ) > 0 ) {
244 $result[
'warnings'] = $warnings;
247 $request = $this->
getMain()->getRequest();
248 $chunkPath = $request->getFileTempname(
'chunk' );
249 $chunkSize = $request->getUpload(
'chunk' )->getSize();
250 $totalSoFar = $this->mParams[
'offset'] + $chunkSize;
254 if ( $totalSoFar > $this->mParams[
'filesize'] ) {
259 if ( $totalSoFar != $this->mParams[
'filesize'] && $chunkSize < $minChunkSize ) {
263 if ( $this->mParams[
'offset'] == 0 ) {
264 $filekey = $this->performStash(
'critical' );
266 $filekey = $this->mParams[
'filekey'];
272 $this->
dieWithError(
'apierror-stashfailed-nosession',
'stashfailed' );
273 } elseif ( $progress[
'result'] !==
'Continue' || $progress[
'stage'] !==
'uploading' ) {
274 $this->
dieWithError(
'apierror-stashfailed-complete',
'stashfailed' );
277 $status = $this->mUpload->addChunk(
278 $chunkPath, $chunkSize, $this->mParams[
'offset'] );
279 if ( !$status->isGood() ) {
281 'offset' => $this->mUpload->getOffset(),
289 if ( $totalSoFar == $this->mParams[
'filesize'] ) {
290 if ( $this->mParams[
'async'] ) {
294 [
'result' =>
'Poll',
295 'stage' =>
'queued',
'status' => Status::newGood() ]
298 Title::makeTitle(
NS_FILE, $filekey ),
300 'filename' => $this->mParams[
'filename'],
301 'filekey' => $filekey,
305 $result[
'result'] =
'Poll';
306 $result[
'stage'] =
'queued';
308 $status = $this->mUpload->concatenateChunks();
309 if ( !$status->isGood() ) {
313 [
'result' =>
'Failure',
'stage' =>
'assembling',
'status' => $status ]
321 $result[
'warnings'] = $warnings;
327 $this->mUpload->stash->removeFile( $filekey );
328 $filekey = $this->mUpload->getStashFile()->getFileKey();
330 $result[
'result'] =
'Success';
337 'result' =>
'Continue',
338 'stage' =>
'uploading',
339 'offset' => $totalSoFar,
340 'status' => Status::newGood(),
343 $result[
'result'] =
'Continue';
344 $result[
'offset'] = $totalSoFar;
347 $result[
'filekey'] = $filekey;
364 private function performStash( $failureMode, &$data =
null ) {
365 $isPartial = (bool)$this->mParams[
'chunk'];
367 $status = $this->mUpload->tryStashFile( $this->
getUser(), $isPartial );
369 if ( $status->isGood() && !$status->getValue() ) {
371 $status->fatal(
new ApiMessage(
'apierror-stashinvalidfile',
'stashfailed' ) );
373 }
catch ( Exception $e ) {
374 $debugMessage =
'Stashing temporary file failed: ' . get_class( $e ) .
' ' . $e->getMessage();
375 wfDebug( __METHOD__ .
' ' . $debugMessage );
377 $e, [
'wrap' =>
new ApiMessage(
'apierror-stashexception',
'stashfailed' ) ]
381 if ( $status->isGood() ) {
382 $stashFile = $status->getValue();
383 $data[
'filekey'] = $stashFile->getFileKey();
385 $data[
'sessionkey'] = $data[
'filekey'];
386 return $data[
'filekey'];
389 if ( $status->getMessage()->getKey() ===
'uploadstash-exception' ) {
392 list( $exceptionType, $message ) = $status->getMessage()->getParams();
393 $debugMessage =
'Stashing temporary file failed: ' . $exceptionType .
' ' . $message;
394 wfDebug( __METHOD__ .
' ' . $debugMessage );
398 if ( $failureMode !==
'optional' ) {
416 private function dieRecoverableError( $errors, $parameter =
null ) {
417 $this->performStash(
'optional', $data );
420 $data[
'invalidparameter'] = $parameter;
424 foreach ( $errors as $error ) {
426 $msg->setApiData( $msg->getApiData() + $data );
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 );
463 $request = $this->
getMain()->getRequest();
466 if ( !$this->mParams[
'chunk'] ) {
468 'filekey',
'file',
'url' );
472 if ( $this->mParams[
'filekey'] && $this->mParams[
'checkstatus'] ) {
473 $progress = UploadBase::getSessionStatus( $this->
getUser(), $this->mParams[
'filekey'] );
475 $this->
dieWithError(
'apierror-upload-missingresult',
'missingresult' );
476 } elseif ( !$progress[
'status']->isGood() ) {
479 if ( isset( $progress[
'status']->value[
'verification'] ) ) {
482 if ( isset( $progress[
'status']->value[
'warnings'] ) ) {
485 $progress[
'warnings'] = $warnings;
488 unset( $progress[
'status'] );
490 if ( isset( $progress[
'imageinfo'] ) ) {
491 $imageinfo = $progress[
'imageinfo'];
492 unset( $progress[
'imageinfo'] );
506 if ( $this->mParams[
'filename'] ===
null ) {
507 $this->
dieWithError( [
'apierror-missingparam',
'filename' ] );
510 if ( $this->mParams[
'chunk'] ) {
513 if ( isset( $this->mParams[
'filekey'] ) ) {
514 if ( $this->mParams[
'offset'] === 0 ) {
515 $this->
dieWithError(
'apierror-upload-filekeynotallowed',
'filekeynotallowed' );
519 $this->mUpload->continueChunks(
520 $this->mParams[
'filename'],
521 $this->mParams[
'filekey'],
522 $request->getUpload(
'chunk' )
525 if ( $this->mParams[
'offset'] !== 0 ) {
526 $this->
dieWithError(
'apierror-upload-filekeyneeded',
'filekeyneeded' );
530 $this->mUpload->initialize(
531 $this->mParams[
'filename'],
532 $request->getUpload(
'chunk' )
535 } elseif ( isset( $this->mParams[
'filekey'] ) ) {
544 $this->mUpload->initialize(
545 $this->mParams[
'filekey'], $this->mParams[
'filename'], !$this->mParams[
'async']
547 } elseif ( isset( $this->mParams[
'file'] ) ) {
551 if ( $this->mParams[
'async'] ) {
552 $this->
dieWithError(
'apierror-cannot-async-upload-file' );
556 $this->mUpload->initialize(
557 $this->mParams[
'filename'],
558 $request->getUpload(
'file' )
560 } elseif ( isset( $this->mParams[
'url'] ) ) {
575 $this->mUpload->
initialize( $this->mParams[
'filename'],
576 $this->mParams[
'url'] );
589 $permission = $this->mUpload->isAllowed( $user );
591 if ( $permission !==
true ) {
592 if ( !$user->isRegistered() ) {
593 $this->
dieWithError( [
'apierror-mustbeloggedin', $this->
msg(
'action-upload' ) ] );
600 if ( $user->isBlockedFromUpload() ) {
606 if ( $user->isBlockedGlobally() ) {
615 if ( $this->mParams[
'chunk'] ) {
616 $maxSize = UploadBase::getMaxUploadSize();
617 if ( $this->mParams[
'filesize'] > $maxSize ) {
620 if ( !$this->mUpload->getTitle() ) {
625 $verification = $this->mUpload->validateName();
626 if ( $verification ===
true ) {
629 } elseif ( $this->mParams[
'async'] && $this->mParams[
'filekey'] ) {
633 $verification = $this->mUpload->validateName();
634 if ( $verification ===
true ) {
638 wfDebug( __METHOD__ .
" about to verify" );
640 $verification = $this->mUpload->verifyUpload();
641 if ( $verification[
'status'] === UploadBase::OK ) {
655 switch ( $verification[
'status'] ) {
657 case UploadBase::MIN_LENGTH_PARTNAME:
658 $this->dieRecoverableError( [
'filename-tooshort' ],
'filename' );
660 case UploadBase::ILLEGAL_FILENAME:
661 $this->dieRecoverableError(
662 [ ApiMessage::create(
663 'illegal-filename',
null, [
'filename' => $verification[
'filtered'] ]
667 case UploadBase::FILENAME_TOO_LONG:
668 $this->dieRecoverableError( [
'filename-toolong' ],
'filename' );
670 case UploadBase::FILETYPE_MISSING:
671 $this->dieRecoverableError( [
'filetype-missing' ],
'filename' );
673 case UploadBase::WINDOWS_NONASCII_FILENAME:
674 $this->dieRecoverableError( [
'windows-nonascii-filename' ],
'filename' );
677 case UploadBase::EMPTY_FILE:
680 case UploadBase::FILE_TOO_LARGE:
684 case UploadBase::FILETYPE_BADTYPE:
686 'filetype' => $verification[
'finalExt'],
687 'allowed' => array_values( array_unique(
688 $this->
getConfig()->
get( MainConfigNames::FileExtensions ) ) )
691 array_unique( $this->
getConfig()->
get( MainConfigNames::FileExtensions ) );
693 'filetype-banned-type',
696 count( $extensions ),
699 ApiResult::setIndexedTagName( $extradata[
'allowed'],
'ext' );
701 if ( isset( $verification[
'blacklistedExt'] ) ) {
703 $msg[4] = count( $verification[
'blacklistedExt'] );
704 $extradata[
'blacklisted'] = array_values( $verification[
'blacklistedExt'] );
705 ApiResult::setIndexedTagName( $extradata[
'blacklisted'],
'ext' );
707 $msg[1] = $verification[
'finalExt'];
711 $this->
dieWithError( $msg,
'filetype-banned', $extradata );
714 case UploadBase::VERIFICATION_ERROR:
715 $msg = ApiMessage::create( $verification[
'details'],
'verification-error' );
717 $details = array_merge( [ $msg->getKey() ], $msg->getParams() );
719 $details = $verification[
'details'];
721 ApiResult::setIndexedTagName( $details,
'detail' );
722 $msg->setApiData( $msg->getApiData() + [
'details' => $details ] );
727 case UploadBase::HOOK_ABORTED:
728 $msg = $verification[
'error'] ===
'' ?
'hookaborted' : $verification[
'error'];
729 $this->
dieWithError( $msg,
'hookaborted', [
'details' => $verification[
'error'] ] );
732 $this->
dieWithError(
'apierror-unknownerror-nocode',
'unknown-error',
733 [
'details' => [
'code' => $verification[
'status'] ] ] );
745 $warnings = UploadBase::makeWarningsSerializable(
746 $this->mUpload->checkWarnings( $this->getUser() )
755 ApiResult::setIndexedTagName( $warnings,
'warning' );
757 if ( isset( $warnings[
'duplicate'] ) ) {
758 $dupes = array_column( $warnings[
'duplicate'],
'fileName' );
759 ApiResult::setIndexedTagName( $dupes,
'duplicate' );
760 $warnings[
'duplicate'] = $dupes;
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'];
770 if ( isset( $warnings[
'no-change'] ) ) {
771 $file = $warnings[
'no-change'];
772 unset( $warnings[
'no-change'] );
774 $warnings[
'nochange'] = [
779 if ( isset( $warnings[
'duplicate-version'] ) ) {
781 foreach ( $warnings[
'duplicate-version'] as $dupe ) {
783 'timestamp' =>
wfTimestamp( TS_ISO_8601, $dupe[
'timestamp'] )
786 unset( $warnings[
'duplicate-version'] );
788 ApiResult::setIndexedTagName( $dupes,
'ver' );
789 $warnings[
'duplicateversions'] = $dupes;
803 switch ( get_class( $e ) ) {
804 case UploadStashFileNotFoundException::class:
805 $wrap =
'apierror-stashedfilenotfound';
807 case UploadStashBadPathException::class:
808 $wrap =
'apierror-stashpathinvalid';
810 case UploadStashFileException::class:
811 $wrap =
'apierror-stashfilestorage';
813 case UploadStashZeroLengthFileException::class:
814 $wrap =
'apierror-stashzerolength';
816 case UploadStashNotLoggedInException::class:
817 return StatusValue::newFatal( ApiMessage::create(
818 [
'apierror-mustbeloggedin', $this->
msg(
'action-upload' ) ],
'stashnotloggedin'
820 case UploadStashWrongOwnerException::class:
821 $wrap =
'apierror-stashwrongowner';
823 case UploadStashNoSuchKeyException::class:
824 $wrap =
'apierror-stashnosuchfilekey';
827 $wrap = [
'uploadstash-exception', get_class( $e ) ];
830 return StatusValue::newFatal(
844 if ( $this->mParams[
'text'] ===
null ) {
845 $this->mParams[
'text'] = $this->mParams[
'comment'];
849 $file = $this->mUpload->getLocalFile();
859 $this->mParams[
'watchlist'],
$title, $user,
'watchdefault'
862 if ( !$watch && $this->mParams[
'watchlist'] ==
'preferences' && !
$file->exists() ) {
871 if ( $this->mParams[
'watch'] ) {
875 if ( $this->mParams[
'tags'] ) {
877 if ( !$status->isOK() ) {
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' );
889 UploadBase::setSessionStatus(
891 $this->mParams[
'filekey'],
892 [
'result' =>
'Poll',
'stage' =>
'queued',
'status' => Status::newGood() ]
895 Title::makeTitle(
NS_FILE, $this->mParams[
'filename'] ),
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'],
903 'watchlistexpiry' => $watchlistExpiry,
907 $result[
'result'] =
'Poll';
908 $result[
'stage'] =
'queued';
911 $status = $this->mUpload->performUpload(
912 $this->mParams[
'comment'],
913 $this->mParams[
'text'],
916 $this->mParams[
'tags'] ?? [],
920 if ( !$status->isGood() ) {
921 $this->dieRecoverableError( $status->getErrors() );
923 $result[
'result'] =
'Success';
926 $result[
'filename'] =
$file->getName();
927 if ( $warnings && count( $warnings ) > 0 ) {
928 $result[
'warnings'] = $warnings;
945 ParamValidator::PARAM_TYPE =>
'string',
948 ParamValidator::PARAM_DEFAULT =>
''
951 ParamValidator::PARAM_TYPE =>
'tags',
952 ParamValidator::PARAM_ISMULTI =>
true,
955 ParamValidator::PARAM_TYPE =>
'text',
958 ParamValidator::PARAM_DEFAULT =>
false,
959 ParamValidator::PARAM_DEPRECATED =>
true,
972 'ignorewarnings' =>
false,
974 ParamValidator::PARAM_TYPE =>
'upload',
979 ParamValidator::PARAM_DEPRECATED =>
true,
984 ParamValidator::PARAM_TYPE =>
'integer',
985 IntegerDef::PARAM_MIN => 0,
986 IntegerDef::PARAM_MAX => UploadBase::getMaxUploadSize(),
989 ParamValidator::PARAM_TYPE =>
'integer',
990 IntegerDef::PARAM_MIN => 0,
993 ParamValidator::PARAM_TYPE =>
'upload',
997 'checkstatus' =>
false,
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',
1018 return 'https://www.mediawiki.org/wiki/Special:MyLanguage/API:Upload';
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.
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.
dieWithError( $msg, $code=null, $data=null, $httpCode=0)
Abort execution with an error.
static dieDebug( $method, $message)
Internal code errors should be reported with this method.
getMain()
Get the main module.
requireOnlyOneParameter( $params,... $required)
Die if none or more than one of a certain set of parameters is set and not false.
getResult()
Get the result object.
extractRequestParams( $options=[])
Using getAllowedParams(), this function makes an array of the values provided by the user,...
getModuleName()
Get the name of the module being executed by this instance.
dieStatus(StatusValue $status)
Throw an ApiUsageException based on the Status object.
dieBlocked(Block $block)
Throw an ApiUsageException, which will (if uncaught) call the main module's error handler and die wit...
This is the main API class, used for both external and internal processing.
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.
checkPermissions( $user)
Checks that the user has permissions to perform this upload.
verifyUpload()
Performs file verification, dies on error.
UploadBase UploadFromChunks $mUpload
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)
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.
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.
static listParam(array $list, $type='text')
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.
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.
trait ApiWatchlistTrait
An ApiWatchlistTrait adds class properties and convenience methods for APIs that allow you to watch a...
Interface for configuration instances.
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.