61 parent::__construct( $mainModule, $moduleName );
62 $this->jobQueueGroup = $jobQueueGroup;
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;
74 if ( !UploadBase::isEnabled() ) {
82 $request = $this->
getMain()->getRequest();
84 $this->mParams[
'async'] = ( $this->mParams[
'async'] &&
85 $this->
getConfig()->get( MainConfigNames::EnableAsyncUploads ) );
87 $this->mParams[
'file'] = $request->getFileName(
'file' );
88 $this->mParams[
'chunk'] = $request->getFileName(
'chunk' );
91 if ( !$this->mParams[
'filekey'] && $this->mParams[
'sessionkey'] ) {
92 $this->mParams[
'filekey'] = $this->mParams[
'sessionkey'];
99 } elseif ( !isset( $this->mUpload ) ) {
100 $this->
dieDebug( __METHOD__,
'No upload module set' );
111 $status = $this->mUpload->fetchFile();
112 if ( !$status->isGood() ) {
122 if ( !$this->mParams[
'stash'] ) {
123 $permErrors = $this->mUpload->verifyTitlePermissions( $user );
124 if ( $permErrors !==
true ) {
125 $this->dieRecoverableError( $permErrors,
'filename' );
131 $result = $this->getContextResult();
140 if ( $result[
'result'] ===
'Success' ) {
141 $imageinfo = $this->mUpload->getImageInfo( $this->
getResult() );
146 $this->mUpload->cleanupTempFile();
153 private function getContextResult() {
155 if ( $warnings && !$this->mParams[
'ignorewarnings'] ) {
157 return $this->getWarningsResult( $warnings );
158 } elseif ( $this->mParams[
'chunk'] ) {
160 return $this->getChunkResult( $warnings );
161 } elseif ( $this->mParams[
'stash'] ) {
163 return $this->getStashResult( $warnings );
176 private function getStashResult( $warnings ) {
178 $result[
'result'] =
'Success';
179 if ( $warnings && count( $warnings ) > 0 ) {
180 $result[
'warnings'] = $warnings;
184 $this->performStash(
'critical', $result );
194 private function getWarningsResult( $warnings ) {
196 $result[
'result'] =
'Warning';
197 $result[
'warnings'] = $warnings;
200 $this->performStash(
'optional', $result );
212 $configured = $config->
get( MainConfigNames::MinUploadChunkSize );
217 ini_get(
'post_max_size' ),
226 UploadBase::getMaxUploadSize(
'file' ),
227 UploadBase::getMaxPhpUploadSize(),
237 private function getChunkResult( $warnings ) {
240 if ( $warnings && count( $warnings ) > 0 ) {
241 $result[
'warnings'] = $warnings;
244 $request = $this->
getMain()->getRequest();
245 $chunkPath = $request->getFileTempname(
'chunk' );
246 $chunkSize = $request->getUpload(
'chunk' )->getSize();
247 $totalSoFar = $this->mParams[
'offset'] + $chunkSize;
251 if ( $totalSoFar > $this->mParams[
'filesize'] ) {
256 if ( $totalSoFar != $this->mParams[
'filesize'] && $chunkSize < $minChunkSize ) {
260 if ( $this->mParams[
'offset'] == 0 ) {
261 $filekey = $this->performStash(
'critical' );
263 $filekey = $this->mParams[
'filekey'];
269 $this->
dieWithError(
'apierror-stashfailed-nosession',
'stashfailed' );
270 } elseif ( $progress[
'result'] !==
'Continue' || $progress[
'stage'] !==
'uploading' ) {
271 $this->
dieWithError(
'apierror-stashfailed-complete',
'stashfailed' );
274 $status = $this->mUpload->addChunk(
275 $chunkPath, $chunkSize, $this->mParams[
'offset'] );
276 if ( !$status->isGood() ) {
278 'offset' => $this->mUpload->getOffset(),
286 if ( $totalSoFar == $this->mParams[
'filesize'] ) {
287 if ( $this->mParams[
'async'] ) {
291 [
'result' =>
'Poll',
292 'stage' =>
'queued',
'status' => Status::newGood() ]
295 Title::makeTitle(
NS_FILE, $filekey ),
297 'filename' => $this->mParams[
'filename'],
298 'filekey' => $filekey,
302 $result[
'result'] =
'Poll';
303 $result[
'stage'] =
'queued';
305 $status = $this->mUpload->concatenateChunks();
306 if ( !$status->isGood() ) {
310 [
'result' =>
'Failure',
'stage' =>
'assembling',
'status' => $status ]
318 $result[
'warnings'] = $warnings;
324 $this->mUpload->stash->removeFile( $filekey );
325 $filekey = $this->mUpload->getStashFile()->getFileKey();
327 $result[
'result'] =
'Success';
334 'result' =>
'Continue',
335 'stage' =>
'uploading',
336 'offset' => $totalSoFar,
337 'status' => Status::newGood(),
340 $result[
'result'] =
'Continue';
341 $result[
'offset'] = $totalSoFar;
344 $result[
'filekey'] = $filekey;
361 private function performStash( $failureMode, &$data =
null ) {
362 $isPartial = (bool)$this->mParams[
'chunk'];
364 $status = $this->mUpload->tryStashFile( $this->
getUser(), $isPartial );
366 if ( $status->isGood() && !$status->getValue() ) {
368 $status->fatal(
new ApiMessage(
'apierror-stashinvalidfile',
'stashfailed' ) );
370 }
catch ( Exception $e ) {
371 $debugMessage =
'Stashing temporary file failed: ' . get_class( $e ) .
' ' . $e->getMessage();
372 wfDebug( __METHOD__ .
' ' . $debugMessage );
374 $e, [
'wrap' =>
new ApiMessage(
'apierror-stashexception',
'stashfailed' ) ]
378 if ( $status->isGood() ) {
379 $stashFile = $status->getValue();
380 $data[
'filekey'] = $stashFile->getFileKey();
382 $data[
'sessionkey'] = $data[
'filekey'];
383 return $data[
'filekey'];
386 if ( $status->getMessage()->getKey() ===
'uploadstash-exception' ) {
389 [ $exceptionType, $message ] = $status->getMessage()->getParams();
390 $debugMessage =
'Stashing temporary file failed: ' . $exceptionType .
' ' . $message;
391 wfDebug( __METHOD__ .
' ' . $debugMessage );
395 if ( $failureMode !==
'optional' ) {
413 private function dieRecoverableError( $errors, $parameter =
null ) {
414 $this->performStash(
'optional', $data );
417 $data[
'invalidparameter'] = $parameter;
421 foreach ( $errors as $error ) {
423 $msg->setApiData( $msg->getApiData() + $data );
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 );
460 $request = $this->
getMain()->getRequest();
463 if ( !$this->mParams[
'chunk'] ) {
465 'filekey',
'file',
'url' );
469 if ( $this->mParams[
'filekey'] && $this->mParams[
'checkstatus'] ) {
470 $progress = UploadBase::getSessionStatus( $this->
getUser(), $this->mParams[
'filekey'] );
472 $this->
dieWithError(
'apierror-upload-missingresult',
'missingresult' );
473 } elseif ( !$progress[
'status']->isGood() ) {
476 if ( isset( $progress[
'status']->value[
'verification'] ) ) {
479 if ( isset( $progress[
'status']->value[
'warnings'] ) ) {
482 $progress[
'warnings'] = $warnings;
485 unset( $progress[
'status'] );
487 if ( isset( $progress[
'imageinfo'] ) ) {
488 $imageinfo = $progress[
'imageinfo'];
489 unset( $progress[
'imageinfo'] );
503 if ( $this->mParams[
'filename'] ===
null ) {
504 $this->
dieWithError( [
'apierror-missingparam',
'filename' ] );
507 if ( $this->mParams[
'chunk'] ) {
510 if ( isset( $this->mParams[
'filekey'] ) ) {
511 if ( $this->mParams[
'offset'] === 0 ) {
512 $this->
dieWithError(
'apierror-upload-filekeynotallowed',
'filekeynotallowed' );
516 $this->mUpload->continueChunks(
517 $this->mParams[
'filename'],
518 $this->mParams[
'filekey'],
519 $request->getUpload(
'chunk' )
522 if ( $this->mParams[
'offset'] !== 0 ) {
523 $this->
dieWithError(
'apierror-upload-filekeyneeded',
'filekeyneeded' );
527 $this->mUpload->initialize(
528 $this->mParams[
'filename'],
529 $request->getUpload(
'chunk' )
532 } elseif ( isset( $this->mParams[
'filekey'] ) ) {
534 if ( !UploadFromStash::isValidKey( $this->mParams[
'filekey'] ) ) {
541 $this->mUpload->initialize(
542 $this->mParams[
'filekey'], $this->mParams[
'filename'], !$this->mParams[
'async']
544 } elseif ( isset( $this->mParams[
'file'] ) ) {
548 if ( $this->mParams[
'async'] ) {
549 $this->
dieWithError(
'apierror-cannot-async-upload-file' );
553 $this->mUpload->initialize(
554 $this->mParams[
'filename'],
555 $request->getUpload(
'file' )
557 } elseif ( isset( $this->mParams[
'url'] ) ) {
572 $this->mUpload->
initialize( $this->mParams[
'filename'],
573 $this->mParams[
'url'] );
586 $permission = $this->mUpload->isAllowed( $user );
588 if ( $permission !==
true ) {
589 if ( !$user->isNamed() ) {
590 $this->
dieWithError( [
'apierror-mustbeloggedin', $this->
msg(
'action-upload' ) ] );
593 $this->
dieStatus( User::newFatalPermissionDeniedStatus( $permission ) );
597 if ( $user->isBlockedFromUpload() ) {
607 if ( $this->mParams[
'chunk'] ) {
608 $maxSize = UploadBase::getMaxUploadSize();
609 if ( $this->mParams[
'filesize'] > $maxSize ) {
612 if ( !$this->mUpload->getTitle() ) {
617 $verification = $this->mUpload->validateName();
618 if ( $verification ===
true ) {
621 } elseif ( $this->mParams[
'async'] && $this->mParams[
'filekey'] ) {
625 $verification = $this->mUpload->validateName();
626 if ( $verification ===
true ) {
630 wfDebug( __METHOD__ .
" about to verify" );
632 $verification = $this->mUpload->verifyUpload();
633 if ( $verification[
'status'] === UploadBase::OK ) {
647 switch ( $verification[
'status'] ) {
649 case UploadBase::MIN_LENGTH_PARTNAME:
650 $this->dieRecoverableError( [
'filename-tooshort' ],
'filename' );
652 case UploadBase::ILLEGAL_FILENAME:
653 $this->dieRecoverableError(
654 [ ApiMessage::create(
655 'illegal-filename',
null, [
'filename' => $verification[
'filtered'] ]
659 case UploadBase::FILENAME_TOO_LONG:
660 $this->dieRecoverableError( [
'filename-toolong' ],
'filename' );
662 case UploadBase::FILETYPE_MISSING:
663 $this->dieRecoverableError( [
'filetype-missing' ],
'filename' );
665 case UploadBase::WINDOWS_NONASCII_FILENAME:
666 $this->dieRecoverableError( [
'windows-nonascii-filename' ],
'filename' );
669 case UploadBase::EMPTY_FILE:
672 case UploadBase::FILE_TOO_LARGE:
676 case UploadBase::FILETYPE_BADTYPE:
678 'filetype' => $verification[
'finalExt'],
679 'allowed' => array_values( array_unique(
680 $this->
getConfig()->
get( MainConfigNames::FileExtensions ) ) )
683 array_unique( $this->
getConfig()->
get( MainConfigNames::FileExtensions ) );
685 'filetype-banned-type',
688 count( $extensions ),
691 ApiResult::setIndexedTagName( $extradata[
'allowed'],
'ext' );
693 if ( isset( $verification[
'blacklistedExt'] ) ) {
695 $msg[4] = count( $verification[
'blacklistedExt'] );
696 $extradata[
'blacklisted'] = array_values( $verification[
'blacklistedExt'] );
697 ApiResult::setIndexedTagName( $extradata[
'blacklisted'],
'ext' );
699 $msg[1] = $verification[
'finalExt'];
703 $this->
dieWithError( $msg,
'filetype-banned', $extradata );
706 case UploadBase::VERIFICATION_ERROR:
707 $msg = ApiMessage::create( $verification[
'details'],
'verification-error' );
709 $details = array_merge( [ $msg->getKey() ], $msg->getParams() );
711 $details = $verification[
'details'];
713 ApiResult::setIndexedTagName( $details,
'detail' );
714 $msg->setApiData( $msg->getApiData() + [
'details' => $details ] );
719 case UploadBase::HOOK_ABORTED:
720 $msg = $verification[
'error'] ===
'' ?
'hookaborted' : $verification[
'error'];
721 $this->
dieWithError( $msg,
'hookaborted', [
'details' => $verification[
'error'] ] );
724 $this->
dieWithError(
'apierror-unknownerror-nocode',
'unknown-error',
725 [
'details' => [
'code' => $verification[
'status'] ] ] );
737 $warnings = UploadBase::makeWarningsSerializable(
738 $this->mUpload->checkWarnings( $this->getUser() )
747 ApiResult::setIndexedTagName( $warnings,
'warning' );
749 if ( isset( $warnings[
'duplicate'] ) ) {
750 $dupes = array_column( $warnings[
'duplicate'],
'fileName' );
751 ApiResult::setIndexedTagName( $dupes,
'duplicate' );
752 $warnings[
'duplicate'] = $dupes;
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'];
762 if ( isset( $warnings[
'no-change'] ) ) {
763 $file = $warnings[
'no-change'];
764 unset( $warnings[
'no-change'] );
766 $warnings[
'nochange'] = [
771 if ( isset( $warnings[
'duplicate-version'] ) ) {
773 foreach ( $warnings[
'duplicate-version'] as $dupe ) {
775 'timestamp' =>
wfTimestamp( TS_ISO_8601, $dupe[
'timestamp'] )
778 unset( $warnings[
'duplicate-version'] );
780 ApiResult::setIndexedTagName( $dupes,
'ver' );
781 $warnings[
'duplicateversions'] = $dupes;
795 switch ( get_class( $e ) ) {
796 case UploadStashFileNotFoundException::class:
797 $wrap =
'apierror-stashedfilenotfound';
799 case UploadStashBadPathException::class:
800 $wrap =
'apierror-stashpathinvalid';
802 case UploadStashFileException::class:
803 $wrap =
'apierror-stashfilestorage';
805 case UploadStashZeroLengthFileException::class:
806 $wrap =
'apierror-stashzerolength';
808 case UploadStashNotLoggedInException::class:
809 return StatusValue::newFatal( ApiMessage::create(
810 [
'apierror-mustbeloggedin', $this->
msg(
'action-upload' ) ],
'stashnotloggedin'
812 case UploadStashWrongOwnerException::class:
813 $wrap =
'apierror-stashwrongowner';
815 case UploadStashNoSuchKeyException::class:
816 $wrap =
'apierror-stashnosuchfilekey';
819 $wrap = [
'uploadstash-exception', get_class( $e ) ];
822 return StatusValue::newFatal(
836 $this->mParams[
'text'] ??= $this->mParams[
'comment'];
839 $file = $this->mUpload->getLocalFile();
841 $title =
$file->getTitle();
849 $this->mParams[
'watchlist'], $title, $user,
'watchdefault'
852 if ( !$watch && $this->mParams[
'watchlist'] ==
'preferences' && !
$file->exists() ) {
861 if ( $this->mParams[
'watch'] ) {
865 if ( $this->mParams[
'tags'] ) {
867 if ( !$status->isOK() ) {
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' );
879 UploadBase::setSessionStatus(
881 $this->mParams[
'filekey'],
882 [
'result' =>
'Poll',
'stage' =>
'queued',
'status' => Status::newGood() ]
885 Title::makeTitle(
NS_FILE, $this->mParams[
'filename'] ),
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'],
893 'watchlistexpiry' => $watchlistExpiry,
897 $result[
'result'] =
'Poll';
898 $result[
'stage'] =
'queued';
901 $status = $this->mUpload->performUpload(
902 $this->mParams[
'comment'],
903 $this->mParams[
'text'],
906 $this->mParams[
'tags'] ?? [],
910 if ( !$status->isGood() ) {
911 $this->dieRecoverableError( $status->getErrors() );
913 $result[
'result'] =
'Success';
916 $result[
'filename'] =
$file->getName();
917 if ( $warnings && count( $warnings ) > 0 ) {
918 $result[
'warnings'] = $warnings;
935 ParamValidator::PARAM_TYPE =>
'string',
938 ParamValidator::PARAM_DEFAULT =>
''
941 ParamValidator::PARAM_TYPE =>
'tags',
942 ParamValidator::PARAM_ISMULTI =>
true,
945 ParamValidator::PARAM_TYPE =>
'text',
948 ParamValidator::PARAM_DEFAULT =>
false,
949 ParamValidator::PARAM_DEPRECATED =>
true,
962 'ignorewarnings' =>
false,
964 ParamValidator::PARAM_TYPE =>
'upload',
969 ParamValidator::PARAM_DEPRECATED =>
true,
974 ParamValidator::PARAM_TYPE =>
'integer',
975 IntegerDef::PARAM_MIN => 0,
976 IntegerDef::PARAM_MAX => UploadBase::getMaxUploadSize(),
979 ParamValidator::PARAM_TYPE =>
'integer',
980 IntegerDef::PARAM_MIN => 0,
983 ParamValidator::PARAM_TYPE =>
'upload',
987 'checkstatus' =>
false,
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',
1008 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 0 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.
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 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.
trait ApiWatchlistTrait
An ApiWatchlistTrait adds class properties and convenience methods for APIs that allow you to watch a...
if(PHP_SAPI !='cli-server') if(!isset( $_SERVER['SCRIPT_FILENAME'])) $file
Item class for a filearchive table row.