diff options
131 files changed, 3382 insertions, 2242 deletions
diff --git a/RELEASE-NOTES-1.29 b/RELEASE-NOTES-1.29 index 21a94c5d4ae8..b055ade42b14 100644 --- a/RELEASE-NOTES-1.29 +++ b/RELEASE-NOTES-1.29 @@ -14,6 +14,14 @@ production. will still be blocked. * The resetpassword right and associated password reset capture feature has been removed. +* The $error parameter to the EmailUser hook should be set to a Status object + or boolean false. This should be compatible with at least MediaWiki 1.23 if + not earlier. Returning a raw HTML string is now deprecated. +* The $message parameter to the ApiCheckCanExecute hook should be set to an + ApiMessage. This is compatible with MediaWiki 1.27 and later. Returning a + code for ApiBase::parseMsg() will no longer work. +* ApiBase::$messageMap is no longer public. Code attempting to access it will + result in a PHP fatal error. === New features in 1.29 === * (T5233) A cookie can now be set when a user is autoblocked, to track that user if @@ -37,8 +45,44 @@ production. body instead. * The capture option for action=resetpassword has been removed * action=clearhasmsg now requires a POST. +* (T47843) API errors and warnings may be requested in non-English languages + using the new 'errorformat', 'errorlang', and 'errorsuselocal' parameters. +* API error codes may have changed. Most notably, errors from modules using + parameter prefixes (e.g. all query submodules) will no longer be prefixed. +* action=emailuser may return a "Warnings" status, and now returns 'warnings' and + 'errors' subelements (as applicable) instead of 'message'. +* action=imagerotate returns an 'errors' subelement rather than 'errormessage'. +* action=move now reports errors when moving the talk page as an array under + key 'talkmove-errors', rather than using 'talkmove-error-code' and + 'talkmove-error-info'. The format for subpage move errors has also changed. +* action=rollback no longer returns a "messageHtml" property on errors. Use + errorformat=html if you're wanting HTML formatting of messages. +* action=upload now reports optional stash failures as an array under key + 'stasherrors' rather than a 'stashfailed' text string. +* action=watch reports 'errors' and 'warnings' instead of a single 'error'. === Action API internal changes in 1.29 === +* New methods were added to ApiBase to handle errors and warnings using i18n + keys. Methods for using hard-coded English messages were deprecated: + * ApiBase::dieUsage() was deprecated + * ApiBase::dieUsageMsg() was deprecated + * ApiBase::dieUsageMsgOrDebug() was deprecated + * ApiBase::getErrorFromStatus() was deprecated + * ApiBase::parseMsg() was deprecated + * ApiBase::setWarning() was deprecated +* ApiBase::$messageMap is no longer public. Code attempting to access it will + result in a PHP fatal error. +* The $message parameter to the ApiCheckCanExecute hook should be set to an + ApiMessage. This is compatible with MediaWiki 1.27 and later. Returning a + code for ApiBase::parseMsg() will no longer work. +* UsageException is deprecated in favor of ApiUsageException. For the time + being ApiUsageException is a subclass of UsageException to allow things that + catch only UsageException to still function properly. +* If, for some strange reason, code was using an ApiErrorFormatter instead of + ApiErrorFormatter_BackCompat, note that the result format has changed and + various methods now take a module path rather than a module name. +* ApiMessageTrait::getApiCode() now strips 'apierror-' and 'apiwarn-' prefixes + from the message key, and maps some message keys for backwards compatibility. === Languages updated in 1.29 === diff --git a/autoload.php b/autoload.php index 0d6407bc20c0..bf36f9ffd96b 100644 --- a/autoload.php +++ b/autoload.php @@ -145,6 +145,7 @@ $wgAutoloadLocalClasses = [ 'ApiUnblock' => __DIR__ . '/includes/api/ApiUnblock.php', 'ApiUndelete' => __DIR__ . '/includes/api/ApiUndelete.php', 'ApiUpload' => __DIR__ . '/includes/api/ApiUpload.php', + 'ApiUsageException' => __DIR__ . '/includes/api/ApiUsageException.php', 'ApiUserrights' => __DIR__ . '/includes/api/ApiUserrights.php', 'ApiWatch' => __DIR__ . '/includes/api/ApiWatch.php', 'ArchivedFile' => __DIR__ . '/includes/filerepo/file/ArchivedFile.php', @@ -1503,7 +1504,7 @@ $wgAutoloadLocalClasses = [ 'UploadStashWrongOwnerException' => __DIR__ . '/includes/upload/UploadStash.php', 'UploadStashZeroLengthFileException' => __DIR__ . '/includes/upload/UploadStash.php', 'UppercaseCollation' => __DIR__ . '/includes/collation/UppercaseCollation.php', - 'UsageException' => __DIR__ . '/includes/api/ApiMain.php', + 'UsageException' => __DIR__ . '/includes/api/ApiUsageException.php', 'User' => __DIR__ . '/includes/user/User.php', 'UserArray' => __DIR__ . '/includes/user/UserArray.php', 'UserArrayFromResult' => __DIR__ . '/includes/user/UserArrayFromResult.php', diff --git a/docs/hooks.txt b/docs/hooks.txt index a73d50f9bd71..b88a87a70126 100644 --- a/docs/hooks.txt +++ b/docs/hooks.txt @@ -358,8 +358,12 @@ authenticate and authorize API clients before executing the module. Return false and set a message to cancel the request. $module: Module object $user: Current user -&$message: API usage message to die with, as a message key or array - as accepted by ApiBase::dieUsageMsg. +&$message: API message to die with. Specific values accepted depend on the + MediaWiki version: + * 1.29+: IApiMessage, Message, string message key, or key+parameters array to + pass to ApiBase::dieWithError(). + * 1.27+: IApiMessage, or a key or key+parameters in ApiBase::$messageMap. + * Earlier: A key or key+parameters in ApiBase::$messageMap. 'APIEditBeforeSave': DEPRECATED! Use EditFilterMergedContent instead. Before saving a page with api.php?action=edit, after @@ -1459,7 +1463,7 @@ true to allow those checks to occur, and false if checking is done. &$from: MailAddress object of sending user &$subject: subject of the mail &$text: text of the mail -&$error: Out-param for an error +&$error: Out-param for an error. Should be set to a Status object or boolean false. 'EmailUserCC': Before sending the copy of the email to the author. &$to: MailAddress object of receiving user diff --git a/includes/FileDeleteForm.php b/includes/FileDeleteForm.php index e6223e81b84e..f850152050c0 100644 --- a/includes/FileDeleteForm.php +++ b/includes/FileDeleteForm.php @@ -152,7 +152,7 @@ class FileDeleteForm { * @param User $user User object performing the request * @param array $tags Tags to apply to the deletion action * @throws MWException - * @return bool|Status + * @return Status */ public static function doDelete( &$title, &$file, &$oldimage, $reason, $suppress, User $user = null, $tags = [] diff --git a/includes/WatchedItemQueryService.php b/includes/WatchedItemQueryService.php index 0c3d52a39fef..cd78b499df3a 100644 --- a/includes/WatchedItemQueryService.php +++ b/includes/WatchedItemQueryService.php @@ -422,10 +422,7 @@ class WatchedItemQueryService { $ownersToken = $watchlistOwner->getOption( 'watchlisttoken' ); $token = $options['watchlistOwnerToken']; if ( $ownersToken == '' || !hash_equals( $ownersToken, $token ) ) { - throw new UsageException( - 'Incorrect watchlist token provided -- please set a correct token in Special:Preferences', - 'bad_wltoken' - ); + throw ApiUsageException::newWithMessage( null, 'apierror-bad-watchlist-token', 'bad_wltoken' ); } return $watchlistOwner->getId(); } diff --git a/includes/api/ApiAMCreateAccount.php b/includes/api/ApiAMCreateAccount.php index 2511e3be99cb..5d12590fdf77 100644 --- a/includes/api/ApiAMCreateAccount.php +++ b/includes/api/ApiAMCreateAccount.php @@ -56,8 +56,8 @@ class ApiAMCreateAccount extends ApiBase { $bits = wfParseUrl( $params['returnurl'] ); if ( !$bits || $bits['scheme'] === '' ) { $encParamName = $this->encodeParamName( 'returnurl' ); - $this->dieUsage( - "Invalid value '{$params['returnurl']}' for url parameter $encParamName", + $this->dieWithError( + [ 'apierror-badurl', $encParamName, wfEscapeWikiText( $params['returnurl'] ) ], "badurl_{$encParamName}" ); } diff --git a/includes/api/ApiAuthManagerHelper.php b/includes/api/ApiAuthManagerHelper.php index 6fafebff3b49..5327d7a99bca 100644 --- a/includes/api/ApiAuthManagerHelper.php +++ b/includes/api/ApiAuthManagerHelper.php @@ -93,7 +93,7 @@ class ApiAuthManagerHelper { /** * Call $manager->securitySensitiveOperationStatus() * @param string $operation Operation being checked. - * @throws UsageException + * @throws ApiUsageException */ public function securitySensitiveOperation( $operation ) { $status = AuthManager::singleton()->securitySensitiveOperationStatus( $operation ); @@ -102,14 +102,10 @@ class ApiAuthManagerHelper { return; case AuthManager::SEC_REAUTH: - $this->module->dieUsage( - 'You have not authenticated recently in this session, please reauthenticate.', 'reauthenticate' - ); + $this->module->dieWithError( 'apierror-reauthenticate' ); case AuthManager::SEC_FAIL: - $this->module->dieUsage( - 'This action is not available as your identify cannot be verified.', 'cannotreauthenticate' - ); + $this->module->dieWithError( 'apierror-cannotreauthenticate' ); default: throw new UnexpectedValueException( "Unknown status \"$status\"" ); diff --git a/includes/api/ApiBase.php b/includes/api/ApiBase.php index 0cd46e42b492..a40593f6bd5a 100644 --- a/includes/api/ApiBase.php +++ b/includes/api/ApiBase.php @@ -545,7 +545,7 @@ abstract class ApiBase extends ContextSource { * @since 1.25 * @param string $path * @return ApiBase|null - * @throws UsageException + * @throws ApiUsageException */ public function getModuleFromPath( $path ) { $module = $this->getMain(); @@ -565,14 +565,14 @@ abstract class ApiBase extends ContextSource { $manager = $parent->getModuleManager(); if ( $manager === null ) { $errorPath = implode( '+', array_slice( $parts, 0, $i ) ); - $this->dieUsage( "The module \"$errorPath\" has no submodules", 'badmodule' ); + $this->dieWithError( [ 'apierror-badmodule-nosubmodules', $errorPath ], 'badmodule' ); } $module = $manager->getModule( $parts[$i] ); if ( $module === null ) { $errorPath = $i ? implode( '+', array_slice( $parts, 0, $i ) ) : $parent->getModuleName(); - $this->dieUsage( - "The module \"$errorPath\" does not have a submodule \"{$parts[$i]}\"", + $this->dieWithError( + [ 'apierror-badmodule-badsubmodule', $errorPath, wfEscapeWikiText( $parts[$i] ) ], 'badmodule' ); } @@ -670,11 +670,18 @@ abstract class ApiBase extends ContextSource { /** * This method mangles parameter name based on the prefix supplied to the constructor. * Override this method to change parameter name during runtime - * @param string $paramName Parameter name - * @return string Prefixed parameter name + * @param string|string[] $paramName Parameter name + * @return string|string[] Prefixed parameter name + * @since 1.29 accepts an array of strings */ public function encodeParamName( $paramName ) { - return $this->mModulePrefix . $paramName; + if ( is_array( $paramName ) ) { + return array_map( function ( $name ) { + return $this->mModulePrefix . $name; + }, $paramName ); + } else { + return $this->mModulePrefix . $paramName; + } } /** @@ -725,20 +732,32 @@ abstract class ApiBase extends ContextSource { public function requireOnlyOneParameter( $params, $required /*...*/ ) { $required = func_get_args(); array_shift( $required ); - $p = $this->getModulePrefix(); $intersection = array_intersect( array_keys( array_filter( $params, [ $this, 'parameterNotEmpty' ] ) ), $required ); if ( count( $intersection ) > 1 ) { - $this->dieUsage( - "The parameters {$p}" . implode( ", {$p}", $intersection ) . ' can not be used together', - 'invalidparammix' ); + $this->dieWithError( [ + 'apierror-invalidparammix', + Message::listParam( array_map( + function ( $p ) { + return '<var>' . $this->encodeParamName( $p ) . '</var>'; + }, + array_values( $intersection ) + ) ), + count( $intersection ), + ] ); } elseif ( count( $intersection ) == 0 ) { - $this->dieUsage( - "One of the parameters {$p}" . implode( ", {$p}", $required ) . ' is required', - 'missingparam' - ); + $this->dieWithError( [ + 'apierror-missingparam-one-of', + Message::listParam( array_map( + function ( $p ) { + return '<var>' . $this->encodeParamName( $p ) . '</var>'; + }, + array_values( $required ) + ) ), + count( $required ), + ], 'missingparam' ); } } @@ -751,16 +770,21 @@ abstract class ApiBase extends ContextSource { public function requireMaxOneParameter( $params, $required /*...*/ ) { $required = func_get_args(); array_shift( $required ); - $p = $this->getModulePrefix(); $intersection = array_intersect( array_keys( array_filter( $params, [ $this, 'parameterNotEmpty' ] ) ), $required ); if ( count( $intersection ) > 1 ) { - $this->dieUsage( - "The parameters {$p}" . implode( ", {$p}", $intersection ) . ' can not be used together', - 'invalidparammix' - ); + $this->dieWithError( [ + 'apierror-invalidparammix', + Message::listParam( array_map( + function ( $p ) { + return '<var>' . $this->encodeParamName( $p ) . '</var>'; + }, + array_values( $intersection ) + ) ), + count( $intersection ), + ] ); } } @@ -774,7 +798,6 @@ abstract class ApiBase extends ContextSource { public function requireAtLeastOneParameter( $params, $required /*...*/ ) { $required = func_get_args(); array_shift( $required ); - $p = $this->getModulePrefix(); $intersection = array_intersect( array_keys( array_filter( $params, [ $this, 'parameterNotEmpty' ] ) ), @@ -782,8 +805,16 @@ abstract class ApiBase extends ContextSource { ); if ( count( $intersection ) == 0 ) { - $this->dieUsage( "At least one of the parameters {$p}" . - implode( ", {$p}", $required ) . ' is required', "{$p}missingparam" ); + $this->dieWithError( [ + 'apierror-missingparam-at-least-one-of', + Message::listParam( array_map( + function ( $p ) { + return '<var>' . $this->encodeParamName( $p ) . '</var>'; + }, + array_values( $required ) + ) ), + count( $required ), + ], 'missingparam' ); } } @@ -812,10 +843,8 @@ abstract class ApiBase extends ContextSource { } if ( $badParams ) { - $this->dieUsage( - 'The following parameters were found in the query string, but must be in the POST body: ' - . join( ', ', $badParams ), - 'mustpostparams' + $this->dieWithError( + [ 'apierror-mustpostparams', join( ', ', $badParams ), count( $badParams ) ] ); } } @@ -848,10 +877,10 @@ abstract class ApiBase extends ContextSource { if ( isset( $params['title'] ) ) { $titleObj = Title::newFromText( $params['title'] ); if ( !$titleObj || $titleObj->isExternal() ) { - $this->dieUsageMsg( [ 'invalidtitle', $params['title'] ] ); + $this->dieWithError( [ 'apierror-invalidtitle', wfEscapeWikiText( $params['title'] ) ] ); } if ( !$titleObj->canExist() ) { - $this->dieUsage( "Namespace doesn't allow actual pages", 'pagecannotexist' ); + $this->dieWithError( 'apierror-pagecannotexist' ); } $pageObj = WikiPage::factory( $titleObj ); if ( $load !== false ) { @@ -863,7 +892,7 @@ abstract class ApiBase extends ContextSource { } $pageObj = WikiPage::newFromID( $params['pageid'], $load ); if ( !$pageObj ) { - $this->dieUsageMsg( [ 'nosuchpageid', $params['pageid'] ] ); + $this->dieWithError( [ 'apierror-nosuchpageid', $params['pageid'] ] ); } } @@ -994,10 +1023,8 @@ abstract class ApiBase extends ContextSource { // accidentally uploaded as a field fails spectacularly) $value = $this->getMain()->getRequest()->unsetVal( $encParamName ); if ( $value !== null ) { - $this->dieUsage( - "File upload param $encParamName is not a file upload; " . - 'be sure to use multipart/form-data for your POST and include ' . - 'a filename in the Content-Disposition header.', + $this->dieWithError( + [ 'apierror-badupload', $encParamName ], "badupload_{$encParamName}" ); } @@ -1032,10 +1059,7 @@ abstract class ApiBase extends ContextSource { // done by WebRequest for $_GET. Let's call that a feature. $value = join( "\x1f", $request->normalizeUnicode( explode( "\x1f", $rawValue ) ) ); } else { - $this->dieUsage( - "U+001F multi-value separation may only be used for multi-valued parameters.", - 'badvalue_notmultivalue' - ); + $this->dieWithError( 'apierror-badvalue-notmultivalue', 'badvalue_notmultivalue' ); } } @@ -1072,7 +1096,7 @@ abstract class ApiBase extends ContextSource { case 'text': case 'password': if ( $required && $value === '' ) { - $this->dieUsageMsg( [ 'missingparam', $paramName ] ); + $this->dieWithError( [ 'apierror-missingparam', $paramName ] ); } break; case 'integer': // Force everything using intval() and optionally validate limits @@ -1175,8 +1199,6 @@ abstract class ApiBase extends ContextSource { // Set a warning if a deprecated parameter has been passed if ( $deprecated && $value !== false ) { - $this->setWarning( "The $encParamName parameter has been deprecated." ); - $feature = $encParamName; $m = $this; while ( !$m->isMain() ) { @@ -1186,10 +1208,10 @@ abstract class ApiBase extends ContextSource { $feature = "{$param}={$name}&{$feature}"; $m = $p; } - $this->logFeatureUsage( $feature ); + $this->addDeprecation( [ 'apiwarn-deprecation-parameter', $encParamName ], $feature ); } } elseif ( $required ) { - $this->dieUsageMsg( [ 'missingparam', $paramName ] ); + $this->dieWithError( [ 'apierror-missingparam', $paramName ] ); } return $value; @@ -1204,11 +1226,7 @@ abstract class ApiBase extends ContextSource { */ protected function handleParamNormalization( $paramName, $value, $rawValue ) { $encParamName = $this->encodeParamName( $paramName ); - $this->setWarning( - "The value passed for '$encParamName' contains invalid or non-normalized data. " - . 'Textual data should be valid, NFC-normalized Unicode without ' - . 'C0 control characters other than HT (\\t), LF (\\n), and CR (\\r).' - ); + $this->addWarning( [ 'apiwarn-badutf8', $encParamName ] ); } /** @@ -1265,9 +1283,10 @@ abstract class ApiBase extends ContextSource { } if ( self::truncateArray( $valuesList, $sizeLimit ) ) { - $this->logFeatureUsage( "too-many-$valueName-for-{$this->getModulePath()}" ); - $this->setWarning( "Too many values supplied for parameter '$valueName': " . - "the limit is $sizeLimit" ); + $this->addDeprecation( + [ 'apiwarn-toomanyvalues', $valueName, $sizeLimit ], + "too-many-$valueName-for-{$this->getModulePath()}" + ); } if ( !$allowMultiple && count( $valuesList ) != 1 ) { @@ -1276,26 +1295,38 @@ abstract class ApiBase extends ContextSource { return $value; } - $possibleValues = is_array( $allowedValues ) - ? "of '" . implode( "', '", $allowedValues ) . "'" - : ''; - $this->dieUsage( - "Only one $possibleValues is allowed for parameter '$valueName'", - "multival_$valueName" - ); + if ( is_array( $allowedValues ) ) { + $values = array_map( function ( $v ) { + return '<kbd>' . wfEscapeWikiText( $v ) . '</kbd>'; + }, $allowedValues ); + $this->dieWithError( [ + 'apierror-multival-only-one-of', + $valueName, + Message::listParam( $values ), + count( $values ), + ], "multival_$valueName" ); + } else { + $this->dieWithError( [ + 'apierror-multival-only-one', + $valueName, + ], "multival_$valueName" ); + } } if ( is_array( $allowedValues ) ) { // Check for unknown values - $unknown = array_diff( $valuesList, $allowedValues ); + $unknown = array_map( 'wfEscapeWikiText', array_diff( $valuesList, $allowedValues ) ); if ( count( $unknown ) ) { if ( $allowMultiple ) { - $s = count( $unknown ) > 1 ? 's' : ''; - $vals = implode( ', ', $unknown ); - $this->setWarning( "Unrecognized value$s for parameter '$valueName': $vals" ); + $this->addWarning( [ + 'apiwarn-unrecognizedvalues', + $valueName, + Message::listParam( $unknown, 'comma' ), + count( $unknown ), + ] ); } else { - $this->dieUsage( - "Unrecognized value for parameter '$valueName': {$valuesList[0]}", + $this->dieWithError( + [ 'apierror-unrecognizedvalue', $valueName, wfEscapeWikiText( $valuesList[0] ) ], "unknown_$valueName" ); } @@ -1321,7 +1352,12 @@ abstract class ApiBase extends ContextSource { $enforceLimits = false ) { if ( !is_null( $min ) && $value < $min ) { - $msg = $this->encodeParamName( $paramName ) . " may not be less than $min (set to $value)"; + $msg = ApiMessage::create( + [ 'apierror-integeroutofrange-belowminimum', + $this->encodeParamName( $paramName ), $min, $value ], + 'integeroutofrange', + [ 'min' => $min, 'max' => $max, 'botMax' => $botMax ?: $max ] + ); $this->warnOrDie( $msg, $enforceLimits ); $value = $min; } @@ -1337,13 +1373,22 @@ abstract class ApiBase extends ContextSource { if ( !is_null( $max ) && $value > $max ) { if ( !is_null( $botMax ) && $this->getMain()->canApiHighLimits() ) { if ( $value > $botMax ) { - $msg = $this->encodeParamName( $paramName ) . - " may not be over $botMax (set to $value) for bots or sysops"; + $msg = ApiMessage::create( + [ 'apierror-integeroutofrange-abovebotmax', + $this->encodeParamName( $paramName ), $botMax, $value ], + 'integeroutofrange', + [ 'min' => $min, 'max' => $max, 'botMax' => $botMax ?: $max ] + ); $this->warnOrDie( $msg, $enforceLimits ); $value = $botMax; } } else { - $msg = $this->encodeParamName( $paramName ) . " may not be over $max (set to $value) for users"; + $msg = ApiMessage::create( + [ 'apierror-integeroutofrange-abovemax', + $this->encodeParamName( $paramName ), $max, $value ], + 'integeroutofrange', + [ 'min' => $min, 'max' => $max, 'botMax' => $botMax ?: $max ] + ); $this->warnOrDie( $msg, $enforceLimits ); $value = $max; } @@ -1361,11 +1406,9 @@ abstract class ApiBase extends ContextSource { // (wfTimestamp() also accepts various non-strings and the string of 14 // ASCII NUL bytes, but those can't get here) if ( !$value ) { - $this->logFeatureUsage( 'unclear-"now"-timestamp' ); - $this->setWarning( - "Passing '$value' for timestamp parameter $encParamName has been deprecated." . - ' If for some reason you need to explicitly specify the current time without' . - ' calculating it client-side, use "now".' + $this->addDeprecation( + [ 'apiwarn-unclearnowtimestamp', $encParamName, wfEscapeWikiText( $value ) ], + 'unclear-"now"-timestamp' ); return wfTimestamp( TS_MW ); } @@ -1377,8 +1420,8 @@ abstract class ApiBase extends ContextSource { $unixTimestamp = wfTimestamp( TS_UNIX, $value ); if ( $unixTimestamp === false ) { - $this->dieUsage( - "Invalid value '$value' for timestamp parameter $encParamName", + $this->dieWithError( + [ 'apierror-badtimestamp', $encParamName, wfEscapeWikiText( $value ) ], "badtimestamp_{$encParamName}" ); } @@ -1433,8 +1476,8 @@ abstract class ApiBase extends ContextSource { private function validateUser( $value, $encParamName ) { $title = Title::makeTitleSafe( NS_USER, $value ); if ( $title === null || $title->hasFragment() ) { - $this->dieUsage( - "Invalid value '$value' for user parameter $encParamName", + $this->dieWithError( + [ 'apierror-baduser', $encParamName, wfEscapeWikiText( $value ) ], "baduser_{$encParamName}" ); } @@ -1490,22 +1533,19 @@ abstract class ApiBase extends ContextSource { if ( !is_null( $params['owner'] ) && !is_null( $params['token'] ) ) { $user = User::newFromName( $params['owner'], false ); if ( !( $user && $user->getId() ) ) { - $this->dieUsage( 'Specified user does not exist', 'bad_wlowner' ); + $this->dieWithError( + [ 'nosuchusershort', wfEscapeWikiText( $params['owner'] ) ], 'bad_wlowner' + ); } $token = $user->getOption( 'watchlisttoken' ); if ( $token == '' || !hash_equals( $token, $params['token'] ) ) { - $this->dieUsage( - 'Incorrect watchlist token provided -- please set a correct token in Special:Preferences', - 'bad_wltoken' - ); + $this->dieWithError( 'apierror-bad-watchlist-token', 'bad_wltoken' ); } } else { if ( !$this->getUser()->isLoggedIn() ) { - $this->dieUsage( 'You must be logged-in to have a watchlist', 'notloggedin' ); - } - if ( !$this->getUser()->isAllowed( 'viewmywatchlist' ) ) { - $this->dieUsage( 'You don\'t have permission to view your watchlist', 'permissiondenied' ); + $this->dieWithError( 'watchlistanontext', 'notloggedin' ); } + $this->checkUserRightsAny( 'viewmywatchlist' ); $user = $this->getUser(); } @@ -1561,6 +1601,39 @@ abstract class ApiBase extends ContextSource { return $msg; } + /** + * Turn an array of message keys or key+param arrays into a Status + * @since 1.29 + * @param array $errors + * @param User|null $user + * @return Status + */ + public function errorArrayToStatus( array $errors, User $user = null ) { + if ( $user === null ) { + $user = $this->getUser(); + } + + $status = Status::newGood(); + foreach ( $errors as $error ) { + if ( is_array( $error ) && $error[0] === 'blockedtext' && $user->getBlock() ) { + $status->fatal( ApiMessage::create( + 'apierror-blocked', + 'blocked', + [ 'blockinfo' => ApiQueryUserInfo::getBlockInfo( $user->getBlock() ) ] + ) ); + } elseif ( is_array( $error ) && $error[0] === 'autoblockedtext' && $user->getBlock() ) { + $status->fatal( ApiMessage::create( + 'apierror-autoblocked', + 'autoblocked', + [ 'blockinfo' => ApiQueryUserInfo::getBlockInfo( $user->getBlock() ) ] + ) ); + } else { + call_user_func_array( [ $status, 'fatal' ], (array)$error ); + } + } + return $status; + } + /**@}*/ /************************************************************************//** @@ -1569,745 +1642,227 @@ abstract class ApiBase extends ContextSource { */ /** - * Set warning section for this module. Users should monitor this - * section to notice any changes in API. Multiple calls to this - * function will result in the warning messages being separated by - * newlines - * @param string $warning Warning message + * Add a warning for this module. + * + * Users should monitor this section to notice any changes in API. Multiple + * calls to this function will result in multiple warning messages. + * + * If $msg is not an ApiMessage, the message code will be derived from the + * message key by stripping any "apiwarn-" or "apierror-" prefix. + * + * @since 1.29 + * @param string|array|Message $msg See ApiErrorFormatter::addWarning() + * @param string|null $code See ApiErrorFormatter::addWarning() + * @param array|null $data See ApiErrorFormatter::addWarning() */ - public function setWarning( $warning ) { - $msg = new ApiRawMessage( $warning, 'warning' ); - $this->getErrorFormatter()->addWarning( $this->getModuleName(), $msg ); + public function addWarning( $msg, $code = null, $data = null ) { + $this->getErrorFormatter()->addWarning( $this->getModulePath(), $msg, $code, $data ); } /** - * Adds a warning to the output, else dies + * Add a deprecation warning for this module. * - * @param string $msg Message to show as a warning, or error message if dying - * @param bool $enforceLimits Whether this is an enforce (die) + * A combination of $this->addWarning() and $this->logFeatureUsage() + * + * @since 1.29 + * @param string|array|Message $msg See ApiErrorFormatter::addWarning() + * @param string|null $feature See ApiBase::logFeatureUsage() + * @param array|null $data See ApiErrorFormatter::addWarning() */ - private function warnOrDie( $msg, $enforceLimits = false ) { - if ( $enforceLimits ) { - $this->dieUsage( $msg, 'integeroutofrange' ); + public function addDeprecation( $msg, $feature, $data = [] ) { + $data = (array)$data; + if ( $feature !== null ) { + $data['feature'] = $feature; + $this->logFeatureUsage( $feature ); } + $this->addWarning( $msg, 'deprecation', $data ); + } - $this->setWarning( $msg ); + /** + * Add an error for this module without aborting + * + * If $msg is not an ApiMessage, the message code will be derived from the + * message key by stripping any "apiwarn-" or "apierror-" prefix. + * + * @note If you want to abort processing, use self::dieWithError() instead. + * @since 1.29 + * @param string|array|Message $msg See ApiErrorFormatter::addError() + * @param string|null $code See ApiErrorFormatter::addError() + * @param array|null $data See ApiErrorFormatter::addError() + */ + public function addError( $msg, $code = null, $data = null ) { + $this->getErrorFormatter()->addError( $this->getModulePath(), $msg, $code, $data ); } /** - * Throw a UsageException, which will (if uncaught) call the main module's - * error handler and die with an error message. + * Add warnings and/or errors from a Status * - * @param string $description One-line human-readable description of the - * error condition, e.g., "The API requires a valid action parameter" - * @param string $errorCode Brief, arbitrary, stable string to allow easy - * automated identification of the error, e.g., 'unknown_action' - * @param int $httpRespCode HTTP response code - * @param array|null $extradata Data to add to the "<error>" element; array in ApiResult format - * @throws UsageException always + * @note If you want to abort processing, use self::dieStatus() instead. + * @since 1.29 + * @param StatusValue $status + * @param string[] $types 'warning' and/or 'error' */ - public function dieUsage( $description, $errorCode, $httpRespCode = 0, $extradata = null ) { - throw new UsageException( - $description, - $this->encodeParamName( $errorCode ), - $httpRespCode, - $extradata - ); + public function addMessagesFromStatus( StatusValue $status, $types = [ 'warning', 'error' ] ) { + $this->getErrorFormatter()->addMessagesFromStatus( $this->getModulePath(), $status, $types ); + } + + /** + * Abort execution with an error + * + * If $msg is not an ApiMessage, the message code will be derived from the + * message key by stripping any "apiwarn-" or "apierror-" prefix. + * + * @since 1.29 + * @param string|array|Message $msg See ApiErrorFormatter::addError() + * @param string|null $code See ApiErrorFormatter::addError() + * @param array|null $data See ApiErrorFormatter::addError() + * @param int|null $httpCode HTTP error code to use + * @throws ApiUsageException always + */ + public function dieWithError( $msg, $code = null, $data = null, $httpCode = null ) { + throw ApiUsageException::newWithMessage( $this, $msg, $code, $data, $httpCode ); } /** - * Throw a UsageException, which will (if uncaught) call the main module's + * Adds a warning to the output, else dies + * + * @param ApiMessage $msg Message to show as a warning, or error message if dying + * @param bool $enforceLimits Whether this is an enforce (die) + */ + private function warnOrDie( ApiMessage $msg, $enforceLimits = false ) { + if ( $enforceLimits ) { + $this->dieWithError( $msg ); + } else { + $this->addWarning( $msg ); + } + } + + /** + * Throw an ApiUsageException, which will (if uncaught) call the main module's * error handler and die with an error message including block info. * * @since 1.27 - * @param Block $block The block used to generate the UsageException - * @throws UsageException always + * @param Block $block The block used to generate the ApiUsageException + * @throws ApiUsageException always */ public function dieBlocked( Block $block ) { // Die using the appropriate message depending on block type if ( $block->getType() == Block::TYPE_AUTO ) { - $this->dieUsage( - 'Your IP address has been blocked automatically, because it was used by a blocked user', + $this->dieWithError( + 'apierror-autoblocked', 'autoblocked', - 0, [ 'blockinfo' => ApiQueryUserInfo::getBlockInfo( $block ) ] ); } else { - $this->dieUsage( - 'You have been blocked from editing', + $this->dieWithError( + 'apierror-blocked', 'blocked', - 0, [ 'blockinfo' => ApiQueryUserInfo::getBlockInfo( $block ) ] ); } } /** - * Get error (as code, string) from a Status object. + * Throw an ApiUsageException based on the Status object. * - * @since 1.23 - * @param Status $status - * @param array|null &$extraData Set if extra data from IApiMessage is available (since 1.27) - * @return array Array of code and error string - * @throws MWException + * @since 1.22 + * @since 1.29 Accepts a StatusValue + * @param StatusValue $status + * @throws ApiUsageException always */ - public function getErrorFromStatus( $status, &$extraData = null ) { + public function dieStatus( StatusValue $status ) { if ( $status->isGood() ) { throw new MWException( 'Successful status passed to ApiBase::dieStatus' ); } - $errors = $status->getErrorsByType( 'error' ); - if ( !$errors ) { - // No errors? Assume the warnings should be treated as errors - $errors = $status->getErrorsByType( 'warning' ); - } - if ( !$errors ) { - // Still no errors? Punt - $errors = [ [ 'message' => 'unknownerror-nocode', 'params' => [] ] ]; - } - - // Cannot use dieUsageMsg() because extensions might return custom - // error messages. - if ( $errors[0]['message'] instanceof Message ) { - $msg = $errors[0]['message']; - if ( $msg instanceof IApiMessage ) { - $extraData = $msg->getApiData(); - $code = $msg->getApiCode(); - } else { - $code = $msg->getKey(); - } - } else { - $code = $errors[0]['message']; - $msg = wfMessage( $code, $errors[0]['params'] ); - } - if ( isset( ApiBase::$messageMap[$code] ) ) { - // Translate message to code, for backwards compatibility - $code = ApiBase::$messageMap[$code]['code']; - } - - return [ $code, $msg->inLanguage( 'en' )->useDatabase( false )->plain() ]; + throw new ApiUsageException( $this, $status ); } /** - * Throw a UsageException based on the errors in the Status object. - * - * @since 1.22 - * @param Status $status - * @throws UsageException always - */ - public function dieStatus( $status ) { - $extraData = null; - list( $code, $msg ) = $this->getErrorFromStatus( $status, $extraData ); - $this->dieUsage( $msg, $code, 0, $extraData ); - } - - // @codingStandardsIgnoreStart Allow long lines. Cannot split these. - /** - * Array that maps message keys to error messages. $1 and friends are replaced. - */ - public static $messageMap = [ - // This one MUST be present, or dieUsageMsg() will recurse infinitely - 'unknownerror' => [ 'code' => 'unknownerror', 'info' => "Unknown error: \"\$1\"" ], - 'unknownerror-nocode' => [ 'code' => 'unknownerror', 'info' => 'Unknown error' ], - - // Messages from Title::getUserPermissionsErrors() - 'ns-specialprotected' => [ - 'code' => 'unsupportednamespace', - 'info' => "Pages in the Special namespace can't be edited" - ], - 'protectedinterface' => [ - 'code' => 'protectednamespace-interface', - 'info' => "You're not allowed to edit interface messages" - ], - 'namespaceprotected' => [ - 'code' => 'protectednamespace', - 'info' => "You're not allowed to edit pages in the \"\$1\" namespace" - ], - 'customcssprotected' => [ - 'code' => 'customcssprotected', - 'info' => "You're not allowed to edit custom CSS pages" - ], - 'customjsprotected' => [ - 'code' => 'customjsprotected', - 'info' => "You're not allowed to edit custom JavaScript pages" - ], - 'cascadeprotected' => [ - 'code' => 'cascadeprotected', - 'info' => "The page you're trying to edit is protected because it's included in a cascade-protected page" - ], - 'protectedpagetext' => [ - 'code' => 'protectedpage', - 'info' => "The \"\$1\" right is required to edit this page" - ], - 'protect-cantedit' => [ - 'code' => 'cantedit', - 'info' => "You can't protect this page because you can't edit it" - ], - 'deleteprotected' => [ - 'code' => 'cantedit', - 'info' => "You can't delete this page because it has been protected" - ], - 'badaccess-group0' => [ - 'code' => 'permissiondenied', - 'info' => 'Permission denied' - ], // Generic permission denied message - 'badaccess-groups' => [ - 'code' => 'permissiondenied', - 'info' => 'Permission denied' - ], - 'titleprotected' => [ - 'code' => 'protectedtitle', - 'info' => 'This title has been protected from creation' - ], - 'nocreate-loggedin' => [ - 'code' => 'cantcreate', - 'info' => "You don't have permission to create new pages" - ], - 'nocreatetext' => [ - 'code' => 'cantcreate-anon', - 'info' => "Anonymous users can't create new pages" - ], - 'movenologintext' => [ - 'code' => 'cantmove-anon', - 'info' => "Anonymous users can't move pages" - ], - 'movenotallowed' => [ - 'code' => 'cantmove', - 'info' => "You don't have permission to move pages" - ], - 'confirmedittext' => [ - 'code' => 'confirmemail', - 'info' => 'You must confirm your email address before you can edit' - ], - 'blockedtext' => [ - 'code' => 'blocked', - 'info' => 'You have been blocked from editing' - ], - 'autoblockedtext' => [ - 'code' => 'autoblocked', - 'info' => 'Your IP address has been blocked automatically, because it was used by a blocked user' - ], - - // Miscellaneous interface messages - 'actionthrottledtext' => [ - 'code' => 'ratelimited', - 'info' => "You've exceeded your rate limit. Please wait some time and try again" - ], - 'alreadyrolled' => [ - 'code' => 'alreadyrolled', - 'info' => 'The page you tried to rollback was already rolled back' - ], - 'cantrollback' => [ - 'code' => 'onlyauthor', - 'info' => 'The page you tried to rollback only has one author' - ], - 'readonlytext' => [ - 'code' => 'readonly', - 'info' => 'The wiki is currently in read-only mode' - ], - 'sessionfailure' => [ - 'code' => 'badtoken', - 'info' => 'Invalid token' ], - 'cannotdelete' => [ - 'code' => 'cantdelete', - 'info' => "Couldn't delete \"\$1\". Maybe it was deleted already by someone else" - ], - 'notanarticle' => [ - 'code' => 'missingtitle', - 'info' => "The page you requested doesn't exist" - ], - 'selfmove' => [ 'code' => 'selfmove', 'info' => "Can't move a page to itself" - ], - 'immobile_namespace' => [ - 'code' => 'immobilenamespace', - 'info' => 'You tried to move pages from or to a namespace that is protected from moving' - ], - 'articleexists' => [ - 'code' => 'articleexists', - 'info' => 'The destination article already exists and is not a redirect to the source article' - ], - 'protectedpage' => [ - 'code' => 'protectedpage', - 'info' => "You don't have permission to perform this move" - ], - 'hookaborted' => [ - 'code' => 'hookaborted', - 'info' => 'The modification you tried to make was aborted by an extension hook' - ], - 'cantmove-titleprotected' => [ - 'code' => 'protectedtitle', - 'info' => 'The destination article has been protected from creation' - ], - 'imagenocrossnamespace' => [ - 'code' => 'nonfilenamespace', - 'info' => "Can't move a file to a non-file namespace" - ], - 'imagetypemismatch' => [ - 'code' => 'filetypemismatch', - 'info' => "The new file extension doesn't match its type" - ], - // 'badarticleerror' => shouldn't happen - // 'badtitletext' => shouldn't happen - 'ip_range_invalid' => [ 'code' => 'invalidrange', 'info' => 'Invalid IP range' ], - 'range_block_disabled' => [ - 'code' => 'rangedisabled', - 'info' => 'Blocking IP ranges has been disabled' - ], - 'nosuchusershort' => [ - 'code' => 'nosuchuser', - 'info' => "The user you specified doesn't exist" - ], - 'badipaddress' => [ 'code' => 'invalidip', 'info' => 'Invalid IP address specified' ], - 'ipb_expiry_invalid' => [ 'code' => 'invalidexpiry', 'info' => 'Invalid expiry time' ], - 'ipb_already_blocked' => [ - 'code' => 'alreadyblocked', - 'info' => 'The user you tried to block was already blocked' - ], - 'ipb_blocked_as_range' => [ - 'code' => 'blockedasrange', - 'info' => "IP address \"\$1\" was blocked as part of range \"\$2\". You can't unblock the IP individually, but you can unblock the range as a whole." - ], - 'ipb_cant_unblock' => [ - 'code' => 'cantunblock', - 'info' => 'The block you specified was not found. It may have been unblocked already' - ], - 'mailnologin' => [ - 'code' => 'cantsend', - 'info' => 'You are not logged in, you do not have a confirmed email address, or you are not allowed to send email to other users, so you cannot send email' - ], - 'ipbblocked' => [ - 'code' => 'ipbblocked', - 'info' => 'You cannot block or unblock users while you are yourself blocked' - ], - 'ipbnounblockself' => [ - 'code' => 'ipbnounblockself', - 'info' => 'You are not allowed to unblock yourself' - ], - 'usermaildisabled' => [ - 'code' => 'usermaildisabled', - 'info' => 'User email has been disabled' - ], - 'blockedemailuser' => [ - 'code' => 'blockedfrommail', - 'info' => 'You have been blocked from sending email' - ], - 'notarget' => [ - 'code' => 'notarget', - 'info' => 'You have not specified a valid target for this action' - ], - 'noemail' => [ - 'code' => 'noemail', - 'info' => 'The user has not specified a valid email address, or has chosen not to receive email from other users' - ], - 'rcpatroldisabled' => [ - 'code' => 'patroldisabled', - 'info' => 'Patrolling is disabled on this wiki' - ], - 'markedaspatrollederror-noautopatrol' => [ - 'code' => 'noautopatrol', - 'info' => "You don't have permission to patrol your own changes" - ], - 'delete-toobig' => [ - 'code' => 'bigdelete', - 'info' => "You can't delete this page because it has more than \$1 revisions" - ], - 'movenotallowedfile' => [ - 'code' => 'cantmovefile', - 'info' => "You don't have permission to move files" - ], - 'userrights-no-interwiki' => [ - 'code' => 'nointerwikiuserrights', - 'info' => "You don't have permission to change user rights on other wikis" - ], - 'userrights-nodatabase' => [ - 'code' => 'nosuchdatabase', - 'info' => "Database \"\$1\" does not exist or is not local" - ], - 'nouserspecified' => [ 'code' => 'invaliduser', 'info' => "Invalid username \"\$1\"" ], - 'noname' => [ 'code' => 'invaliduser', 'info' => "Invalid username \"\$1\"" ], - 'summaryrequired' => [ 'code' => 'summaryrequired', 'info' => 'Summary required' ], - 'import-rootpage-invalid' => [ - 'code' => 'import-rootpage-invalid', - 'info' => 'Root page is an invalid title' - ], - 'import-rootpage-nosubpage' => [ - 'code' => 'import-rootpage-nosubpage', - 'info' => 'Namespace "$1" of the root page does not allow subpages' - ], - - // API-specific messages - 'readrequired' => [ - 'code' => 'readapidenied', - 'info' => 'You need read permission to use this module' - ], - 'writedisabled' => [ - 'code' => 'noapiwrite', - 'info' => "Editing of this wiki through the API is disabled. Make sure the \$wgEnableWriteAPI=true; statement is included in the wiki's LocalSettings.php file" - ], - 'writerequired' => [ - 'code' => 'writeapidenied', - 'info' => "You're not allowed to edit this wiki through the API" - ], - 'missingparam' => [ 'code' => 'no$1', 'info' => "The \$1 parameter must be set" ], - 'invalidtitle' => [ 'code' => 'invalidtitle', 'info' => "Bad title \"\$1\"" ], - 'nosuchpageid' => [ 'code' => 'nosuchpageid', 'info' => "There is no page with ID \$1" ], - 'nosuchrevid' => [ 'code' => 'nosuchrevid', 'info' => "There is no revision with ID \$1" ], - 'nosuchuser' => [ 'code' => 'nosuchuser', 'info' => "User \"\$1\" doesn't exist" ], - 'invaliduser' => [ 'code' => 'invaliduser', 'info' => "Invalid username \"\$1\"" ], - 'invalidexpiry' => [ 'code' => 'invalidexpiry', 'info' => "Invalid expiry time \"\$1\"" ], - 'pastexpiry' => [ 'code' => 'pastexpiry', 'info' => "Expiry time \"\$1\" is in the past" ], - 'create-titleexists' => [ - 'code' => 'create-titleexists', - 'info' => "Existing titles can't be protected with 'create'" - ], - 'missingtitle-createonly' => [ - 'code' => 'missingtitle-createonly', - 'info' => "Missing titles can only be protected with 'create'" - ], - 'cantblock' => [ 'code' => 'cantblock', - 'info' => "You don't have permission to block users" - ], - 'canthide' => [ - 'code' => 'canthide', - 'info' => "You don't have permission to hide user names from the block log" - ], - 'cantblock-email' => [ - 'code' => 'cantblock-email', - 'info' => "You don't have permission to block users from sending email through the wiki" - ], - 'unblock-notarget' => [ - 'code' => 'notarget', - 'info' => 'Either the id or the user parameter must be set' - ], - 'unblock-idanduser' => [ - 'code' => 'idanduser', - 'info' => "The id and user parameters can't be used together" - ], - 'cantunblock' => [ - 'code' => 'permissiondenied', - 'info' => "You don't have permission to unblock users" - ], - 'cannotundelete' => [ - 'code' => 'cantundelete', - 'info' => "Couldn't undelete: the requested revisions may not exist, or may have been undeleted already" - ], - 'permdenied-undelete' => [ - 'code' => 'permissiondenied', - 'info' => "You don't have permission to restore deleted revisions" - ], - 'createonly-exists' => [ - 'code' => 'articleexists', - 'info' => 'The article you tried to create has been created already' - ], - 'nocreate-missing' => [ - 'code' => 'missingtitle', - 'info' => "The article you tried to edit doesn't exist" - ], - 'cantchangecontentmodel' => [ - 'code' => 'cantchangecontentmodel', - 'info' => "You don't have permission to change the content model of a page" - ], - 'nosuchrcid' => [ - 'code' => 'nosuchrcid', - 'info' => "There is no change with rcid \"\$1\"" - ], - 'nosuchlogid' => [ - 'code' => 'nosuchlogid', - 'info' => "There is no log entry with ID \"\$1\"" - ], - 'protect-invalidaction' => [ - 'code' => 'protect-invalidaction', - 'info' => "Invalid protection type \"\$1\"" - ], - 'protect-invalidlevel' => [ - 'code' => 'protect-invalidlevel', - 'info' => "Invalid protection level \"\$1\"" - ], - 'toofewexpiries' => [ - 'code' => 'toofewexpiries', - 'info' => "\$1 expiry timestamps were provided where \$2 were needed" - ], - 'cantimport' => [ - 'code' => 'cantimport', - 'info' => "You don't have permission to import pages" - ], - 'cantimport-upload' => [ - 'code' => 'cantimport-upload', - 'info' => "You don't have permission to import uploaded pages" - ], - 'importnofile' => [ 'code' => 'nofile', 'info' => "You didn't upload a file" ], - 'importuploaderrorsize' => [ - 'code' => 'filetoobig', - 'info' => 'The file you uploaded is bigger than the maximum upload size' - ], - 'importuploaderrorpartial' => [ - 'code' => 'partialupload', - 'info' => 'The file was only partially uploaded' - ], - 'importuploaderrortemp' => [ - 'code' => 'notempdir', - 'info' => 'The temporary upload directory is missing' - ], - 'importcantopen' => [ - 'code' => 'cantopenfile', - 'info' => "Couldn't open the uploaded file" - ], - 'import-noarticle' => [ - 'code' => 'badinterwiki', - 'info' => 'Invalid interwiki title specified' - ], - 'importbadinterwiki' => [ - 'code' => 'badinterwiki', - 'info' => 'Invalid interwiki title specified' - ], - 'import-unknownerror' => [ - 'code' => 'import-unknownerror', - 'info' => "Unknown error on import: \"\$1\"" - ], - 'cantoverwrite-sharedfile' => [ - 'code' => 'cantoverwrite-sharedfile', - 'info' => 'The target file exists on a shared repository and you do not have permission to override it' - ], - 'sharedfile-exists' => [ - 'code' => 'fileexists-sharedrepo-perm', - 'info' => 'The target file exists on a shared repository. Use the ignorewarnings parameter to override it.' - ], - 'mustbeposted' => [ - 'code' => 'mustbeposted', - 'info' => "The \$1 module requires a POST request" - ], - 'show' => [ - 'code' => 'show', - 'info' => 'Incorrect parameter - mutually exclusive values may not be supplied' - ], - 'specialpage-cantexecute' => [ - 'code' => 'specialpage-cantexecute', - 'info' => "You don't have permission to view the results of this special page" - ], - 'invalidoldimage' => [ - 'code' => 'invalidoldimage', - 'info' => 'The oldimage parameter has invalid format' - ], - 'nodeleteablefile' => [ - 'code' => 'nodeleteablefile', - 'info' => 'No such old version of the file' - ], - 'fileexists-forbidden' => [ - 'code' => 'fileexists-forbidden', - 'info' => 'A file with name "$1" already exists, and cannot be overwritten.' - ], - 'fileexists-shared-forbidden' => [ - 'code' => 'fileexists-shared-forbidden', - 'info' => 'A file with name "$1" already exists in the shared file repository, and cannot be overwritten.' - ], - 'filerevert-badversion' => [ - 'code' => 'filerevert-badversion', - 'info' => 'There is no previous local version of this file with the provided timestamp.' - ], - - // ApiEditPage messages - 'noimageredirect-anon' => [ - 'code' => 'noimageredirect-anon', - 'info' => "Anonymous users can't create image redirects" - ], - 'noimageredirect-logged' => [ - 'code' => 'noimageredirect', - 'info' => "You don't have permission to create image redirects" - ], - 'spamdetected' => [ - 'code' => 'spamdetected', - 'info' => "Your edit was refused because it contained a spam fragment: \"\$1\"" - ], - 'contenttoobig' => [ - 'code' => 'contenttoobig', - 'info' => "The content you supplied exceeds the article size limit of \$1 kilobytes" - ], - 'noedit-anon' => [ 'code' => 'noedit-anon', 'info' => "Anonymous users can't edit pages" ], - 'noedit' => [ 'code' => 'noedit', 'info' => "You don't have permission to edit pages" ], - 'wasdeleted' => [ - 'code' => 'pagedeleted', - 'info' => 'The page has been deleted since you fetched its timestamp' - ], - 'blankpage' => [ - 'code' => 'emptypage', - 'info' => 'Creating new, empty pages is not allowed' - ], - 'editconflict' => [ 'code' => 'editconflict', 'info' => 'Edit conflict detected' ], - 'hashcheckfailed' => [ 'code' => 'badmd5', 'info' => 'The supplied MD5 hash was incorrect' ], - 'missingtext' => [ - 'code' => 'notext', - 'info' => 'One of the text, appendtext, prependtext and undo parameters must be set' - ], - 'emptynewsection' => [ - 'code' => 'emptynewsection', - 'info' => 'Creating empty new sections is not possible.' - ], - 'revwrongpage' => [ - 'code' => 'revwrongpage', - 'info' => "r\$1 is not a revision of \"\$2\"" - ], - 'undo-failure' => [ - 'code' => 'undofailure', - 'info' => 'Undo failed due to conflicting intermediate edits' - ], - 'content-not-allowed-here' => [ - 'code' => 'contentnotallowedhere', - 'info' => 'Content model "$1" is not allowed at title "$2"' - ], - - // Messages from WikiPage::doEit(] - 'edit-hook-aborted' => [ - 'code' => 'edit-hook-aborted', - 'info' => 'Your edit was aborted by an ArticleSave hook' - ], - 'edit-gone-missing' => [ - 'code' => 'edit-gone-missing', - 'info' => "The page you tried to edit doesn't seem to exist anymore" - ], - 'edit-conflict' => [ 'code' => 'editconflict', 'info' => 'Edit conflict detected' ], - 'edit-already-exists' => [ - 'code' => 'edit-already-exists', - 'info' => 'It seems the page you tried to create already exist' - ], - - // uploadMsgs - 'invalid-file-key' => [ 'code' => 'invalid-file-key', 'info' => 'Not a valid file key' ], - 'nouploadmodule' => [ 'code' => 'nouploadmodule', 'info' => 'No upload module set' ], - 'uploaddisabled' => [ - 'code' => 'uploaddisabled', - 'info' => 'Uploads are not enabled. Make sure $wgEnableUploads is set to true in LocalSettings.php and the PHP ini setting file_uploads is true' - ], - 'copyuploaddisabled' => [ - 'code' => 'copyuploaddisabled', - 'info' => 'Uploads by URL is not enabled. Make sure $wgAllowCopyUploads is set to true in LocalSettings.php.' - ], - 'copyuploadbaddomain' => [ - 'code' => 'copyuploadbaddomain', - 'info' => 'Uploads by URL are not allowed from this domain.' - ], - 'copyuploadbadurl' => [ - 'code' => 'copyuploadbadurl', - 'info' => 'Upload not allowed from this URL.' - ], - - 'filename-tooshort' => [ - 'code' => 'filename-tooshort', - 'info' => 'The filename is too short' - ], - 'filename-toolong' => [ 'code' => 'filename-toolong', 'info' => 'The filename is too long' ], - 'illegal-filename' => [ - 'code' => 'illegal-filename', - 'info' => 'The filename is not allowed' - ], - 'filetype-missing' => [ - 'code' => 'filetype-missing', - 'info' => 'The file is missing an extension' - ], - - 'mustbeloggedin' => [ 'code' => 'mustbeloggedin', 'info' => 'You must be logged in to $1.' ] - ]; - // @codingStandardsIgnoreEnd - - /** * Helper function for readonly errors * - * @throws UsageException always + * @throws ApiUsageException always */ public function dieReadOnly() { - $parsed = $this->parseMsg( [ 'readonlytext' ] ); - $this->dieUsage( $parsed['info'], $parsed['code'], /* http error */ 0, - [ 'readonlyreason' => wfReadOnlyReason() ] ); + $this->dieWithError( + 'apierror-readonly', + 'readonly', + [ 'readonlyreason' => wfReadOnlyReason() ] + ); } /** - * Output the error message related to a certain array - * @param array|string|MessageSpecifier $error Element of a getUserPermissionsErrors()-style array - * @throws UsageException always + * Helper function for permission-denied errors + * @since 1.29 + * @param string|string[] $rights + * @param User|null $user + * @throws ApiUsageException if the user doesn't have any of the rights. + * The error message is based on $rights[0]. */ - public function dieUsageMsg( $error ) { - # most of the time we send a 1 element, so we might as well send it as - # a string and make this an array here. - if ( is_string( $error ) ) { - $error = [ $error ]; + public function checkUserRightsAny( $rights, $user = null ) { + if ( !$user ) { + $user = $this->getUser(); + } + $rights = (array)$rights; + if ( !call_user_func_array( [ $user, 'isAllowedAny' ], $rights ) ) { + $this->dieWithError( [ 'apierror-permissiondenied', $this->msg( "action-{$rights[0]}" ) ] ); } - $parsed = $this->parseMsg( $error ); - $extraData = isset( $parsed['data'] ) ? $parsed['data'] : null; - $this->dieUsage( $parsed['info'], $parsed['code'], 0, $extraData ); } /** - * Will only set a warning instead of failing if the global $wgDebugAPI - * is set to true. Otherwise behaves exactly as dieUsageMsg(). - * @param array|string|MessageSpecifier $error Element of a getUserPermissionsErrors()-style array - * @throws UsageException - * @since 1.21 + * Helper function for permission-denied errors + * @since 1.29 + * @param Title $title + * @param string|string[] $actions + * @param User|null $user + * @throws ApiUsageException if the user doesn't have all of the rights. */ - public function dieUsageMsgOrDebug( $error ) { - if ( $this->getConfig()->get( 'DebugAPI' ) !== true ) { - $this->dieUsageMsg( $error ); + public function checkTitleUserPermissions( Title $title, $actions, $user = null ) { + if ( !$user ) { + $user = $this->getUser(); } - if ( is_string( $error ) ) { - $error = [ $error ]; + $errors = []; + foreach ( (array)$actions as $action ) { + $errors = array_merge( $errors, $title->getUserPermissionsErrors( $action, $user ) ); + } + if ( $errors ) { + $this->dieStatus( $this->errorArrayToStatus( $errors, $user ) ); } - $parsed = $this->parseMsg( $error ); - $this->setWarning( '$wgDebugAPI: ' . $parsed['code'] . ' - ' . $parsed['info'] ); } /** - * Die with the $prefix.'badcontinue' error. This call is common enough to - * make it into the base method. - * @param bool $condition Will only die if this value is true - * @throws UsageException - * @since 1.21 + * Will only set a warning instead of failing if the global $wgDebugAPI + * is set to true. Otherwise behaves exactly as self::dieWithError(). + * + * @since 1.29 + * @param string|array|Message $msg + * @param string|null $code + * @param array|null $data + * @param int|null $httpCode + * @throws ApiUsageException */ - protected function dieContinueUsageIf( $condition ) { - if ( $condition ) { - $this->dieUsage( - 'Invalid continue param. You should pass the original value returned by the previous query', - 'badcontinue' ); + public function dieWithErrorOrDebug( $msg, $code = null, $data = null, $httpCode = null ) { + if ( $this->getConfig()->get( 'DebugAPI' ) !== true ) { + $this->dieWithError( $msg, $code, $data, $httpCode ); + } else { + $this->addWarning( $msg, $code, $data ); } } /** - * Return the error message related to a certain array - * @param array|string|MessageSpecifier $error Element of a getUserPermissionsErrors()-style array - * @return [ 'code' => code, 'info' => info ] + * Die with the 'badcontinue' error. + * + * This call is common enough to make it into the base method. + * + * @param bool $condition Will only die if this value is true + * @throws ApiUsageException + * @since 1.21 */ - public function parseMsg( $error ) { - // Check whether someone passed the whole array, instead of one element as - // documented. This breaks if it's actually an array of fallback keys, but - // that's long-standing misbehavior introduced in r87627 to incorrectly - // fix T30797. - if ( is_array( $error ) ) { - $first = reset( $error ); - if ( is_array( $first ) ) { - wfDebug( __METHOD__ . ' was passed an array of arrays. ' . wfGetAllCallers( 5 ) ); - $error = $first; - } - } - - $msg = Message::newFromSpecifier( $error ); - - if ( $msg instanceof IApiMessage ) { - return [ - 'code' => $msg->getApiCode(), - 'info' => $msg->inLanguage( 'en' )->useDatabase( false )->text(), - 'data' => $msg->getApiData() - ]; - } - - $key = $msg->getKey(); - if ( isset( self::$messageMap[$key] ) ) { - $params = $msg->getParams(); - return [ - 'code' => wfMsgReplaceArgs( self::$messageMap[$key]['code'], $params ), - 'info' => wfMsgReplaceArgs( self::$messageMap[$key]['info'], $params ) - ]; + protected function dieContinueUsageIf( $condition ) { + if ( $condition ) { + $this->dieWithError( 'apierror-badcontinue' ); } - - // If the key isn't present, throw an "unknown error" - return $this->parseMsg( [ 'unknownerror', $key ] ); } /** @@ -2323,6 +1878,7 @@ abstract class ApiBase extends ContextSource { /** * Write logging information for API features to a debug log, for usage * analysis. + * @note Consider using $this->addDeprecation() instead to both warn and log. * @param string $feature Feature being used. */ public function logFeatureUsage( $feature ) { @@ -2790,6 +2346,300 @@ abstract class ApiBase extends ContextSource { } } + /** + * @deprecated since 1.29, use ApiBase::addWarning() instead + * @param string $warning Warning message + */ + public function setWarning( $warning ) { + $msg = new ApiRawMessage( $warning, 'warning' ); + $this->getErrorFormatter()->addWarning( $this->getModulePath(), $msg ); + } + + /** + * Throw an ApiUsageException, which will (if uncaught) call the main module's + * error handler and die with an error message. + * + * @deprecated since 1.29, use self::dieWithError() instead + * @param string $description One-line human-readable description of the + * error condition, e.g., "The API requires a valid action parameter" + * @param string $errorCode Brief, arbitrary, stable string to allow easy + * automated identification of the error, e.g., 'unknown_action' + * @param int $httpRespCode HTTP response code + * @param array|null $extradata Data to add to the "<error>" element; array in ApiResult format + * @throws ApiUsageException always + */ + public function dieUsage( $description, $errorCode, $httpRespCode = 0, $extradata = null ) { + $this->dieWithError( + new RawMessage( '$1', [ $description ] ), + $errorCode, + $extradata, + $httpRespCode + ); + } + + /** + * Get error (as code, string) from a Status object. + * + * @since 1.23 + * @deprecated since 1.29, use ApiErrorFormatter::arrayFromStatus instead + * @param Status $status + * @param array|null &$extraData Set if extra data from IApiMessage is available (since 1.27) + * @return array Array of code and error string + * @throws MWException + */ + public function getErrorFromStatus( $status, &$extraData = null ) { + if ( $status->isGood() ) { + throw new MWException( 'Successful status passed to ApiBase::dieStatus' ); + } + + $errors = $status->getErrorsByType( 'error' ); + if ( !$errors ) { + // No errors? Assume the warnings should be treated as errors + $errors = $status->getErrorsByType( 'warning' ); + } + if ( !$errors ) { + // Still no errors? Punt + $errors = [ [ 'message' => 'unknownerror-nocode', 'params' => [] ] ]; + } + + if ( $errors[0]['message'] instanceof MessageSpecifier ) { + $msg = $errors[0]['message']; + } else { + $msg = new Message( $errors[0]['message'], $errors[0]['params'] ); + } + if ( !$msg instanceof IApiMessage ) { + $key = $msg->getKey(); + $params = $msg->getParams(); + array_unshift( $params, isset( self::$messageMap[$key] ) ? self::$messageMap[$key] : $key ); + $msg = ApiMessage::create( $params ); + } + + return [ + $msg->getApiCode(), + ApiErrorFormatter::stripMarkup( $msg->inLanguage( 'en' )->useDatabase( false )->text() ) + ]; + } + + /** + * @deprecated since 1.29. Prior to 1.29, this was a public mapping from + * arbitrary strings (often message keys used elsewhere in MediaWiki) to + * API codes and message texts, and a few interfaces required poking + * something in here. Now we're repurposing it to map those same strings + * to i18n messages, and declaring that any interface that requires poking + * at this is broken and needs replacing ASAP. + */ + private static $messageMap = [ + 'unknownerror' => 'apierror-unknownerror', + 'unknownerror-nocode' => 'apierror-unknownerror-nocode', + 'ns-specialprotected' => 'ns-specialprotected', + 'protectedinterface' => 'protectedinterface', + 'namespaceprotected' => 'namespaceprotected', + 'customcssprotected' => 'customcssprotected', + 'customjsprotected' => 'customjsprotected', + 'cascadeprotected' => 'cascadeprotected', + 'protectedpagetext' => 'protectedpagetext', + 'protect-cantedit' => 'protect-cantedit', + 'deleteprotected' => 'deleteprotected', + 'badaccess-group0' => 'badaccess-group0', + 'badaccess-groups' => 'badaccess-groups', + 'titleprotected' => 'titleprotected', + 'nocreate-loggedin' => 'nocreate-loggedin', + 'nocreatetext' => 'nocreatetext', + 'movenologintext' => 'movenologintext', + 'movenotallowed' => 'movenotallowed', + 'confirmedittext' => 'confirmedittext', + 'blockedtext' => 'apierror-blocked', + 'autoblockedtext' => 'apierror-autoblocked', + 'actionthrottledtext' => 'apierror-ratelimited', + 'alreadyrolled' => 'alreadyrolled', + 'cantrollback' => 'cantrollback', + 'readonlytext' => 'readonlytext', + 'sessionfailure' => 'sessionfailure', + 'cannotdelete' => 'cannotdelete', + 'notanarticle' => 'apierror-missingtitle', + 'selfmove' => 'selfmove', + 'immobile_namespace' => 'apierror-immobilenamespace', + 'articleexists' => 'articleexists', + 'hookaborted' => 'hookaborted', + 'cantmove-titleprotected' => 'cantmove-titleprotected', + 'imagenocrossnamespace' => 'imagenocrossnamespace', + 'imagetypemismatch' => 'imagetypemismatch', + 'ip_range_invalid' => 'ip_range_invalid', + 'range_block_disabled' => 'range_block_disabled', + 'nosuchusershort' => 'nosuchusershort', + 'badipaddress' => 'badipaddress', + 'ipb_expiry_invalid' => 'ipb_expiry_invalid', + 'ipb_already_blocked' => 'ipb_already_blocked', + 'ipb_blocked_as_range' => 'ipb_blocked_as_range', + 'ipb_cant_unblock' => 'ipb_cant_unblock', + 'mailnologin' => 'apierror-cantsend', + 'ipbblocked' => 'ipbblocked', + 'ipbnounblockself' => 'ipbnounblockself', + 'usermaildisabled' => 'usermaildisabled', + 'blockedemailuser' => 'apierror-blockedfrommail', + 'notarget' => 'apierror-notarget', + 'noemail' => 'noemail', + 'rcpatroldisabled' => 'rcpatroldisabled', + 'markedaspatrollederror-noautopatrol' => 'markedaspatrollederror-noautopatrol', + 'delete-toobig' => 'delete-toobig', + 'movenotallowedfile' => 'movenotallowedfile', + 'userrights-no-interwiki' => 'userrights-no-interwiki', + 'userrights-nodatabase' => 'userrights-nodatabase', + 'nouserspecified' => 'nouserspecified', + 'noname' => 'noname', + 'summaryrequired' => 'apierror-summaryrequired', + 'import-rootpage-invalid' => 'import-rootpage-invalid', + 'import-rootpage-nosubpage' => 'import-rootpage-nosubpage', + 'readrequired' => 'apierror-readapidenied', + 'writedisabled' => 'apierror-noapiwrite', + 'writerequired' => 'apierror-writeapidenied', + 'missingparam' => 'apierror-missingparam', + 'invalidtitle' => 'apierror-invalidtitle', + 'nosuchpageid' => 'apierror-nosuchpageid', + 'nosuchrevid' => 'apierror-nosuchrevid', + 'nosuchuser' => 'nosuchusershort', + 'invaliduser' => 'apierror-invaliduser', + 'invalidexpiry' => 'apierror-invalidexpiry', + 'pastexpiry' => 'apierror-pastexpiry', + 'create-titleexists' => 'apierror-create-titleexists', + 'missingtitle-createonly' => 'apierror-missingtitle-createonly', + 'cantblock' => 'apierror-cantblock', + 'canthide' => 'apierror-canthide', + 'cantblock-email' => 'apierror-cantblock-email', + 'cantunblock' => 'apierror-permissiondenied-generic', + 'cannotundelete' => 'cannotundelete', + 'permdenied-undelete' => 'apierror-permissiondenied-generic', + 'createonly-exists' => 'apierror-articleexists', + 'nocreate-missing' => 'apierror-missingtitle', + 'cantchangecontentmodel' => 'apierror-cantchangecontentmodel', + 'nosuchrcid' => 'apierror-nosuchrcid', + 'nosuchlogid' => 'apierror-nosuchlogid', + 'protect-invalidaction' => 'apierror-protect-invalidaction', + 'protect-invalidlevel' => 'apierror-protect-invalidlevel', + 'toofewexpiries' => 'apierror-toofewexpiries', + 'cantimport' => 'apierror-cantimport', + 'cantimport-upload' => 'apierror-cantimport-upload', + 'importnofile' => 'importnofile', + 'importuploaderrorsize' => 'importuploaderrorsize', + 'importuploaderrorpartial' => 'importuploaderrorpartial', + 'importuploaderrortemp' => 'importuploaderrortemp', + 'importcantopen' => 'importcantopen', + 'import-noarticle' => 'import-noarticle', + 'importbadinterwiki' => 'importbadinterwiki', + 'import-unknownerror' => 'apierror-import-unknownerror', + 'cantoverwrite-sharedfile' => 'apierror-cantoverwrite-sharedfile', + 'sharedfile-exists' => 'apierror-fileexists-sharedrepo-perm', + 'mustbeposted' => 'apierror-mustbeposted', + 'show' => 'apierror-show', + 'specialpage-cantexecute' => 'apierror-specialpage-cantexecute', + 'invalidoldimage' => 'apierror-invalidoldimage', + 'nodeleteablefile' => 'apierror-nodeleteablefile', + 'fileexists-forbidden' => 'fileexists-forbidden', + 'fileexists-shared-forbidden' => 'fileexists-shared-forbidden', + 'filerevert-badversion' => 'filerevert-badversion', + 'noimageredirect-anon' => 'apierror-noimageredirect-anon', + 'noimageredirect-logged' => 'apierror-noimageredirect', + 'spamdetected' => 'apierror-spamdetected', + 'contenttoobig' => 'apierror-contenttoobig', + 'noedit-anon' => 'apierror-noedit-anon', + 'noedit' => 'apierror-noedit', + 'wasdeleted' => 'apierror-pagedeleted', + 'blankpage' => 'apierror-emptypage', + 'editconflict' => 'editconflict', + 'hashcheckfailed' => 'apierror-badmd5', + 'missingtext' => 'apierror-notext', + 'emptynewsection' => 'apierror-emptynewsection', + 'revwrongpage' => 'apierror-revwrongpage', + 'undo-failure' => 'undo-failure', + 'content-not-allowed-here' => 'content-not-allowed-here', + 'edit-hook-aborted' => 'edit-hook-aborted', + 'edit-gone-missing' => 'edit-gone-missing', + 'edit-conflict' => 'edit-conflict', + 'edit-already-exists' => 'edit-already-exists', + 'invalid-file-key' => 'apierror-invalid-file-key', + 'nouploadmodule' => 'apierror-nouploadmodule', + 'uploaddisabled' => 'uploaddisabled', + 'copyuploaddisabled' => 'copyuploaddisabled', + 'copyuploadbaddomain' => 'apierror-copyuploadbaddomain', + 'copyuploadbadurl' => 'apierror-copyuploadbadurl', + 'filename-tooshort' => 'filename-tooshort', + 'filename-toolong' => 'filename-toolong', + 'illegal-filename' => 'illegal-filename', + 'filetype-missing' => 'filetype-missing', + 'mustbeloggedin' => 'apierror-mustbeloggedin', + ]; + + /** + * @deprecated do not use + * @param array|string|MessageSpecifier $error Element of a getUserPermissionsErrors()-style array + * @return ApiMessage + */ + private function parseMsgInternal( $error ) { + $msg = Message::newFromSpecifier( $error ); + if ( !$msg instanceof IApiMessage ) { + $key = $msg->getKey(); + if ( isset( self::$messageMap[$key] ) ) { + $params = $msg->getParams(); + array_unshift( $params, self::$messageMap[$key] ); + } else { + $params = [ 'apierror-unknownerror', wfEscapeWikiText( $key ) ]; + } + $msg = ApiMessage::create( $params ); + } + return $msg; + } + + /** + * Return the error message related to a certain array + * @deprecated since 1.29 + * @param array|string|MessageSpecifier $error Element of a getUserPermissionsErrors()-style array + * @return [ 'code' => code, 'info' => info ] + */ + public function parseMsg( $error ) { + // Check whether someone passed the whole array, instead of one element as + // documented. This breaks if it's actually an array of fallback keys, but + // that's long-standing misbehavior introduced in r87627 to incorrectly + // fix T30797. + if ( is_array( $error ) ) { + $first = reset( $error ); + if ( is_array( $first ) ) { + wfDebug( __METHOD__ . ' was passed an array of arrays. ' . wfGetAllCallers( 5 ) ); + $error = $first; + } + } + + $msg = $this->parseMsgInternal( $error ); + return [ + 'code' => $msg->getApiCode(), + 'info' => ApiErrorFormatter::stripMarkup( + $msg->inLanguage( 'en' )->useDatabase( false )->text() + ), + 'data' => $msg->getApiData() + ]; + } + + /** + * Output the error message related to a certain array + * @deprecated since 1.29, use ApiBase::dieWithError() instead + * @param array|string|MessageSpecifier $error Element of a getUserPermissionsErrors()-style array + * @throws ApiUsageException always + */ + public function dieUsageMsg( $error ) { + $this->dieWithError( $this->parseMsgInternal( $error ) ); + } + + /** + * Will only set a warning instead of failing if the global $wgDebugAPI + * is set to true. Otherwise behaves exactly as dieUsageMsg(). + * @deprecated since 1.29, use ApiBase::dieWithErrorOrDebug() instead + * @param array|string|MessageSpecifier $error Element of a getUserPermissionsErrors()-style array + * @throws ApiUsageException + * @since 1.21 + */ + public function dieUsageMsgOrDebug( $error ) { + $this->dieWithErrorOrDebug( $this->parseMsgInternal( $error ) ); + } + /**@}*/ } diff --git a/includes/api/ApiBlock.php b/includes/api/ApiBlock.php index e4c9d0a80b17..a4ea3857bb71 100644 --- a/includes/api/ApiBlock.php +++ b/includes/api/ApiBlock.php @@ -41,22 +41,18 @@ class ApiBlock extends ApiBase { public function execute() { global $wgContLang; + $this->checkUserRightsAny( 'block' ); + $user = $this->getUser(); $params = $this->extractRequestParams(); - if ( !$user->isAllowed( 'block' ) ) { - $this->dieUsageMsg( 'cantblock' ); - } - # bug 15810: blocked admins should have limited access here if ( $user->isBlocked() ) { $status = SpecialBlock::checkUnblockSelf( $params['user'], $user ); if ( $status !== true ) { - $msg = $this->parseMsg( $status ); - $this->dieUsage( - $msg['info'], - $msg['code'], - 0, + $this->dieWithError( + $status, + null, [ 'blockinfo' => ApiQueryUserInfo::getBlockInfo( $user->getBlock() ) ] ); } @@ -68,14 +64,14 @@ class ApiBlock extends ApiBase { if ( $target instanceof User && ( $target->isAnon() /* doesn't exist */ || !User::isUsableName( $target->getName() ) ) ) { - $this->dieUsageMsg( [ 'nosuchuser', $params['user'] ] ); + $this->dieWithError( [ 'nosuchusershort', $params['user'] ], 'nosuchuser' ); } if ( $params['hidename'] && !$user->isAllowed( 'hideuser' ) ) { - $this->dieUsageMsg( 'canthide' ); + $this->dieWithError( 'apierror-canthide' ); } if ( $params['noemail'] && !SpecialBlock::canBlockEmail( $user ) ) { - $this->dieUsageMsg( 'cantblock-email' ); + $this->dieWithError( 'apierror-cantblock-email' ); } $data = [ @@ -100,8 +96,7 @@ class ApiBlock extends ApiBase { $retval = SpecialBlock::processForm( $data, $this->getContext() ); if ( $retval !== true ) { - // We don't care about multiple errors, just report one of them - $this->dieUsageMsg( $retval ); + $this->dieStatus( $this->errorArrayToStatus( $retval ) ); } list( $target, /*...*/ ) = SpecialBlock::getTargetAndType( $params['user'] ); diff --git a/includes/api/ApiCSPReport.php b/includes/api/ApiCSPReport.php index 5a0edfcd82dd..4139019ccf0a 100644 --- a/includes/api/ApiCSPReport.php +++ b/includes/api/ApiCSPReport.php @@ -137,8 +137,11 @@ class ApiCSPReport extends ApiBase { } $status = FormatJson::parse( $postBody, FormatJson::FORCE_ASSOC ); if ( !$status->isGood() ) { - list( $code, ) = $this->getErrorFromStatus( $status ); - $this->error( $code, __METHOD__ ); + $msg = $status->getErrors()[0]['message']; + if ( $msg instanceof Message ) { + $msg = $msg->getKey(); + } + $this->error( $msg, __METHOD__ ); } $report = $status->getValue(); @@ -176,7 +179,7 @@ class ApiCSPReport extends ApiBase { * * @param $code String error code * @param $method String method that made error - * @throws UsageException Always + * @throws ApiUsageException Always */ private function error( $code, $method ) { $this->log->info( 'Error reading CSP report: ' . $code, [ @@ -184,7 +187,9 @@ class ApiCSPReport extends ApiBase { 'user-agent' => $this->getRequest()->getHeader( 'user-agent' ) ] ); // 500 so it shows up in browser's developer console. - $this->dieUsage( "Error processing CSP report: $code", 'cspreport-' . $code, 500 ); + $this->dieWithError( + [ 'apierror-csp-report', wfEscapeWikiText( $code ) ], 'cspreport-' . $code, [], 500 + ); } public function getAllowedParams() { diff --git a/includes/api/ApiChangeAuthenticationData.php b/includes/api/ApiChangeAuthenticationData.php index aea28195f0a9..c25920e72859 100644 --- a/includes/api/ApiChangeAuthenticationData.php +++ b/includes/api/ApiChangeAuthenticationData.php @@ -35,7 +35,7 @@ class ApiChangeAuthenticationData extends ApiBase { public function execute() { if ( !$this->getUser()->isLoggedIn() ) { - $this->dieUsage( 'Must be logged in to change authentication data', 'notloggedin' ); + $this->dieWithError( 'apierror-mustbeloggedin-changeauthenticationdata', 'notloggedin' ); } $helper = new ApiAuthManagerHelper( $this ); @@ -50,7 +50,7 @@ class ApiChangeAuthenticationData extends ApiBase { $this->getConfig()->get( 'ChangeCredentialsBlacklist' ) ); if ( count( $reqs ) !== 1 ) { - $this->dieUsage( 'Failed to create change request', 'badrequest' ); + $this->dieWithError( 'apierror-changeauth-norequest', 'badrequest' ); } $req = reset( $reqs ); diff --git a/includes/api/ApiCheckToken.php b/includes/api/ApiCheckToken.php index dd88b5fe3a5e..3cc7a8a058df 100644 --- a/includes/api/ApiCheckToken.php +++ b/includes/api/ApiCheckToken.php @@ -43,9 +43,7 @@ class ApiCheckToken extends ApiBase { ); if ( substr( $token, -strlen( urldecode( Token::SUFFIX ) ) ) === urldecode( Token::SUFFIX ) ) { - $this->setWarning( - "Check that symbols such as \"+\" in the token are properly percent-encoded in the URL." - ); + $this->addWarning( 'apiwarn-checktoken-percentencoding' ); } if ( $tokenObj->match( $token, $maxage ) ) { diff --git a/includes/api/ApiClientLogin.php b/includes/api/ApiClientLogin.php index cbb1524cc7da..3f5bc0c0c899 100644 --- a/includes/api/ApiClientLogin.php +++ b/includes/api/ApiClientLogin.php @@ -57,8 +57,8 @@ class ApiClientLogin extends ApiBase { $bits = wfParseUrl( $params['returnurl'] ); if ( !$bits || $bits['scheme'] === '' ) { $encParamName = $this->encodeParamName( 'returnurl' ); - $this->dieUsage( - "Invalid value '{$params['returnurl']}' for url parameter $encParamName", + $this->dieWithError( + [ 'apierror-badurl', $encParamName, wfEscapeWikiText( $params['returnurl'] ) ], "badurl_{$encParamName}" ); } diff --git a/includes/api/ApiComparePages.php b/includes/api/ApiComparePages.php index 7eb0bf3e8191..d6867eb52dff 100644 --- a/includes/api/ApiComparePages.php +++ b/includes/api/ApiComparePages.php @@ -34,8 +34,7 @@ class ApiComparePages extends ApiBase { $revision = Revision::newFromId( $rev1 ); if ( !$revision ) { - $this->dieUsage( 'The diff cannot be retrieved, ' . - 'one revision does not exist or you do not have permission to view it.', 'baddiff' ); + $this->dieWithError( 'apierror-baddiff' ); } $contentHandler = $revision->getContentHandler(); @@ -65,11 +64,7 @@ class ApiComparePages extends ApiBase { $difftext = $de->getDiffBody(); if ( $difftext === false ) { - $this->dieUsage( - 'The diff cannot be retrieved. Maybe one or both revisions do ' . - 'not exist or you do not have permission to view them.', - 'baddiff' - ); + $this->dieWithError( 'apierror-baddiff' ); } ApiResult::setContentValue( $vals, 'body', $difftext ); @@ -89,22 +84,19 @@ class ApiComparePages extends ApiBase { } elseif ( $titleText ) { $title = Title::newFromText( $titleText ); if ( !$title || $title->isExternal() ) { - $this->dieUsageMsg( [ 'invalidtitle', $titleText ] ); + $this->dieWithError( [ 'apierror-invalidtitle', wfEscapeWikiText( $titleText ) ] ); } return $title->getLatestRevID(); } elseif ( $titleId ) { $title = Title::newFromID( $titleId ); if ( !$title ) { - $this->dieUsageMsg( [ 'nosuchpageid', $titleId ] ); + $this->dieWithError( [ 'apierror-nosuchpageid', $titleId ] ); } return $title->getLatestRevID(); } - $this->dieUsage( - 'A title, a page ID, or a revision number is needed for both the from and the to parameters', - 'inputneeded' - ); + $this->dieWithError( 'apierror-compare-inputneeded', 'inputneeded' ); } public function getAllowedParams() { diff --git a/includes/api/ApiContinuationManager.php b/includes/api/ApiContinuationManager.php index 19e2453944b1..7da8ed9a5b19 100644 --- a/includes/api/ApiContinuationManager.php +++ b/includes/api/ApiContinuationManager.php @@ -40,7 +40,7 @@ class ApiContinuationManager { * @param ApiBase $module Module starting the continuation * @param ApiBase[] $allModules Contains ApiBase instances that will be executed * @param array $generatedModules Names of modules that depend on the generator - * @throws UsageException + * @throws ApiUsageException */ public function __construct( ApiBase $module, array $allModules = [], array $generatedModules = [] @@ -57,10 +57,7 @@ class ApiContinuationManager { if ( $continue !== '' ) { $continue = explode( '||', $continue ); if ( count( $continue ) !== 2 ) { - throw new UsageException( - 'Invalid continue param. You should pass the original value returned by the previous query', - 'badcontinue' - ); + throw ApiUsageException::newWithMessage( $module->getMain(), 'apierror-badcontinue' ); } $this->generatorDone = ( $continue[0] === '-' ); $skip = explode( '|', $continue[1] ); diff --git a/includes/api/ApiDelete.php b/includes/api/ApiDelete.php index 993c23e58244..50c24aeca8b5 100644 --- a/includes/api/ApiDelete.php +++ b/includes/api/ApiDelete.php @@ -45,7 +45,7 @@ class ApiDelete extends ApiBase { $pageObj = $this->getTitleOrPageId( $params, 'fromdbmaster' ); if ( !$pageObj->exists() ) { - $this->dieUsageMsg( 'notanarticle' ); + $this->dieWithError( 'apierror-missingtitle' ); } $titleObj = $pageObj->getTitle(); @@ -53,10 +53,7 @@ class ApiDelete extends ApiBase { $user = $this->getUser(); // Check that the user is allowed to carry out the deletion - $errors = $titleObj->getUserPermissionsErrors( 'delete', $user ); - if ( count( $errors ) ) { - $this->dieUsageMsg( $errors[0] ); - } + $this->checkTitleUserPermissions( $titleObj, 'delete' ); // If change tagging was requested, check that the user is allowed to tag, // and the tags are valid @@ -80,9 +77,6 @@ class ApiDelete extends ApiBase { $status = self::delete( $pageObj, $user, $reason, $params['tags'] ); } - if ( is_array( $status ) ) { - $this->dieUsageMsg( $status[0] ); - } if ( !$status->isGood() ) { $this->dieStatus( $status ); } @@ -112,7 +106,7 @@ class ApiDelete extends ApiBase { * @param User $user User doing the action * @param string|null $reason Reason for the deletion. Autogenerated if null * @param array $tags Tags to tag the deletion with - * @return Status|array + * @return Status */ protected static function delete( Page $page, User $user, &$reason = null, $tags = [] ) { $title = $page->getTitle(); @@ -124,7 +118,7 @@ class ApiDelete extends ApiBase { $hasHistory = false; $reason = $page->getAutoDeleteReason( $hasHistory ); if ( $reason === false ) { - return [ [ 'cannotdelete', $title->getPrefixedText() ] ]; + return Status::newFatal( 'cannotdelete', $title->getPrefixedText() ); } } @@ -141,7 +135,7 @@ class ApiDelete extends ApiBase { * @param string $reason Reason for the deletion. Autogenerated if null. * @param bool $suppress Whether to mark all deleted versions as restricted * @param array $tags Tags to tag the deletion with - * @return Status|array + * @return Status */ protected static function deleteFile( Page $page, User $user, $oldimage, &$reason = null, $suppress = false, $tags = [] @@ -155,11 +149,11 @@ class ApiDelete extends ApiBase { if ( $oldimage ) { if ( !FileDeleteForm::isValidOldSpec( $oldimage ) ) { - return [ [ 'invalidoldimage' ] ]; + return Status::newFatal( 'invalidoldimage' ); } $oldfile = RepoGroup::singleton()->getLocalRepo()->newFromArchiveName( $title, $oldimage ); if ( !$oldfile->exists() || !$oldfile->isLocal() || $oldfile->getRedirected() ) { - return [ [ 'nodeleteablefile' ] ]; + return Status::newFatal( 'nodeleteablefile' ); } } diff --git a/includes/api/ApiDisabled.php b/includes/api/ApiDisabled.php index fc9752205ee1..41bf9b69c7b8 100644 --- a/includes/api/ApiDisabled.php +++ b/includes/api/ApiDisabled.php @@ -37,7 +37,7 @@ class ApiDisabled extends ApiBase { public function execute() { - $this->dieUsage( "The \"{$this->getModuleName()}\" module has been disabled.", 'moduledisabled' ); + $this->dieWithError( [ 'apierror-moduledisabled', $this->getModuleName() ] ); } public function isReadMode() { diff --git a/includes/api/ApiEditPage.php b/includes/api/ApiEditPage.php index d6de83430193..6b568701fd1d 100644 --- a/includes/api/ApiEditPage.php +++ b/includes/api/ApiEditPage.php @@ -40,12 +40,7 @@ class ApiEditPage extends ApiBase { $user = $this->getUser(); $params = $this->extractRequestParams(); - if ( is_null( $params['text'] ) && is_null( $params['appendtext'] ) && - is_null( $params['prependtext'] ) && - $params['undo'] == 0 - ) { - $this->dieUsageMsg( 'missingtext' ); - } + $this->requireAtLeastOneParameter( $params, 'text', 'appendtext', 'prependtext', 'undo' ); $pageObj = $this->getTitleOrPageId( $params ); $titleObj = $pageObj->getTitle(); @@ -55,9 +50,7 @@ class ApiEditPage extends ApiBase { if ( $params['prependtext'] === null && $params['appendtext'] === null && $params['section'] !== 'new' ) { - $this->dieUsage( 'You have attempted to edit using the "redirect"-following' - . ' mode, which must be used in conjuction with section=new, prependtext' - . ', or appendtext.', 'redirect-appendonly' ); + $this->dieWithError( 'apierror-redirect-appendonly' ); } if ( $titleObj->isRedirect() ) { $oldTitle = $titleObj; @@ -105,10 +98,7 @@ class ApiEditPage extends ApiBase { if ( $params['undo'] > 0 ) { // allow undo via api } elseif ( $contentHandler->supportsDirectApiEditing() === false ) { - $this->dieUsage( - "Direct editing via API is not supported for content model $model used by $name", - 'no-direct-editing' - ); + $this->dieWithError( [ 'apierror-no-direct-editing', $model, $name ] ); } if ( !isset( $params['contentformat'] ) || $params['contentformat'] == '' ) { @@ -118,49 +108,21 @@ class ApiEditPage extends ApiBase { } if ( !$contentHandler->isSupportedFormat( $contentFormat ) ) { - - $this->dieUsage( "The requested format $contentFormat is not supported for content model " . - " $model used by $name", 'badformat' ); + $this->dieWithError( [ 'apierror-badformat', $contentFormat, $model, $name ] ); } if ( $params['createonly'] && $titleObj->exists() ) { - $this->dieUsageMsg( 'createonly-exists' ); + $this->dieWithError( 'apierror-articleexists' ); } if ( $params['nocreate'] && !$titleObj->exists() ) { - $this->dieUsageMsg( 'nocreate-missing' ); + $this->dieWithError( 'apierror-missingtitle' ); } // Now let's check whether we're even allowed to do this - $errors = $titleObj->getUserPermissionsErrors( 'edit', $user ); - if ( !$titleObj->exists() ) { - $errors = array_merge( $errors, $titleObj->getUserPermissionsErrors( 'create', $user ) ); - } - if ( count( $errors ) ) { - if ( is_array( $errors[0] ) ) { - switch ( $errors[0][0] ) { - case 'blockedtext': - $this->dieUsage( - 'You have been blocked from editing', - 'blocked', - 0, - [ 'blockinfo' => ApiQueryUserInfo::getBlockInfo( $user->getBlock() ) ] - ); - break; - case 'autoblockedtext': - $this->dieUsage( - 'Your IP address has been blocked automatically, because it was used by a blocked user', - 'autoblocked', - 0, - [ 'blockinfo' => ApiQueryUserInfo::getBlockInfo( $user->getBlock() ) ] - ); - break; - default: - $this->dieUsageMsg( $errors[0] ); - } - } else { - $this->dieUsageMsg( $errors[0] ); - } - } + $this->checkTitleUserPermissions( + $titleObj, + $titleObj->exists() ? 'edit' : [ 'edit', 'create' ] + ); $toMD5 = $params['text']; if ( !is_null( $params['appendtext'] ) || !is_null( $params['prependtext'] ) ) { @@ -178,8 +140,11 @@ class ApiEditPage extends ApiBase { try { $content = ContentHandler::makeContent( $text, $this->getTitle() ); } catch ( MWContentSerializationException $ex ) { - $this->dieUsage( $ex->getMessage(), 'parseerror' ); - + // @todo: Internationalize MWContentSerializationException + $this->dieWithError( + [ 'apierror-contentserializationexception', wfEscapeWikiText( $ex->getMessage() ) ], + 'parseerror' + ); return; } } else { @@ -191,17 +156,14 @@ class ApiEditPage extends ApiBase { // @todo Add support for appending/prepending to the Content interface if ( !( $content instanceof TextContent ) ) { - $mode = $contentHandler->getModelID(); - $this->dieUsage( "Can't append to pages using content model $mode", 'appendnotsupported' ); + $modelName = $contentHandler->getModelID(); + $this->dieWithError( [ 'apierror-appendnotsupported', $modelName ] ); } if ( !is_null( $params['section'] ) ) { if ( !$contentHandler->supportsSections() ) { $modelName = $contentHandler->getModelID(); - $this->dieUsage( - "Sections are not supported for this content model: $modelName.", - 'sectionsnotsupported' - ); + $this->dieWithError( [ 'apierror-sectionsnotsupported', $modelName ] ); } if ( $params['section'] == 'new' ) { @@ -213,7 +175,7 @@ class ApiEditPage extends ApiBase { $content = $content->getSection( $section ); if ( !$content ) { - $this->dieUsage( "There is no section {$section}.", 'nosuchsection' ); + $this->dieWithError( [ 'apierror-nosuchsection', wfEscapeWikiText( $section ) ] ); } } } @@ -238,22 +200,22 @@ class ApiEditPage extends ApiBase { } $undoRev = Revision::newFromId( $params['undo'] ); if ( is_null( $undoRev ) || $undoRev->isDeleted( Revision::DELETED_TEXT ) ) { - $this->dieUsageMsg( [ 'nosuchrevid', $params['undo'] ] ); + $this->dieWithError( [ 'apierror-nosuchrevid', $params['undo'] ] ); } if ( $params['undoafter'] == 0 ) { $undoafterRev = $undoRev->getPrevious(); } if ( is_null( $undoafterRev ) || $undoafterRev->isDeleted( Revision::DELETED_TEXT ) ) { - $this->dieUsageMsg( [ 'nosuchrevid', $params['undoafter'] ] ); + $this->dieWithError( [ 'apierror-nosuchrevid', $params['undoafter'] ] ); } if ( $undoRev->getPage() != $pageObj->getId() ) { - $this->dieUsageMsg( [ 'revwrongpage', $undoRev->getId(), + $this->dieWithError( [ 'apierror-revwrongpage', $undoRev->getId(), $titleObj->getPrefixedText() ] ); } if ( $undoafterRev->getPage() != $pageObj->getId() ) { - $this->dieUsageMsg( [ 'revwrongpage', $undoafterRev->getId(), + $this->dieWithError( [ 'apierror-revwrongpage', $undoafterRev->getId(), $titleObj->getPrefixedText() ] ); } @@ -264,7 +226,7 @@ class ApiEditPage extends ApiBase { ); if ( !$newContent ) { - $this->dieUsageMsg( 'undo-failure' ); + $this->dieWithError( 'undo-failure', 'undofailure' ); } if ( empty( $params['contentmodel'] ) && empty( $params['contentformat'] ) @@ -293,7 +255,7 @@ class ApiEditPage extends ApiBase { // See if the MD5 hash checks out if ( !is_null( $params['md5'] ) && md5( $toMD5 ) !== $params['md5'] ) { - $this->dieUsageMsg( 'hashcheckfailed' ); + $this->dieWithError( 'apierror-badmd5' ); } // EditPage wants to parse its stuff from a WebRequest @@ -347,14 +309,13 @@ class ApiEditPage extends ApiBase { if ( !is_null( $params['section'] ) ) { $section = $params['section']; if ( !preg_match( '/^((T-)?\d+|new)$/', $section ) ) { - $this->dieUsage( "The section parameter must be a valid section id or 'new'", - 'invalidsection' ); + $this->dieWithError( 'apierror-invalidsection' ); } $content = $pageObj->getContent(); if ( $section !== '0' && $section != 'new' && ( !$content || !$content->getSection( $section ) ) ) { - $this->dieUsage( "There is no section {$section}.", 'nosuchsection' ); + $this->dieWithError( [ 'apierror-nosuchsection', $section ] ); } $requestArray['wpSection'] = $params['section']; } else { @@ -423,7 +384,7 @@ class ApiEditPage extends ApiBase { return; } - $this->dieUsageMsg( 'hookaborted' ); + $this->dieWithError( 'hookaborted' ); } // Do the actual save @@ -445,67 +406,22 @@ class ApiEditPage extends ApiBase { $r['result'] = 'Failure'; $apiResult->addValue( null, $this->getModuleName(), $r ); return; - } else { - $this->dieUsageMsg( 'hookaborted' ); } - - case EditPage::AS_PARSE_ERROR: - $this->dieUsage( $status->getMessage(), 'parseerror' ); - - case EditPage::AS_IMAGE_REDIRECT_ANON: - $this->dieUsageMsg( 'noimageredirect-anon' ); - - case EditPage::AS_IMAGE_REDIRECT_LOGGED: - $this->dieUsageMsg( 'noimageredirect-logged' ); - - case EditPage::AS_SPAM_ERROR: - $this->dieUsageMsg( [ 'spamdetected', $result['spam'] ] ); + if ( !$status->getErrors() ) { + $status->fatal( 'hookaborted' ); + } + $this->dieStatus( $status ); case EditPage::AS_BLOCKED_PAGE_FOR_USER: - $this->dieUsage( - 'You have been blocked from editing', + $this->dieWithError( + 'apierror-blocked', 'blocked', - 0, [ 'blockinfo' => ApiQueryUserInfo::getBlockInfo( $user->getBlock() ) ] ); - case EditPage::AS_MAX_ARTICLE_SIZE_EXCEEDED: - case EditPage::AS_CONTENT_TOO_BIG: - $this->dieUsageMsg( [ 'contenttoobig', $this->getConfig()->get( 'MaxArticleSize' ) ] ); - - case EditPage::AS_READ_ONLY_PAGE_ANON: - $this->dieUsageMsg( 'noedit-anon' ); - - case EditPage::AS_READ_ONLY_PAGE_LOGGED: - $this->dieUsageMsg( 'noedit' ); - case EditPage::AS_READ_ONLY_PAGE: $this->dieReadOnly(); - case EditPage::AS_RATE_LIMITED: - $this->dieUsageMsg( 'actionthrottledtext' ); - - case EditPage::AS_ARTICLE_WAS_DELETED: - $this->dieUsageMsg( 'wasdeleted' ); - - case EditPage::AS_NO_CREATE_PERMISSION: - $this->dieUsageMsg( 'nocreate-loggedin' ); - - case EditPage::AS_NO_CHANGE_CONTENT_MODEL: - $this->dieUsageMsg( 'cantchangecontentmodel' ); - - case EditPage::AS_BLANK_ARTICLE: - $this->dieUsageMsg( 'blankpage' ); - - case EditPage::AS_CONFLICT_DETECTED: - $this->dieUsageMsg( 'editconflict' ); - - case EditPage::AS_TEXTBOX_EMPTY: - $this->dieUsageMsg( 'emptynewsection' ); - - case EditPage::AS_CHANGE_TAG_ERROR: - $this->dieStatus( $status ); - case EditPage::AS_SUCCESS_NEW_ARTICLE: $r['new'] = true; // fall-through @@ -526,15 +442,39 @@ class ApiEditPage extends ApiBase { } break; - case EditPage::AS_SUMMARY_NEEDED: - // Shouldn't happen since we set wpIgnoreBlankSummary, but just in case - $this->dieUsageMsg( 'summaryrequired' ); - - case EditPage::AS_END: default: - // $status came from WikiPage::doEditContent() - $errors = $status->getErrorsArray(); - $this->dieUsageMsg( $errors[0] ); // TODO: Add new errors to message map + // EditPage sometimes only sets the status code without setting + // any actual error messages. Supply defaults for those cases. + $maxArticleSize = $this->getConfig()->get( 'MaxArticleSize' ); + $defaultMessages = [ + // Currently needed + EditPage::AS_IMAGE_REDIRECT_ANON => [ 'apierror-noimageredirect-anon' ], + EditPage::AS_IMAGE_REDIRECT_LOGGED => [ 'apierror-noimageredirect-logged' ], + EditPage::AS_CONTENT_TOO_BIG => [ 'apierror-contenttoobig', $maxArticleSize ], + EditPage::AS_MAX_ARTICLE_SIZE_EXCEEDED => [ 'apierror-contenttoobig', $maxArticleSize ], + EditPage::AS_READ_ONLY_PAGE_ANON => [ 'apierror-noedit-anon' ], + EditPage::AS_NO_CHANGE_CONTENT_MODEL => [ 'apierror-cantchangecontentmodel' ], + EditPage::AS_ARTICLE_WAS_DELETED => [ 'apierror-pagedeleted' ], + EditPage::AS_CONFLICT_DETECTED => [ 'editconflict' ], + + // Currently shouldn't be needed + EditPage::AS_SPAM_ERROR => [ 'apierror-spamdetected', wfEscapeWikiText( $result['spam'] ) ], + EditPage::AS_READ_ONLY_PAGE_LOGGED => [ 'apierror-noedit' ], + EditPage::AS_RATE_LIMITED => [ 'apierror-ratelimited' ], + EditPage::AS_NO_CREATE_PERMISSION => [ 'nocreate-loggedin' ], + EditPage::AS_BLANK_ARTICLE => [ 'apierror-emptypage' ], + EditPage::AS_TEXTBOX_EMPTY => [ 'apierror-emptynewsection' ], + EditPage::AS_SUMMARY_NEEDED => [ 'apierror-summaryrequired' ], + ]; + if ( !$status->getErrors() ) { + if ( isset( $defaultMessages[$status->value] ) ) { + call_user_func_array( [ $status, 'fatal' ], $defaultMessages[$status->value] ); + } else { + wfWarn( __METHOD__ . ": Unknown EditPage code {$status->value} with no message" ); + $status->fatal( 'apierror-unknownerror-editpage', $status->value ); + } + } + $this->dieStatus( $status ); break; } $apiResult->addValue( null, $this->getModuleName(), $r ); diff --git a/includes/api/ApiEmailUser.php b/includes/api/ApiEmailUser.php index 192378e8e871..8aff6f8afd46 100644 --- a/includes/api/ApiEmailUser.php +++ b/includes/api/ApiEmailUser.php @@ -36,7 +36,16 @@ class ApiEmailUser extends ApiBase { // Validate target $targetUser = SpecialEmailUser::getTarget( $params['target'] ); if ( !( $targetUser instanceof User ) ) { - $this->dieUsageMsg( [ $targetUser ] ); + switch ( $targetUser ) { + case 'notarget': + $this->dieWithError( 'apierror-notarget' ); + case 'noemail': + $this->dieWithError( [ 'noemail', $params['target'] ] ); + case 'nowikiemail': + $this->dieWithError( 'nowikiemailtext', 'nowikiemail' ); + default: + $this->dieWithError( [ 'apierror-unknownerror', $targetUser ] ); + } } // Check permissions and errors @@ -46,7 +55,7 @@ class ApiEmailUser extends ApiBase { $this->getConfig() ); if ( $error ) { - $this->dieUsageMsg( [ $error ] ); + $this->dieWithError( $error ); } $data = [ @@ -56,25 +65,16 @@ class ApiEmailUser extends ApiBase { 'CCMe' => $params['ccme'], ]; $retval = SpecialEmailUser::submit( $data, $this->getContext() ); - - if ( $retval instanceof Status ) { - // SpecialEmailUser sometimes returns a status - // sometimes it doesn't. - if ( $retval->isGood() ) { - $retval = true; - } else { - $retval = $retval->getErrorsArray(); - } + if ( !$retval instanceof Status ) { + // This is probably the reason + $retval = Status::newFatal( 'hookaborted' ); } - if ( $retval === true ) { - $result = [ 'result' => 'Success' ]; - } else { - $result = [ - 'result' => 'Failure', - 'message' => $retval - ]; - } + $result = array_filter( [ + 'result' => $retval->isGood() ? 'Success' : $retval->isOk() ? 'Warnings' : 'Failure', + 'warnings' => $this->getErrorFormatter()->arrayFromStatus( $retval, 'warning' ), + 'errors' => $this->getErrorFormatter()->arrayFromStatus( $retval, 'error' ), + ] ); $this->getResult()->addValue( null, $this->getModuleName(), $result ); } diff --git a/includes/api/ApiErrorFormatter.php b/includes/api/ApiErrorFormatter.php index 6d9184f7818f..4fb19b88d5e9 100644 --- a/includes/api/ApiErrorFormatter.php +++ b/includes/api/ApiErrorFormatter.php @@ -43,7 +43,9 @@ class ApiErrorFormatter { * @param ApiResult $result Into which data will be added * @param Language $lang Used for i18n * @param string $format - * - text: Error message as wikitext + * - plaintext: Error message as something vaguely like plaintext + * (it's basically wikitext with HTML tags stripped and entities decoded) + * - wikitext: Error message as wikitext * - html: Error message as HTML * - raw: Raw message key and parameters, no human-readable text * - none: Code and data only, no human-readable text @@ -57,6 +59,15 @@ class ApiErrorFormatter { } /** + * Fetch the Language for this formatter + * @since 1.29 + * @return Language + */ + public function getLanguage() { + return $this->lang; + } + + /** * Fetch a dummy title to set on Messages * @return Title */ @@ -69,53 +80,49 @@ class ApiErrorFormatter { /** * Add a warning to the result - * @param string $moduleName - * @param MessageSpecifier|array|string $msg i18n message for the warning - * @param string $code Machine-readable code for the warning. Defaults as - * for IApiMessage::getApiCode(). - * @param array $data Machine-readable data for the warning, if any. - * Uses IApiMessage::getApiData() if $msg implements that interface. + * @param string|null $modulePath + * @param Message|array|string $msg Warning message. See ApiMessage::create(). + * @param string|null $code See ApiMessage::create(). + * @param array|null $data See ApiMessage::create(). */ - public function addWarning( $moduleName, $msg, $code = null, $data = null ) { + public function addWarning( $modulePath, $msg, $code = null, $data = null ) { $msg = ApiMessage::create( $msg, $code, $data ) ->inLanguage( $this->lang ) ->title( $this->getDummyTitle() ) ->useDatabase( $this->useDB ); - $this->addWarningOrError( 'warning', $moduleName, $msg ); + $this->addWarningOrError( 'warning', $modulePath, $msg ); } /** * Add an error to the result - * @param string $moduleName - * @param MessageSpecifier|array|string $msg i18n message for the error - * @param string $code Machine-readable code for the warning. Defaults as - * for IApiMessage::getApiCode(). - * @param array $data Machine-readable data for the warning, if any. - * Uses IApiMessage::getApiData() if $msg implements that interface. + * @param string|null $modulePath + * @param Message|array|string $msg Warning message. See ApiMessage::create(). + * @param string|null $code See ApiMessage::create(). + * @param array|null $data See ApiMessage::create(). */ - public function addError( $moduleName, $msg, $code = null, $data = null ) { + public function addError( $modulePath, $msg, $code = null, $data = null ) { $msg = ApiMessage::create( $msg, $code, $data ) ->inLanguage( $this->lang ) ->title( $this->getDummyTitle() ) ->useDatabase( $this->useDB ); - $this->addWarningOrError( 'error', $moduleName, $msg ); + $this->addWarningOrError( 'error', $modulePath, $msg ); } /** - * Add warnings and errors from a Status object to the result - * @param string $moduleName - * @param Status $status + * Add warnings and errors from a StatusValue object to the result + * @param string|null $modulePath + * @param StatusValue $status * @param string[] $types 'warning' and/or 'error' */ public function addMessagesFromStatus( - $moduleName, Status $status, $types = [ 'warning', 'error' ] + $modulePath, StatusValue $status, $types = [ 'warning', 'error' ] ) { - if ( $status->isGood() || !$status->errors ) { + if ( $status->isGood() || !$status->getErrors() ) { return; } $types = (array)$types; - foreach ( $status->errors as $error ) { + foreach ( $status->getErrors() as $error ) { if ( !in_array( $error['type'], $types, true ) ) { continue; } @@ -127,40 +134,37 @@ class ApiErrorFormatter { $tag = 'warning'; } - if ( is_array( $error ) && isset( $error['message'] ) ) { - // Normal case - if ( $error['message'] instanceof Message ) { - $msg = ApiMessage::create( $error['message'], null, [] ); - } else { - $args = isset( $error['params'] ) ? $error['params'] : []; - array_unshift( $args, $error['message'] ); - $error += [ 'params' => [] ]; - $msg = ApiMessage::create( $args, null, [] ); - } - } elseif ( is_array( $error ) ) { - // Weird case handled by Message::getErrorMessage - $msg = ApiMessage::create( $error, null, [] ); - } else { - // Another weird case handled by Message::getErrorMessage - $msg = ApiMessage::create( $error, null, [] ); - } - - $msg->inLanguage( $this->lang ) + $msg = ApiMessage::create( $error ) + ->inLanguage( $this->lang ) ->title( $this->getDummyTitle() ) ->useDatabase( $this->useDB ); - $this->addWarningOrError( $tag, $moduleName, $msg ); + $this->addWarningOrError( $tag, $modulePath, $msg ); } } /** - * Format messages from a Status as an array - * @param Status $status + * Format a message as an array + * @param Message|array|string $msg Message. See ApiMessage::create(). + * @param string|null $format + * @return array + */ + public function formatMessage( $msg, $format = null ) { + $msg = ApiMessage::create( $msg ) + ->inLanguage( $this->lang ) + ->title( $this->getDummyTitle() ) + ->useDatabase( $this->useDB ); + return $this->formatMessageInternal( $msg, $format ?: $this->format ); + } + + /** + * Format messages from a StatusValue as an array + * @param StatusValue $status * @param string $type 'warning' or 'error' * @param string|null $format * @return array */ - public function arrayFromStatus( Status $status, $type = 'error', $format = null ) { - if ( $status->isGood() || !$status->errors ) { + public function arrayFromStatus( StatusValue $status, $type = 'error', $format = null ) { + if ( $status->isGood() || !$status->getErrors() ) { return []; } @@ -168,24 +172,69 @@ class ApiErrorFormatter { $formatter = new ApiErrorFormatter( $result, $this->lang, $format ?: $this->format, $this->useDB ); - $formatter->addMessagesFromStatus( 'dummy', $status, [ $type ] ); + $formatter->addMessagesFromStatus( null, $status, [ $type ] ); switch ( $type ) { case 'error': - return (array)$result->getResultData( [ 'errors', 'dummy' ] ); + return (array)$result->getResultData( [ 'errors' ] ); case 'warning': - return (array)$result->getResultData( [ 'warnings', 'dummy' ] ); + return (array)$result->getResultData( [ 'warnings' ] ); } } /** - * Actually add the warning or error to the result - * @param string $tag 'warning' or 'error' - * @param string $moduleName + * Turn wikitext into something resembling plaintext + * @since 1.29 + * @param string $text + * @return string + */ + public static function stripMarkup( $text ) { + // Turn semantic quoting tags to quotes + $ret = preg_replace( '!</?(var|kbd|samp|code)>!', '"', $text ); + + // Strip tags and decode. + $ret = html_entity_decode( strip_tags( $ret ), ENT_QUOTES | ENT_HTML5 ); + + return $ret; + } + + /** + * Format a Message object for raw format + * @param MessageSpecifier $msg + * @return array + */ + private function formatRawMessage( MessageSpecifier $msg ) { + $ret = [ + 'key' => $msg->getKey(), + 'params' => $msg->getParams(), + ]; + ApiResult::setIndexedTagName( $ret['params'], 'param' ); + + // Transform Messages as parameters in the style of Message::fooParam(). + foreach ( $ret['params'] as $i => $param ) { + if ( $param instanceof MessageSpecifier ) { + $ret['params'][$i] = [ 'message' => $this->formatRawMessage( $param ) ]; + } + } + return $ret; + } + + /** + * Format a message as an array + * @since 1.29 * @param ApiMessage|ApiRawMessage $msg + * @param string|null $format + * @return array */ - protected function addWarningOrError( $tag, $moduleName, $msg ) { + protected function formatMessageInternal( $msg, $format ) { $value = [ 'code' => $msg->getApiCode() ]; - switch ( $this->format ) { + switch ( $format ) { + case 'plaintext': + $value += [ + 'text' => self::stripMarkup( $msg->text() ), + ApiResult::META_CONTENT => 'text', + ]; + break; + case 'wikitext': $value += [ 'text' => $msg->text(), @@ -201,19 +250,34 @@ class ApiErrorFormatter { break; case 'raw': - $value += [ - 'key' => $msg->getKey(), - 'params' => $msg->getParams(), - ]; - ApiResult::setIndexedTagName( $value['params'], 'param' ); + $value += $this->formatRawMessage( $msg ); break; case 'none': break; } - $value += $msg->getApiData(); + $data = $msg->getApiData(); + if ( $data ) { + $value['data'] = $msg->getApiData() + [ + ApiResult::META_TYPE => 'assoc', + ]; + } + return $value; + } - $path = [ $tag . 's', $moduleName ]; + /** + * Actually add the warning or error to the result + * @param string $tag 'warning' or 'error' + * @param string|null $modulePath + * @param ApiMessage|ApiRawMessage $msg + */ + protected function addWarningOrError( $tag, $modulePath, $msg ) { + $value = $this->formatMessageInternal( $msg, $this->format ); + if ( $modulePath !== null ) { + $value += [ 'module' => $modulePath ]; + } + + $path = [ $tag . 's' ]; $existing = $this->result->getResultData( $path ); if ( $existing === null || !in_array( $value, $existing ) ) { $flags = ApiResult::NO_SIZE_CHECK; @@ -243,19 +307,19 @@ class ApiErrorFormatter_BackCompat extends ApiErrorFormatter { parent::__construct( $result, Language::factory( 'en' ), 'none', false ); } - public function arrayFromStatus( Status $status, $type = 'error', $format = null ) { - if ( $status->isGood() || !$status->errors ) { + public function arrayFromStatus( StatusValue $status, $type = 'error', $format = null ) { + if ( $status->isGood() || !$status->getErrors() ) { return []; } $result = []; foreach ( $status->getErrorsByType( $type ) as $error ) { - if ( $error['message'] instanceof Message ) { - $error = [ - 'message' => $error['message']->getKey(), - 'params' => $error['message']->getParams(), - ] + $error; - } + $msg = ApiMessage::create( $error ); + $error = [ + 'message' => $msg->getKey(), + 'params' => $msg->getParams(), + 'code' => $msg->getApiCode(), + ] + $error; ApiResult::setIndexedTagName( $error['params'], 'param' ); $result[] = $error; } @@ -264,24 +328,32 @@ class ApiErrorFormatter_BackCompat extends ApiErrorFormatter { return $result; } - protected function addWarningOrError( $tag, $moduleName, $msg ) { - $value = $msg->plain(); + protected function formatMessageInternal( $msg, $format ) { + return [ + 'code' => $msg->getApiCode(), + 'info' => $msg->text(), + ] + $msg->getApiData(); + } + + protected function addWarningOrError( $tag, $modulePath, $msg ) { + $value = self::stripMarkup( $msg->text() ); if ( $tag === 'error' ) { // In BC mode, only one error - $code = $msg->getApiCode(); - if ( isset( ApiBase::$messageMap[$code] ) ) { - // Backwards compatibility - $code = ApiBase::$messageMap[$code]['code']; - } - $value = [ - 'code' => $code, + 'code' => $msg->getApiCode(), 'info' => $value, ] + $msg->getApiData(); $this->result->addValue( null, 'error', $value, ApiResult::OVERRIDE | ApiResult::ADD_ON_TOP | ApiResult::NO_SIZE_CHECK ); } else { + if ( $modulePath === null ) { + $moduleName = 'unknown'; + } else { + $i = strrpos( $modulePath, '+' ); + $moduleName = $i === false ? $modulePath : substr( $modulePath, $i + 1 ); + } + // Don't add duplicate warnings $tag .= 's'; $path = [ $tag, $moduleName ]; diff --git a/includes/api/ApiExpandTemplates.php b/includes/api/ApiExpandTemplates.php index 10fb1824be73..6f7cf652c3f4 100644 --- a/includes/api/ApiExpandTemplates.php +++ b/includes/api/ApiExpandTemplates.php @@ -42,11 +42,9 @@ class ApiExpandTemplates extends ApiBase { $this->requireMaxOneParameter( $params, 'prop', 'generatexml' ); if ( $params['prop'] === null ) { - $this->logFeatureUsage( 'action=expandtemplates&!prop' ); - $this->setWarning( 'Because no values have been specified for the prop parameter, a ' . - 'legacy format has been used for the output. This format is deprecated, and in ' . - 'the future, a default value will be set for the prop parameter, causing the new' . - 'format to always be used.' ); + $this->addDeprecation( + 'apiwarn-deprecation-expandtemplates-prop', 'action=expandtemplates&!prop' + ); $prop = []; } else { $prop = array_flip( $params['prop'] ); @@ -57,13 +55,13 @@ class ApiExpandTemplates extends ApiBase { if ( $revid !== null ) { $rev = Revision::newFromId( $revid ); if ( !$rev ) { - $this->dieUsage( "There is no revision ID $revid", 'missingrev' ); + $this->dieWithError( [ 'apierror-nosuchrevid', $revid ] ); } $title_obj = $rev->getTitle(); } else { $title_obj = Title::newFromText( $params['title'] ); if ( !$title_obj || $title_obj->isExternal() ) { - $this->dieUsageMsg( [ 'invalidtitle', $params['title'] ] ); + $this->dieWithError( [ 'apierror-invalidtitle', wfEscapeWikiText( $params['title'] ) ] ); } } @@ -161,9 +159,7 @@ class ApiExpandTemplates extends ApiBase { } if ( isset( $prop['modules'] ) && !isset( $prop['jsconfigvars'] ) && !isset( $prop['encodedjsconfigvars'] ) ) { - $this->setWarning( "Property 'modules' was set but not 'jsconfigvars' " . - "or 'encodedjsconfigvars'. Configuration variables are necessary " . - 'for proper module usage.' ); + $this->addWarning( 'apiwarn-moduleswithoutvars' ); } } } diff --git a/includes/api/ApiFeedContributions.php b/includes/api/ApiFeedContributions.php index c7dc303ada45..97720c6e9f84 100644 --- a/includes/api/ApiFeedContributions.php +++ b/includes/api/ApiFeedContributions.php @@ -43,16 +43,16 @@ class ApiFeedContributions extends ApiBase { $config = $this->getConfig(); if ( !$config->get( 'Feed' ) ) { - $this->dieUsage( 'Syndication feeds are not available', 'feed-unavailable' ); + $this->dieWithError( 'feed-unavailable' ); } $feedClasses = $config->get( 'FeedClasses' ); if ( !isset( $feedClasses[$params['feedformat']] ) ) { - $this->dieUsage( 'Invalid subscription feed type', 'feed-invalid' ); + $this->dieWithError( 'feed-invalid' ); } if ( $params['showsizediff'] && $this->getConfig()->get( 'MiserMode' ) ) { - $this->dieUsage( 'Size difference is disabled in Miser Mode', 'sizediffdisabled' ); + $this->dieWithError( 'apierror-sizediffdisabled' ); } $msg = wfMessage( 'Contributions' )->inContentLanguage()->text(); diff --git a/includes/api/ApiFeedRecentChanges.php b/includes/api/ApiFeedRecentChanges.php index 813ac013a02f..e0e50edd9c68 100644 --- a/includes/api/ApiFeedRecentChanges.php +++ b/includes/api/ApiFeedRecentChanges.php @@ -47,12 +47,12 @@ class ApiFeedRecentChanges extends ApiBase { $this->params = $this->extractRequestParams(); if ( !$config->get( 'Feed' ) ) { - $this->dieUsage( 'Syndication feeds are not available', 'feed-unavailable' ); + $this->dieWithError( 'feed-unavailable' ); } $feedClasses = $config->get( 'FeedClasses' ); if ( !isset( $feedClasses[$this->params['feedformat']] ) ) { - $this->dieUsage( 'Invalid subscription feed type', 'feed-invalid' ); + $this->dieWithError( 'feed-invalid' ); } $this->getMain()->setCacheMode( 'public' ); @@ -98,7 +98,7 @@ class ApiFeedRecentChanges extends ApiBase { if ( $specialClass === 'SpecialRecentchangeslinked' ) { $title = Title::newFromText( $this->params['target'] ); if ( !$title ) { - $this->dieUsageMsg( [ 'invalidtitle', $this->params['target'] ] ); + $this->dieWithError( [ 'apierror-invalidtitle', wfEscapeWikiText( $this->params['target'] ) ] ); } $feed = new ChangesFeed( $feedFormat, false ); diff --git a/includes/api/ApiFeedWatchlist.php b/includes/api/ApiFeedWatchlist.php index af5b1afc28e5..b9bb761b3c3d 100644 --- a/includes/api/ApiFeedWatchlist.php +++ b/includes/api/ApiFeedWatchlist.php @@ -56,11 +56,11 @@ class ApiFeedWatchlist extends ApiBase { $params = $this->extractRequestParams(); if ( !$config->get( 'Feed' ) ) { - $this->dieUsage( 'Syndication feeds are not available', 'feed-unavailable' ); + $this->dieWithError( 'feed-unavailable' ); } if ( !isset( $feedClasses[$params['feedformat']] ) ) { - $this->dieUsage( 'Invalid subscription feed type', 'feed-invalid' ); + $this->dieWithError( 'feed-invalid' ); } // limit to the number of hours going from now back @@ -151,15 +151,26 @@ class ApiFeedWatchlist extends ApiBase { $msg = wfMessage( 'watchlist' )->inContentLanguage()->escaped(); $feed = new $feedClasses[$feedFormat] ( $feedTitle, $msg, $feedUrl ); - if ( $e instanceof UsageException ) { - $errorCode = $e->getCodeString(); + if ( $e instanceof ApiUsageException ) { + foreach ( $e->getStatusValue()->getErrors() as $error ) { + $msg = ApiMessage::create( $error ) + ->inLanguage( $this->getLanguage() ); + $errorTitle = $this->msg( 'api-feed-error-title', $msg->getApiCode() ); + $errorText = $msg->text(); + $feedItems[] = new FeedItem( $errorTitle, $errorText, '', '', '' ); + } } else { - // Something is seriously wrong - $errorCode = 'internal_api_error'; + if ( $e instanceof UsageException ) { + $errorCode = $e->getCodeString(); + } else { + // Something is seriously wrong + $errorCode = 'internal_api_error'; + } + $errorTitle = $this->msg( 'api-feed-error-title', $msg->getApiCode() ); + $errorText = $e->getMessage(); + $feedItems[] = new FeedItem( $errorTitle, $errorText, '', '', '' ); } - $errorText = $e->getMessage(); - $feedItems[] = new FeedItem( "Error ($errorCode)", $errorText, '', '', '' ); ApiFormatFeedWrapper::setResult( $this->getResult(), $feed, $feedItems ); } } diff --git a/includes/api/ApiFileRevert.php b/includes/api/ApiFileRevert.php index 97464d61db73..736898edcfa7 100644 --- a/includes/api/ApiFileRevert.php +++ b/includes/api/ApiFileRevert.php @@ -45,7 +45,7 @@ class ApiFileRevert extends ApiBase { $this->validateParameters(); // Check whether we're allowed to revert this file - $this->checkPermissions( $this->getUser() ); + $this->checkTitleUserPermissions( $this->file->getTitle(), [ 'edit', 'upload' ] ); $sourceUrl = $this->file->getArchiveVirtualUrl( $this->archiveName ); $status = $this->file->upload( @@ -71,23 +71,6 @@ class ApiFileRevert extends ApiBase { } /** - * Checks that the user has permissions to perform this revert. - * Dies with usage message on inadequate permissions. - * @param User $user The user to check. - */ - protected function checkPermissions( $user ) { - $title = $this->file->getTitle(); - $permissionErrors = array_merge( - $title->getUserPermissionsErrors( 'edit', $user ), - $title->getUserPermissionsErrors( 'upload', $user ) - ); - - if ( $permissionErrors ) { - $this->dieUsageMsg( $permissionErrors[0] ); - } - } - - /** * Validate the user parameters and set $this->archiveName and $this->file. * Throws an error if validation fails */ @@ -95,21 +78,23 @@ class ApiFileRevert extends ApiBase { // Validate the input title $title = Title::makeTitleSafe( NS_FILE, $this->params['filename'] ); if ( is_null( $title ) ) { - $this->dieUsageMsg( [ 'invalidtitle', $this->params['filename'] ] ); + $this->dieWithError( + [ 'apierror-invalidtitle', wfEscapeWikiText( $this->params['filename'] ) ] + ); } $localRepo = RepoGroup::singleton()->getLocalRepo(); // Check if the file really exists $this->file = $localRepo->newFile( $title ); if ( !$this->file->exists() ) { - $this->dieUsageMsg( 'notanarticle' ); + $this->dieWithError( 'apierror-missingtitle' ); } // Check if the archivename is valid for this file $this->archiveName = $this->params['archivename']; $oldFile = $localRepo->newFromArchiveName( $title, $this->archiveName ); if ( !$oldFile->exists() ) { - $this->dieUsageMsg( 'filerevert-badversion' ); + $this->dieWithError( 'filerevert-badversion' ); } } diff --git a/includes/api/ApiFormatJson.php b/includes/api/ApiFormatJson.php index 2e917e1a4fc5..8ebfe48cf88a 100644 --- a/includes/api/ApiFormatJson.php +++ b/includes/api/ApiFormatJson.php @@ -84,8 +84,8 @@ class ApiFormatJson extends ApiFormatBase { break; default: - $this->dieUsage( __METHOD__ . - ': Unknown value for \'formatversion\'', 'unknownformatversion' ); + // Should have been caught during parameter validation + $this->dieDebug( __METHOD__, 'Unknown value for \'formatversion\'' ); } } $data = $this->getResult()->getResultData( null, $transform ); diff --git a/includes/api/ApiFormatPhp.php b/includes/api/ApiFormatPhp.php index fc25f4772390..a744f57becf1 100644 --- a/includes/api/ApiFormatPhp.php +++ b/includes/api/ApiFormatPhp.php @@ -55,7 +55,8 @@ class ApiFormatPhp extends ApiFormatBase { break; default: - $this->dieUsage( __METHOD__ . ': Unknown value for \'formatversion\'', 'unknownformatversion' ); + // Should have been caught during parameter validation + $this->dieDebug( __METHOD__, 'Unknown value for \'formatversion\'' ); } $text = serialize( $this->getResult()->getResultData( null, $transforms ) ); @@ -67,11 +68,7 @@ class ApiFormatPhp extends ApiFormatBase { in_array( 'wfOutputHandler', ob_list_handlers(), true ) && preg_match( '/\<\s*cross-domain-policy(?=\s|\>)/i', $text ) ) { - $this->dieUsage( - 'This response cannot be represented using format=php. ' . - 'See https://phabricator.wikimedia.org/T68776', - 'internalerror' - ); + $this->dieWithError( 'apierror-formatphp', 'internalerror' ); } $this->printText( $text ); diff --git a/includes/api/ApiFormatRaw.php b/includes/api/ApiFormatRaw.php index 9da040ca0b20..228b47ea651f 100644 --- a/includes/api/ApiFormatRaw.php +++ b/includes/api/ApiFormatRaw.php @@ -49,7 +49,7 @@ class ApiFormatRaw extends ApiFormatBase { public function getMimeType() { $data = $this->getResult()->getResultData(); - if ( isset( $data['error'] ) ) { + if ( isset( $data['error'] ) || isset( $data['errors'] ) ) { return $this->errorFallback->getMimeType(); } @@ -62,7 +62,7 @@ class ApiFormatRaw extends ApiFormatBase { public function initPrinter( $unused = false ) { $data = $this->getResult()->getResultData(); - if ( isset( $data['error'] ) ) { + if ( isset( $data['error'] ) || isset( $data['errors'] ) ) { $this->errorFallback->initPrinter( $unused ); if ( $this->mFailWithHTTPError ) { $this->getMain()->getRequest()->response()->statusHeader( 400 ); @@ -74,7 +74,7 @@ class ApiFormatRaw extends ApiFormatBase { public function closePrinter() { $data = $this->getResult()->getResultData(); - if ( isset( $data['error'] ) ) { + if ( isset( $data['error'] ) || isset( $data['errors'] ) ) { $this->errorFallback->closePrinter(); } else { parent::closePrinter(); @@ -83,7 +83,7 @@ class ApiFormatRaw extends ApiFormatBase { public function execute() { $data = $this->getResult()->getResultData(); - if ( isset( $data['error'] ) ) { + if ( isset( $data['error'] ) || isset( $data['errors'] ) ) { $this->errorFallback->execute(); return; } diff --git a/includes/api/ApiFormatXml.php b/includes/api/ApiFormatXml.php index a45dbebfb51f..e4dfda0f572c 100644 --- a/includes/api/ApiFormatXml.php +++ b/includes/api/ApiFormatXml.php @@ -269,17 +269,17 @@ class ApiFormatXml extends ApiFormatBase { protected function addXslt() { $nt = Title::newFromText( $this->mXslt ); if ( is_null( $nt ) || !$nt->exists() ) { - $this->setWarning( 'Invalid or non-existent stylesheet specified' ); + $this->addWarning( 'apiwarn-invalidxmlstylesheet' ); return; } if ( $nt->getNamespace() != NS_MEDIAWIKI ) { - $this->setWarning( 'Stylesheet should be in the MediaWiki namespace.' ); + $this->addWarning( 'apiwarn-invalidxmlstylesheetns' ); return; } if ( substr( $nt->getText(), -4 ) !== '.xsl' ) { - $this->setWarning( 'Stylesheet should have .xsl extension.' ); + $this->addWarning( 'apiwarn-invalidxmlstylesheetext' ); return; } diff --git a/includes/api/ApiImageRotate.php b/includes/api/ApiImageRotate.php index 37cb80a9fc29..72fb16d19ba4 100644 --- a/includes/api/ApiImageRotate.php +++ b/includes/api/ApiImageRotate.php @@ -56,23 +56,29 @@ class ApiImageRotate extends ApiBase { $file = wfFindFile( $title, [ 'latest' => true ] ); if ( !$file ) { $r['result'] = 'Failure'; - $r['errormessage'] = 'File does not exist'; + $r['errors'] = $this->getErrorFormatter()->arrayFromStatus( + Status::newFatal( 'apierror-filedoesnotexist' ) + ); $result[] = $r; continue; } $handler = $file->getHandler(); if ( !$handler || !$handler->canRotate() ) { $r['result'] = 'Failure'; - $r['errormessage'] = 'File type cannot be rotated'; + $r['errors'] = $this->getErrorFormatter()->arrayFromStatus( + Status::newFatal( 'apierror-filetypecannotberotated' ) + ); $result[] = $r; continue; } // Check whether we're allowed to rotate this file - $permError = $this->checkPermissions( $this->getUser(), $file->getTitle() ); - if ( $permError !== null ) { + $permError = $this->checkTitleUserPermissions( $file->getTitle(), [ 'edit', 'upload' ] ); + if ( $permError ) { $r['result'] = 'Failure'; - $r['errormessage'] = $permError; + $r['errors'] = $this->getErrorFormatter()->arrayFromStatus( + $this->errorArrayToStatus( $permError ) + ); $result[] = $r; continue; } @@ -80,7 +86,9 @@ class ApiImageRotate extends ApiBase { $srcPath = $file->getLocalRefPath(); if ( $srcPath === false ) { $r['result'] = 'Failure'; - $r['errormessage'] = 'Cannot get local file path'; + $r['errors'] = $this->getErrorFormatter()->arrayFromStatus( + Status::newFatal( 'apierror-filenopath' ) + ); $result[] = $r; continue; } @@ -102,11 +110,13 @@ class ApiImageRotate extends ApiBase { $r['result'] = 'Success'; } else { $r['result'] = 'Failure'; - $r['errormessage'] = $this->getErrorFormatter()->arrayFromStatus( $status ); + $r['errors'] = $this->getErrorFormatter()->arrayFromStatus( $status ); } } else { $r['result'] = 'Failure'; - $r['errormessage'] = $err->toText(); + $r['errors'] = $this->getErrorFormatter()->arrayFromStatus( + Status::newFatal( ApiMessage::create( $err->getMsg() ) ) + ); } $result[] = $r; } @@ -130,28 +140,6 @@ class ApiImageRotate extends ApiBase { return $this->mPageSet; } - /** - * Checks that the user has permissions to perform rotations. - * @param User $user The user to check - * @param Title $title - * @return string|null Permission error message, or null if there is no error - */ - protected function checkPermissions( $user, $title ) { - $permissionErrors = array_merge( - $title->getUserPermissionsErrors( 'edit', $user ), - $title->getUserPermissionsErrors( 'upload', $user ) - ); - - if ( $permissionErrors ) { - // Just return the first error - $msg = $this->parseMsg( $permissionErrors[0] ); - - return $msg['info']; - } - - return null; - } - public function mustBePosted() { return true; } diff --git a/includes/api/ApiImport.php b/includes/api/ApiImport.php index 10106ff02b4e..3f48f38a0ef1 100644 --- a/includes/api/ApiImport.php +++ b/includes/api/ApiImport.php @@ -42,10 +42,10 @@ class ApiImport extends ApiBase { $isUpload = false; if ( isset( $params['interwikisource'] ) ) { if ( !$user->isAllowed( 'import' ) ) { - $this->dieUsageMsg( 'cantimport' ); + $this->dieWithError( 'apierror-cantimport' ); } if ( !isset( $params['interwikipage'] ) ) { - $this->dieUsageMsg( [ 'missingparam', 'interwikipage' ] ); + $this->dieWithError( [ 'apierror-missingparam', 'interwikipage' ] ); } $source = ImportStreamSource::newFromInterwiki( $params['interwikisource'], @@ -56,7 +56,7 @@ class ApiImport extends ApiBase { } else { $isUpload = true; if ( !$user->isAllowed( 'importupload' ) ) { - $this->dieUsageMsg( 'cantimport-upload' ); + $this->dieWithError( 'apierror-cantimport-upload' ); } $source = ImportStreamSource::newFromUpload( 'xml' ); } @@ -83,7 +83,7 @@ class ApiImport extends ApiBase { try { $importer->doImport(); } catch ( Exception $e ) { - $this->dieUsageMsg( [ 'import-unknownerror', $e->getMessage() ] ); + $this->dieWithError( [ 'apierror-import-unknownerror', wfEscapeWikiText( $e->getMessage() ) ] ); } $resultData = $reporter->getData(); diff --git a/includes/api/ApiLinkAccount.php b/includes/api/ApiLinkAccount.php index 1017607ce6a2..9a21e7620cf5 100644 --- a/includes/api/ApiLinkAccount.php +++ b/includes/api/ApiLinkAccount.php @@ -49,7 +49,7 @@ class ApiLinkAccount extends ApiBase { public function execute() { if ( !$this->getUser()->isLoggedIn() ) { - $this->dieUsage( 'Must be logged in to link accounts', 'notloggedin' ); + $this->dieWithError( 'apierror-mustbeloggedin-linkaccounts', 'notloggedin' ); } $params = $this->extractRequestParams(); @@ -60,8 +60,8 @@ class ApiLinkAccount extends ApiBase { $bits = wfParseUrl( $params['returnurl'] ); if ( !$bits || $bits['scheme'] === '' ) { $encParamName = $this->encodeParamName( 'returnurl' ); - $this->dieUsage( - "Invalid value '{$params['returnurl']}' for url parameter $encParamName", + $this->dieWithError( + [ 'apierror-badurl', $encParamName, wfEscapeWikiText( $params['returnurl'] ) ], "badurl_{$encParamName}" ); } diff --git a/includes/api/ApiLogin.php b/includes/api/ApiLogin.php index 6ac261dd3a00..723dc33c78a3 100644 --- a/includes/api/ApiLogin.php +++ b/includes/api/ApiLogin.php @@ -72,10 +72,11 @@ class ApiLogin extends ApiBase { try { $this->requirePostedParameters( [ 'password', 'token' ] ); - } catch ( UsageException $ex ) { + } catch ( ApiUsageException $ex ) { // Make this a warning for now, upgrade to an error in 1.29. - $this->setWarning( $ex->getMessage() ); - $this->logFeatureUsage( 'login-params-in-query-string' ); + foreach ( $ex->getStatusValue()->getErrors() as $error ) { + $this->addDeprecation( $error, 'login-params-in-query-string' ); + } } $params = $this->extractRequestParams(); @@ -146,15 +147,10 @@ class ApiLogin extends ApiBase { switch ( $res->status ) { case AuthenticationResponse::PASS: if ( $this->getConfig()->get( 'EnableBotPasswords' ) ) { - $warn = 'Main-account login via action=login is deprecated and may stop working ' . - 'without warning.'; - $warn .= ' To continue login with action=login, see [[Special:BotPasswords]].'; - $warn .= ' To safely continue using main-account login, see action=clientlogin.'; + $this->addDeprecation( 'apiwarn-deprecation-login-botpw', 'main-account-login' ); } else { - $warn = 'Login via action=login is deprecated and may stop working without warning.'; - $warn .= ' To safely log in, see action=clientlogin.'; + $this->addDeprecation( 'apiwarn-deprecation-login-nobotpw', 'main-account-login' ); } - $this->setWarning( $warn ); $authRes = 'Success'; $loginType = 'AuthManager'; break; @@ -194,16 +190,16 @@ class ApiLogin extends ApiBase { case 'NeedToken': $result['token'] = $token->toString(); - $this->setWarning( 'Fetching a token via action=login is deprecated. ' . - 'Use action=query&meta=tokens&type=login instead.' ); - $this->logFeatureUsage( 'action=login&!lgtoken' ); + $this->addDeprecation( 'apiwarn-deprecation-login-token', 'action=login&!lgtoken' ); break; case 'WrongToken': break; case 'Failed': - $result['reason'] = $message->useDatabase( 'false' )->inLanguage( 'en' )->text(); + $result['reason'] = ApiErrorFormatter::stripMarkup( + $message->useDatabase( false )->inLanguage( 'en' )->text() + ); break; case 'Aborted': diff --git a/includes/api/ApiLogout.php b/includes/api/ApiLogout.php index 6a26e2e3502b..d5c28f1d6a05 100644 --- a/includes/api/ApiLogout.php +++ b/includes/api/ApiLogout.php @@ -45,9 +45,11 @@ class ApiLogout extends ApiBase { // Make sure it's possible to log out if ( !$session->canSetUser() ) { - $this->dieUsage( - 'Cannot log out when using ' . - $session->getProvider()->describe( Language::factory( 'en' ) ), + $this->dieWithError( + [ + 'cannotlogoutnow-text', + $session->getProvider()->describe( $this->getErrorFormatter()->getLanguage() ) + ], 'cannotlogout' ); } diff --git a/includes/api/ApiMain.php b/includes/api/ApiMain.php index 38299b471163..fe6ed417cafa 100644 --- a/includes/api/ApiMain.php +++ b/includes/api/ApiMain.php @@ -47,6 +47,11 @@ class ApiMain extends ApiBase { const API_DEFAULT_FORMAT = 'jsonfm'; /** + * When no uselang parameter is given, this language will be used + */ + const API_DEFAULT_USELANG = 'user'; + + /** * List of available modules: action name => module class */ private static $Modules = [ @@ -140,7 +145,7 @@ class ApiMain extends ApiBase { */ private $mPrinter; - private $mModuleMgr, $mResult, $mErrorFormatter; + private $mModuleMgr, $mResult, $mErrorFormatter = null; /** @var ApiContinuationManager|null */ private $mContinuationManager; private $mAction; @@ -229,7 +234,11 @@ class ApiMain extends ApiBase { } } - $uselang = $this->getParameter( 'uselang' ); + $this->mResult = new ApiResult( $this->getConfig()->get( 'APIMaxResultSize' ) ); + + // Setup uselang. This doesn't use $this->getParameter() + // because we're not ready to handle errors yet. + $uselang = $request->getVal( 'uselang', self::API_DEFAULT_USELANG ); if ( $uselang === 'user' ) { // Assume the parent context is going to return the user language // for uselang=user (see T85635). @@ -247,6 +256,29 @@ class ApiMain extends ApiBase { } } + // Set up the error formatter. This doesn't use $this->getParameter() + // because we're not ready to handle errors yet. + $errorFormat = $request->getVal( 'errorformat', 'bc' ); + $errorLangCode = $request->getVal( 'errorlang', 'uselang' ); + $errorsUseDB = $request->getCheck( 'errorsuselocal' ); + if ( in_array( $errorFormat, [ 'plaintext', 'wikitext', 'html', 'raw', 'none' ], true ) ) { + if ( $errorLangCode === 'uselang' ) { + $errorLang = $this->getLanguage(); + } elseif ( $errorLangCode === 'content' ) { + global $wgContLang; + $errorLang = $wgContLang; + } else { + $errorLangCode = RequestContext::sanitizeLangCode( $errorLangCode ); + $errorLang = Language::factory( $errorLangCode ); + } + $this->mErrorFormatter = new ApiErrorFormatter( + $this->mResult, $errorLang, $errorFormat, $errorsUseDB + ); + } else { + $this->mErrorFormatter = new ApiErrorFormatter_BackCompat( $this->mResult ); + } + $this->mResult->setErrorFormatter( $this->getErrorFormatter() ); + $this->mModuleMgr = new ApiModuleManager( $this ); $this->mModuleMgr->addModules( self::$Modules, 'action' ); $this->mModuleMgr->addModules( $config->get( 'APIModules' ), 'action' ); @@ -255,9 +287,6 @@ class ApiMain extends ApiBase { Hooks::run( 'ApiMain::moduleManager', [ $this->mModuleMgr ] ); - $this->mResult = new ApiResult( $this->getConfig()->get( 'APIMaxResultSize' ) ); - $this->mErrorFormatter = new ApiErrorFormatter_BackCompat( $this->mResult ); - $this->mResult->setErrorFormatter( $this->mErrorFormatter ); $this->mContinuationManager = null; $this->mEnableWrite = $enableWrite; @@ -464,7 +493,9 @@ class ApiMain extends ApiBase { public function createPrinterByName( $format ) { $printer = $this->mModuleMgr->getModule( $format, 'format' ); if ( $printer === null ) { - $this->dieUsage( "Unrecognized format: {$format}", 'unknown_format' ); + $this->dieWithError( + [ 'apierror-unknownformat', wfEscapeWikiText( $format ) ], 'unknown_format' + ); } return $printer; @@ -542,7 +573,7 @@ class ApiMain extends ApiBase { */ protected function handleException( Exception $e ) { // Bug 63145: Rollback any open database transactions - if ( !( $e instanceof UsageException ) ) { + if ( !( $e instanceof ApiUsageException || $e instanceof UsageException ) ) { // UsageExceptions are intentional, so don't rollback if that's the case try { MWExceptionHandler::rollbackMasterChangesAndLog( $e ); @@ -557,7 +588,7 @@ class ApiMain extends ApiBase { Hooks::run( 'ApiMain::onException', [ $this, $e ] ); // Log it - if ( !( $e instanceof UsageException ) ) { + if ( !( $e instanceof ApiUsageException || $e instanceof UsageException ) ) { MWExceptionHandler::logException( $e ); } @@ -565,13 +596,13 @@ class ApiMain extends ApiBase { // If this fails, an unhandled exception should be thrown so that global error // handler will process and log it. - $errCode = $this->substituteResultWithError( $e ); + $errCodes = $this->substituteResultWithError( $e ); // Error results should not be cached $this->setCacheMode( 'private' ); $response = $this->getRequest()->response(); - $headerStr = 'MediaWiki-API-Error: ' . $errCode; + $headerStr = 'MediaWiki-API-Error: ' . join( ', ', $errCodes ); $response->header( $headerStr ); // Reset and print just the error message @@ -580,14 +611,31 @@ class ApiMain extends ApiBase { // Printer may not be initialized if the extractRequestParams() fails for the main module $this->createErrorPrinter(); + $failed = false; try { $this->printResult( $e->getCode() ); + } catch ( ApiUsageException $ex ) { + // The error printer itself is failing. Try suppressing its request + // parameters and redo. + $failed = true; + $this->addWarning( 'apiwarn-errorprinterfailed' ); + foreach ( $ex->getStatusValue()->getErrors() as $error ) { + try { + $this->mPrinter->addWarning( $error ); + } catch ( Exception $ex2 ) { + // WTF? + $this->addWarning( $error ); + } + } } catch ( UsageException $ex ) { // The error printer itself is failing. Try suppressing its request // parameters and redo. - $this->setWarning( - 'Error printer failed (will retry without params): ' . $ex->getMessage() + $failed = true; + $this->addWarning( + [ 'apiwarn-errorprinterfailed-ex', $ex->getMessage() ], 'errorprinterfailed' ); + } + if ( $failed ) { $this->mPrinter = null; $this->createErrorPrinter(); $this->mPrinter->forceDefaultParams(); @@ -958,99 +1006,145 @@ class ApiMain extends ApiBase { /** * Create an error message for the given exception. * - * If the exception is a UsageException then - * UsageException::getMessageArray() will be called to create the message. + * If an ApiUsageException, errors/warnings will be extracted from the + * embedded StatusValue. + * + * If a base UsageException, the getMessageArray() method will be used to + * extract the code and English message for a single error (no warnings). + * + * Any other exception will be returned with a generic code and wrapper + * text around the exception's (presumably English) message as a single + * error (no warnings). * * @param Exception $e - * @return array ['code' => 'some string', 'info' => 'some other string'] + * @param string $type 'error' or 'warning' + * @return ApiMessage[] * @since 1.27 */ - protected function errorMessageFromException( $e ) { - if ( $e instanceof UsageException ) { + protected function errorMessagesFromException( $e, $type = 'error' ) { + $messages = []; + if ( $e instanceof ApiUsageException ) { + foreach ( $e->getStatusValue()->getErrorsByType( $type ) as $error ) { + $messages[] = ApiMessage::create( $error ); + } + } elseif ( $type !== 'error' ) { + // None of the rest have any messages for non-error types + } elseif ( $e instanceof UsageException ) { // User entered incorrect parameters - generate error response - $errMessage = $e->getMessageArray(); + $data = $e->getMessageArray(); + $code = $data['code']; + $info = $data['info']; + unset( $data['code'], $data['info'] ); + $messages[] = new ApiRawMessage( [ '$1', $info ], $code, $data ); } else { - $config = $this->getConfig(); // Something is seriously wrong + $config = $this->getConfig(); + $code = 'internal_api_error_' . get_class( $e ); if ( ( $e instanceof DBQueryError ) && !$config->get( 'ShowSQLErrors' ) ) { - $info = 'Database query error'; + $params = [ 'apierror-databaseerror', WebRequest::getRequestId() ]; } else { - $info = "Exception Caught: {$e->getMessage()}"; + $params = [ + 'apierror-exceptioncaught', + WebRequest::getRequestId(), + wfEscapeWikiText( $e->getMessage() ) + ]; } - - $errMessage = [ - 'code' => 'internal_api_error_' . get_class( $e ), - 'info' => '[' . WebRequest::getRequestId() . '] ' . $info, - ]; + $messages[] = ApiMessage::create( $params, $code ); } - return $errMessage; + return $messages; } /** * Replace the result data with the information about an exception. - * Returns the error code * @param Exception $e - * @return string + * @return string[] Error codes */ protected function substituteResultWithError( $e ) { $result = $this->getResult(); + $formatter = $this->getErrorFormatter(); $config = $this->getConfig(); + $errorCodes = []; - $errMessage = $this->errorMessageFromException( $e ); - if ( $e instanceof UsageException ) { - // User entered incorrect parameters - generate error response + // Remember existing warnings and errors across the reset + $errors = $result->getResultData( [ 'errors' ] ); + $warnings = $result->getResultData( [ 'warnings' ] ); + $result->reset(); + if ( $warnings !== null ) { + $result->addValue( null, 'warnings', $warnings, ApiResult::NO_SIZE_CHECK ); + } + if ( $errors !== null ) { + $result->addValue( null, 'errors', $errors, ApiResult::NO_SIZE_CHECK ); + + // Collect the copied error codes for the return value + foreach ( $errors as $error ) { + if ( isset( $error['code'] ) ) { + $errorCodes[$error['code']] = true; + } + } + } + + // Add errors from the exception + $modulePath = $e instanceof ApiUsageException ? $e->getModulePath() : null; + foreach ( $this->errorMessagesFromException( $e, 'error' ) as $msg ) { + $errorCodes[$msg->getApiCode()] = true; + $formatter->addError( $modulePath, $msg ); + } + foreach ( $this->errorMessagesFromException( $e, 'warning' ) as $msg ) { + $formatter->addWarning( $modulePath, $msg ); + } + + // Add additional data. Path depends on whether we're in BC mode or not. + // Data depends on the type of exception. + if ( $formatter instanceof ApiErrorFormatter_BackCompat ) { + $path = [ 'error' ]; + } else { + $path = null; + } + if ( $e instanceof ApiUsageException || $e instanceof UsageException ) { $link = wfExpandUrl( wfScript( 'api' ) ); - ApiResult::setContentValue( $errMessage, 'docref', "See $link for API usage" ); + $result->addContentValue( + $path, + 'docref', + $this->msg( 'api-usage-docref', $link )->inLanguage( $formatter->getLanguage() )->text() + ); } else { - // Something is seriously wrong if ( $config->get( 'ShowExceptionDetails' ) ) { - ApiResult::setContentValue( - $errMessage, + $result->addContentValue( + $path, 'trace', - MWExceptionHandler::getRedactedTraceAsString( $e ) + $this->msg( 'api-exception-trace', + get_class( $e ), + $e->getFile(), + $e->getLine(), + MWExceptionHandler::getRedactedTraceAsString( $e ) + )->inLanguage( $formatter->getLanguage() )->text() ); } } - // Remember all the warnings to re-add them later - $warnings = $result->getResultData( [ 'warnings' ] ); + // Add the id and such + $this->addRequestedFields( [ 'servedby' ] ); - $result->reset(); - // Re-add the id - $requestid = $this->getParameter( 'requestid' ); - if ( !is_null( $requestid ) ) { - $result->addValue( null, 'requestid', $requestid, ApiResult::NO_SIZE_CHECK ); - } - if ( $config->get( 'ShowHostnames' ) ) { - // servedby is especially useful when debugging errors - $result->addValue( null, 'servedby', wfHostname(), ApiResult::NO_SIZE_CHECK ); - } - if ( $warnings !== null ) { - $result->addValue( null, 'warnings', $warnings, ApiResult::NO_SIZE_CHECK ); - } - - $result->addValue( null, 'error', $errMessage, ApiResult::NO_SIZE_CHECK ); - - return $errMessage['code']; + return array_keys( $errorCodes ); } /** - * Set up for the execution. - * @return array + * Add requested fields to the result + * @param string[] $force Which fields to force even if not requested. Accepted values are: + * - servedby */ - protected function setupExecuteAction() { - // First add the id to the top element + protected function addRequestedFields( $force = [] ) { $result = $this->getResult(); + $requestid = $this->getParameter( 'requestid' ); - if ( !is_null( $requestid ) ) { - $result->addValue( null, 'requestid', $requestid ); + if ( $requestid !== null ) { + $result->addValue( null, 'requestid', $requestid, ApiResult::NO_SIZE_CHECK ); } - if ( $this->getConfig()->get( 'ShowHostnames' ) ) { - $servedby = $this->getParameter( 'servedby' ); - if ( $servedby ) { - $result->addValue( null, 'servedby', wfHostname() ); - } + if ( $this->getConfig()->get( 'ShowHostnames' ) && ( + in_array( 'servedby', $force, true ) || $this->getParameter( 'servedby' ) + ) ) { + $result->addValue( null, 'servedby', wfHostname(), ApiResult::NO_SIZE_CHECK ); } if ( $this->getParameter( 'curtimestamp' ) ) { @@ -1058,13 +1152,23 @@ class ApiMain extends ApiBase { ApiResult::NO_SIZE_CHECK ); } - $params = $this->extractRequestParams(); + if ( $this->getParameter( 'responselanginfo' ) ) { + $result->addValue( null, 'uselang', $this->getLanguage()->getCode(), + ApiResult::NO_SIZE_CHECK ); + $result->addValue( null, 'errorlang', $this->getErrorFormatter()->getLanguage()->getCode(), + ApiResult::NO_SIZE_CHECK ); + } + } - $this->mAction = $params['action']; + /** + * Set up for the execution. + * @return array + */ + protected function setupExecuteAction() { + $this->addRequestedFields(); - if ( !is_string( $this->mAction ) ) { - $this->dieUsage( 'The API requires a valid action parameter', 'unknown_action' ); - } + $params = $this->extractRequestParams(); + $this->mAction = $params['action']; return $params; } @@ -1073,13 +1177,15 @@ class ApiMain extends ApiBase { * Set up the module for response * @return ApiBase The module that will handle this action * @throws MWException - * @throws UsageException + * @throws ApiUsageException */ protected function setupModule() { // Instantiate the module requested by the user $module = $this->mModuleMgr->getModule( $this->mAction, 'action' ); if ( $module === null ) { - $this->dieUsage( 'The API requires a valid action parameter', 'unknown_action' ); + $this->dieWithError( + [ 'apierror-unknownaction', wfEscapeWikiText( $this->mAction ) ], 'unknown_action' + ); } $moduleParams = $module->extractRequestParams(); @@ -1098,13 +1204,13 @@ class ApiMain extends ApiBase { } if ( !isset( $moduleParams['token'] ) ) { - $this->dieUsageMsg( [ 'missingparam', 'token' ] ); + $module->dieWithError( [ 'apierror-missingparam', 'token' ] ); } $module->requirePostedParameters( [ 'token' ] ); if ( !$module->validateToken( $moduleParams['token'], $moduleParams ) ) { - $this->dieUsageMsg( 'sessionfailure' ); + $module->dieWithError( 'apierror-badtoken' ); } } @@ -1128,10 +1234,10 @@ class ApiMain extends ApiBase { $response->header( 'X-Database-Lag: ' . intval( $lag ) ); if ( $this->getConfig()->get( 'ShowHostnames' ) ) { - $this->dieUsage( "Waiting for $host: $lag seconds lagged", 'maxlag' ); + $this->dieWithError( [ 'apierror-maxlag', $lag, $host ] ); } - $this->dieUsage( "Waiting for a database server: $lag seconds lagged", 'maxlag' ); + $this->dieWithError( [ 'apierror-maxlag-generic', $lag ], 'maxlag' ); } } @@ -1262,19 +1368,16 @@ class ApiMain extends ApiBase { if ( $module->isReadMode() && !User::isEveryoneAllowed( 'read' ) && !$user->isAllowed( 'read' ) ) { - $this->dieUsageMsg( 'readrequired' ); + $this->dieWithError( 'apierror-readapidenied' ); } if ( $module->isWriteMode() ) { if ( !$this->mEnableWrite ) { - $this->dieUsageMsg( 'writedisabled' ); + $this->dieWithError( 'apierror-noapiwrite' ); } elseif ( !$user->isAllowed( 'writeapi' ) ) { - $this->dieUsageMsg( 'writerequired' ); + $this->dieWithError( 'apierror-writeapidenied' ); } elseif ( $this->getRequest()->getHeader( 'Promise-Non-Write-API-Action' ) ) { - $this->dieUsage( - 'Promise-Non-Write-API-Action HTTP header cannot be sent to write API modules', - 'promised-nonwrite-api' - ); + $this->dieWithError( 'apierror-promised-nonwrite-api' ); } $this->checkReadOnly( $module ); @@ -1283,7 +1386,7 @@ class ApiMain extends ApiBase { // Allow extensions to stop execution for arbitrary reasons. $message = false; if ( !Hooks::run( 'ApiCheckCanExecute', [ $module, $user, &$message ] ) ) { - $this->dieUsageMsg( $message ); + $this->dieWithError( $message ); } } @@ -1329,12 +1432,9 @@ class ApiMain extends ApiBase { "Api request failed as read only because the following DBs are lagged: $laggedServers" ); - $parsed = $this->parseMsg( [ 'readonlytext' ] ); - $this->dieUsage( - $parsed['info'], - $parsed['code'], - /* http error */ - 0, + $this->dieWithError( + 'readonly_lag', + 'readonly', [ 'readonlyreason' => "Waiting for $numLagged lagged database(s)" ] ); } @@ -1350,12 +1450,12 @@ class ApiMain extends ApiBase { switch ( $params['assert'] ) { case 'user': if ( $user->isAnon() ) { - $this->dieUsage( 'Assertion that the user is logged in failed', 'assertuserfailed' ); + $this->dieWithError( 'apierror-assertuserfailed' ); } break; case 'bot': if ( !$user->isAllowed( 'bot' ) ) { - $this->dieUsage( 'Assertion that the user has the bot right failed', 'assertbotfailed' ); + $this->dieWithError( 'apierror-assertbotfailed' ); } break; } @@ -1363,9 +1463,8 @@ class ApiMain extends ApiBase { if ( isset( $params['assertuser'] ) ) { $assertUser = User::newFromName( $params['assertuser'], false ); if ( !$assertUser || !$this->getUser()->equals( $assertUser ) ) { - $this->dieUsage( - 'Assertion that the user is "' . $params['assertuser'] . '" failed', - 'assertnameduserfailed' + $this->dieWithError( + [ 'apierror-assertnameduserfailed', wfEscapeWikiText( $params['assertuser'] ) ] ); } } @@ -1381,7 +1480,7 @@ class ApiMain extends ApiBase { if ( !$request->wasPosted() && $module->mustBePosted() ) { // Module requires POST. GET request might still be allowed // if $wgDebugApi is true, otherwise fail. - $this->dieUsageMsgOrDebug( [ 'mustbeposted', $this->mAction ] ); + $this->dieWithErrorOrDebug( [ 'apierror-mustbeposted', $this->mAction ] ); } // See if custom printer is used @@ -1396,8 +1495,7 @@ class ApiMain extends ApiBase { ( $this->getUser()->isLoggedIn() && $this->getUser()->requiresHTTPS() ) ) ) { - $this->logFeatureUsage( 'https-expected' ); - $this->setWarning( 'HTTP used when HTTPS was expected' ); + $this->addDeprecation( 'apiwarn-deprecation-httpsexpected', 'https-expected' ); } } @@ -1481,7 +1579,9 @@ class ApiMain extends ApiBase { ]; if ( $e ) { - $logCtx['errorCodes'][] = $this->errorMessageFromException( $e )['code']; + foreach ( $this->errorMessagesFromException( $e ) as $msg ) { + $logCtx['errorCodes'][] = $msg->getApiCode(); + } } // Construct space separated message for 'api' log channel @@ -1560,9 +1660,7 @@ class ApiMain extends ApiBase { if ( $this->getRequest()->getArray( $name ) !== null ) { // See bug 10262 for why we don't just implode( '|', ... ) the // array. - $this->setWarning( - "Parameter '$name' uses unsupported PHP array syntax" - ); + $this->addWarning( [ 'apiwarn-unsupportedarray', $name ] ); } $ret = $default; } @@ -1602,8 +1700,7 @@ class ApiMain extends ApiBase { if ( !$this->mInternalMode ) { // Printer has not yet executed; don't warn that its parameters are unused - $printerParams = array_map( - [ $this->mPrinter, 'encodeParamName' ], + $printerParams = $this->mPrinter->encodeParamName( array_keys( $this->mPrinter->getFinalParams() ?: [] ) ); $unusedParams = array_diff( $allParams, $paramsUsed, $printerParams ); @@ -1612,8 +1709,11 @@ class ApiMain extends ApiBase { } if ( count( $unusedParams ) ) { - $s = count( $unusedParams ) > 1 ? 's' : ''; - $this->setWarning( "Unrecognized parameter$s: '" . implode( $unusedParams, "', '" ) . "'" ); + $this->addWarning( [ + 'apierror-unrecognizedparams', + Message::listParam( array_map( 'wfEscapeWikiText', $unusedParams ), 'comma' ), + count( $unusedParams ) + ] ); } } @@ -1624,7 +1724,7 @@ class ApiMain extends ApiBase { */ protected function printResult( $httpCode = 0 ) { if ( $this->getConfig()->get( 'DebugAPI' ) !== false ) { - $this->setWarning( 'SECURITY WARNING: $wgDebugAPI is enabled' ); + $this->addWarning( 'apiwarn-wgDebugAPI' ); } $printer = $this->mPrinter; @@ -1678,9 +1778,20 @@ class ApiMain extends ApiBase { 'requestid' => null, 'servedby' => false, 'curtimestamp' => false, + 'responselanginfo' => false, 'origin' => null, 'uselang' => [ - ApiBase::PARAM_DFLT => 'user', + ApiBase::PARAM_DFLT => self::API_DEFAULT_USELANG, + ], + 'errorformat' => [ + ApiBase::PARAM_TYPE => [ 'plaintext', 'wikitext', 'html', 'raw', 'none', 'bc' ], + ApiBase::PARAM_DFLT => 'bc', + ], + 'errorlang' => [ + ApiBase::PARAM_DFLT => 'uselang', + ], + 'errorsuselocal' => [ + ApiBase::PARAM_DFLT => false, ], ]; } @@ -1732,7 +1843,7 @@ class ApiMain extends ApiBase { $help['permissions'] .= Html::rawElement( 'dd', null, $this->msg( 'api-help-permissions-granted-to' ) ->numParams( count( $groups ) ) - ->params( $this->getLanguage()->commaList( $groups ) ) + ->params( Message::listParam( $groups ) ) ->parse() ); } @@ -1832,70 +1943,6 @@ class ApiMain extends ApiBase { } /** - * This exception will be thrown when dieUsage is called to stop module execution. - * - * @ingroup API - */ -class UsageException extends MWException { - - private $mCodestr; - - /** - * @var null|array - */ - private $mExtraData; - - /** - * @param string $message - * @param string $codestr - * @param int $code - * @param array|null $extradata - */ - public function __construct( $message, $codestr, $code = 0, $extradata = null ) { - parent::__construct( $message, $code ); - $this->mCodestr = $codestr; - $this->mExtraData = $extradata; - - // This should never happen, so throw an exception about it that will - // hopefully get logged with a backtrace (T138585) - if ( !is_string( $codestr ) || $codestr === '' ) { - throw new InvalidArgumentException( 'Invalid $codestr, was ' . - ( $codestr === '' ? 'empty string' : gettype( $codestr ) ) - ); - } - } - - /** - * @return string - */ - public function getCodeString() { - return $this->mCodestr; - } - - /** - * @return array - */ - public function getMessageArray() { - $result = [ - 'code' => $this->mCodestr, - 'info' => $this->getMessage() - ]; - if ( is_array( $this->mExtraData ) ) { - $result = array_merge( $result, $this->mExtraData ); - } - - return $result; - } - - /** - * @return string - */ - public function __toString() { - return "{$this->getCodeString()}: {$this->getMessage()}"; - } -} - -/** * For really cool vim folding this needs to be at the end: * vim: foldmarker=@{,@} foldmethod=marker */ diff --git a/includes/api/ApiManageTags.php b/includes/api/ApiManageTags.php index 617db227df7a..3299f73b5b0e 100644 --- a/includes/api/ApiManageTags.php +++ b/includes/api/ApiManageTags.php @@ -32,11 +32,9 @@ class ApiManageTags extends ApiBase { if ( $params['operation'] !== 'delete' && !$this->getUser()->isAllowed( 'managechangetags' ) ) { - $this->dieUsage( "You don't have permission to manage change tags", - 'permissiondenied' ); + $this->dieWithError( 'tags-manage-no-permission', 'permissiondenied' ); } elseif ( !$this->getUser()->isAllowed( 'deletechangetags' ) ) { - $this->dieUsage( "You don't have permission to delete change tags", - 'permissiondenied' ); + $this->dieWithError( 'tags-delete-no-permission', 'permissiondenied' ); } $result = $this->getResult(); diff --git a/includes/api/ApiMergeHistory.php b/includes/api/ApiMergeHistory.php index 276f1c0ebeee..357698e13c40 100644 --- a/includes/api/ApiMergeHistory.php +++ b/includes/api/ApiMergeHistory.php @@ -42,24 +42,24 @@ class ApiMergeHistory extends ApiBase { if ( isset( $params['from'] ) ) { $fromTitle = Title::newFromText( $params['from'] ); if ( !$fromTitle || $fromTitle->isExternal() ) { - $this->dieUsageMsg( [ 'invalidtitle', $params['from'] ] ); + $this->dieWithError( [ 'apierror-invalidtitle', wfEscapeWikiText( $params['from'] ) ] ); } } elseif ( isset( $params['fromid'] ) ) { $fromTitle = Title::newFromID( $params['fromid'] ); if ( !$fromTitle ) { - $this->dieUsageMsg( [ 'nosuchpageid', $params['fromid'] ] ); + $this->dieWithError( [ 'apierror-nosuchpageid', $params['fromid'] ] ); } } if ( isset( $params['to'] ) ) { $toTitle = Title::newFromText( $params['to'] ); if ( !$toTitle || $toTitle->isExternal() ) { - $this->dieUsageMsg( [ 'invalidtitle', $params['to'] ] ); + $this->dieWithError( [ 'apierror-invalidtitle', wfEscapeWikiText( $params['to'] ) ] ); } } elseif ( isset( $params['toid'] ) ) { $toTitle = Title::newFromID( $params['toid'] ); if ( !$toTitle ) { - $this->dieUsageMsg( [ 'nosuchpageid', $params['toid'] ] ); + $this->dieWithError( [ 'apierror-nosuchpageid', $params['toid'] ] ); } } diff --git a/includes/api/ApiMessage.php b/includes/api/ApiMessage.php index ae6677864157..9d69a771d423 100644 --- a/includes/api/ApiMessage.php +++ b/includes/api/ApiMessage.php @@ -36,9 +36,10 @@ interface IApiMessage extends MessageSpecifier { /** * Returns a machine-readable code for use by the API * - * The message key is often sufficient, but sometimes there are multiple - * messages used for what is really the same underlying condition (e.g. - * badaccess-groups and badaccess-group0) + * If no code was specifically set, the message key is used as the code + * after removing "apiwarn-" or "apierror-" prefixes and applying + * backwards-compatibility mappings. + * * @return string */ public function getApiCode(); @@ -51,7 +52,7 @@ interface IApiMessage extends MessageSpecifier { /** * Sets the machine-readable code for use by the API - * @param string|null $code If null, the message key should be returned by self::getApiCode() + * @param string|null $code If null, uses the default (see self::getApiCode()) * @param array|null $data If non-null, passed to self::setApiData() */ public function setApiCode( $code, array $data = null ); @@ -69,14 +70,95 @@ interface IApiMessage extends MessageSpecifier { * @ingroup API */ trait ApiMessageTrait { + + /** + * Compatibility code mappings for various MW messages. + * @todo Ideally anything relying on this should be changed to use ApiMessage. + */ + protected static $messageMap = [ + 'actionthrottledtext' => 'ratelimited', + 'autoblockedtext' => 'autoblocked', + 'badaccess-group0' => 'permissiondenied', + 'badaccess-groups' => 'permissiondenied', + 'badipaddress' => 'invalidip', + 'blankpage' => 'emptypage', + 'blockedtext' => 'blocked', + 'cannotdelete' => 'cantdelete', + 'cannotundelete' => 'cantundelete', + 'cantmove-titleprotected' => 'protectedtitle', + 'cantrollback' => 'onlyauthor', + 'confirmedittext' => 'confirmemail', + 'content-not-allowed-here' => 'contentnotallowedhere', + 'deleteprotected' => 'cantedit', + 'delete-toobig' => 'bigdelete', + 'edit-conflict' => 'editconflict', + 'imagenocrossnamespace' => 'nonfilenamespace', + 'imagetypemismatch' => 'filetypemismatch', + 'importbadinterwiki' => 'badinterwiki', + 'importcantopen' => 'cantopenfile', + 'import-noarticle' => 'badinterwiki', + 'importnofile' => 'nofile', + 'importuploaderrorpartial' => 'partialupload', + 'importuploaderrorsize' => 'filetoobig', + 'importuploaderrortemp' => 'notempdir', + 'ipb_already_blocked' => 'alreadyblocked', + 'ipb_blocked_as_range' => 'blockedasrange', + 'ipb_cant_unblock' => 'cantunblock', + 'ipb_expiry_invalid' => 'invalidexpiry', + 'ip_range_invalid' => 'invalidrange', + 'mailnologin' => 'cantsend', + 'markedaspatrollederror-noautopatrol' => 'noautopatrol', + 'movenologintext' => 'cantmove-anon', + 'movenotallowed' => 'cantmove', + 'movenotallowedfile' => 'cantmovefile', + 'namespaceprotected' => 'protectednamespace', + 'nocreate-loggedin' => 'cantcreate', + 'nocreatetext' => 'cantcreate-anon', + 'noname' => 'invaliduser', + 'nosuchusershort' => 'nosuchuser', + 'notanarticle' => 'missingtitle', + 'nouserspecified' => 'invaliduser', + 'ns-specialprotected' => 'unsupportednamespace', + 'protect-cantedit' => 'cantedit', + 'protectedinterface' => 'protectednamespace-interface', + 'protectedpagetext' => 'protectedpage', + 'range_block_disabled' => 'rangedisabled', + 'rcpatroldisabled' => 'patroldisabled', + 'readonlytext' => 'readonly', + 'sessionfailure' => 'badtoken', + 'titleprotected' => 'protectedtitle', + 'undo-failure' => 'undofailure', + 'userrights-nodatabase' => 'nosuchdatabase', + 'userrights-no-interwiki' => 'nointerwikiuserrights', + ]; + protected $apiCode = null; protected $apiData = []; public function getApiCode() { - return $this->apiCode === null ? $this->getKey() : $this->apiCode; + if ( $this->apiCode === null ) { + $key = $this->getKey(); + if ( isset( self::$messageMap[$key] ) ) { + $this->apiCode = self::$messageMap[$key]; + } elseif ( $key === 'apierror-missingparam' ) { + /// @todo: Kill this case along with ApiBase::$messageMap + $this->apiCode = 'no' . $this->getParams()[0]; + } elseif ( substr( $key, 0, 8 ) === 'apiwarn-' ) { + $this->apiCode = substr( $key, 8 ); + } elseif ( substr( $key, 0, 9 ) === 'apierror-' ) { + $this->apiCode = substr( $key, 9 ); + } else { + $this->apiCode = $key; + } + } + return $this->apiCode; } public function setApiCode( $code, array $data = null ) { + if ( $code !== null && !( is_string( $code ) && $code !== '' ) ) { + throw new InvalidArgumentException( "Invalid code \"$code\"" ); + } + $this->apiCode = $code; if ( $data !== null ) { $this->setApiData( $data ); @@ -124,9 +206,25 @@ class ApiMessage extends Message implements IApiMessage { * @param Message|RawMessage|array|string $msg * @param string|null $code * @param array|null $data - * @return ApiMessage + * @return IApiMessage */ public static function create( $msg, $code = null, array $data = null ) { + if ( is_array( $msg ) ) { + // From StatusValue + if ( isset( $msg['message'] ) ) { + if ( isset( $msg['params'] ) ) { + $msg = array_merge( [ $msg['message'] ], $msg['params'] ); + } else { + $msg = [ $msg['message'] ]; + } + } + + // Weirdness that comes in sometimes, including the above + if ( $msg[0] instanceof MessageSpecifier ) { + $msg = $msg[0]; + } + } + if ( $msg instanceof IApiMessage ) { return $msg; } elseif ( $msg instanceof RawMessage ) { @@ -143,7 +241,6 @@ class ApiMessage extends Message implements IApiMessage { * - string: passed to Message::__construct * @param string|null $code * @param array|null $data - * @return ApiMessage */ public function __construct( $msg, $code = null, array $data = null ) { if ( $msg instanceof Message ) { @@ -158,8 +255,7 @@ class ApiMessage extends Message implements IApiMessage { } else { parent::__construct( $msg ); } - $this->apiCode = $code; - $this->apiData = (array)$data; + $this->setApiCode( $code, $data ); } } @@ -192,7 +288,6 @@ class ApiRawMessage extends RawMessage implements IApiMessage { } else { parent::__construct( $msg ); } - $this->apiCode = $code; - $this->apiData = (array)$data; + $this->setApiCode( $code, $data ); } } diff --git a/includes/api/ApiMove.php b/includes/api/ApiMove.php index 29e67b07cd57..7c8aa90ce9ce 100644 --- a/includes/api/ApiMove.php +++ b/includes/api/ApiMove.php @@ -41,23 +41,23 @@ class ApiMove extends ApiBase { if ( isset( $params['from'] ) ) { $fromTitle = Title::newFromText( $params['from'] ); if ( !$fromTitle || $fromTitle->isExternal() ) { - $this->dieUsageMsg( [ 'invalidtitle', $params['from'] ] ); + $this->dieWithError( [ 'apierror-invalidtitle', wfEscapeWikiText( $params['from'] ) ] ); } } elseif ( isset( $params['fromid'] ) ) { $fromTitle = Title::newFromID( $params['fromid'] ); if ( !$fromTitle ) { - $this->dieUsageMsg( [ 'nosuchpageid', $params['fromid'] ] ); + $this->dieWithError( [ 'apierror-nosuchpageid', $params['fromid'] ] ); } } if ( !$fromTitle->exists() ) { - $this->dieUsageMsg( 'notanarticle' ); + $this->dieWithError( 'apierror-missingtitle' ); } $fromTalk = $fromTitle->getTalkPage(); $toTitle = Title::newFromText( $params['to'] ); if ( !$toTitle || $toTitle->isExternal() ) { - $this->dieUsageMsg( [ 'invalidtitle', $params['to'] ] ); + $this->dieWithError( [ 'apierror-invalidtitle', wfEscapeWikiText( $params['to'] ) ] ); } $toTalk = $toTitle->getTalkPage(); @@ -66,15 +66,15 @@ class ApiMove extends ApiBase { && wfFindFile( $toTitle ) ) { if ( !$params['ignorewarnings'] && $user->isAllowed( 'reupload-shared' ) ) { - $this->dieUsageMsg( 'sharedfile-exists' ); + $this->dieWithError( 'apierror-fileexists-sharedrepo-perm' ); } elseif ( !$user->isAllowed( 'reupload-shared' ) ) { - $this->dieUsageMsg( 'cantoverwrite-sharedfile' ); + $this->dieWithError( 'apierror-cantoverwrite-sharedfile' ); } } // Rate limit if ( $user->pingLimiter( 'move' ) ) { - $this->dieUsageMsg( 'actionthrottledtext' ); + $this->dieWithError( 'apierror-ratelimited' ); } // Move the page @@ -108,10 +108,8 @@ class ApiMove extends ApiBase { $r['talkto'] = $toTalk->getPrefixedText(); $r['talkmoveoverredirect'] = $toTalkExists; } else { - // We're not gonna dieUsage() on failure, since we already changed something - $error = $this->getErrorFromStatus( $status ); - $r['talkmove-error-code'] = $error[0]; - $r['talkmove-error-info'] = $error[1]; + // We're not going to dieWithError() on failure, since we already changed something + $r['talkmove-errors'] = $this->getErrorFormatter()->arrayFromStatus( $status ); } } @@ -184,7 +182,8 @@ class ApiMove extends ApiBase { $retval = []; $success = $fromTitle->moveSubpages( $toTitle, true, $reason, !$noredirect ); if ( isset( $success[0] ) ) { - return [ 'error' => $this->parseMsg( $success ) ]; + $status = $this->errorArrayToStatus( $success ); + return [ 'errors' => $this->getErrorFormatter()->arrayFromStatus( $status ) ]; } // At least some pages could be moved @@ -192,7 +191,8 @@ class ApiMove extends ApiBase { foreach ( $success as $oldTitle => $newTitle ) { $r = [ 'from' => $oldTitle ]; if ( is_array( $newTitle ) ) { - $r['error'] = $this->parseMsg( reset( $newTitle ) ); + $status = $this->errorArrayToStatus( $newTitle ); + $r['errors'] = $this->getErrorFormatter()->arrayFromStatus( $status ); } else { // Success $r['to'] = $newTitle; diff --git a/includes/api/ApiOpenSearch.php b/includes/api/ApiOpenSearch.php index ace776c92396..e6fe27ca2a34 100644 --- a/includes/api/ApiOpenSearch.php +++ b/includes/api/ApiOpenSearch.php @@ -391,14 +391,14 @@ class ApiOpenSearchFormatJson extends ApiFormatJson { } public function execute() { - if ( !$this->getResult()->getResultData( 'error' ) ) { - $result = $this->getResult(); - + $result = $this->getResult(); + if ( !$result->getResultData( 'error' ) && !$result->getResultData( 'errors' ) ) { // Ignore warnings or treat as errors, as requested $warnings = $result->removeValue( 'warnings', null ); if ( $this->warningsAsError && $warnings ) { - $this->dieUsage( - 'Warnings cannot be represented in OpenSearch JSON format', 'warnings', 0, + $this->dieWithError( + 'apierror-opensearch-json-warnings', + 'warnings', [ 'warnings' => $warnings ] ); } diff --git a/includes/api/ApiOptions.php b/includes/api/ApiOptions.php index 8bfe447df59c..466d1865d68c 100644 --- a/includes/api/ApiOptions.php +++ b/includes/api/ApiOptions.php @@ -36,22 +36,26 @@ class ApiOptions extends ApiBase { */ public function execute() { if ( $this->getUser()->isAnon() ) { - $this->dieUsage( 'Anonymous users cannot change preferences', 'notloggedin' ); - } elseif ( !$this->getUser()->isAllowed( 'editmyoptions' ) ) { - $this->dieUsage( "You don't have permission to edit your options", 'permissiondenied' ); + $this->dieWithError( + [ 'apierror-mustbeloggedin', $this->msg( 'action-editmyoptions' ) ], 'notloggedin' + ); } + $this->checkUserRightsAny( 'editmyoptions' ); + $params = $this->extractRequestParams(); $changed = false; if ( isset( $params['optionvalue'] ) && !isset( $params['optionname'] ) ) { - $this->dieUsageMsg( [ 'missingparam', 'optionname' ] ); + $this->dieWithError( [ 'apierror-missingparam', 'optionname' ] ); } // Load the user from the master to reduce CAS errors on double post (T95839) $user = $this->getUser()->getInstanceForUpdate(); if ( !$user ) { - $this->dieUsage( 'Anonymous users cannot change preferences', 'notloggedin' ); + $this->dieWithError( + [ 'apierror-mustbeloggedin', $this->msg( 'action-editmyoptions' ) ], 'notloggedin' + ); } if ( $params['reset'] ) { @@ -71,7 +75,7 @@ class ApiOptions extends ApiBase { $changes[$params['optionname']] = $newValue; } if ( !$changed && !count( $changes ) ) { - $this->dieUsage( 'No changes were requested', 'nochanges' ); + $this->dieWithError( 'apierror-nochanges' ); } $prefs = Preferences::getPreferences( $user, $this->getContext() ); @@ -98,26 +102,26 @@ class ApiOptions extends ApiBase { case 'userjs': // Allow non-default preferences prefixed with 'userjs-', to be set by user scripts if ( strlen( $key ) > 255 ) { - $validation = 'key too long (no more than 255 bytes allowed)'; + $validation = $this->msg( 'apiwarn-validationfailed-keytoolong', Message::numParam( 255 ) ); } elseif ( preg_match( '/[^a-zA-Z0-9_-]/', $key ) !== 0 ) { - $validation = 'invalid key (only a-z, A-Z, 0-9, _, - allowed)'; + $validation = $this->msg( 'apiwarn-validationfailed-badchars' ); } else { $validation = true; } break; case 'special': - $validation = 'cannot be set by this module'; + $validation = $this->msg( 'apiwarn-validationfailed-cannotset' ); break; case 'unused': default: - $validation = 'not a valid preference'; + $validation = $this->msg( 'apiwarn-validationfailed-badpref' ); break; } if ( $validation === true ) { $user->setOption( $key, $value ); $changed = true; } else { - $this->setWarning( "Validation error for '$key': $validation" ); + $this->addWarning( [ 'apiwarn-validationfailed', wfEscapeWikitext( $key ), $validation ] ); } } diff --git a/includes/api/ApiPageSet.php b/includes/api/ApiPageSet.php index 853a8056be7a..4cf896f1dcf4 100644 --- a/includes/api/ApiPageSet.php +++ b/includes/api/ApiPageSet.php @@ -155,10 +155,10 @@ class ApiPageSet extends ApiBase { } $generator = $dbSource->getModuleManager()->getModule( $generatorName, null, true ); if ( $generator === null ) { - $this->dieUsage( 'Unknown generator=' . $generatorName, 'badgenerator' ); + $this->dieWithError( [ 'apierror-badgenerator-unknown', $generatorName ], 'badgenerator' ); } if ( !$generator instanceof ApiQueryGeneratorBase ) { - $this->dieUsage( "Module $generatorName cannot be used as a generator", 'badgenerator' ); + $this->dieWithError( [ 'apierror-badgenerator-notgenerator', $generatorName ], 'badgenerator' ); } // Create a temporary pageset to store generator's output, // add any additional fields generator may need, and execute pageset to populate titles/pageids @@ -194,13 +194,27 @@ class ApiPageSet extends ApiBase { } if ( isset( $this->mParams['pageids'] ) ) { if ( isset( $dataSource ) ) { - $this->dieUsage( "Cannot use 'pageids' at the same time as '$dataSource'", 'multisource' ); + $this->dieWithError( + [ + 'apierror-invalidparammix-cannotusewith', + $this->encodeParamName( 'pageids' ), + $this->encodeParamName( $dataSource ) + ], + 'multisource' + ); } $dataSource = 'pageids'; } if ( isset( $this->mParams['revids'] ) ) { if ( isset( $dataSource ) ) { - $this->dieUsage( "Cannot use 'revids' at the same time as '$dataSource'", 'multisource' ); + $this->dieWithError( + [ + 'apierror-invalidparammix-cannotusewith', + $this->encodeParamName( 'revids' ), + $this->encodeParamName( $dataSource ) + ], + 'multisource' + ); } $dataSource = 'revids'; } @@ -216,9 +230,7 @@ class ApiPageSet extends ApiBase { break; case 'revids': if ( $this->mResolveRedirects ) { - $this->setWarning( 'Redirect resolution cannot be used ' . - 'together with the revids= parameter. Any redirects ' . - 'the revids= point to have not been resolved.' ); + $this->addWarning( 'apiwarn-redirectsandrevids' ); } $this->mResolveRedirects = false; $this->initFromRevIDs( $this->mParams['revids'] ); diff --git a/includes/api/ApiParamInfo.php b/includes/api/ApiParamInfo.php index ffc3fc2e3217..a9b3dde94722 100644 --- a/includes/api/ApiParamInfo.php +++ b/includes/api/ApiParamInfo.php @@ -66,14 +66,17 @@ class ApiParamInfo extends ApiBase { if ( $submodules ) { try { $module = $this->getModuleFromPath( $path ); - } catch ( UsageException $ex ) { - $this->setWarning( $ex->getMessage() ); + } catch ( ApiUsageException $ex ) { + foreach ( $ex->getStatusValue()->getErrors() as $error ) { + $this->addWarning( $error ); + } + continue; } $submodules = $this->listAllSubmodules( $module, $recursive ); if ( $submodules ) { $modules = array_merge( $modules, $submodules ); } else { - $this->setWarning( "Module $path has no submodules" ); + $this->addWarning( [ 'apierror-badmodule-nosubmodules', $path ], 'badmodule' ); } } else { $modules[] = $path; @@ -108,8 +111,10 @@ class ApiParamInfo extends ApiBase { foreach ( $modules as $m ) { try { $module = $this->getModuleFromPath( $m ); - } catch ( UsageException $ex ) { - $this->setWarning( $ex->getMessage() ); + } catch ( ApiUsageException $ex ) { + foreach ( $ex->getStatusValue()->getErrors() as $error ) { + $this->addWarning( $error ); + } continue; } $key = 'modules'; diff --git a/includes/api/ApiParse.php b/includes/api/ApiParse.php index 0cad5dee9ce0..2263b8f83c34 100644 --- a/includes/api/ApiParse.php +++ b/includes/api/ApiParse.php @@ -36,18 +36,18 @@ class ApiParse extends ApiBase { /** @var Content $pstContent */ private $pstContent = null; - private function checkReadPermissions( Title $title ) { - if ( !$title->userCan( 'read', $this->getUser() ) ) { - $this->dieUsage( "You don't have permission to view this page", 'permissiondenied' ); - } - } - public function execute() { // The data is hot but user-dependent, like page views, so we set vary cookies $this->getMain()->setCacheMode( 'anon-public-user-private' ); // Get parameters $params = $this->extractRequestParams(); + + // No easy way to say that text & title are allowed together while the + // rest aren't, so just do it in two calls. + $this->requireMaxOneParameter( $params, 'page', 'pageid', 'oldid', 'text' ); + $this->requireMaxOneParameter( $params, 'page', 'pageid', 'oldid', 'title' ); + $text = $params['text']; $title = $params['title']; if ( $title === null ) { @@ -65,21 +65,12 @@ class ApiParse extends ApiBase { $model = $params['contentmodel']; $format = $params['contentformat']; - if ( !is_null( $page ) && ( !is_null( $text ) || $titleProvided ) ) { - $this->dieUsage( - 'The page parameter cannot be used together with the text and title parameters', - 'params' - ); - } - $prop = array_flip( $params['prop'] ); if ( isset( $params['section'] ) ) { $this->section = $params['section']; if ( !preg_match( '/^((T-)?\d+|new)$/', $this->section ) ) { - $this->dieUsage( - 'The section parameter must be a valid section id or "new"', 'invalidsection' - ); + $this->dieWithError( 'apierror-invalidsection' ); } } else { $this->section = false; @@ -97,21 +88,20 @@ class ApiParse extends ApiBase { if ( !is_null( $oldid ) || !is_null( $pageid ) || !is_null( $page ) ) { if ( $this->section === 'new' ) { - $this->dieUsage( - 'section=new cannot be combined with oldid, pageid or page parameters. ' . - 'Please use text', 'params' - ); + $this->dieWithError( 'apierror-invalidparammix-parse-new-section', 'invalidparammix' ); } if ( !is_null( $oldid ) ) { // Don't use the parser cache $rev = Revision::newFromId( $oldid ); if ( !$rev ) { - $this->dieUsage( "There is no revision ID $oldid", 'missingrev' ); + $this->dieWithError( [ 'apierror-nosuchrevid', $oldid ] ); } - $this->checkReadPermissions( $rev->getTitle() ); + $this->checkTitleUserPermissions( $rev->getTitle(), 'read' ); if ( !$rev->userCan( Revision::DELETED_TEXT, $this->getUser() ) ) { - $this->dieUsage( "You don't have permission to view deleted revisions", 'permissiondenied' ); + $this->dieWithError( + [ 'apierror-permissiondenied', $this->msg( 'action-deletedtext' ) ] + ); } $titleObj = $rev->getTitle(); @@ -131,7 +121,9 @@ class ApiParse extends ApiBase { $this->content = $rev->getContent( Revision::FOR_THIS_USER, $this->getUser() ); if ( $this->section !== false ) { - $this->content = $this->getSectionContent( $this->content, 'r' . $rev->getId() ); + $this->content = $this->getSectionContent( + $this->content, $this->msg( 'revid', $rev->getId() ) + ); } // Should we save old revision parses to the parser cache? @@ -167,10 +159,10 @@ class ApiParse extends ApiBase { $pageObj = $this->getTitleOrPageId( $pageParams, 'fromdb' ); $titleObj = $pageObj->getTitle(); if ( !$titleObj || !$titleObj->exists() ) { - $this->dieUsage( "The page you specified doesn't exist", 'missingtitle' ); + $this->dieWithError( 'apierror-missingtitle' ); } - $this->checkReadPermissions( $titleObj ); + $this->checkTitleUserPermissions( $titleObj, 'read' ); $wgTitle = $titleObj; if ( isset( $prop['revid'] ) ) { @@ -201,7 +193,7 @@ class ApiParse extends ApiBase { } else { // Not $oldid, $pageid, $page. Hence based on $text $titleObj = Title::newFromText( $title ); if ( !$titleObj || $titleObj->isExternal() ) { - $this->dieUsageMsg( [ 'invalidtitle', $title ] ); + $this->dieWithError( [ 'apierror-invalidtitle', wfEscapeWikiText( $title ) ] ); } $wgTitle = $titleObj; if ( $titleObj->canExist() ) { @@ -217,10 +209,7 @@ class ApiParse extends ApiBase { if ( !$textProvided ) { if ( $titleProvided && ( $prop || $params['generatexml'] ) ) { - $this->setWarning( - "'title' used without 'text', and parsed page properties were requested " . - "(did you mean to use 'page' instead of 'title'?)" - ); + $this->addWarning( 'apiwarn-parse-titlewithouttext' ); } // Prevent warning from ContentHandler::makeContent() $text = ''; @@ -230,13 +219,17 @@ class ApiParse extends ApiBase { // API title, but default to wikitext to keep BC. if ( $textProvided && !$titleProvided && is_null( $model ) ) { $model = CONTENT_MODEL_WIKITEXT; - $this->setWarning( "No 'title' or 'contentmodel' was given, assuming $model." ); + $this->addWarning( [ 'apiwarn-parse-nocontentmodel', $model ] ); } try { $this->content = ContentHandler::makeContent( $text, $titleObj, $model, $format ); } catch ( MWContentSerializationException $ex ) { - $this->dieUsage( $ex->getMessage(), 'parseerror' ); + // @todo: Internationalize MWContentSerializationException + $this->dieWithError( + [ 'apierror-contentserializationexception', wfEscapeWikiText( $ex->getMessage() ) ], + 'parseerror' + ); } if ( $this->section !== false ) { @@ -357,10 +350,7 @@ class ApiParse extends ApiBase { if ( isset( $prop['headitems'] ) ) { $result_array['headitems'] = $this->formatHeadItems( $p_result->getHeadItems() ); - $this->logFeatureUsage( 'action=parse&prop=headitems' ); - $this->setWarning( 'headitems is deprecated since MediaWiki 1.28. ' - . 'Use prop=headhtml when creating new HTML documents, or ' - . 'prop=modules|jsconfigvars when updating a document client-side.' ); + $this->addDeprecation( 'apiwarn-deprecation-parse-headitems', 'action=parse&prop=headitems' ); } if ( isset( $prop['headhtml'] ) ) { @@ -397,9 +387,7 @@ class ApiParse extends ApiBase { if ( isset( $prop['modules'] ) && !isset( $prop['jsconfigvars'] ) && !isset( $prop['encodedjsconfigvars'] ) ) { - $this->setWarning( 'Property "modules" was set but not "jsconfigvars" ' . - 'or "encodedjsconfigvars". Configuration variables are necessary ' . - 'for proper module usage.' ); + $this->addWarning( 'apiwarn-moduleswithoutvars' ); } if ( isset( $prop['indicators'] ) ) { @@ -435,7 +423,7 @@ class ApiParse extends ApiBase { if ( isset( $prop['parsetree'] ) || $params['generatexml'] ) { if ( $this->content->getModel() != CONTENT_MODEL_WIKITEXT ) { - $this->dieUsage( 'parsetree is only supported for wikitext content', 'notwikitext' ); + $this->dieWithError( 'apierror-parsetree-notwikitext', 'notwikitext' ); } $wgParser->startExternalParse( $titleObj, $popts, Parser::OT_PREPROCESS ); @@ -516,7 +504,7 @@ class ApiParse extends ApiBase { // getParserOutput will save to Parser cache if able $pout = $page->getParserOutput( $popts ); if ( !$pout ) { - $this->dieUsage( "There is no revision ID {$page->getLatest()}", 'missingrev' ); + $this->dieWithError( [ 'apierror-nosuchrevid', $page->getLatest() ] ); } if ( $getWikitext ) { $this->content = $page->getContent( Revision::RAW ); @@ -538,7 +526,9 @@ class ApiParse extends ApiBase { if ( $this->section !== false && $content !== null ) { $content = $this->getSectionContent( $content, - !is_null( $pageId ) ? 'page id ' . $pageId : $page->getTitle()->getPrefixedText() + !is_null( $pageId ) + ? $this->msg( 'pageid', $pageId ) + : $page->getTitle()->getPrefixedText() ); } return $content; @@ -548,17 +538,17 @@ class ApiParse extends ApiBase { * Extract the requested section from the given Content * * @param Content $content - * @param string $what Identifies the content in error messages, e.g. page title. + * @param string|Message $what Identifies the content in error messages, e.g. page title. * @return Content|bool */ private function getSectionContent( Content $content, $what ) { // Not cached (save or load) $section = $content->getSection( $this->section ); if ( $section === false ) { - $this->dieUsage( "There is no section {$this->section} in $what", 'nosuchsection' ); + $this->dieWithError( [ 'apierror-nosuchsection-what', $this->section, $what ], 'nosuchsection' ); } if ( $section === null ) { - $this->dieUsage( "Sections are not supported by $what", 'nosuchsection' ); + $this->dieWithError( [ 'apierror-sectionsnotsupported-what', $what ], 'nosuchsection' ); $section = false; } diff --git a/includes/api/ApiPatrol.php b/includes/api/ApiPatrol.php index 62528825ded0..c33542f1c7aa 100644 --- a/includes/api/ApiPatrol.php +++ b/includes/api/ApiPatrol.php @@ -40,19 +40,16 @@ class ApiPatrol extends ApiBase { if ( isset( $params['rcid'] ) ) { $rc = RecentChange::newFromId( $params['rcid'] ); if ( !$rc ) { - $this->dieUsageMsg( [ 'nosuchrcid', $params['rcid'] ] ); + $this->dieWithError( [ 'apierror-nosuchrcid', $params['rcid'] ] ); } } else { $rev = Revision::newFromId( $params['revid'] ); if ( !$rev ) { - $this->dieUsageMsg( [ 'nosuchrevid', $params['revid'] ] ); + $this->dieWithError( [ 'apierror-nosuchrevid', $params['revid'] ] ); } $rc = $rev->getRecentChange(); if ( !$rc ) { - $this->dieUsage( - 'The revision ' . $params['revid'] . " can't be patrolled as it's too old", - 'notpatrollable' - ); + $this->dieWithError( [ 'apierror-notpatrollable', $params['revid'] ] ); } } @@ -70,7 +67,7 @@ class ApiPatrol extends ApiBase { $retval = $rc->doMarkPatrolled( $user, false, $tags ); if ( $retval ) { - $this->dieUsageMsg( reset( $retval ) ); + $this->dieStatus( $this->errorArrayToStatus( $retval, $user ) ); } $result = [ 'rcid' => intval( $rc->getAttribute( 'rc_id' ) ) ]; diff --git a/includes/api/ApiProtect.php b/includes/api/ApiProtect.php index d28906069fc0..746dc9a16bb7 100644 --- a/includes/api/ApiProtect.php +++ b/includes/api/ApiProtect.php @@ -36,11 +36,7 @@ class ApiProtect extends ApiBase { $pageObj = $this->getTitleOrPageId( $params, 'fromdbmaster' ); $titleObj = $pageObj->getTitle(); - $errors = $titleObj->getUserPermissionsErrors( 'protect', $this->getUser() ); - if ( $errors ) { - // We don't care about multiple errors, just report one of them - $this->dieUsageMsg( reset( $errors ) ); - } + $this->checkTitleUserPermissions( $titleObj, 'protect' ); $user = $this->getUser(); $tags = $params['tags']; @@ -58,8 +54,8 @@ class ApiProtect extends ApiBase { if ( count( $expiry ) == 1 ) { $expiry = array_fill( 0, count( $params['protections'] ), $expiry[0] ); } else { - $this->dieUsageMsg( [ - 'toofewexpiries', + $this->dieWithError( [ + 'apierror-toofewexpiries', count( $expiry ), count( $params['protections'] ) ] ); @@ -76,17 +72,17 @@ class ApiProtect extends ApiBase { $protections[$p[0]] = ( $p[1] == 'all' ? '' : $p[1] ); if ( $titleObj->exists() && $p[0] == 'create' ) { - $this->dieUsageMsg( 'create-titleexists' ); + $this->dieWithError( 'apierror-create-titleexists' ); } if ( !$titleObj->exists() && $p[0] != 'create' ) { - $this->dieUsageMsg( 'missingtitle-createonly' ); + $this->dieWithError( 'apierror-missingtitle-createonly' ); } if ( !in_array( $p[0], $restrictionTypes ) && $p[0] != 'create' ) { - $this->dieUsageMsg( [ 'protect-invalidaction', $p[0] ] ); + $this->dieWithError( [ 'apierror-protect-invalidaction', wfEscapeWikiText( $p[0] ) ] ); } if ( !in_array( $p[1], $this->getConfig()->get( 'RestrictionLevels' ) ) && $p[1] != 'all' ) { - $this->dieUsageMsg( [ 'protect-invalidlevel', $p[1] ] ); + $this->dieWithError( [ 'apierror-protect-invalidlevel', wfEscapeWikiText( $p[1] ) ] ); } if ( wfIsInfinity( $expiry[$i] ) ) { @@ -94,12 +90,12 @@ class ApiProtect extends ApiBase { } else { $exp = strtotime( $expiry[$i] ); if ( $exp < 0 || !$exp ) { - $this->dieUsageMsg( [ 'invalidexpiry', $expiry[$i] ] ); + $this->dieWithError( [ 'apierror-invalidexpiry', wfEscapeWikiText( $expiry[$i] ) ] ); } $exp = wfTimestamp( TS_MW, $exp ); if ( $exp < wfTimestampNow() ) { - $this->dieUsageMsg( [ 'pastexpiry', $expiry[$i] ] ); + $this->dieWithError( [ 'apierror-pastexpiry', wfEscapeWikiText( $expiry[$i] ) ] ); } $expiryarray[$p[0]] = $exp; } diff --git a/includes/api/ApiPurge.php b/includes/api/ApiPurge.php index 8bbd88dfec46..324d030fdbf4 100644 --- a/includes/api/ApiPurge.php +++ b/includes/api/ApiPurge.php @@ -39,8 +39,7 @@ class ApiPurge extends ApiBase { public function execute() { $main = $this->getMain(); if ( !$main->isInternalMode() && !$main->getRequest()->wasPosted() ) { - $this->logFeatureUsage( 'purge-via-GET' ); - $this->setWarning( 'Use of action=purge via GET is deprecated. Use POST instead.' ); + $this->addDeprecation( 'apiwarn-deprecation-purge-get', 'purge-via-GET' ); } $params = $this->extractRequestParams(); @@ -69,8 +68,7 @@ class ApiPurge extends ApiBase { $page->doPurge( $flags ); $r['purged'] = true; } else { - $error = $this->parseMsg( [ 'actionthrottledtext' ] ); - $this->setWarning( $error['info'] ); + $this->addWarning( 'apierror-ratelimited' ); } if ( $forceLinkUpdate || $forceRecursiveLinkUpdate ) { @@ -114,8 +112,7 @@ class ApiPurge extends ApiBase { } } } else { - $error = $this->parseMsg( [ 'actionthrottledtext' ] ); - $this->setWarning( $error['info'] ); + $this->addWarning( 'apierror-ratelimited' ); $forceLinkUpdate = false; } } diff --git a/includes/api/ApiQuery.php b/includes/api/ApiQuery.php index 16bd725e3cb0..8196cfa2bbfd 100644 --- a/includes/api/ApiQuery.php +++ b/includes/api/ApiQuery.php @@ -310,7 +310,7 @@ class ApiQuery extends ApiBase { ApiBase::dieDebug( __METHOD__, 'Error instantiating module' ); } if ( !$wasPosted && $instance->mustBePosted() ) { - $this->dieUsageMsgOrDebug( [ 'mustbeposted', $moduleName ] ); + $this->dieWithErrorOrDebug( [ 'apierror-mustbeposted', $moduleName ] ); } // Ignore duplicates. TODO 2.0: die()? if ( !array_key_exists( $moduleName, $modules ) ) { @@ -415,11 +415,7 @@ class ApiQuery extends ApiBase { } if ( !$fit ) { - $this->dieUsage( - 'The value of $wgAPIMaxResultSize on this wiki is ' . - 'too small to hold basic result information', - 'badconfig' - ); + $this->dieWithError( 'apierror-badconfig-resulttoosmall', 'badconfig' ); } if ( $this->mParams['export'] ) { diff --git a/includes/api/ApiQueryAllDeletedRevisions.php b/includes/api/ApiQueryAllDeletedRevisions.php index 3073a951b022..b09b97702db5 100644 --- a/includes/api/ApiQueryAllDeletedRevisions.php +++ b/includes/api/ApiQueryAllDeletedRevisions.php @@ -41,15 +41,10 @@ class ApiQueryAllDeletedRevisions extends ApiQueryRevisionsBase { * @return void */ protected function run( ApiPageSet $resultPageSet = null ) { - $user = $this->getUser(); // Before doing anything at all, let's check permissions - if ( !$user->isAllowed( 'deletedhistory' ) ) { - $this->dieUsage( - 'You don\'t have permission to view deleted revision information', - 'permissiondenied' - ); - } + $this->checkUserRightsAny( 'deletedhistory' ); + $user = $this->getUser(); $db = $this->getDB(); $params = $this->extractRequestParams( false ); @@ -75,16 +70,20 @@ class ApiQueryAllDeletedRevisions extends ApiQueryRevisionsBase { foreach ( [ 'from', 'to', 'prefix', 'excludeuser' ] as $param ) { if ( !is_null( $params[$param] ) ) { $p = $this->getModulePrefix(); - $this->dieUsage( "The '{$p}{$param}' parameter cannot be used with '{$p}user'", - 'badparams' ); + $this->dieWithError( + [ 'apierror-invalidparammix-cannotusewith', $p.$param, "{$p}user" ], + 'invalidparammix' + ); } } } else { foreach ( [ 'start', 'end' ] as $param ) { if ( !is_null( $params[$param] ) ) { $p = $this->getModulePrefix(); - $this->dieUsage( "The '{$p}{$param}' parameter may only be used with '{$p}user'", - 'badparams' ); + $this->dieWithError( + [ 'apierror-invalidparammix-mustusewith', $p.$param, "{$p}user" ], + 'invalidparammix' + ); } } } @@ -100,7 +99,7 @@ class ApiQueryAllDeletedRevisions extends ApiQueryRevisionsBase { $optimizeGenerateTitles = true; } else { $p = $this->getModulePrefix(); - $this->setWarning( "For better performance when generating titles, set {$p}dir=newer" ); + $this->addWarning( [ 'apiwarn-alldeletedrevisions-performance', $p ], 'performance' ); } } @@ -148,12 +147,7 @@ class ApiQueryAllDeletedRevisions extends ApiQueryRevisionsBase { $this->addFields( [ 'ar_text', 'ar_flags', 'old_text', 'old_flags' ] ); // This also means stricter restrictions - if ( !$user->isAllowedAny( 'undelete', 'deletedtext' ) ) { - $this->dieUsage( - 'You don\'t have permission to view deleted revision content', - 'permissiondenied' - ); - } + $this->checkUserRightsAny( [ 'deletedtext', 'undelete' ] ); } $miser_ns = null; diff --git a/includes/api/ApiQueryAllImages.php b/includes/api/ApiQueryAllImages.php index 8734f380bb6a..e3e5ed6c9f48 100644 --- a/includes/api/ApiQueryAllImages.php +++ b/includes/api/ApiQueryAllImages.php @@ -64,11 +64,7 @@ class ApiQueryAllImages extends ApiQueryGeneratorBase { */ public function executeGenerator( $resultPageSet ) { if ( $resultPageSet->isResolvingRedirects() ) { - $this->dieUsage( - 'Use "gaifilterredir=nonredirects" option instead of "redirects" ' . - 'when using allimages as a generator', - 'params' - ); + $this->dieWithError( 'apierror-allimages-redirect', 'invalidparammix' ); } $this->run( $resultPageSet ); @@ -81,10 +77,7 @@ class ApiQueryAllImages extends ApiQueryGeneratorBase { private function run( $resultPageSet = null ) { $repo = $this->mRepo; if ( !$repo instanceof LocalRepo ) { - $this->dieUsage( - 'Local file repository does not support querying all images', - 'unsupportedrepo' - ); + $this->dieWithError( 'apierror-unsupportedrepo' ); } $prefix = $this->getModulePrefix(); @@ -109,16 +102,24 @@ class ApiQueryAllImages extends ApiQueryGeneratorBase { $disallowed = [ 'start', 'end', 'user' ]; foreach ( $disallowed as $pname ) { if ( isset( $params[$pname] ) ) { - $this->dieUsage( - "Parameter '{$prefix}{$pname}' can only be used with {$prefix}sort=timestamp", - 'badparams' + $this->dieWithError( + [ + 'apierror-invalidparammix-mustusewith', + "{$prefix}{$pname}", + "{$prefix}sort=timestamp" + ], + 'invalidparammix' ); } } if ( $params['filterbots'] != 'all' ) { - $this->dieUsage( - "Parameter '{$prefix}filterbots' can only be used with {$prefix}sort=timestamp", - 'badparams' + $this->dieWithError( + [ + 'apierror-invalidparammix-mustusewith', + "{$prefix}filterbots", + "{$prefix}sort=timestamp" + ], + 'invalidparammix' ); } @@ -146,18 +147,21 @@ class ApiQueryAllImages extends ApiQueryGeneratorBase { $disallowed = [ 'from', 'to', 'prefix' ]; foreach ( $disallowed as $pname ) { if ( isset( $params[$pname] ) ) { - $this->dieUsage( - "Parameter '{$prefix}{$pname}' can only be used with {$prefix}sort=name", - 'badparams' + $this->dieWithError( + [ + 'apierror-invalidparammix-mustusewith', + "{$prefix}{$pname}", + "{$prefix}sort=name" + ], + 'invalidparammix' ); } } if ( !is_null( $params['user'] ) && $params['filterbots'] != 'all' ) { // Since filterbots checks if each user has the bot right, it // doesn't make sense to use it with user - $this->dieUsage( - "Parameters '{$prefix}user' and '{$prefix}filterbots' cannot be used together", - 'badparams' + $this->dieWithError( + [ 'apierror-invalidparammix-cannotusewith', "{$prefix}user", "{$prefix}filterbots" ] ); } @@ -214,13 +218,13 @@ class ApiQueryAllImages extends ApiQueryGeneratorBase { if ( isset( $params['sha1'] ) ) { $sha1 = strtolower( $params['sha1'] ); if ( !$this->validateSha1Hash( $sha1 ) ) { - $this->dieUsage( 'The SHA1 hash provided is not valid', 'invalidsha1hash' ); + $this->dieWithError( 'apierror-invalidsha1hash' ); } $sha1 = Wikimedia\base_convert( $sha1, 16, 36, 31 ); } elseif ( isset( $params['sha1base36'] ) ) { $sha1 = strtolower( $params['sha1base36'] ); if ( !$this->validateSha1Base36Hash( $sha1 ) ) { - $this->dieUsage( 'The SHA1Base36 hash provided is not valid', 'invalidsha1base36hash' ); + $this->dieWithError( 'apierror-invalidsha1base36hash' ); } } if ( $sha1 ) { @@ -229,7 +233,7 @@ class ApiQueryAllImages extends ApiQueryGeneratorBase { if ( !is_null( $params['mime'] ) ) { if ( $this->getConfig()->get( 'MiserMode' ) ) { - $this->dieUsage( 'MIME search disabled in Miser Mode', 'mimesearchdisabled' ); + $this->dieWithError( 'apierror-mimesearchdisabled' ); } $mimeConds = []; diff --git a/includes/api/ApiQueryAllLinks.php b/includes/api/ApiQueryAllLinks.php index ac906056a3c5..c3636c6bcf8e 100644 --- a/includes/api/ApiQueryAllLinks.php +++ b/includes/api/ApiQueryAllLinks.php @@ -116,9 +116,13 @@ class ApiQueryAllLinks extends ApiQueryGeneratorBase { $matches = array_intersect_key( $prop, $this->props + [ 'ids' => 1 ] ); if ( $matches ) { $p = $this->getModulePrefix(); - $this->dieUsage( - "Cannot use {$p}prop=" . implode( '|', array_keys( $matches ) ) . " with {$p}unique", - 'params' + $this->dieWithError( + [ + 'apierror-invalidparammix-cannotusewith', + "{$p}prop=" . implode( '|', array_keys( $matches ) ), + "{$p}unique" + ], + 'invalidparammix' ); } $this->addOption( 'DISTINCT' ); diff --git a/includes/api/ApiQueryAllMessages.php b/includes/api/ApiQueryAllMessages.php index e0ba4ea1c19c..244effc5238a 100644 --- a/includes/api/ApiQueryAllMessages.php +++ b/includes/api/ApiQueryAllMessages.php @@ -41,7 +41,9 @@ class ApiQueryAllMessages extends ApiQueryBase { if ( is_null( $params['lang'] ) ) { $langObj = $this->getLanguage(); } elseif ( !Language::isValidCode( $params['lang'] ) ) { - $this->dieUsage( 'Invalid language code for parameter lang', 'invalidlang' ); + $this->dieWithError( + [ 'apierror-invalidlang', $this->encodeParamName( 'lang' ) ], 'invalidlang' + ); } else { $langObj = Language::factory( $params['lang'] ); } @@ -50,7 +52,7 @@ class ApiQueryAllMessages extends ApiQueryBase { if ( !is_null( $params['title'] ) ) { $title = Title::newFromText( $params['title'] ); if ( !$title || $title->isExternal() ) { - $this->dieUsageMsg( [ 'invalidtitle', $params['title'] ] ); + $this->dieWithError( [ 'apierror-invalidtitle', wfEscapeWikiText( $params['title'] ) ] ); } } else { $title = Title::newFromText( 'API' ); diff --git a/includes/api/ApiQueryAllPages.php b/includes/api/ApiQueryAllPages.php index 6a0f124fafd1..7460bd537759 100644 --- a/includes/api/ApiQueryAllPages.php +++ b/includes/api/ApiQueryAllPages.php @@ -50,11 +50,7 @@ class ApiQueryAllPages extends ApiQueryGeneratorBase { */ public function executeGenerator( $resultPageSet ) { if ( $resultPageSet->isResolvingRedirects() ) { - $this->dieUsage( - 'Use "gapfilterredir=nonredirects" option instead of "redirects" ' . - 'when using allpages as a generator', - 'params' - ); + $this->dieWithError( 'apierror-allpages-generator-redirects', 'params' ); } $this->run( $resultPageSet ); @@ -157,7 +153,9 @@ class ApiQueryAllPages extends ApiQueryGeneratorBase { $this->addOption( 'DISTINCT' ); } elseif ( isset( $params['prlevel'] ) ) { - $this->dieUsage( 'prlevel may not be used without prtype', 'params' ); + $this->dieWithError( + [ 'apierror-invalidparammix-mustusewith', 'prlevel', 'prtype' ], 'invalidparammix' + ); } if ( $params['filterlanglinks'] == 'withoutlanglinks' ) { diff --git a/includes/api/ApiQueryAllUsers.php b/includes/api/ApiQueryAllUsers.php index b7ed9dda007f..2e2ac320fd58 100644 --- a/includes/api/ApiQueryAllUsers.php +++ b/includes/api/ApiQueryAllUsers.php @@ -110,9 +110,7 @@ class ApiQueryAllUsers extends ApiQueryBase { } } - if ( !is_null( $params['group'] ) && !is_null( $params['excludegroup'] ) ) { - $this->dieUsage( 'group and excludegroup cannot be used together', 'group-excludegroup' ); - } + $this->requireMaxOneParameter( $params, 'group', 'excludegroup' ); if ( !is_null( $params['group'] ) && count( $params['group'] ) ) { // Filter only users that belong to a given group. This might diff --git a/includes/api/ApiQueryBacklinks.php b/includes/api/ApiQueryBacklinks.php index fb502e40e75f..4c323206ef70 100644 --- a/includes/api/ApiQueryBacklinks.php +++ b/includes/api/ApiQueryBacklinks.php @@ -348,8 +348,8 @@ class ApiQueryBacklinks extends ApiQueryGeneratorBase { // only image titles are allowed for the root in imageinfo mode if ( !$this->hasNS && $this->rootTitle->getNamespace() !== NS_FILE ) { - $this->dieUsage( - "The title for {$this->getModuleName()} query must be a file", + $this->dieWithError( + [ 'apierror-imageusage-badtitle', $this->getModuleName() ], 'bad_image_title' ); } diff --git a/includes/api/ApiQueryBacklinksprop.php b/includes/api/ApiQueryBacklinksprop.php index 8e89c32e50d0..ef7b9af9869f 100644 --- a/includes/api/ApiQueryBacklinksprop.php +++ b/includes/api/ApiQueryBacklinksprop.php @@ -238,7 +238,7 @@ class ApiQueryBacklinksprop extends ApiQueryGeneratorBase { if ( isset( $show['fragment'] ) && isset( $show['!fragment'] ) || isset( $show['redirect'] ) && isset( $show['!redirect'] ) ) { - $this->dieUsageMsg( 'show' ); + $this->dieWithError( 'apierror-show' ); } $this->addWhereIf( "rd_fragment != $emptyString", isset( $show['fragment'] ) ); $this->addWhereIf( diff --git a/includes/api/ApiQueryBase.php b/includes/api/ApiQueryBase.php index bba53755c111..af2aed550476 100644 --- a/includes/api/ApiQueryBase.php +++ b/includes/api/ApiQueryBase.php @@ -421,7 +421,7 @@ abstract class ApiQueryBase extends ApiBase { $likeQuery = LinkFilter::makeLikeArray( $query, $protocol ); if ( !$likeQuery ) { - $this->dieUsage( 'Invalid query', 'bad_query' ); + $this->dieWithError( 'apierror-badquery' ); } $likeQuery = LinkFilter::keepOneWildcard( $likeQuery ); @@ -547,7 +547,7 @@ abstract class ApiQueryBase extends ApiBase { $t = Title::makeTitleSafe( $namespace, $titlePart . 'x' ); if ( !$t || $t->hasFragment() ) { // Invalid title (e.g. bad chars) or contained a '#'. - $this->dieUsageMsg( [ 'invalidtitle', $titlePart ] ); + $this->dieWithError( [ 'apierror-invalidtitle', wfEscapeWikiText( $titlePart ) ] ); } if ( $namespace != $t->getNamespace() || $t->isExternal() ) { // This can happen in two cases. First, if you call titlePartToKey with a title part @@ -555,7 +555,7 @@ abstract class ApiQueryBase extends ApiBase { // difficult to handle such a case. Such cases cannot exist and are therefore treated // as invalid user input. The second case is when somebody specifies a title interwiki // prefix. - $this->dieUsageMsg( [ 'invalidtitle', $titlePart ] ); + $this->dieWithError( [ 'apierror-invalidtitle', wfEscapeWikiText( $titlePart ) ] ); } return substr( $t->getDBkey(), 0, -1 ); @@ -573,7 +573,7 @@ abstract class ApiQueryBase extends ApiBase { $t = Title::newFromText( $titlePart . 'x', $defaultNamespace ); if ( !$t || $t->hasFragment() || $t->isExternal() ) { // Invalid title (e.g. bad chars) or contained a '#'. - $this->dieUsageMsg( [ 'invalidtitle', $titlePart ] ); + $this->dieWithError( [ 'apierror-invalidtitle', wfEscapeWikiText( $titlePart ) ] ); } return [ $t->getNamespace(), substr( $t->getDBkey(), 0, -1 ) ]; diff --git a/includes/api/ApiQueryBlocks.php b/includes/api/ApiQueryBlocks.php index 5d7c664aac28..ef79efd35875 100644 --- a/includes/api/ApiQueryBlocks.php +++ b/includes/api/ApiQueryBlocks.php @@ -114,16 +114,13 @@ class ApiQueryBlocks extends ApiQueryBase { $cidrLimit = $blockCIDRLimit['IPv6']; $prefixLen = 3; // IP::toHex output is prefixed with "v6-" } else { - $this->dieUsage( 'IP parameter is not valid', 'param_ip' ); + $this->dieWithError( 'apierror-badip', 'param_ip' ); } # Check range validity, if it's a CIDR list( $ip, $range ) = IP::parseCIDR( $params['ip'] ); if ( $ip !== false && $range !== false && $range < $cidrLimit ) { - $this->dieUsage( - "$type CIDR ranges broader than /$cidrLimit are not accepted", - 'cidrtoobroad' - ); + $this->dieWithError( [ 'apierror-cidrtoobroad', $type, $cidrLimit ] ); } # Let IP::parseRange handle calculating $upper, instead of duplicating the logic here. @@ -154,7 +151,7 @@ class ApiQueryBlocks extends ApiQueryBase { || ( isset( $show['range'] ) && isset( $show['!range'] ) ) || ( isset( $show['temp'] ) && isset( $show['!temp'] ) ) ) { - $this->dieUsageMsg( 'show' ); + $this->dieWithError( 'apierror-show' ); } $this->addWhereIf( 'ipb_user = 0', isset( $show['!account'] ) ); @@ -237,13 +234,19 @@ class ApiQueryBlocks extends ApiQueryBase { protected function prepareUsername( $user ) { if ( !$user ) { - $this->dieUsage( 'User parameter may not be empty', 'param_user' ); + $encParamName = $this->encodeParamName( 'users' ); + $this->dieWithError( [ 'apierror-baduser', $encParamName, wfEscapeWikiText( $user ) ], + "baduser_{$encParamName}" + ); } $name = User::isIP( $user ) ? $user : User::getCanonicalName( $user, 'valid' ); if ( $name === false ) { - $this->dieUsage( "User name {$user} is not valid", 'param_user' ); + $encParamName = $this->encodeParamName( 'users' ); + $this->dieWithError( [ 'apierror-baduser', $encParamName, wfEscapeWikiText( $user ) ], + "baduser_{$encParamName}" + ); } return $name; } diff --git a/includes/api/ApiQueryCategories.php b/includes/api/ApiQueryCategories.php index 63d0f6da13fd..f2498cae20cf 100644 --- a/includes/api/ApiQueryCategories.php +++ b/includes/api/ApiQueryCategories.php @@ -74,7 +74,7 @@ class ApiQueryCategories extends ApiQueryGeneratorBase { foreach ( $params['categories'] as $cat ) { $title = Title::newFromText( $cat ); if ( !$title || $title->getNamespace() != NS_CATEGORY ) { - $this->setWarning( "\"$cat\" is not a category" ); + $this->addWarning( [ 'apiwarn-invalidcategory', wfEscapeWikiText( $cat ) ] ); } else { $cats[] = $title->getDBkey(); } @@ -96,7 +96,7 @@ class ApiQueryCategories extends ApiQueryGeneratorBase { } if ( isset( $show['hidden'] ) && isset( $show['!hidden'] ) ) { - $this->dieUsageMsg( 'show' ); + $this->dieWithError( 'apierror-show' ); } if ( isset( $show['hidden'] ) || isset( $show['!hidden'] ) || isset( $prop['hidden'] ) ) { $this->addOption( 'STRAIGHT_JOIN' ); diff --git a/includes/api/ApiQueryCategoryMembers.php b/includes/api/ApiQueryCategoryMembers.php index 4865ad56f916..02961aa299d7 100644 --- a/includes/api/ApiQueryCategoryMembers.php +++ b/includes/api/ApiQueryCategoryMembers.php @@ -65,7 +65,7 @@ class ApiQueryCategoryMembers extends ApiQueryGeneratorBase { $categoryTitle = $this->getTitleOrPageId( $params )->getTitle(); if ( $categoryTitle->getNamespace() != NS_CATEGORY ) { - $this->dieUsage( 'The category name you entered is not valid', 'invalidcategory' ); + $this->dieWithError( 'apierror-invalidcategory' ); } $prop = array_flip( $params['prop'] ); @@ -153,7 +153,8 @@ class ApiQueryCategoryMembers extends ApiQueryGeneratorBase { $startsortkey = Collation::singleton()->getSortKey( $params['startsortkeyprefix'] ); } elseif ( $params['starthexsortkey'] !== null ) { if ( !$this->validateHexSortkey( $params['starthexsortkey'] ) ) { - $this->dieUsage( 'The starthexsortkey provided is not valid', 'bad_starthexsortkey' ); + $encParamName = $this->encodeParamName( 'starthexsortkey' ); + $this->dieWithError( [ 'apierror-badparameter', $encParamName ], "badvalue_$encParamName" ); } $startsortkey = hex2bin( $params['starthexsortkey'] ); } else { @@ -163,7 +164,8 @@ class ApiQueryCategoryMembers extends ApiQueryGeneratorBase { $endsortkey = Collation::singleton()->getSortKey( $params['endsortkeyprefix'] ); } elseif ( $params['endhexsortkey'] !== null ) { if ( !$this->validateHexSortkey( $params['endhexsortkey'] ) ) { - $this->dieUsage( 'The endhexsortkey provided is not valid', 'bad_endhexsortkey' ); + $encParamName = $this->encodeParamName( 'endhexsortkey' ); + $this->dieWithError( [ 'apierror-badparameter', $encParamName ], "badvalue_$encParamName" ); } $endsortkey = hex2bin( $params['endhexsortkey'] ); } else { diff --git a/includes/api/ApiQueryDeletedRevisions.php b/includes/api/ApiQueryDeletedRevisions.php index cfd0653d9e5d..d0b82144690b 100644 --- a/includes/api/ApiQueryDeletedRevisions.php +++ b/includes/api/ApiQueryDeletedRevisions.php @@ -39,12 +39,7 @@ class ApiQueryDeletedRevisions extends ApiQueryRevisionsBase { protected function run( ApiPageSet $resultPageSet = null ) { $user = $this->getUser(); // Before doing anything at all, let's check permissions - if ( !$user->isAllowed( 'deletedhistory' ) ) { - $this->dieUsage( - 'You don\'t have permission to view deleted revision information', - 'permissiondenied' - ); - } + $this->checkUserRightsAny( 'deletedhistory' ); $pageSet = $this->getPageSet(); $pageMap = $pageSet->getGoodAndMissingTitlesByNamespace(); @@ -63,9 +58,7 @@ class ApiQueryDeletedRevisions extends ApiQueryRevisionsBase { $db = $this->getDB(); - if ( !is_null( $params['user'] ) && !is_null( $params['excludeuser'] ) ) { - $this->dieUsage( 'user and excludeuser cannot be used together', 'badparams' ); - } + $this->requireMaxOneParameter( $params, 'user', 'excludeuser' ); $this->addTables( 'archive' ); if ( $resultPageSet === null ) { @@ -106,12 +99,7 @@ class ApiQueryDeletedRevisions extends ApiQueryRevisionsBase { $this->addFields( [ 'ar_text', 'ar_flags', 'old_text', 'old_flags' ] ); // This also means stricter restrictions - if ( !$user->isAllowedAny( 'undelete', 'deletedtext' ) ) { - $this->dieUsage( - 'You don\'t have permission to view deleted revision content', - 'permissiondenied' - ); - } + $this->checkUserRightsAny( [ 'deletedtext', 'undelete' ] ); } $dir = $params['dir']; diff --git a/includes/api/ApiQueryDeletedrevs.php b/includes/api/ApiQueryDeletedrevs.php index d58efa1d5a08..6a259cd00a80 100644 --- a/includes/api/ApiQueryDeletedrevs.php +++ b/includes/api/ApiQueryDeletedrevs.php @@ -37,21 +37,12 @@ class ApiQueryDeletedrevs extends ApiQueryBase { } public function execute() { - $user = $this->getUser(); // Before doing anything at all, let's check permissions - if ( !$user->isAllowed( 'deletedhistory' ) ) { - $this->dieUsage( - 'You don\'t have permission to view deleted revision information', - 'permissiondenied' - ); - } + $this->checkUserRightsAny( 'deletedhistory' ); - $this->setWarning( - 'list=deletedrevs has been deprecated. Please use prop=deletedrevisions or ' . - 'list=alldeletedrevisions instead.' - ); - $this->logFeatureUsage( 'action=query&list=deletedrevs' ); + $this->addDeprecation( 'apiwarn-deprecation-deletedrevs', 'action=query&list=deletedrevs' ); + $user = $this->getUser(); $db = $this->getDB(); $params = $this->extractRequestParams( false ); $prop = array_flip( $params['prop'] ); @@ -70,9 +61,6 @@ class ApiQueryDeletedrevs extends ApiQueryBase { if ( isset( $prop['token'] ) ) { $p = $this->getModulePrefix(); - $this->setWarning( - "{$p}prop=token has been deprecated. Please use action=query&meta=tokens instead." - ); } // If we're in a mode that breaks the same-origin policy, no tokens can @@ -105,19 +93,19 @@ class ApiQueryDeletedrevs extends ApiQueryBase { // Ignore namespace and unique due to inability to know whether they were purposely set foreach ( [ 'from', 'to', 'prefix', /*'namespace', 'unique'*/ ] as $p ) { if ( !is_null( $params[$p] ) ) { - $this->dieUsage( "The '{$p}' parameter cannot be used in modes 1 or 2", 'badparams' ); + $this->dieWithError( [ 'apierror-deletedrevs-param-not-1-2', $p ], 'badparams' ); } } } else { foreach ( [ 'start', 'end' ] as $p ) { if ( !is_null( $params[$p] ) ) { - $this->dieUsage( "The {$p} parameter cannot be used in mode 3", 'badparams' ); + $this->dieWithError( [ 'apierror-deletedrevs-param-not-3', $p ], 'badparams' ); } } } if ( !is_null( $params['user'] ) && !is_null( $params['excludeuser'] ) ) { - $this->dieUsage( 'user and excludeuser cannot be used together', 'badparams' ); + $this->dieWithError( 'user and excludeuser cannot be used together', 'badparams' ); } $this->addTables( 'archive' ); @@ -162,12 +150,7 @@ class ApiQueryDeletedrevs extends ApiQueryBase { $this->addFields( [ 'ar_text', 'ar_flags', 'ar_text_id', 'old_text', 'old_flags' ] ); // This also means stricter restrictions - if ( !$user->isAllowedAny( 'undelete', 'deletedtext' ) ) { - $this->dieUsage( - 'You don\'t have permission to view deleted revision content', - 'permissiondenied' - ); - } + $this->checkUserRightsAny( [ 'deletedtext', 'undelete' ] ); } // Check limits $userMax = $fld_content ? ApiBase::LIMIT_SML1 : ApiBase::LIMIT_BIG1; diff --git a/includes/api/ApiQueryDisabled.php b/includes/api/ApiQueryDisabled.php index e1c97e149b8b..9476066dfc0a 100644 --- a/includes/api/ApiQueryDisabled.php +++ b/includes/api/ApiQueryDisabled.php @@ -37,7 +37,7 @@ class ApiQueryDisabled extends ApiQueryBase { public function execute() { - $this->setWarning( "The \"{$this->getModuleName()}\" module has been disabled." ); + $this->addWarning( [ 'apierror-moduledisabled', $this->getModuleName() ] ); } public function getAllowedParams() { diff --git a/includes/api/ApiQueryFilearchive.php b/includes/api/ApiQueryFilearchive.php index 03be491e7cc1..116dbb3d34db 100644 --- a/includes/api/ApiQueryFilearchive.php +++ b/includes/api/ApiQueryFilearchive.php @@ -38,15 +38,10 @@ class ApiQueryFilearchive extends ApiQueryBase { } public function execute() { - $user = $this->getUser(); // Before doing anything at all, let's check permissions - if ( !$user->isAllowed( 'deletedhistory' ) ) { - $this->dieUsage( - 'You don\'t have permission to view deleted file information', - 'permissiondenied' - ); - } + $this->checkUserRightsAny( 'deletedhistory' ); + $user = $this->getUser(); $db = $this->getDB(); $params = $this->extractRequestParams(); @@ -112,13 +107,13 @@ class ApiQueryFilearchive extends ApiQueryBase { if ( $sha1Set ) { $sha1 = strtolower( $params['sha1'] ); if ( !$this->validateSha1Hash( $sha1 ) ) { - $this->dieUsage( 'The SHA1 hash provided is not valid', 'invalidsha1hash' ); + $this->dieWithError( 'apierror-invalidsha1hash' ); } $sha1 = Wikimedia\base_convert( $sha1, 16, 36, 31 ); } elseif ( $sha1base36Set ) { $sha1 = strtolower( $params['sha1base36'] ); if ( !$this->validateSha1Base36Hash( $sha1 ) ) { - $this->dieUsage( 'The SHA1Base36 hash provided is not valid', 'invalidsha1base36hash' ); + $this->dieWithError( 'apierror-invalidsha1base36hash' ); } } if ( $sha1 ) { diff --git a/includes/api/ApiQueryIWBacklinks.php b/includes/api/ApiQueryIWBacklinks.php index 75681077dec7..6e2fb67b8d99 100644 --- a/includes/api/ApiQueryIWBacklinks.php +++ b/includes/api/ApiQueryIWBacklinks.php @@ -51,7 +51,14 @@ class ApiQueryIWBacklinks extends ApiQueryGeneratorBase { $params = $this->extractRequestParams(); if ( isset( $params['title'] ) && !isset( $params['prefix'] ) ) { - $this->dieUsageMsg( [ 'missingparam', 'prefix' ] ); + $this->dieWithError( + [ + 'apierror-invalidparammix-mustusewith', + $this->encodeParamName( 'title' ), + $this->encodeParamName( 'prefix' ), + ], + 'invalidparammix' + ); } if ( !is_null( $params['continue'] ) ) { diff --git a/includes/api/ApiQueryIWLinks.php b/includes/api/ApiQueryIWLinks.php index 6d9c2ca86115..cfd990b21301 100644 --- a/includes/api/ApiQueryIWLinks.php +++ b/includes/api/ApiQueryIWLinks.php @@ -45,7 +45,14 @@ class ApiQueryIWLinks extends ApiQueryBase { $prop = array_flip( (array)$params['prop'] ); if ( isset( $params['title'] ) && !isset( $params['prefix'] ) ) { - $this->dieUsageMsg( [ 'missingparam', 'prefix' ] ); + $this->dieWithError( + [ + 'apierror-invalidparammix-mustusewith', + $this->encodeParamName( 'title' ), + $this->encodeParamName( 'prefix' ), + ], + 'invalidparammix' + ); } // Handle deprecated param diff --git a/includes/api/ApiQueryImageInfo.php b/includes/api/ApiQueryImageInfo.php index d1fcfa3f07c9..0bbfad3a836b 100644 --- a/includes/api/ApiQueryImageInfo.php +++ b/includes/api/ApiQueryImageInfo.php @@ -280,8 +280,7 @@ class ApiQueryImageInfo extends ApiQueryBase { $h = $image->getHandler(); if ( !$h ) { - $this->setWarning( 'Could not create thumbnail because ' . - $image->getName() . ' does not have an associated image handler' ); + $this->addWarning( [ 'apiwarn-nothumb-noimagehandler', wfEscapeWikiText( $image->getName() ) ] ); return $thumbParams; } @@ -292,23 +291,24 @@ class ApiQueryImageInfo extends ApiQueryBase { // we could still render the image using width and height parameters, // and this type of thing could happen between different versions of // handlers. - $this->setWarning( "Could not parse {$p}urlparam for " . $image->getName() - . '. Using only width and height' ); + $this->addWarning( [ 'apiwarn-badurlparam', $p, wfEscapeWikiText( $image->getName() ) ] ); $this->checkParameterNormalise( $image, $thumbParams ); return $thumbParams; } if ( isset( $paramList['width'] ) && isset( $thumbParams['width'] ) ) { if ( intval( $paramList['width'] ) != intval( $thumbParams['width'] ) ) { - $this->setWarning( "Ignoring width value set in {$p}urlparam ({$paramList['width']}) " - . "in favor of width value derived from {$p}urlwidth/{$p}urlheight " - . "({$thumbParams['width']})" ); + $this->addWarning( + [ 'apiwarn-urlparamwidth', $p, $paramList['width'], $thumbParams['width'] ] + ); } } foreach ( $paramList as $name => $value ) { if ( !$h->validateParam( $name, $value ) ) { - $this->dieUsage( "Invalid value for {$p}urlparam ($name=$value)", 'urlparam' ); + $this->dieWithError( + [ 'apierror-invalidurlparam', $p, wfEscapeWikiText( $name ), wfEscapeWikiText( $value ) ] + ); } } @@ -337,8 +337,7 @@ class ApiQueryImageInfo extends ApiQueryBase { // in the actual normalised version, only if we can actually normalise them, // so we use the functions scope to throw away the normalisations. if ( !$h->normaliseParams( $image, $finalParams ) ) { - $this->dieUsage( 'Could not normalise image parameters for ' . - $image->getName(), 'urlparamnormal' ); + $this->dieWithError( [ 'apierror-urlparamnormal', wfEscapeWikiText( $image->getName() ) ] ); } } diff --git a/includes/api/ApiQueryImages.php b/includes/api/ApiQueryImages.php index e04d8c888f1f..ae6f5bf564aa 100644 --- a/includes/api/ApiQueryImages.php +++ b/includes/api/ApiQueryImages.php @@ -90,7 +90,7 @@ class ApiQueryImages extends ApiQueryGeneratorBase { foreach ( $params['images'] as $img ) { $title = Title::newFromText( $img ); if ( !$title || $title->getNamespace() != NS_FILE ) { - $this->setWarning( "\"$img\" is not a file" ); + $this->addWarning( [ 'apiwarn-notfile', wfEscapeWikiText( $img ) ] ); } else { $images[] = $title->getDBkey(); } diff --git a/includes/api/ApiQueryInfo.php b/includes/api/ApiQueryInfo.php index d28702053625..fd6503801f77 100644 --- a/includes/api/ApiQueryInfo.php +++ b/includes/api/ApiQueryInfo.php @@ -427,7 +427,7 @@ class ApiQueryInfo extends ApiQueryBase { foreach ( $this->params['token'] as $t ) { $val = call_user_func( $tokenFunctions[$t], $pageid, $title ); if ( $val === false ) { - $this->setWarning( "Action '$t' is not allowed for the current user" ); + $this->addWarning( [ 'apiwarn-tokennotallowed', $t ] ); } else { $pageInfo[$t . 'token'] = $val; } diff --git a/includes/api/ApiQueryLangBacklinks.php b/includes/api/ApiQueryLangBacklinks.php index a6153de96102..8d5b5f3ea6a2 100644 --- a/includes/api/ApiQueryLangBacklinks.php +++ b/includes/api/ApiQueryLangBacklinks.php @@ -51,7 +51,14 @@ class ApiQueryLangBacklinks extends ApiQueryGeneratorBase { $params = $this->extractRequestParams(); if ( isset( $params['title'] ) && !isset( $params['lang'] ) ) { - $this->dieUsageMsg( [ 'missingparam', 'lang' ] ); + $this->dieWithError( + [ + 'apierror-invalidparammix-mustusewith', + $this->encodeParamName( 'title' ), + $this->encodeParamName( 'lang' ) + ], + 'nolang' + ); } if ( !is_null( $params['continue'] ) ) { diff --git a/includes/api/ApiQueryLangLinks.php b/includes/api/ApiQueryLangLinks.php index 67f2c9eceaa6..55e3c85265a5 100644 --- a/includes/api/ApiQueryLangLinks.php +++ b/includes/api/ApiQueryLangLinks.php @@ -44,7 +44,14 @@ class ApiQueryLangLinks extends ApiQueryBase { $prop = array_flip( (array)$params['prop'] ); if ( isset( $params['title'] ) && !isset( $params['lang'] ) ) { - $this->dieUsageMsg( [ 'missingparam', 'lang' ] ); + $this->dieWithError( + [ + 'apierror-invalidparammix-mustusewith', + $this->encodeParamName( 'title' ), + $this->encodeParamName( 'lang' ), + ], + 'invalidparammix' + ); } // Handle deprecated param diff --git a/includes/api/ApiQueryLinks.php b/includes/api/ApiQueryLinks.php index 6e5239f7b932..e9ae132df723 100644 --- a/includes/api/ApiQueryLinks.php +++ b/includes/api/ApiQueryLinks.php @@ -94,7 +94,7 @@ class ApiQueryLinks extends ApiQueryGeneratorBase { foreach ( $params[$this->titlesParam] as $t ) { $title = Title::newFromText( $t ); if ( !$title ) { - $this->setWarning( "\"$t\" is not a valid title" ); + $this->addWarning( [ 'apiwarn-invalidtitle', wfEscapeWikiText( $t ) ] ); } else { $lb->addObj( $title ); } diff --git a/includes/api/ApiQueryLogEvents.php b/includes/api/ApiQueryLogEvents.php index 122594d1753f..2dcd0b4f8838 100644 --- a/includes/api/ApiQueryLogEvents.php +++ b/includes/api/ApiQueryLogEvents.php @@ -121,10 +121,10 @@ class ApiQueryLogEvents extends ApiQueryBase { } if ( !$valid ) { - $valueName = $this->encodeParamName( 'action' ); - $this->dieUsage( - "Unrecognized value for parameter '$valueName': {$logAction}", - "unknown_$valueName" + $encParamName = $this->encodeParamName( 'action' ); + $this->dieWithError( + [ 'apierror-unrecognizedvalue', $encParamName, wfEscapeWikiText( $logAction ) ], + "unknown_$encParamName" ); } @@ -173,7 +173,7 @@ class ApiQueryLogEvents extends ApiQueryBase { if ( !is_null( $title ) ) { $titleObj = Title::newFromText( $title ); if ( is_null( $titleObj ) ) { - $this->dieUsage( "Bad title value '$title'", 'param_title' ); + $this->dieWithError( [ 'apierror-invalidtitle', wfEscapeWikiText( $title ) ] ); } $this->addWhereFld( 'log_namespace', $titleObj->getNamespace() ); $this->addWhereFld( 'log_title', $titleObj->getDBkey() ); @@ -187,12 +187,12 @@ class ApiQueryLogEvents extends ApiQueryBase { if ( !is_null( $prefix ) ) { if ( $this->getConfig()->get( 'MiserMode' ) ) { - $this->dieUsage( 'Prefix search disabled in Miser Mode', 'prefixsearchdisabled' ); + $this->dieWithError( 'apierror-prefixsearchdisabled' ); } $title = Title::newFromText( $prefix ); if ( is_null( $title ) ) { - $this->dieUsage( "Bad title value '$prefix'", 'param_prefix' ); + $this->dieWithError( [ 'apierror-invalidtitle', wfEscapeWikiText( $prefix ) ] ); } $this->addWhereFld( 'log_namespace', $title->getNamespace() ); $this->addWhere( 'log_title ' . $db->buildLike( $title->getDBkey(), $db->anyString() ) ); diff --git a/includes/api/ApiQueryMyStashedFiles.php b/includes/api/ApiQueryMyStashedFiles.php index 0c70a8a4ef8e..1324f2ff498a 100644 --- a/includes/api/ApiQueryMyStashedFiles.php +++ b/includes/api/ApiQueryMyStashedFiles.php @@ -36,7 +36,7 @@ class ApiQueryMyStashedFiles extends ApiQueryBase { $user = $this->getUser(); if ( $user->isAnon() ) { - $this->dieUsage( 'The upload stash is only available to logged-in users.', 'stashnotloggedin' ); + $this->dieWithError( 'apierror-mustbeloggedin-uploadstash', 'stashnotloggedin' ); } // Note: If user is logged in but cannot upload, they can still see diff --git a/includes/api/ApiQueryQueryPage.php b/includes/api/ApiQueryQueryPage.php index 9ba757c0784c..908cdee667b9 100644 --- a/includes/api/ApiQueryQueryPage.php +++ b/includes/api/ApiQueryQueryPage.php @@ -62,7 +62,7 @@ class ApiQueryQueryPage extends ApiQueryGeneratorBase { /** @var $qp QueryPage */ $qp = new $this->qpMap[$params['page']](); if ( !$qp->userCanExecute( $this->getUser() ) ) { - $this->dieUsageMsg( 'specialpage-cantexecute' ); + $this->dieWithError( 'apierror-specialpage-cantexecute' ); } $r = [ 'name' => $params['page'] ]; diff --git a/includes/api/ApiQueryRecentChanges.php b/includes/api/ApiQueryRecentChanges.php index 8b11dc2a47d2..8d149274fd5d 100644 --- a/includes/api/ApiQueryRecentChanges.php +++ b/includes/api/ApiQueryRecentChanges.php @@ -195,7 +195,7 @@ class ApiQueryRecentChanges extends ApiQueryGeneratorBase { || ( isset( $show['patrolled'] ) && isset( $show['unpatrolled'] ) ) || ( isset( $show['!patrolled'] ) && isset( $show['unpatrolled'] ) ) ) { - $this->dieUsageMsg( 'show' ); + $this->dieWithError( 'apierror-show' ); } // Check permissions @@ -204,10 +204,7 @@ class ApiQueryRecentChanges extends ApiQueryGeneratorBase { || isset( $show['unpatrolled'] ) ) { if ( !$user->useRCPatrol() && !$user->useNPPatrol() ) { - $this->dieUsage( - 'You need patrol or patrolmarks permission to request the patrolled flag', - 'permissiondenied' - ); + $this->dieWithError( 'apierror-permissiondenied-patrolflag', 'permissiondenied' ); } } @@ -239,9 +236,7 @@ class ApiQueryRecentChanges extends ApiQueryGeneratorBase { ); } - if ( !is_null( $params['user'] ) && !is_null( $params['excludeuser'] ) ) { - $this->dieUsage( 'user and excludeuser cannot be used together', 'user-excludeuser' ); - } + $this->requireMaxOneParameter( $params, 'user', 'excludeuser' ); if ( !is_null( $params['user'] ) ) { $this->addWhereFld( 'rc_user_text', $params['user'] ); @@ -274,10 +269,7 @@ class ApiQueryRecentChanges extends ApiQueryGeneratorBase { $this->initProperties( $prop ); if ( $this->fld_patrolled && !$user->useRCPatrol() && !$user->useNPPatrol() ) { - $this->dieUsage( - 'You need patrol or patrolmarks permission to request the patrolled flag', - 'permissiondenied' - ); + $this->dieWithError( 'apierror-permissiondenied-patrolflag', 'permissiondenied' ); } /* Add fields to our query if they are specified as a needed parameter. */ @@ -571,7 +563,7 @@ class ApiQueryRecentChanges extends ApiQueryGeneratorBase { $val = call_user_func( $tokenFunctions[$t], $row->rc_cur_id, $title, RecentChange::newFromRow( $row ) ); if ( $val === false ) { - $this->setWarning( "Action '$t' is not allowed for the current user" ); + $this->addWarning( [ 'apiwarn-tokennotallowed', $t ] ); } else { $vals[$t . 'token'] = $val; } diff --git a/includes/api/ApiQueryRevisions.php b/includes/api/ApiQueryRevisions.php index 3259927a23b6..48f604664fbb 100644 --- a/includes/api/ApiQueryRevisions.php +++ b/includes/api/ApiQueryRevisions.php @@ -110,19 +110,14 @@ class ApiQueryRevisions extends ApiQueryRevisionsBase { } if ( $revCount > 0 && $enumRevMode ) { - $this->dieUsage( - 'The revids= parameter may not be used with the list options ' . - '(limit, startid, endid, dirNewer, start, end).', - 'revids' + $this->dieWithError( + [ 'apierror-revisions-nolist', $this->getModulePrefix() ], 'invalidparammix' ); } if ( $pageCount > 1 && $enumRevMode ) { - $this->dieUsage( - 'titles, pageids or a generator was used to supply multiple pages, ' . - 'but the limit, startid, endid, dirNewer, user, excludeuser, start ' . - 'and end parameters may only be used on a single page.', - 'multpages' + $this->dieWithError( + [ 'apierror-revisions-singlepage', $this->getModulePrefix() ], 'invalidparammix' ); } @@ -170,14 +165,19 @@ class ApiQueryRevisions extends ApiQueryRevisionsBase { if ( $this->fetchContent ) { // For each page we will request, the user must have read rights for that page $user = $this->getUser(); + $status = Status::newGood(); /** @var $title Title */ foreach ( $pageSet->getGoodTitles() as $title ) { if ( !$title->userCan( 'read', $user ) ) { - $this->dieUsage( - 'The current user is not allowed to read ' . $title->getPrefixedText(), - 'accessdenied' ); + $status->fatal( ApiMessage::create( + [ 'apierror-cannotviewtitle', wfEscapeWikiText( $title->getPrefixedText() ) ], + 'accessdenied' + ) ); } } + if ( !$status->isGood() ) { + $this->dieStatus( $status ); + } $this->addTables( 'text' ); $this->addJoinConds( @@ -201,17 +201,9 @@ class ApiQueryRevisions extends ApiQueryRevisionsBase { // page_timestamp or usertext_timestamp if we have an IP rvuser // This is mostly to prevent parameter errors (and optimize SQL?) - if ( $params['startid'] !== null && $params['start'] !== null ) { - $this->dieUsage( 'start and startid cannot be used together', 'badparams' ); - } - - if ( $params['endid'] !== null && $params['end'] !== null ) { - $this->dieUsage( 'end and endid cannot be used together', 'badparams' ); - } - - if ( $params['user'] !== null && $params['excludeuser'] !== null ) { - $this->dieUsage( 'user and excludeuser cannot be used together', 'badparams' ); - } + $this->requireMaxOneParameter( $params, 'startid', 'start' ); + $this->requireMaxOneParameter( $params, 'endid', 'end' ); + $this->requireMaxOneParameter( $params, 'user', 'excludeuser' ); if ( $params['continue'] !== null ) { $cont = explode( '|', $params['continue'] ); @@ -344,7 +336,7 @@ class ApiQueryRevisions extends ApiQueryRevisionsBase { foreach ( $this->token as $t ) { $val = call_user_func( $tokenFunctions[$t], $title->getArticleID(), $title, $revision ); if ( $val === false ) { - $this->setWarning( "Action '$t' is not allowed for the current user" ); + $this->addWarning( [ 'apiwarn-tokennotallowed', $t ] ); } else { $rev[$t . 'token'] = $val; } diff --git a/includes/api/ApiQueryRevisionsBase.php b/includes/api/ApiQueryRevisionsBase.php index 266d6999bac0..696ec878670c 100644 --- a/includes/api/ApiQueryRevisionsBase.php +++ b/includes/api/ApiQueryRevisionsBase.php @@ -70,10 +70,7 @@ abstract class ApiQueryRevisionsBase extends ApiQueryGeneratorBase { && $params['diffto'] != 'prev' && $params['diffto'] != 'next' ) { $p = $this->getModulePrefix(); - $this->dieUsage( - "{$p}diffto must be set to a non-negative number, \"prev\", \"next\" or \"cur\"", - 'diffto' - ); + $this->dieWithError( [ 'apierror-baddiffto', $p ], 'diffto' ); } // Check whether the revision exists and is readable, // DifferenceEngine returns a rather ambiguous empty @@ -81,10 +78,10 @@ abstract class ApiQueryRevisionsBase extends ApiQueryGeneratorBase { if ( $params['diffto'] != 0 ) { $difftoRev = Revision::newFromId( $params['diffto'] ); if ( !$difftoRev ) { - $this->dieUsageMsg( [ 'nosuchrevid', $params['diffto'] ] ); + $this->dieWithError( [ 'apierror-nosuchrevid', $params['diffto'] ] ); } if ( !$difftoRev->userCan( Revision::DELETED_TEXT, $this->getUser() ) ) { - $this->setWarning( "Couldn't diff to r{$difftoRev->getId()}: content is hidden" ); + $this->addWarning( [ 'apiwarn-difftohidden', $difftoRev->getId() ] ); $params['diffto'] = null; } } @@ -262,8 +259,12 @@ abstract class ApiQueryRevisionsBase extends ApiQueryGeneratorBase { if ( $content && $this->section !== false ) { $content = $content->getSection( $this->section, false ); if ( !$content ) { - $this->dieUsage( - "There is no section {$this->section} in r" . $revision->getId(), + $this->dieWithError( + [ + 'apierror-nosuchsection-what', + wfEscapeWikiText( $this->section ), + $this->msg( 'revid', $revision->getId() ) + ], 'nosuchsection' ); } @@ -294,9 +295,14 @@ abstract class ApiQueryRevisionsBase extends ApiQueryGeneratorBase { $vals['parsetree'] = $xml; } else { $vals['badcontentformatforparsetree'] = true; - $this->setWarning( 'Conversion to XML is supported for wikitext only, ' . - $title->getPrefixedDBkey() . - ' uses content model ' . $content->getModel() ); + $this->addWarning( + [ + 'apierror-parsetree-notwikitext-title', + wfEscapeWikiText( $title->getPrefixedText() ), + $content->getModel() + ], + 'parsetree-notwikitext' + ); } } } @@ -315,9 +321,11 @@ abstract class ApiQueryRevisionsBase extends ApiQueryGeneratorBase { ParserOptions::newFromContext( $this->getContext() ) ); } else { - $this->setWarning( 'Template expansion is supported for wikitext only, ' . - $title->getPrefixedDBkey() . - ' uses content model ' . $content->getModel() ); + $this->addWarning( [ + 'apierror-templateexpansion-notwikitext', + wfEscapeWikiText( $title->getPrefixedText() ), + $content->getModel() + ] ); $vals['badcontentformat'] = true; $text = false; } @@ -336,9 +344,8 @@ abstract class ApiQueryRevisionsBase extends ApiQueryGeneratorBase { $model = $content->getModel(); if ( !$content->isSupportedFormat( $format ) ) { - $name = $title->getPrefixedDBkey(); - $this->setWarning( "The requested format {$this->contentFormat} is not " . - "supported for content model $model used by $name" ); + $name = wfEscapeWikiText( $title->getPrefixedText() ); + $this->addWarning( [ 'apierror-badformat', $this->contentFormat, $model, $name ] ); $vals['badcontentformat'] = true; $text = false; } else { @@ -370,9 +377,8 @@ abstract class ApiQueryRevisionsBase extends ApiQueryGeneratorBase { if ( $this->contentFormat && !ContentHandler::getForModelID( $model )->isSupportedFormat( $this->contentFormat ) ) { - $name = $title->getPrefixedDBkey(); - $this->setWarning( "The requested format {$this->contentFormat} is not " . - "supported for content model $model used by $name" ); + $name = wfEscapeWikiText( $title->getPrefixedText() ); + $this->addWarning( [ 'apierror-badformat', $this->contentFormat, $model, $name ] ); $vals['diff']['badcontentformat'] = true; $engine = null; } else { diff --git a/includes/api/ApiQuerySearch.php b/includes/api/ApiQuerySearch.php index 9962d5ec2031..64bc43f07b69 100644 --- a/includes/api/ApiQuerySearch.php +++ b/includes/api/ApiQuerySearch.php @@ -64,12 +64,14 @@ class ApiQuerySearch extends ApiQueryGeneratorBase { // Deprecated parameters if ( isset( $prop['hasrelated'] ) ) { - $this->logFeatureUsage( 'action=search&srprop=hasrelated' ); - $this->setWarning( 'srprop=hasrelated has been deprecated' ); + $this->addDeprecation( + [ 'apiwarn-deprecation-parameter', 'srprop=hasrelated' ], 'action=search&srprop=hasrelated' + ); } if ( isset( $prop['score'] ) ) { - $this->logFeatureUsage( 'action=search&srprop=score' ); - $this->setWarning( 'srprop=score has been deprecated' ); + $this->addDeprecation( + [ 'apiwarn-deprecation-parameter', 'srprop=score' ], 'action=search&srprop=score' + ); } // Create search engine instance and set options @@ -122,10 +124,10 @@ class ApiQuerySearch extends ApiQueryGeneratorBase { $status ); } else { - $this->dieUsage( $status->getWikiText( false, false, 'en' ), 'search-error' ); + $this->dieStatus( $status ); } } elseif ( is_null( $matches ) ) { - $this->dieUsage( "{$what} search is disabled", "search-{$what}-disabled" ); + $this->dieWithError( [ 'apierror-searchdisabled', $what ], "search-{$what}-disabled" ); } if ( $resultPageSet === null ) { diff --git a/includes/api/ApiQuerySiteinfo.php b/includes/api/ApiQuerySiteinfo.php index 19e0c939e912..6fc6aa370c00 100644 --- a/includes/api/ApiQuerySiteinfo.php +++ b/includes/api/ApiQuerySiteinfo.php @@ -447,10 +447,7 @@ class ApiQuerySiteinfo extends ApiQueryBase { $showHostnames = $this->getConfig()->get( 'ShowHostnames' ); if ( $includeAll ) { if ( !$showHostnames ) { - $this->dieUsage( - 'Cannot view all servers info unless $wgShowHostnames is true', - 'includeAllDenied' - ); + $this->dieWithError( 'apierror-siteinfo-includealldenied', 'includeAllDenied' ); } $lags = $lb->getLagTimes(); diff --git a/includes/api/ApiQueryStashImageInfo.php b/includes/api/ApiQueryStashImageInfo.php index b039a1ec45d9..981cb0948359 100644 --- a/includes/api/ApiQueryStashImageInfo.php +++ b/includes/api/ApiQueryStashImageInfo.php @@ -33,7 +33,7 @@ class ApiQueryStashImageInfo extends ApiQueryImageInfo { public function execute() { if ( !$this->getUser()->isLoggedIn() ) { - $this->dieUsage( 'You must be logged-in to have an upload stash', 'notloggedin' ); + $this->dieWithError( 'apierror-mustbeloggedin-uploadstash', 'notloggedin' ); } $params = $this->extractRequestParams(); @@ -45,9 +45,7 @@ class ApiQueryStashImageInfo extends ApiQueryImageInfo { $result = $this->getResult(); - if ( !$params['filekey'] && !$params['sessionkey'] ) { - $this->dieUsage( 'One of filekey or sessionkey must be supplied', 'nofilekey' ); - } + $this->requireAtLeastOneParameter( $params, 'filekey', 'sessionkey' ); // Alias sessionkey to filekey, but give an existing filekey precedence. if ( !$params['filekey'] && $params['sessionkey'] ) { @@ -65,10 +63,11 @@ class ApiQueryStashImageInfo extends ApiQueryImageInfo { $result->addIndexedTagName( [ 'query', $this->getModuleName() ], $modulePrefix ); } // @todo Update exception handling here to understand current getFile exceptions + // @todo Internationalize the exceptions } catch ( UploadStashFileNotFoundException $e ) { - $this->dieUsage( 'File not found: ' . $e->getMessage(), 'invalidsessiondata' ); + $this->dieWithError( [ 'apierror-stashedfilenotfound', wfEscapeWikiText( $e->getMessage() ) ] ); } catch ( UploadStashBadPathException $e ) { - $this->dieUsage( 'Bad path: ' . $e->getMessage(), 'invalidsessiondata' ); + $this->dieWithError( [ 'apierror-stashpathinvalid', wfEscapeWikiText( $e->getMessage() ) ] ); } } diff --git a/includes/api/ApiQueryTokens.php b/includes/api/ApiQueryTokens.php index de5a377417d3..5b700dbc9c0d 100644 --- a/includes/api/ApiQueryTokens.php +++ b/includes/api/ApiQueryTokens.php @@ -40,7 +40,7 @@ class ApiQueryTokens extends ApiQueryBase { ]; if ( $this->lacksSameOriginSecurity() ) { - $this->setWarning( 'Tokens may not be obtained when the same-origin policy is not applied' ); + $this->addWarning( [ 'apiwarn-tokens-origin' ] ); return; } diff --git a/includes/api/ApiQueryUserContributions.php b/includes/api/ApiQueryUserContributions.php index b85bec4899c5..b6d871b81797 100644 --- a/includes/api/ApiQueryUserContributions.php +++ b/includes/api/ApiQueryUserContributions.php @@ -78,11 +78,17 @@ class ApiQueryContributions extends ApiQueryBase { $this->params['user'] = [ $this->params['user'] ]; } if ( !count( $this->params['user'] ) ) { - $this->dieUsage( 'User parameter may not be empty.', 'param_user' ); + $encParamName = $this->encodeParamName( 'user' ); + $this->dieWithError( + [ 'apierror-paramempty', $encParamName ], "paramempty_$encParamName" + ); } foreach ( $this->params['user'] as $u ) { if ( is_null( $u ) || $u === '' ) { - $this->dieUsage( 'User parameter may not be empty', 'param_user' ); + $encParamName = $this->encodeParamName( 'user' ); + $this->dieWithError( + [ 'apierror-paramempty', $encParamName ], "paramempty_$encParamName" + ); } if ( User::isIP( $u ) ) { @@ -91,7 +97,10 @@ class ApiQueryContributions extends ApiQueryBase { } else { $name = User::getCanonicalName( $u, 'valid' ); if ( $name === false ) { - $this->dieUsage( "User name {$u} is not valid", 'param_user' ); + $encParamName = $this->encodeParamName( 'user' ); + $this->dieWithError( + [ 'apierror-baduser', $encParamName, wfEscapeWikiText( $u ) ], "baduser_$encParamName" + ); } $this->usernames[] = $name; } @@ -254,7 +263,7 @@ class ApiQueryContributions extends ApiQueryBase { || ( isset( $show['top'] ) && isset( $show['!top'] ) ) || ( isset( $show['new'] ) && isset( $show['!new'] ) ) ) { - $this->dieUsageMsg( 'show' ); + $this->dieWithError( 'apierror-show' ); } $this->addWhereIf( 'rev_minor_edit = 0', isset( $show['!minor'] ) ); @@ -285,10 +294,7 @@ class ApiQueryContributions extends ApiQueryBase { $this->fld_patrolled ) { if ( !$user->useRCPatrol() && !$user->useNPPatrol() ) { - $this->dieUsage( - 'You need the patrol right to request the patrolled flag', - 'permissiondenied' - ); + $this->dieWithError( 'apierror-permissiondenied-patrolflag', 'permissiondenied' ); } // Use a redundant join condition on both diff --git a/includes/api/ApiQueryUserInfo.php b/includes/api/ApiQueryUserInfo.php index d3cd0c48c41d..3b604786ab55 100644 --- a/includes/api/ApiQueryUserInfo.php +++ b/includes/api/ApiQueryUserInfo.php @@ -170,8 +170,13 @@ class ApiQueryUserInfo extends ApiQueryBase { if ( isset( $this->prop['preferencestoken'] ) ) { $p = $this->getModulePrefix(); - $this->setWarning( - "{$p}prop=preferencestoken has been deprecated. Please use action=query&meta=tokens instead." + $this->addDeprecation( + [ + 'apiwarn-deprecation-withreplacement', + "{$p}prop=preferencestoken", + 'action=query&meta=tokens', + ], + "meta=userinfo&{$p}prop=preferencestoken" ); } if ( isset( $this->prop['preferencestoken'] ) && diff --git a/includes/api/ApiQueryUsers.php b/includes/api/ApiQueryUsers.php index 9b45b9192392..65d3797a7471 100644 --- a/includes/api/ApiQueryUsers.php +++ b/includes/api/ApiQueryUsers.php @@ -226,7 +226,7 @@ class ApiQueryUsers extends ApiQueryBase { foreach ( $params['token'] as $t ) { $val = call_user_func( $tokenFunctions[$t], $user ); if ( $val === false ) { - $this->setWarning( "Action '$t' is not allowed for the current user" ); + $this->addWarning( [ 'apiwarn-tokennotallowed', $t ] ); } else { $data[$name][$t . 'token'] = $val; } @@ -253,7 +253,7 @@ class ApiQueryUsers extends ApiQueryBase { foreach ( $params['token'] as $t ) { $val = call_user_func( $tokenFunctions[$t], $iwUser ); if ( $val === false ) { - $this->setWarning( "Action '$t' is not allowed for the current user" ); + $this->addWarning( [ 'apiwarn-tokennotallowed', $t ] ); } else { $data[$u][$t . 'token'] = $val; } diff --git a/includes/api/ApiQueryWatchlist.php b/includes/api/ApiQueryWatchlist.php index 42ea55dd7055..6b5ceb703fc1 100644 --- a/includes/api/ApiQueryWatchlist.php +++ b/includes/api/ApiQueryWatchlist.php @@ -82,7 +82,7 @@ class ApiQueryWatchlist extends ApiQueryGeneratorBase { if ( $this->fld_patrol ) { if ( !$user->useRCPatrol() && !$user->useNPPatrol() ) { - $this->dieUsage( 'patrol property is not available', 'patrol' ); + $this->dieWithError( 'apierror-permissiondenied-patrolflag', 'patrol' ); } } } @@ -134,7 +134,7 @@ class ApiQueryWatchlist extends ApiQueryGeneratorBase { /* Check for conflicting parameters. */ if ( $this->showParamsConflicting( $show ) ) { - $this->dieUsageMsg( 'show' ); + $this->dieWithError( 'apierror-show' ); } // Check permissions. @@ -142,10 +142,7 @@ class ApiQueryWatchlist extends ApiQueryGeneratorBase { || isset( $show[WatchedItemQueryService::FILTER_NOT_PATROLLED] ) ) { if ( !$user->useRCPatrol() && !$user->useNPPatrol() ) { - $this->dieUsage( - 'You need the patrol right to request the patrolled flag', - 'permissiondenied' - ); + $this->dieWithError( 'apierror-permissiondenied-patrolflag', 'permissiondenied' ); } } @@ -160,9 +157,7 @@ class ApiQueryWatchlist extends ApiQueryGeneratorBase { } } - if ( !is_null( $params['user'] ) && !is_null( $params['excludeuser'] ) ) { - $this->dieUsage( 'user and excludeuser cannot be used together', 'user-excludeuser' ); - } + $this->requireMaxOneParameter( $params, 'user', 'excludeuser' ); if ( !is_null( $params['user'] ) ) { $options['onlyByUser'] = $params['user']; } diff --git a/includes/api/ApiQueryWatchlistRaw.php b/includes/api/ApiQueryWatchlistRaw.php index 806861e8009c..a1078a5d4824 100644 --- a/includes/api/ApiQueryWatchlistRaw.php +++ b/includes/api/ApiQueryWatchlistRaw.php @@ -60,7 +60,7 @@ class ApiQueryWatchlistRaw extends ApiQueryGeneratorBase { if ( isset( $show[WatchedItemQueryService::FILTER_CHANGED] ) && isset( $show[WatchedItemQueryService::FILTER_NOT_CHANGED] ) ) { - $this->dieUsageMsg( 'show' ); + $this->dieWithError( 'apierror-show' ); } $options = []; diff --git a/includes/api/ApiRemoveAuthenticationData.php b/includes/api/ApiRemoveAuthenticationData.php index d72c8a407ef6..359d045fdd32 100644 --- a/includes/api/ApiRemoveAuthenticationData.php +++ b/includes/api/ApiRemoveAuthenticationData.php @@ -45,7 +45,7 @@ class ApiRemoveAuthenticationData extends ApiBase { public function execute() { if ( !$this->getUser()->isLoggedIn() ) { - $this->dieUsage( 'Must be logged in to remove authentication data', 'notloggedin' ); + $this->dieWithError( 'apierror-mustbeloggedin-removeauth', 'notloggedin' ); } $params = $this->extractRequestParams(); @@ -67,7 +67,7 @@ class ApiRemoveAuthenticationData extends ApiBase { } ); if ( count( $reqs ) !== 1 ) { - $this->dieUsage( 'Failed to create change request', 'badrequest' ); + $this->dieWithError( 'apierror-changeauth-norequest', 'badrequest' ); } $req = reset( $reqs ); diff --git a/includes/api/ApiResetPassword.php b/includes/api/ApiResetPassword.php index 2d7f5dff2335..b5fa8ed859e0 100644 --- a/includes/api/ApiResetPassword.php +++ b/includes/api/ApiResetPassword.php @@ -52,7 +52,7 @@ class ApiResetPassword extends ApiBase { public function execute() { if ( !$this->hasAnyRoutes() ) { - $this->dieUsage( 'No password reset routes are available.', 'moduledisabled' ); + $this->dieWithError( 'apihelp-resetpassword-description-noroutes', 'moduledisabled' ); } $params = $this->extractRequestParams() + [ diff --git a/includes/api/ApiResult.php b/includes/api/ApiResult.php index 6e27fc892044..61a4394e74d3 100644 --- a/includes/api/ApiResult.php +++ b/includes/api/ApiResult.php @@ -413,11 +413,9 @@ class ApiResult implements ApiSerializable { $newsize = $this->size + self::size( $value ); if ( $this->maxSize !== false && $newsize > $this->maxSize ) { - /// @todo Add i18n message when replacing calls to ->setWarning() - $msg = new ApiRawMessage( 'This result was truncated because it would otherwise ' . - 'be larger than the limit of $1 bytes', 'truncatedresult' ); - $msg->numParams( $this->maxSize ); - $this->errorFormatter->addWarning( 'result', $msg ); + $this->errorFormatter->addWarning( + 'result', [ 'apiwarn-truncatedresult', Message::numParam( $this->maxSize ) ] + ); return false; } $this->size = $newsize; diff --git a/includes/api/ApiRevisionDelete.php b/includes/api/ApiRevisionDelete.php index ed9fba27c01e..0251bdbddd13 100644 --- a/includes/api/ApiRevisionDelete.php +++ b/includes/api/ApiRevisionDelete.php @@ -36,24 +36,22 @@ class ApiRevisionDelete extends ApiBase { $params = $this->extractRequestParams(); $user = $this->getUser(); - if ( !$user->isAllowed( RevisionDeleter::getRestriction( $params['type'] ) ) ) { - $this->dieUsageMsg( 'badaccess-group0' ); - } + $this->checkUserRightsAny( RevisionDeleter::getRestriction( $params['type'] ) ); if ( $user->isBlocked() ) { $this->dieBlocked( $user->getBlock() ); } if ( !$params['ids'] ) { - $this->dieUsage( "At least one value is required for 'ids'", 'badparams' ); + $this->dieWithError( [ 'apierror-paramempty', 'ids' ], 'paramempty_ids' ); } $hide = $params['hide'] ?: []; $show = $params['show'] ?: []; if ( array_intersect( $hide, $show ) ) { - $this->dieUsage( "Mutually exclusive values for 'hide' and 'show'", 'badparams' ); + $this->dieWithError( 'apierror-revdel-mutuallyexclusive', 'badparams' ); } elseif ( !$hide && !$show ) { - $this->dieUsage( "At least one value is required for 'hide' or 'show'", 'badparams' ); + $this->dieWithError( 'apierror-revdel-paramneeded', 'badparams' ); } $bits = [ 'content' => RevisionDeleter::getRevdelConstant( $params['type'] ), @@ -72,9 +70,7 @@ class ApiRevisionDelete extends ApiBase { } if ( $params['suppress'] === 'yes' ) { - if ( !$user->isAllowed( 'suppressrevision' ) ) { - $this->dieUsageMsg( 'badaccess-group0' ); - } + $this->checkUserRightsAny( 'suppressrevision' ); $bitfield[Revision::DELETED_RESTRICTED] = 1; } elseif ( $params['suppress'] === 'no' ) { $bitfield[Revision::DELETED_RESTRICTED] = 0; @@ -88,7 +84,7 @@ class ApiRevisionDelete extends ApiBase { } $targetObj = RevisionDeleter::suggestTarget( $params['type'], $targetObj, $params['ids'] ); if ( $targetObj === null ) { - $this->dieUsage( 'A target title is required for this RevDel type', 'needtarget' ); + $this->dieWithError( [ 'apierror-revdel-needtarget' ], 'needtarget' ); } $list = RevisionDeleter::createList( diff --git a/includes/api/ApiRollback.php b/includes/api/ApiRollback.php index b9911da1385b..c8020872311b 100644 --- a/includes/api/ApiRollback.php +++ b/includes/api/ApiRollback.php @@ -69,24 +69,8 @@ class ApiRollback extends ApiBase { $params['tags'] ); - // We don't care about multiple errors, just report one of them if ( $retval ) { - if ( isset( $retval[0][0] ) && - ( $retval[0][0] == 'alreadyrolled' || $retval[0][0] == 'cantrollback' ) - ) { - $error = $retval[0]; - $userMessage = $this->msg( $error[0], array_slice( $error, 1 ) ); - // dieUsageMsg() doesn't support $extraData - $errorCode = $error[0]; - $errorInfo = isset( ApiBase::$messageMap[$errorCode] ) ? - ApiBase::$messageMap[$errorCode]['info'] : - $errorCode; - $this->dieUsage( $errorInfo, $errorCode, 0, [ - 'messageHtml' => $userMessage->parseAsBlock() - ] ); - } - - $this->dieUsageMsg( reset( $retval ) ); + $this->dieStatus( $this->errorArrayToStatus( $retval, $user ) ); } $watch = 'preferences'; @@ -181,7 +165,7 @@ class ApiRollback extends ApiBase { ? $params['user'] : User::getCanonicalName( $params['user'] ); if ( !$this->mUser ) { - $this->dieUsageMsg( [ 'invaliduser', $params['user'] ] ); + $this->dieWithError( [ 'apierror-invaliduser', wfEscapeWikiText( $params['user'] ) ] ); } return $this->mUser; @@ -202,17 +186,17 @@ class ApiRollback extends ApiBase { if ( isset( $params['title'] ) ) { $this->mTitleObj = Title::newFromText( $params['title'] ); if ( !$this->mTitleObj || $this->mTitleObj->isExternal() ) { - $this->dieUsageMsg( [ 'invalidtitle', $params['title'] ] ); + $this->dieWithError( [ 'apierror-invalidtitle', wfEscapeWikiText( $params['title'] ) ] ); } } elseif ( isset( $params['pageid'] ) ) { $this->mTitleObj = Title::newFromID( $params['pageid'] ); if ( !$this->mTitleObj ) { - $this->dieUsageMsg( [ 'nosuchpageid', $params['pageid'] ] ); + $this->dieWithError( [ 'apierror-nosuchpageid', $params['pageid'] ] ); } } if ( !$this->mTitleObj->exists() ) { - $this->dieUsageMsg( 'notanarticle' ); + $this->dieWithError( 'apierror-missingtitle' ); } return $this->mTitleObj; diff --git a/includes/api/ApiSetNotificationTimestamp.php b/includes/api/ApiSetNotificationTimestamp.php index 3412f38ed3c2..5769ff6d3917 100644 --- a/includes/api/ApiSetNotificationTimestamp.php +++ b/includes/api/ApiSetNotificationTimestamp.php @@ -38,11 +38,9 @@ class ApiSetNotificationTimestamp extends ApiBase { $user = $this->getUser(); if ( $user->isAnon() ) { - $this->dieUsage( 'Anonymous users cannot use watchlist change notifications', 'notloggedin' ); - } - if ( !$user->isAllowed( 'editmywatchlist' ) ) { - $this->dieUsage( 'You don\'t have permission to edit your watchlist', 'permissiondenied' ); + $this->dieWithError( 'watchlistanontext', 'notloggedin' ); } + $this->checkUserRightsAny( 'editmywatchlist' ); $params = $this->extractRequestParams(); $this->requireMaxOneParameter( $params, 'timestamp', 'torevid', 'newerthanrevid' ); @@ -52,8 +50,12 @@ class ApiSetNotificationTimestamp extends ApiBase { $pageSet = $this->getPageSet(); if ( $params['entirewatchlist'] && $pageSet->getDataSource() !== null ) { - $this->dieUsage( - "Cannot use 'entirewatchlist' at the same time as '{$pageSet->getDataSource()}'", + $this->dieWithError( + [ + 'apierror-invalidparammix-cannotusewith', + $this->encodeParamName( 'entirewatchlist' ), + $pageSet->encodeParamName( $pageSet->getDataSource() ) + ], 'multisource' ); } @@ -71,7 +73,7 @@ class ApiSetNotificationTimestamp extends ApiBase { if ( isset( $params['torevid'] ) ) { if ( $params['entirewatchlist'] || $pageSet->getGoodTitleCount() > 1 ) { - $this->dieUsage( 'torevid may only be used with a single page', 'multpages' ); + $this->dieWithError( [ 'apierror-multpages', $this->encodeParamName( 'torevid' ) ] ); } $title = reset( $pageSet->getGoodTitles() ); if ( $title ) { @@ -85,7 +87,7 @@ class ApiSetNotificationTimestamp extends ApiBase { } } elseif ( isset( $params['newerthanrevid'] ) ) { if ( $params['entirewatchlist'] || $pageSet->getGoodTitleCount() > 1 ) { - $this->dieUsage( 'newerthanrevid may only be used with a single page', 'multpages' ); + $this->dieWithError( [ 'apierror-multpages', $this->encodeParamName( 'newerthanrevid' ) ] ); } $title = reset( $pageSet->getGoodTitles() ); if ( $title ) { diff --git a/includes/api/ApiStashEdit.php b/includes/api/ApiStashEdit.php index 92cbe9053a7b..e29fda536f6f 100644 --- a/includes/api/ApiStashEdit.php +++ b/includes/api/ApiStashEdit.php @@ -51,7 +51,7 @@ class ApiStashEdit extends ApiBase { $params = $this->extractRequestParams(); if ( $user->isBot() ) { // sanity - $this->dieUsage( 'This interface is not supported for bots', 'botsnotsupported' ); + $this->dieWithError( 'apierror-botsnotsupported' ); } $cache = ObjectCache::getLocalClusterInstance(); @@ -61,9 +61,14 @@ class ApiStashEdit extends ApiBase { if ( !ContentHandler::getForModelID( $params['contentmodel'] ) ->isSupportedFormat( $params['contentformat'] ) ) { - $this->dieUsage( 'Unsupported content model/format', 'badmodelformat' ); + $this->dieWithError( + [ 'apierror-badformat-generic', $params['contentformat'], $params['contentmodel'] ], + 'badmodelformat' + ); } + $this->requireAtLeastOneParameter( $params, 'stashedtexthash', 'text' ); + $text = null; $textHash = null; if ( strlen( $params['stashedtexthash'] ) ) { @@ -72,15 +77,18 @@ class ApiStashEdit extends ApiBase { $textKey = $cache->makeKey( 'stashedit', 'text', $textHash ); $text = $cache->get( $textKey ); if ( !is_string( $text ) ) { - $this->dieUsage( 'No stashed text found with the given hash', 'missingtext' ); + $this->dieWithError( 'apierror-stashedit-missingtext', 'missingtext' ); } } elseif ( $params['text'] !== null ) { // Trim and fix newlines so the key SHA1's match (see WebRequest::getText()) $text = rtrim( str_replace( "\r\n", "\n", $params['text'] ) ); $textHash = sha1( $text ); } else { - $this->dieUsage( - 'The text or stashedtexthash parameter must be given', 'missingtextparam' ); + $this->dieWithError( [ + 'apierror-missingparam-at-least-one-of', + Message::listParam( [ '<var>stashedtexthash</var>', '<var>text</var>' ] ), + 2, + ], 'missingparam' ); } $textContent = ContentHandler::makeContent( @@ -91,11 +99,11 @@ class ApiStashEdit extends ApiBase { // Page exists: get the merged content with the proposed change $baseRev = Revision::newFromPageId( $page->getId(), $params['baserevid'] ); if ( !$baseRev ) { - $this->dieUsage( "No revision ID {$params['baserevid']}", 'missingrev' ); + $this->dieWithError( [ 'apierror-nosuchrevid', $params['baserevid'] ] ); } $currentRev = $page->getRevision(); if ( !$currentRev ) { - $this->dieUsage( "No current revision of page ID {$page->getId()}", 'missingrev' ); + $this->dieWithError( [ 'apierror-missingrev-pageid', $page->getId() ], 'missingrev' ); } // Merge in the new version of the section to get the proposed version $editContent = $page->replaceSectionAtRev( @@ -105,7 +113,7 @@ class ApiStashEdit extends ApiBase { $baseRev->getId() ); if ( !$editContent ) { - $this->dieUsage( 'Could not merge updated section.', 'replacefailed' ); + $this->dieWithError( 'apierror-sectionreplacefailed', 'replacefailed' ); } if ( $currentRev->getId() == $baseRev->getId() ) { // Base revision was still the latest; nothing to merge @@ -115,7 +123,7 @@ class ApiStashEdit extends ApiBase { $baseContent = $baseRev->getContent(); $currentContent = $currentRev->getContent(); if ( !$baseContent || !$currentContent ) { - $this->dieUsage( "Missing content for page ID {$page->getId()}", 'missingrev' ); + $this->dieWithError( [ 'apierror-missingcontent-pageid', $page->getId() ], 'missingrev' ); } $handler = ContentHandler::getForModelID( $baseContent->getModel() ); $content = $handler->merge3( $baseContent, $editContent, $currentContent ); diff --git a/includes/api/ApiTag.php b/includes/api/ApiTag.php index f88c2dbc62f2..f6c058454784 100644 --- a/includes/api/ApiTag.php +++ b/includes/api/ApiTag.php @@ -30,10 +30,7 @@ class ApiTag extends ApiBase { $user = $this->getUser(); // make sure the user is allowed - if ( !$user->isAllowed( 'changetags' ) ) { - $this->dieUsage( "You don't have permission to add or remove change tags from individual edits", - 'permissiondenied' ); - } + $this->checkUserRightsAny( 'changetags' ); if ( $user->isBlocked() ) { $this->dieBlocked( $user->getBlock() ); @@ -88,7 +85,8 @@ class ApiTag extends ApiBase { if ( !$valid ) { $idResult['status'] = 'error'; - $idResult += $this->parseMsg( [ "nosuch$type", $id ] ); + // Messages: apierror-nosuchrcid apierror-nosuchrevid apierror-nosuchlogid + $idResult += $this->getErrorFormatter()->formatMessage( [ "apierror-nosuch$type", $id ] ); return $idResult; } diff --git a/includes/api/ApiTokens.php b/includes/api/ApiTokens.php index 4940394fe824..fc2951a9db8b 100644 --- a/includes/api/ApiTokens.php +++ b/includes/api/ApiTokens.php @@ -31,10 +31,10 @@ class ApiTokens extends ApiBase { public function execute() { - $this->setWarning( - 'action=tokens has been deprecated. Please use action=query&meta=tokens instead.' + $this->addDeprecation( + [ 'apiwarn-deprecation-withreplacement', 'action=tokens', 'action=query&meta=tokens' ], + 'action=tokens' ); - $this->logFeatureUsage( 'action=tokens' ); $params = $this->extractRequestParams(); $res = [ @@ -46,7 +46,7 @@ class ApiTokens extends ApiBase { $val = call_user_func( $types[$type], null, null ); if ( $val === false ) { - $this->setWarning( "Action '$type' is not allowed for the current user" ); + $this->addWarning( [ 'apiwarn-tokennotallowed', $type ] ); } else { $res[$type . 'token'] = $val; } diff --git a/includes/api/ApiUnblock.php b/includes/api/ApiUnblock.php index ace41a4e364c..523a888d1277 100644 --- a/includes/api/ApiUnblock.php +++ b/includes/api/ApiUnblock.php @@ -39,25 +39,18 @@ class ApiUnblock extends ApiBase { $user = $this->getUser(); $params = $this->extractRequestParams(); - if ( is_null( $params['id'] ) && is_null( $params['user'] ) ) { - $this->dieUsageMsg( 'unblock-notarget' ); - } - if ( !is_null( $params['id'] ) && !is_null( $params['user'] ) ) { - $this->dieUsageMsg( 'unblock-idanduser' ); - } + $this->requireOnlyOneParameter( $params, 'id', 'user' ); if ( !$user->isAllowed( 'block' ) ) { - $this->dieUsageMsg( 'cantunblock' ); + $this->dieWithError( 'apierror-permissiondenied-unblock', 'permissiondenied' ); } # bug 15810: blocked admins should have limited access here if ( $user->isBlocked() ) { $status = SpecialBlock::checkUnblockSelf( $params['user'], $user ); if ( $status !== true ) { - $msg = $this->parseMsg( $status ); - $this->dieUsage( - $msg['info'], - $msg['code'], - 0, + $this->dieWithError( + $status, + null, [ 'blockinfo' => ApiQueryUserInfo::getBlockInfo( $user->getBlock() ) ] ); } @@ -79,7 +72,7 @@ class ApiUnblock extends ApiBase { $block = Block::newFromTarget( $data['Target'] ); $retval = SpecialUnblock::processUnblock( $data, $this->getContext() ); if ( $retval !== true ) { - $this->dieUsageMsg( $retval[0] ); + $this->dieStatus( $this->errorArrayToStatus( $retval ) ); } $res['id'] = $block->getId(); diff --git a/includes/api/ApiUndelete.php b/includes/api/ApiUndelete.php index e24f2ced59c1..7fda1ea01a40 100644 --- a/includes/api/ApiUndelete.php +++ b/includes/api/ApiUndelete.php @@ -33,18 +33,16 @@ class ApiUndelete extends ApiBase { $this->useTransactionalTimeLimit(); $params = $this->extractRequestParams(); - $user = $this->getUser(); - if ( !$user->isAllowed( 'undelete' ) ) { - $this->dieUsageMsg( 'permdenied-undelete' ); - } + $this->checkUserRightsAny( 'undelete' ); + $user = $this->getUser(); if ( $user->isBlocked() ) { $this->dieBlocked( $user->getBlock() ); } $titleObj = Title::newFromText( $params['title'] ); if ( !$titleObj || $titleObj->isExternal() ) { - $this->dieUsageMsg( [ 'invalidtitle', $params['title'] ] ); + $this->dieWithError( [ 'apierror-invalidtitle', wfEscapeWikiText( $params['title'] ) ] ); } // Check if user can add tags @@ -76,7 +74,7 @@ class ApiUndelete extends ApiBase { $params['tags'] ); if ( !is_array( $retval ) ) { - $this->dieUsageMsg( 'cannotundelete' ); + $this->dieWithError( 'apierror-cantundelete' ); } if ( $retval[1] ) { diff --git a/includes/api/ApiUpload.php b/includes/api/ApiUpload.php index 7b44f409932c..6bdd68f93783 100644 --- a/includes/api/ApiUpload.php +++ b/includes/api/ApiUpload.php @@ -36,7 +36,7 @@ class ApiUpload extends ApiBase { public function execute() { // Check whether upload is enabled if ( !UploadBase::isEnabled() ) { - $this->dieUsageMsg( 'uploaddisabled' ); + $this->dieWithError( 'uploaddisabled' ); } $user = $this->getUser(); @@ -61,11 +61,10 @@ class ApiUpload extends ApiBase { if ( !$this->selectUploadModule() ) { return; // not a true upload, but a status request or similar } elseif ( !isset( $this->mUpload ) ) { - $this->dieUsage( 'No upload module set', 'nomodule' ); + $this->dieDebug( __METHOD__, 'No upload module set' ); } } catch ( UploadStashException $e ) { // XXX: don't spam exception log - list( $msg, $code ) = $this->handleStashException( get_class( $e ), $e->getMessage() ); - $this->dieUsage( $msg, $code ); + $this->dieStatus( $this->handleStashException( $e ) ); } // First check permission to upload @@ -75,19 +74,17 @@ class ApiUpload extends ApiBase { /** @var $status Status */ $status = $this->mUpload->fetchFile(); if ( !$status->isGood() ) { - $errors = $status->getErrorsArray(); - $error = array_shift( $errors[0] ); - $this->dieUsage( 'Error fetching file from remote source', $error, 0, $errors[0] ); + $this->dieStatus( $status ); } // Check if the uploaded file is sane if ( $this->mParams['chunk'] ) { $maxSize = UploadBase::getMaxUploadSize(); if ( $this->mParams['filesize'] > $maxSize ) { - $this->dieUsage( 'The file you submitted was too large', 'file-too-large' ); + $this->dieWithError( 'file-too-large' ); } if ( !$this->mUpload->getTitle() ) { - $this->dieUsage( 'Invalid file title supplied', 'internal-error' ); + $this->dieWithError( 'illegal-filename' ); } } elseif ( $this->mParams['async'] && $this->mParams['filekey'] ) { // defer verification to background process @@ -102,7 +99,7 @@ class ApiUpload extends ApiBase { if ( !$this->mParams['stash'] ) { $permErrors = $this->mUpload->verifyTitlePermissions( $user ); if ( $permErrors !== true ) { - $this->dieRecoverableError( $permErrors[0], 'filename' ); + $this->dieRecoverableError( $permErrors, 'filename' ); } } @@ -110,8 +107,7 @@ class ApiUpload extends ApiBase { try { $result = $this->getContextResult(); } catch ( UploadStashException $e ) { // XXX: don't spam exception log - list( $msg, $code ) = $this->handleStashException( get_class( $e ), $e->getMessage() ); - $this->dieUsage( $msg, $code ); + $this->dieStatus( $this->handleStashException( $e ) ); } $this->getResult()->addValue( null, $this->getModuleName(), $result ); @@ -146,7 +142,7 @@ class ApiUpload extends ApiBase { // Check throttle after we've handled warnings if ( UploadBase::isThrottled( $this->getUser() ) ) { - $this->dieUsageMsg( 'actionthrottledtext' ); + $this->dieWithError( 'apierror-ratelimited' ); } // This is the most common case -- a normal upload with no warnings @@ -208,16 +204,12 @@ class ApiUpload extends ApiBase { // Sanity check sizing if ( $totalSoFar > $this->mParams['filesize'] ) { - $this->dieUsage( - 'Offset plus current chunk is greater than claimed file size', 'invalid-chunk' - ); + $this->dieWithError( 'apierror-invalid-chunk' ); } // Enforce minimum chunk size if ( $totalSoFar != $this->mParams['filesize'] && $chunkSize < $minChunkSize ) { - $this->dieUsage( - "Minimum chunk size is $minChunkSize bytes for non-final chunks", 'chunk-too-small' - ); + $this->dieWithError( [ 'apierror-chunk-too-small', Message::numParam( $minChunkSize ) ] ); } if ( $this->mParams['offset'] == 0 ) { @@ -229,11 +221,9 @@ class ApiUpload extends ApiBase { $progress = UploadBase::getSessionStatus( $this->getUser(), $filekey ); if ( !$progress ) { // Probably can't get here, but check anyway just in case - $this->dieUsage( 'No chunked upload session with this key', 'stashfailed' ); + $this->dieWithError( 'apierror-stashfailed-nosession', 'stashfailed' ); } elseif ( $progress['result'] !== 'Continue' || $progress['stage'] !== 'uploading' ) { - $this->dieUsage( - 'Chunked upload is already completed, check status for details', 'stashfailed' - ); + $this->dieWithError( 'apierror-stashfailed-complete', 'stashfailed' ); } $status = $this->mUpload->addChunk( @@ -352,16 +342,13 @@ class ApiUpload extends ApiBase { list( $exceptionType, $message ) = $status->getMessage()->getParams(); $debugMessage = 'Stashing temporary file failed: ' . $exceptionType . ' ' . $message; wfDebug( __METHOD__ . ' ' . $debugMessage . "\n" ); - list( $msg, $code ) = $this->handleStashException( $exceptionType, $message ); - $status = Status::newFatal( new ApiRawMessage( $msg, $code ) ); } // Bad status if ( $failureMode !== 'optional' ) { $this->dieStatus( $status ); } else { - list( $code, $msg ) = $this->getErrorFromStatus( $status ); - $data['stashfailed'] = $msg; + $data['stasherrors'] = $this->getErrorFormatter()->arrayFromStatus( $status ); return null; } } @@ -370,25 +357,25 @@ class ApiUpload extends ApiBase { * Throw an error that the user can recover from by providing a better * value for $parameter * - * @param array|string|MessageSpecifier $error Error suitable for passing to dieUsageMsg() - * @param string $parameter Parameter that needs revising - * @param array $data Optional extra data to pass to the user - * @param string $code Error code to use if the error is unknown - * @throws UsageException + * @param array $errors Array of Message objects, message keys, key+param + * arrays, or StatusValue::getErrors()-style arrays + * @param string|null $parameter Parameter that needs revising + * @throws ApiUsageException */ - private function dieRecoverableError( $error, $parameter, $data = [], $code = 'unknownerror' ) { + private function dieRecoverableError( $errors, $parameter = null ) { $this->performStash( 'optional', $data ); - $data['invalidparameter'] = $parameter; - $parsed = $this->parseMsg( $error ); - if ( isset( $parsed['data'] ) ) { - $data = array_merge( $data, $parsed['data'] ); - } - if ( $parsed['code'] === 'unknownerror' ) { - $parsed['code'] = $code; + if ( $parameter ) { + $data['invalidparameter'] = $parameter; } - $this->dieUsage( $parsed['info'], $parsed['code'], 0, $data ); + $sv = StatusValue::newGood(); + foreach ( $errors as $error ) { + $msg = ApiMessage::create( $error ); + $msg->setApiData( $msg->getApiData() + $data ); + $sv->fatal( $msg ); + } + $this->dieStatus( $sv ); } /** @@ -398,20 +385,18 @@ class ApiUpload extends ApiBase { * @param Status $status * @param string $overrideCode Error code to use if there isn't one from IApiMessage * @param array|null $moreExtraData - * @throws UsageException + * @throws ApiUsageException */ public function dieStatusWithCode( $status, $overrideCode, $moreExtraData = null ) { - $extraData = null; - list( $code, $msg ) = $this->getErrorFromStatus( $status, $extraData ); - $errors = $status->getErrorsByType( 'error' ) ?: $status->getErrorsByType( 'warning' ); - if ( !( $errors[0]['message'] instanceof IApiMessage ) ) { - $code = $overrideCode; - } - if ( $moreExtraData ) { - $extraData = $extraData ?: []; - $extraData += $moreExtraData; + $sv = StatusValue::newGood(); + foreach ( $status->getErrors() as $error ) { + $msg = ApiMessage::create( $error, $overrideCode ); + if ( $moreExtraData ) { + $msg->setApiData( $msg->getApiData() + $moreExtraData ); + } + $sv->fatal( $msg ); } - $this->dieUsage( $msg, $code, 0, $extraData ); + $this->dieStatus( $sv ); } /** @@ -434,7 +419,7 @@ class ApiUpload extends ApiBase { if ( $this->mParams['filekey'] && $this->mParams['checkstatus'] ) { $progress = UploadBase::getSessionStatus( $this->getUser(), $this->mParams['filekey'] ); if ( !$progress ) { - $this->dieUsage( 'No result in status data', 'missingresult' ); + $this->dieWithError( 'api-upload-missingresult', 'missingresult' ); } elseif ( !$progress['status']->isGood() ) { $this->dieStatusWithCode( $progress['status'], 'stashfailed' ); } @@ -466,7 +451,7 @@ class ApiUpload extends ApiBase { // The following modules all require the filename parameter to be set if ( is_null( $this->mParams['filename'] ) ) { - $this->dieUsageMsg( [ 'missingparam', 'filename' ] ); + $this->dieWithError( [ 'apierror-missingparam', 'filename' ] ); } if ( $this->mParams['chunk'] ) { @@ -474,7 +459,7 @@ class ApiUpload extends ApiBase { $this->mUpload = new UploadFromChunks( $this->getUser() ); if ( isset( $this->mParams['filekey'] ) ) { if ( $this->mParams['offset'] === 0 ) { - $this->dieUsage( 'Cannot supply a filekey when offset is 0', 'badparams' ); + $this->dieWithError( 'apierror-upload-filekeynotallowed', 'filekeynotallowed' ); } // handle new chunk @@ -485,7 +470,7 @@ class ApiUpload extends ApiBase { ); } else { if ( $this->mParams['offset'] !== 0 ) { - $this->dieUsage( 'Must supply a filekey when offset is non-zero', 'badparams' ); + $this->dieWithError( 'apierror-upload-filekeyneeded', 'filekeyneeded' ); } // handle first chunk @@ -497,7 +482,7 @@ class ApiUpload extends ApiBase { } elseif ( isset( $this->mParams['filekey'] ) ) { // Upload stashed in a previous request if ( !UploadFromStash::isValidKey( $this->mParams['filekey'] ) ) { - $this->dieUsageMsg( 'invalid-file-key' ); + $this->dieWithError( 'apierror-invalid-file-key' ); } $this->mUpload = new UploadFromStash( $this->getUser() ); @@ -515,15 +500,15 @@ class ApiUpload extends ApiBase { } elseif ( isset( $this->mParams['url'] ) ) { // Make sure upload by URL is enabled: if ( !UploadFromUrl::isEnabled() ) { - $this->dieUsageMsg( 'copyuploaddisabled' ); + $this->dieWithError( 'copyuploaddisabled' ); } if ( !UploadFromUrl::isAllowedHost( $this->mParams['url'] ) ) { - $this->dieUsageMsg( 'copyuploadbaddomain' ); + $this->dieWithError( 'apierror-copyuploadbaddomain' ); } if ( !UploadFromUrl::isAllowedUrl( $this->mParams['url'] ) ) { - $this->dieUsageMsg( 'copyuploadbadurl' ); + $this->dieWithError( 'apierror-copyuploadbadurl' ); } $this->mUpload = new UploadFromUrl; @@ -545,10 +530,10 @@ class ApiUpload extends ApiBase { if ( $permission !== true ) { if ( !$user->isLoggedIn() ) { - $this->dieUsageMsg( [ 'mustbeloggedin', 'upload' ] ); + $this->dieWithError( [ 'apierror-mustbeloggedin', $this->msg( 'action-upload' ) ] ); } - $this->dieUsageMsg( 'badaccess-groups' ); + $this->dieStatus( User::newFatalPermissionDeniedStatus( $permission ) ); } // Check blocks @@ -583,28 +568,31 @@ class ApiUpload extends ApiBase { switch ( $verification['status'] ) { // Recoverable errors case UploadBase::MIN_LENGTH_PARTNAME: - $this->dieRecoverableError( 'filename-tooshort', 'filename' ); + $this->dieRecoverableError( [ 'filename-tooshort' ], 'filename' ); break; case UploadBase::ILLEGAL_FILENAME: - $this->dieRecoverableError( 'illegal-filename', 'filename', - [ 'filename' => $verification['filtered'] ] ); + $this->dieRecoverableError( + [ ApiMessage::create( + 'illegal-filename', null, [ 'filename' => $verification['filtered'] ] + ) ], 'filename' + ); break; case UploadBase::FILENAME_TOO_LONG: - $this->dieRecoverableError( 'filename-toolong', 'filename' ); + $this->dieRecoverableError( [ 'filename-toolong' ], 'filename' ); break; case UploadBase::FILETYPE_MISSING: - $this->dieRecoverableError( 'filetype-missing', 'filename' ); + $this->dieRecoverableError( [ 'filetype-missing' ], 'filename' ); break; case UploadBase::WINDOWS_NONASCII_FILENAME: - $this->dieRecoverableError( 'windows-nonascii-filename', 'filename' ); + $this->dieRecoverableError( [ 'windows-nonascii-filename' ], 'filename' ); break; // Unrecoverable errors case UploadBase::EMPTY_FILE: - $this->dieUsage( 'The file you submitted was empty', 'empty-file' ); + $this->dieWithError( 'empty-file' ); break; case UploadBase::FILE_TOO_LARGE: - $this->dieUsage( 'The file you submitted was too large', 'file-too-large' ); + $this->dieWithError( 'file-too-large' ); break; case UploadBase::FILETYPE_BADTYPE: @@ -612,57 +600,47 @@ class ApiUpload extends ApiBase { 'filetype' => $verification['finalExt'], 'allowed' => array_values( array_unique( $this->getConfig()->get( 'FileExtensions' ) ) ) ]; + $extensions = array_unique( $this->getConfig()->get( 'FileExtensions' ) ); + $msg = [ + 'filetype-banned-type', + null, // filled in below + Message::listParam( $extensions, 'comma' ), + count( $extensions ), + null, // filled in below + ]; ApiResult::setIndexedTagName( $extradata['allowed'], 'ext' ); - $msg = 'Filetype not permitted: '; if ( isset( $verification['blacklistedExt'] ) ) { - $msg .= implode( ', ', $verification['blacklistedExt'] ); + $msg[1] = Message::listParam( $verification['blacklistedExt'], 'comma' ); + $msg[4] = count( $verification['blacklistedExt'] ); $extradata['blacklisted'] = array_values( $verification['blacklistedExt'] ); ApiResult::setIndexedTagName( $extradata['blacklisted'], 'ext' ); } else { - $msg .= $verification['finalExt']; + $msg[1] = $verification['finalExt']; + $msg[4] = 1; } - $this->dieUsage( $msg, 'filetype-banned', 0, $extradata ); + + $this->dieWithError( $msg, 'filetype-banned', $extradata ); break; + case UploadBase::VERIFICATION_ERROR: - $parsed = $this->parseMsg( $verification['details'] ); - $info = "This file did not pass file verification: {$parsed['info']}"; - if ( $verification['details'][0] instanceof IApiMessage ) { - $code = $parsed['code']; - } else { - // For backwards-compatibility, all of the errors from UploadBase::verifyFile() are - // reported as 'verification-error', and the real error code is reported in 'details'. - $code = 'verification-error'; - } - if ( $verification['details'][0] instanceof IApiMessage ) { - $msg = $verification['details'][0]; + $msg = ApiMessage::create( $verification['details'], 'verification-error' ); + if ( $verification['details'][0] instanceof MessageSpecifier ) { $details = array_merge( [ $msg->getKey() ], $msg->getParams() ); } else { $details = $verification['details']; } ApiResult::setIndexedTagName( $details, 'detail' ); - $data = [ 'details' => $details ]; - if ( isset( $parsed['data'] ) ) { - $data = array_merge( $data, $parsed['data'] ); - } - - $this->dieUsage( $info, $code, 0, $data ); + $msg->setApiData( $msg->getApiData() + [ 'details' => $details ] ); + $this->dieWithError( $msg ); break; + case UploadBase::HOOK_ABORTED: - if ( is_array( $verification['error'] ) ) { - $params = $verification['error']; - } elseif ( $verification['error'] !== '' ) { - $params = [ $verification['error'] ]; - } else { - $params = [ 'hookaborted' ]; - } - $key = array_shift( $params ); - $msg = $this->msg( $key, $params )->inLanguage( 'en' )->useDatabase( false )->text(); - $this->dieUsage( $msg, 'hookaborted', 0, [ 'details' => $verification['error'] ] ); + $this->dieWithError( $params, 'hookaborted', [ 'details' => $verification['error'] ] ); break; default: - $this->dieUsage( 'An unknown error occurred', 'unknown-error', - 0, [ 'details' => [ 'code' => $verification['status'] ] ] ); + $this->dieWithError( 'apierror-unknownerror-nocode', 'unknown-error', + [ 'details' => [ 'code' => $verification['status'] ] ] ); break; } } @@ -735,41 +713,31 @@ class ApiUpload extends ApiBase { /** * Handles a stash exception, giving a useful error to the user. - * @param string $exceptionType Class name of the exception we encountered. - * @param string $message Message of the exception we encountered. - * @return array Array of message and code, suitable for passing to dieUsage() + * @todo Internationalize the exceptions + * @param Exception $e + * @return StatusValue */ - protected function handleStashException( $exceptionType, $message ) { - switch ( $exceptionType ) { + protected function handleStashException( $e ) { + $err = wfEscapeWikiText( $e->getMessage() ); + switch ( get_class( $exception ) ) { case 'UploadStashFileNotFoundException': - return [ - 'Could not find the file in the stash: ' . $message, - 'stashedfilenotfound' - ]; + return StatusValue::newFatal( 'apierror-stashedfilenotfound', $err ); case 'UploadStashBadPathException': - return [ - 'File key of improper format or otherwise invalid: ' . $message, - 'stashpathinvalid' - ]; + return StatusValue::newFatal( 'apierror-stashpathinvalid', $err ); case 'UploadStashFileException': - return [ - 'Could not store upload in the stash: ' . $message, - 'stashfilestorage' - ]; + return StatusValue::newFatal( 'apierror-stashfilestorage', $err ); case 'UploadStashZeroLengthFileException': - return [ - 'File is of zero length, and could not be stored in the stash: ' . - $message, - 'stashzerolength' - ]; + return StatusValue::newFatal( 'apierror-stashzerolength', $err ); case 'UploadStashNotLoggedInException': - return [ 'Not logged in: ' . $message, 'stashnotloggedin' ]; + return StatusValue::newFatal( ApiMessage::create( + [ 'apierror-mustbeloggedin', $this->msg( 'action-upload' ) ], 'stashnotloggedin' + ) ); case 'UploadStashWrongOwnerException': - return [ 'Wrong owner: ' . $message, 'stashwrongowner' ]; + return StatusValue::newFatal( 'apierror-stashwrongowner', $err ); case 'UploadStashNoSuchKeyException': - return [ 'No such filekey: ' . $message, 'stashnosuchfilekey' ]; + return StatusValue::newFatal( 'apierror-stashnosuchfilekey', $err ); default: - return [ $exceptionType . ': ' . $message, 'stasherror' ]; + return StatusValue::newFatal( 'uploadstash-exception', get_class( $e ), $err ); } } @@ -821,7 +789,7 @@ class ApiUpload extends ApiBase { if ( $this->mParams['async'] ) { $progress = UploadBase::getSessionStatus( $this->getUser(), $this->mParams['filekey'] ); if ( $progress && $progress['result'] === 'Poll' ) { - $this->dieUsage( 'Upload from stash already in progress.', 'publishfailed' ); + $this->dieWithError( 'apierror-upload-inprogress', 'publishfailed' ); } UploadBase::setSessionStatus( $this->getUser(), @@ -848,14 +816,7 @@ class ApiUpload extends ApiBase { $this->mParams['text'], $watch, $this->getUser(), $this->mParams['tags'] ); if ( !$status->isGood() ) { - // Is there really no better way to do this? - $errors = $status->getErrorsByType( 'error' ); - $msg = array_merge( [ $errors[0]['message'] ], $errors[0]['params'] ); - $data = $status->getErrorsArray(); - ApiResult::setIndexedTagName( $data, 'error' ); - // For backwards-compatibility, we use the 'internal-error' fallback key and merge $data - // into the root of the response (rather than something sane like [ 'details' => $data ]). - $this->dieRecoverableError( $msg, null, $data, 'internal-error' ); + $this->dieRecoverableError( $status->getErrors() ); } $result['result'] = 'Success'; } diff --git a/includes/api/ApiUsageException.php b/includes/api/ApiUsageException.php new file mode 100644 index 000000000000..7e21ab5ba446 --- /dev/null +++ b/includes/api/ApiUsageException.php @@ -0,0 +1,217 @@ +<?php +/** + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * http://www.gnu.org/copyleft/gpl.html + * + * @file + * @defgroup API API + */ + +/** + * This exception will be thrown when dieUsage is called to stop module execution. + * + * @ingroup API + * @deprecated since 1.29, use ApiUsageException instead + */ +class UsageException extends MWException { + + private $mCodestr; + + /** + * @var null|array + */ + private $mExtraData; + + /** + * @param string $message + * @param string $codestr + * @param int $code + * @param array|null $extradata + */ + public function __construct( $message, $codestr, $code = 0, $extradata = null ) { + parent::__construct( $message, $code ); + $this->mCodestr = $codestr; + $this->mExtraData = $extradata; + + // This should never happen, so throw an exception about it that will + // hopefully get logged with a backtrace (T138585) + if ( !is_string( $codestr ) || $codestr === '' ) { + throw new InvalidArgumentException( 'Invalid $codestr, was ' . + ( $codestr === '' ? 'empty string' : gettype( $codestr ) ) + ); + } + } + + /** + * @return string + */ + public function getCodeString() { + return $this->mCodestr; + } + + /** + * @return array + */ + public function getMessageArray() { + $result = [ + 'code' => $this->mCodestr, + 'info' => $this->getMessage() + ]; + if ( is_array( $this->mExtraData ) ) { + $result = array_merge( $result, $this->mExtraData ); + } + + return $result; + } + + /** + * @return string + */ + public function __toString() { + return "{$this->getCodeString()}: {$this->getMessage()}"; + } +} + +/** + * Exception used to abort API execution with an error + * + * If possible, use ApiBase::dieWithError() instead of throwing this directly. + * + * @ingroup API + * @note This currently extends UsageException for backwards compatibility, so + * all the existing code that catches UsageException won't break when stuff + * starts throwing ApiUsageException. Eventually UsageException will go away + * and this will (probably) extend MWException directly. + */ +class ApiUsageException extends UsageException { + + protected $modulePath; + protected $status; + + /** + * @param ApiBase|null $module API module responsible for the error, if known + * @param StatusValue $status Status holding errors + * @param int $httpCode HTTP error code to use + */ + public function __construct( + ApiBase $module = null, StatusValue $status, $httpCode = 0 + ) { + if ( $status->isOK() ) { + throw new InvalidArgumentException( __METHOD__ . ' requires a fatal Status' ); + } + + $this->modulePath = $module ? $module->getModulePath() : null; + $this->status = $status; + + // Bug T46111: Messages in the log files should be in English and not + // customized by the local wiki. + $enMsg = clone $this->getApiMessage(); + $enMsg->inLanguage( 'en' )->useDatabase( false ); + parent::__construct( + ApiErrorFormatter::stripMarkup( $enMsg->text() ), + $enMsg->getApiCode(), + $httpCode, + $enMsg->getApiData() + ); + } + + /** + * @param ApiBase|null $module API module responsible for the error, if known + * @param string|array|Message $msg See ApiMessage::create() + * @param string|null $code See ApiMessage::create() + * @param array|null $data See ApiMessage::create() + * @param int $httpCode HTTP error code to use + * @return static + */ + public static function newWithMessage( + ApiBase $module = null, $msg, $code = null, $data = null, $httpCode = 0 + ) { + return new static( + $module, + StatusValue::newFatal( ApiMessage::create( $msg, $code, $data ) ), + $httpCode + ); + } + + /** + * @returns ApiMessage + */ + private function getApiMessage() { + $errors = $this->status->getErrorsByType( 'error' ); + if ( !$errors ) { + $errors = $this->status->getErrors(); + } + if ( !$errors ) { + $msg = new ApiMessage( 'apierror-unknownerror-nocode', 'unknownerror' ); + } else { + $msg = ApiMessage::create( $errors[0] ); + } + return $msg; + } + + /** + * Fetch the responsible module name + * @return string|null + */ + public function getModulePath() { + return $this->modulePath; + } + + /** + * Fetch the error status + * @return StatusValue + */ + public function getStatusValue() { + return $this->status; + } + + /** + * @deprecated Do not use. This only exists here because UsageException is in + * the inheritance chain for backwards compatibility. + * @inheritdoc + */ + public function getCodeString() { + return $this->getApiMessage()->getApiCode(); + } + + /** + * @deprecated Do not use. This only exists here because UsageException is in + * the inheritance chain for backwards compatibility. + * @inheritdoc + */ + public function getMessageArray() { + $enMsg = clone $this->getApiMessage(); + $enMsg->inLanguage( 'en' )->useDatabase( false ); + + return [ + 'code' => $enMsg->getApiCode(), + 'info' => ApiErrorFormatter::stripMarkup( $enMsg->text() ), + ] + $enMsg->getApiData(); + } + + /** + * @return string + */ + public function __toString() { + $enMsg = clone $this->getApiMessage(); + $enMsg->inLanguage( 'en' )->useDatabase( false ); + $text = ApiErrorFormatter::stripMarkup( $enMsg->text() ); + + return get_class( $this ) . ": {$enMsg->getApiCode()}: {$text} " + . "in {$this->getFile()}:{$this->getLine()}\n" + . "Stack trace:\n{$this->getTraceAsString()}"; + } + +} diff --git a/includes/api/ApiWatch.php b/includes/api/ApiWatch.php index 3a7a082148dc..d257e9005fe3 100644 --- a/includes/api/ApiWatch.php +++ b/includes/api/ApiWatch.php @@ -35,12 +35,10 @@ class ApiWatch extends ApiBase { public function execute() { $user = $this->getUser(); if ( !$user->isLoggedIn() ) { - $this->dieUsage( 'You must be logged-in to have a watchlist', 'notloggedin' ); + $this->dieWithError( 'watchlistanontext', 'notloggedin' ); } - if ( !$user->isAllowed( 'editmywatchlist' ) ) { - $this->dieUsage( 'You don\'t have permission to edit your watchlist', 'permissiondenied' ); - } + $this->checkUserRightsAny( 'editmywatchlist' ); $params = $this->extractRequestParams(); @@ -78,16 +76,19 @@ class ApiWatch extends ApiBase { } ) ); if ( $extraParams ) { - $p = $this->getModulePrefix(); - $this->dieUsage( - "The parameter {$p}title can not be used with " . implode( ', ', $extraParams ), + $this->dieWithError( + [ + 'apierror-invalidparammix-cannotusewith', + $this->encodeParamName( 'title' ), + $pageSet->encodeParamName( $extraParams[0] ) + ], 'invalidparammix' ); } $title = Title::newFromText( $params['title'] ); if ( !$title || !$title->isWatchable() ) { - $this->dieUsageMsg( [ 'invalidtitle', $params['title'] ] ); + $this->dieWithError( [ 'invalidtitle', $params['title'] ] ); } $res = $this->watchTitle( $title, $user, $params, true ); } @@ -128,7 +129,11 @@ class ApiWatch extends ApiBase { if ( $compatibilityMode ) { $this->dieStatus( $status ); } - $res['error'] = $this->getErrorFromStatus( $status ); + $res['errors'] = $this->getErrorFormatter()->arrayFromStatus( $status, 'error' ); + $res['warnings'] = $this->getErrorFormatter()->arrayFromStatus( $status, 'warning' ); + if ( !$res['warnings'] ) { + unset( $res['warnings'] ); + } } return $res; diff --git a/includes/api/i18n/en.json b/includes/api/i18n/en.json index 28cd746adc09..442bdf4cf273 100644 --- a/includes/api/i18n/en.json +++ b/includes/api/i18n/en.json @@ -17,8 +17,12 @@ "apihelp-main-param-requestid": "Any value given here will be included in the response. May be used to distinguish requests.", "apihelp-main-param-servedby": "Include the hostname that served the request in the results.", "apihelp-main-param-curtimestamp": "Include the current timestamp in the result.", + "apihelp-main-param-responselanginfo": "Include the languages used for <var>uselang</var> and <var>errorlang</var> in the result.", "apihelp-main-param-origin": "When accessing the API using a cross-domain AJAX request (CORS), set this to the originating domain. This must be included in any pre-flight request, and therefore must be part of the request URI (not the POST body).\n\nFor authenticated requests, this must match one of the origins in the <code>Origin</code> header exactly, so it has to be set to something like <kbd>https://en.wikipedia.org</kbd> or <kbd>https://meta.wikimedia.org</kbd>. If this parameter does not match the <code>Origin</code> header, a 403 response will be returned. If this parameter matches the <code>Origin</code> header and the origin is whitelisted, the <code>Access-Control-Allow-Origin</code> and <code>Access-Control-Allow-Credentials</code> headers will be set.\n\nFor non-authenticated requests, specify the value <kbd>*</kbd>. This will cause the <code>Access-Control-Allow-Origin</code> header to be set, but <code>Access-Control-Allow-Credentials</code> will be <code>false</code> and all user-specific data will be restricted.", "apihelp-main-param-uselang": "Language to use for message translations. <kbd>[[Special:ApiHelp/query+siteinfo|action=query&meta=siteinfo]]</kbd> with <kbd>siprop=languages</kbd> returns a list of language codes, or specify <kbd>user</kbd> to use the current user's language preference, or specify <kbd>content</kbd> to use this wiki's content language.", + "apihelp-main-param-errorformat": "Format to use for warning and error text output.\n; plaintext: Wikitext with HTML tags removed and entities replaced.\n; wikitext: Unparsed wikitext.\n; html: HTML.\n; raw: Message key and parameters.\n; none: No text output, only the error codes.\n; bc: Format used prior to MediaWiki 1.29. <var>errorlang</var> and <var>errorsusedb</var> are ignored.", + "apihelp-main-param-errorlang": "Language to use for warnings and errors. <kbd>[[Special:ApiHelp/query+siteinfo|action=query&meta=siteinfo]]</kbd> with <kbd>siprop=languages</kbd> returns a list of language codes, or specify <kbd>content</kbd> to use this wiki's content language, or specify <kbd>uselang</kbd> to use the same value as the <var>uselang</var> parameter.", + "apihelp-main-param-errorsuselocal": "If given, error texts will use locally-customized messages from the {{ns:MediaWiki}} namespace.", "apihelp-block-description": "Block a user.", "apihelp-block-param-user": "Username, IP address, or IP address range to block.", @@ -485,7 +489,7 @@ "apihelp-query+allmessages-param-prop": "Which properties to get.", "apihelp-query+allmessages-param-enableparser": "Set to enable parser, will preprocess the wikitext of message (substitute magic words, handle templates, etc.).", "apihelp-query+allmessages-param-nocontent": "If set, do not include the content of the messages in the output.", - "apihelp-query+allmessages-param-includelocal": "Also include local messages, i.e. messages that don't exist in the software but do exist as a MediaWiki: page.\nThis lists all MediaWiki: pages, so it will also list those that aren't really messages such as [[MediaWiki:Common.js|Common.js]].", + "apihelp-query+allmessages-param-includelocal": "Also include local messages, i.e. messages that don't exist in the software but do exist as in the {{ns:MediaWiki}} namespace.\nThis lists all {{ns:MediaWiki}}-namespace pages, so it will also list those that aren't really messages such as [[MediaWiki:Common.js|Common.js]].", "apihelp-query+allmessages-param-args": "Arguments to be substituted into message.", "apihelp-query+allmessages-param-filter": "Return only messages with names that contain this string.", "apihelp-query+allmessages-param-customised": "Return only messages in this customisation state.", @@ -1443,7 +1447,7 @@ "apihelp-phpfm-description": "Output data in serialized PHP format (pretty-print in HTML).", "apihelp-rawfm-description": "Output data, including debugging elements, in JSON format (pretty-print in HTML).", "apihelp-xml-description": "Output data in XML format.", - "apihelp-xml-param-xslt": "If specified, adds the named page as an XSL stylesheet. The value must be a title in the {{ns:mediawiki}} namespace ending in <code>.xsl</code>.", + "apihelp-xml-param-xslt": "If specified, adds the named page as an XSL stylesheet. The value must be a title in the {{ns:MediaWiki}} namespace ending in <code>.xsl</code>.", "apihelp-xml-param-includexmlnamespace": "If specified, adds an XML namespace.", "apihelp-xmlfm-description": "Output data in XML format (pretty-print in HTML).", @@ -1526,6 +1530,238 @@ "api-help-authmanagerhelper-continue": "This request is a continuation after an earlier <samp>UI</samp> or <samp>REDIRECT</samp> response. Either this or <var>$1returnurl</var> is required.", "api-help-authmanagerhelper-additional-params": "This module accepts additional parameters depending on the available authentication requests. Use <kbd>[[Special:ApiHelp/query+authmanagerinfo|action=query&meta=authmanagerinfo]]</kbd> with <kbd>amirequestsfor=$1</kbd> (or a previous response from this module, if applicable) to determine the requests available and the fields that they use.", + "apierror-allimages-redirect": "Use <kbd>gaifilterredir=nonredirects</kbd> instead of <var>redirects</var> when using <kbd>allimages</kbd> as a generator.", + "apierror-allpages-generator-redirects": "Use <kbd>gapfilterredir=nonredirects</kbd> instead of <var>redirects</var> when using <kbd>allpages</kbd> as a generator.", + "apierror-appendnotsupported": "Can't append to pages using content model $1.", + "apierror-articleexists": "The article you tried to create has been created already.", + "apierror-assertbotfailed": "Assertion that the user has the <code>bot</code> right failed.", + "apierror-assertnameduserfailed": "Assertion that the user is \"$1\" failed.", + "apierror-assertuserfailed": "Assertion that the user is logged in failed.", + "apierror-autoblocked": "Your IP address has been blocked automatically, because it was used by a blocked user.", + "apierror-badconfig-resulttoosmall": "The value of <code>$wgAPIMaxResultSize</code> on this wiki is too small to hold basic result information.", + "apierror-badcontinue": "Invalid continue param. You should pass the original value returned by the previous query.", + "apierror-baddiff": "The diff cannot be retrieved, one or both revisions do not exist or you do not have permission to view them.", + "apierror-baddiffto": "<var>$1diffto</var> must be set to a non-negative number, <kbd>prev</kbd>, <kbd>next</kbd> or <kbd>cur</kbd>.", + "apierror-badformat-generic": "The requested format $1 is not supported for content model $2.", + "apierror-badformat": "The requested format $1 is not supported for content model $2 used by $3.", + "apierror-badgenerator-notgenerator": "Module <kbd>$1</kbd> cannot be used as a generator.", + "apierror-badgenerator-unknown": "Unknown <kbd>generator=$1</kbd>.", + "apierror-badip": "IP parameter is not valid.", + "apierror-badmd5": "The supplied MD5 hash was incorrect.", + "apierror-badmodule-badsubmodule": "The module <kbd>$1</kbd> does not have a submodule \"$2\".", + "apierror-badmodule-nosubmodules": "The module <kbd>$1</kbd> has no submodules.", + "apierror-badparameter": "Invalid value for parameter <var>$1</var>.", + "apierror-badquery": "Invalid query.", + "apierror-badtimestamp": "Invalid value \"$2\" for timestamp parameter <var>$1</var>.", + "apierror-badtoken": "Invalid CSRF token.", + "apierror-badupload": "File upload parameter <var>$1</var> is not a file upload; be sure to use <code>multipart/form-data</code> for your POST and include a filename in the <code>Content-Disposition</code> header.", + "apierror-badurl": "Invalid value \"$2\" for URL parameter <var>$1</var>.", + "apierror-baduser": "Invalid value \"$2\" for user parameter <var>$1</var>.", + "apierror-badvalue-notmultivalue": "U+001F multi-value separation may only be used for multi-valued parameters.", + "apierror-bad-watchlist-token": "Incorrect watchlist token provided. Please set a correct token in [[Special:Preferences]].", + "apierror-blockedfrommail": "You have been blocked from sending email.", + "apierror-blocked": "You have been blocked from editing.", + "apierror-botsnotsupported": "This interface is not supported for bots.", + "apierror-cannotreauthenticate": "This action is not available as your identity cannot be verified.", + "apierror-cannotviewtitle": "You are not allowed to view $1.", + "apierror-cantblock-email": "You don't have permission to block users from sending email through the wiki.", + "apierror-cantblock": "You don't have permission to block users.", + "apierror-cantchangecontentmodel": "You don't have permission to change the content model of a page.", + "apierror-canthide": "You don't have permission to hide user names from the block log.", + "apierror-cantimport-upload": "You don't have permission to import uploaded pages.", + "apierror-cantimport": "You don't have permission to import pages.", + "apierror-cantoverwrite-sharedfile": "The target file exists on a shared repository and you do not have permission to override it.", + "apierror-cantsend": "You are not logged in, you do not have a confirmed email address, or you are not allowed to send email to other users, so you cannot send email.", + "apierror-cantundelete": "Couldn't undelete: the requested revisions may not exist, or may have been undeleted already.", + "apierror-changeauth-norequest": "Failed to create change request.", + "apierror-chunk-too-small": "Minimum chunk size is $1 {{PLURAL:$1|byte|bytes}} for non-final chunks.", + "apierror-cidrtoobroad": "$1 CIDR ranges broader than /$2 are not accepted.", + "apierror-compare-inputneeded": "A title, a page ID, or a revision number is needed for both the <var>from</var> and the <var>to</var> parameters.", + "apierror-contentserializationexception": "Content serialization failed: $1", + "apierror-contenttoobig": "The content you supplied exceeds the article size limit of $1 {{PLURAL:$1|kilobyte|kilobytes}}.", + "apierror-copyuploadbaddomain": "Uploads by URL are not allowed from this domain.", + "apierror-copyuploadbadurl": "Upload not allowed from this URL.", + "apierror-create-titleexists": "Existing titles can't be protected with <kbd>create</kbd>.", + "apierror-csp-report": "Error processing CSP report: $1.", + "apierror-databaseerror": "[$1] Database query error.", + "apierror-deletedrevs-param-not-1-2": "The <var>$1</var> parameter cannot be used in modes 1 or 2.", + "apierror-deletedrevs-param-not-3": "The <var>$1</var> parameter cannot be used in mode 3.", + "apierror-emptynewsection": "Creating empty new sections is not possible.", + "apierror-emptypage": "Creating new, empty pages is not allowed.", + "apierror-exceptioncaught": "[$1] Exception caught: $2", + "apierror-filedoesnotexist": "File does not exist.", + "apierror-fileexists-sharedrepo-perm": "The target file exists on a shared repository. Use the <var>ignorewarnings</var> parameter to override it.", + "apierror-filenopath": "Cannot get local file path.", + "apierror-filetypecannotberotated": "File type cannot be rotated.", + "apierror-formatphp": "This response cannot be represented using <kbd>format=php</kbd>. See https://phabricator.wikimedia.org/T68776.", + "apierror-imageusage-badtitle": "The title for <kbd>$1</kbd> must be a file.", + "apierror-import-unknownerror": "Unknown error on import: $1.", + "apierror-integeroutofrange-abovebotmax": "<var>$1</var> may not be over $2 (set to $3) for bots or sysops.", + "apierror-integeroutofrange-abovemax": "<var>$1</var> may not be over $2 (set to $3) for users.", + "apierror-integeroutofrange-belowminimum": "<var>$1</var> may not be less than $2 (set to $3).", + "apierror-invalidcategory": "The category name you entered is not valid.", + "apierror-invalid-chunk": "Offset plus current chunk is greater than claimed file size.", + "apierror-invalidexpiry": "Invalid expiry time \"$1\".", + "apierror-invalid-file-key": "Not a valid file key.", + "apierror-invalidlang": "Invalid language code for parameter <var>$1</var>.", + "apierror-invalidoldimage": "The oldimage parameter has invalid format.", + "apierror-invalidparammix-cannotusewith": "The <kbd>$1</kbd> parameter cannot be used with <kbd>$2</kbd>.", + "apierror-invalidparammix-mustusewith": "The <kbd>$1</kbd> parameter may only be used with <kbd>$2</kbd>.", + "apierror-invalidparammix-parse-new-section": "<kbd>section=new</kbd> cannot be combined with the <var>oldid</var>, <var>pageid</var> or <var>page</var> parameters. Please use <var>title</var> and <var>text</var>.", + "apierror-invalidparammix": "The {{PLURAL:$2|parameters}} $1 can not be used together.", + "apierror-invalidsection": "The section parameter must be a valid section ID or <kbd>new</kbd>.", + "apierror-invalidsha1base36hash": "The SHA1Base36 hash provided is not valid.", + "apierror-invalidsha1hash": "The SHA1 hash provided is not valid.", + "apierror-invalidtitle": "Bad title \"$1\".", + "apierror-invalidurlparam": "Invalid value for <var>$1urlparam</var> (<kbd>$2=$3</kbd>).", + "apierror-invaliduser": "Invalid username \"$1\".", + "apierror-maxlag-generic": "Waiting for a database server: $1 {{PLURAL:$1|second|seconds}} lagged.", + "apierror-maxlag": "Waiting for $2: $1 {{PLURAL:$1|second|seconds}} lagged.", + "apierror-mimesearchdisabled": "MIME search is disabled in Miser Mode.", + "apierror-missingcontent-pageid": "Missing content for page ID $1.", + "apierror-missingparam-at-least-one-of": "{{PLURAL:$2|The parameter|At least one of the parameters}} $1 is required.", + "apierror-missingparam-one-of": "{{PLURAL:$2|The parameter|One of the parameters}} $1 is required.", + "apierror-missingparam": "The <var>$1</var> parameter must be set.", + "apierror-missingrev-pageid": "No current revision of page ID $1.", + "apierror-missingtitle-createonly": "Missing titles can only be protected with <kbd>create</kbd>.", + "apierror-missingtitle": "The page you specified doesn't exist.", + "apierror-missingtitle-byname": "The page $1 doesn't exist.", + "apierror-moduledisabled": "The <kbd>$1</kbd> module has been disabled.", + "apierror-multival-only-one-of": "{{PLURAL:$3|Only|Only one of}} $2 is allowed for parameter <var>$1</var>.", + "apierror-multival-only-one": "Only one value is allowed for parameter <var>$1</var>.", + "apierror-multpages": "<var>$1</var> may only be used with a single page.", + "apierror-mustbeloggedin-changeauth": "You must be logged in to change authentication data.", + "apierror-mustbeloggedin-generic": "You must be logged in.", + "apierror-mustbeloggedin-linkaccounts": "You must be logged in to link accounts.", + "apierror-mustbeloggedin-removeauth": "You must be logged in to remove authentication data.", + "apierror-mustbeloggedin-uploadstash": "The upload stash is only available to logged-in users.", + "apierror-mustbeloggedin": "You must be logged in to $1.", + "apierror-mustbeposted": "The <kbd>$1</kbd> module requires a POST request.", + "apierror-mustpostparams": "The following {{PLURAL:$2|parameter was|parameters were}} found in the query string, but must be in the POST body: $1.", + "apierror-noapiwrite": "Editing of this wiki through the API is disabled. Make sure the <code>$wgEnableWriteAPI=true;</code> statement is included in the wiki's <code>LocalSettings.php</code> file.", + "apierror-nochanges": "No changes were requested.", + "apierror-nodeleteablefile": "No such old version of the file.", + "apierror-no-direct-editing": "Direct editing via API is not supported for content model $1 used by $2.", + "apierror-noedit-anon": "Anonymous users can't edit pages.", + "apierror-noedit": "You don't have permission to edit pages.", + "apierror-noimageredirect-anon": "Anonymous users can't create image redirects.", + "apierror-noimageredirect": "You don't have permission to create image redirects.", + "apierror-nosuchlogid": "There is no log entry with ID $1.", + "apierror-nosuchpageid": "There is no page with ID $1.", + "apierror-nosuchrcid": "There is no recent change with ID $1.", + "apierror-nosuchrevid": "There is no revision with ID $1.", + "apierror-nosuchsection": "There is no section $1.", + "apierror-nosuchsection-what": "There is no section $1 in $2.", + "apierror-notarget": "You have not specified a valid target for this action.", + "apierror-notpatrollable": "The revision r$1 can't be patrolled as it's too old.", + "apierror-nouploadmodule": "No upload module set.", + "apierror-opensearch-json-warnings": "Warnings cannot be represented in OpenSearch JSON format.", + "apierror-pagecannotexist": "Namespace doesn't allow actual pages.", + "apierror-pagedeleted": "The page has been deleted since you fetched its timestamp.", + "apierror-paramempty": "The parameter <var>$1</var> may not be empty.", + "apierror-parsetree-notwikitext": "<kbd>prop=parsetree</kbd> is only supported for wikitext content.", + "apierror-parsetree-notwikitext-title": "<kbd>prop=parsetree</kbd> is only supported for wikitext content. $1 uses content model $2.", + "apierror-pastexpiry": "Expiry time \"$1\" is in the past.", + "apierror-permissiondenied": "You don't have permission to $1.", + "apierror-permissiondenied-generic": "Permission denied.", + "apierror-permissiondenied-patrolflag": "You need the <code>patrol</code> or <code>patrolmarks</code> right to request the patrolled flag.", + "apierror-permissiondenied-unblock": "You don't have permission to unblock users.", + "apierror-prefixsearchdisabled": "Prefix search is disabled in Miser Mode.", + "apierror-promised-nonwrite-api": "The <code>Promise-Non-Write-API-Action</code> HTTP header cannot be sent to write-mode API modules.", + "apierror-protect-invalidaction": "Invalid protection type \"$1\".", + "apierror-protect-invalidlevel": "Invalid protection level \"$1\".", + "apierror-ratelimited": "You've exceeded your rate limit. Please wait some time and try again.", + "apierror-readapidenied": "You need read permission to use this module.", + "apierror-readonly": "The wiki is currently in read-only mode.", + "apierror-reauthenticate": "You have not authenticated recently in this session, please reauthenticate.", + "apierror-redirect-appendonly": "You have attempted to edit using the redirect-following mode, which must be used in conjuction with <kbd>section=new</kbd>, <var>prependtext</var>, or <var>appendtext</var>.", + "apierror-revdel-mutuallyexclusive": "The same field cannot be used in both <var>hide</var> and <var>show</var>.", + "apierror-revdel-needtarget": "A target title is required for this RevDel type.", + "apierror-revdel-paramneeded": "At least one value is required for <var>hide</var> and/or <var>show</var>.", + "apierror-revisions-norevids": "The <var>revids</var> parameter may not be used with the list options (<var>$1limit</var>, <var>$1startid</var>, <var>$1endid</var>, <kbd>$1dir=newer</kbd>, <var>$1user</var>, <var>$1excludeuser</var>, <var>$1start</var>, and <var>$1end</var>).", + "apierror-revisions-singlepage": "<var>titles</var>, <var>pageids</var> or a generator was used to supply multiple pages, but the <var>$1limit</var>, <var>$1startid</var>, <var>$1endid</var>, <kbd>$1dir=newer</kbd>, <var>$1user</var>, <var>$1excludeuser</var>, <var>$1start</var>, and <var>$1end</var> parameters may only be used on a single page.", + "apierror-revwrongpage": "r$1 is not a revision of $2.", + "apierror-searchdisabled": "<var>$1</var> search is disabled.", + "apierror-sectionreplacefailed": "Could not merge updated section.", + "apierror-sectionsnotsupported": "Sections are not supported for content model $1.", + "apierror-sectionsnotsupported-what": "Sections are not supported by $1.", + "apierror-show": "Incorrect parameter - mutually exclusive values may not be supplied.", + "apierror-siteinfo-includealldenied": "Cannot view all servers' info unless <var>$wgShowHostNames</var> is true.", + "apierror-sizediffdisabled": "Size difference is disabled in Miser Mode.", + "apierror-spamdetected": "Your edit was refused because it contained a spam fragment: <code>$1</code>.", + "apierror-specialpage-cantexecute": "You don't have permission to view the results of this special page.", + "apierror-stashedfilenotfound": "Could not find the file in the stash: $1.", + "apierror-stashedit-missingtext": "No stashed text found with the given hash.", + "apierror-stashfailed-complete": "Chunked upload is already completed, check status for details.", + "apierror-stashfailed-nosession": "No chunked upload session with this key.", + "apierror-stashfilestorage": "Could not store upload in the stash: $1", + "apierror-stashnosuchfilekey": "No such filekey: $1.", + "apierror-stashpathinvalid": "File key of improper format or otherwise invalid: $1.", + "apierror-stashwrongowner": "Wrong owner: $1", + "apierror-stashzerolength": "File is of zero length, and could not be stored in the stash: $1.", + "apierror-templateexpansion-notwikitext": "Template expansion is only supported for wikitext content. $1 uses content model $2.", + "apierror-toofewexpiries": "$1 expiry {{PLURAL:$1|timestamp was|timestamps were}} provided where $2 {{PLURAL:$2|was|were}} needed.", + "apierror-unknownaction": "The action specified, <kbd>$1</kbd>, is not recognized.", + "apierror-unknownerror-editpage": "Unknown EditPage error: $1.", + "apierror-unknownerror-nocode": "Unknown error.", + "apierror-unknownerror": "Unknown error: \"$1\".", + "apierror-unknownformat": "Unrecognized format \"$1\".", + "apierror-unrecognizedparams": "Unrecognized {{PLURAL:$2|parameter|parameters}}: $1.", + "apierror-unrecognizedvalue": "Unrecognized value for parameter <var>$1</var>: $2.", + "apierror-unsupportedrepo": "Local file repository does not support querying all images.", + "apierror-upload-filekeyneeded": "Must supply a <var>filekey</var> when <var>offset</var> is non-zero.", + "apierror-upload-filekeynotallowed": "Cannot supply a <var>filekey</var> when <var>offset</var> is 0.", + "apierror-upload-inprogress": "Upload from stash already in progress.", + "apierror-upload-missingresult": "No result in status data.", + "apierror-urlparamnormal": "Could not normalize image parameters for $1.", + "apierror-writeapidenied": "You're not allowed to edit this wiki through the API.", + + "apiwarn-alldeletedrevisions-performance": "For better performance when generating titles, set <kbd>$1dir=newer</kbd>.", + "apiwarn-badurlparam": "Could not parse <var>$1urlparam</var> for $2. Using only width and height.", + "apiwarn-badutf8": "The value passed for <var>$1</var> contains invalid or non-normalized data. Textual data should be valid, NFC-normalized Unicode without C0 control characters other than HT (\\t), LF (\\n), and CR (\\r).", + "apiwarn-checktoken-percentencoding": "Check that symbols such as \"+\" in the token are properly percent-encoded in the URL.", + "apiwarn-deprecation-deletedrevs": "<kbd>list=deletedrevs</kbd> has been deprecated. Please use <kbd>prop=deletedrevisions</kbd> or <kbd>list=alldeletedrevisions</kbd> instead.", + "apiwarn-deprecation-expandtemplates-prop": "Because no values have been specified for the <var>prop</var> parameter, a legacy format has been used for the output. This format is deprecated, and in the future, a default value will be set for the <var>prop</var> parameter, causing the new format to always be used.", + "apiwarn-deprecation-httpsexpected": "HTTP used when HTTPS was expected.", + "apiwarn-deprecation-login-botpw": "Main-account login via <kbd>action=login</kbd> is deprecated and may stop working without warning. To continue login with <kbd>action=login</kbd>, see [[Special:BotPasswords]]. To safely continue using main-account login, see <kbd>action=clientlogin</kbd>.", + "apiwarn-deprecation-login-nobotpw": "Main-account login via <kbd>action=login</kbd> is deprecated and may stop working without warning. To safely log in, see <kbd>action=clientlogin</kbd>.", + "apiwarn-deprecation-login-token": "Fetching a token via <kbd>action=login</kbd> is deprecated. Use <kbd>action=query&meta=tokens&type=login</kbd> instead.", + "apiwarn-deprecation-parameter": "The parameter <var>$1</var> has been deprecated.", + "apiwarn-deprecation-parse-headitems": "<kbd>prop=headitems</kbd> is deprecated since MediaWiki 1.28. Use <kbd>prop=headhtml</kbd> when creating new HTML documents, or <kbd>prop=modules|jsconfigvars</kbd> when updating a document client-side.", + "apiwarn-deprecation-purge-get": "Use of <kbd>action=purge</kbd> via GET is deprecated. Use POST instead.", + "apiwarn-deprecation-withreplacement": "<kbd>$1</kbd> has been deprecated. Please use <kbd>$2</kbd> instead.", + "apiwarn-difftohidden": "Couldn't diff to r$1: content is hidden.", + "apiwarn-errorprinterfailed": "Error printer failed. Will retry without params.", + "apiwarn-errorprinterfailed-ex": "Error printer failed (will retry without params): $1", + "apiwarn-invalidcategory": "\"$1\" is not a category.", + "apiwarn-invalidtitle": "\"$1\" is not a valid title.", + "apiwarn-invalidxmlstylesheetext": "Stylesheet should have <code>.xsl</code> extension.", + "apiwarn-invalidxmlstylesheet": "Invalid or non-existent stylesheet specified.", + "apiwarn-invalidxmlstylesheetns": "Stylesheet should be in the {{ns:MediaWiki}} namespace.", + "apiwarn-moduleswithoutvars": "Property <kbd>modules</kbd> was set but not <kbd>jsconfigvars</kbd> or <kbd>encodedjsconfigvars</kbd>. Configuration variables are necessary for proper module usage.", + "apiwarn-notfile": "\"$1\" is not a file.", + "apiwarn-nothumb-noimagehandler": "Could not create thumbnail because $1 does not have an associated image handler.", + "apiwarn-parse-nocontentmodel": "No <var>title</var> or <var>contentmodel</var> was given, assuming $1.", + "apiwarn-parse-titlewithouttext": "<var>title</var> used without <var>text</var>, and parsed page properties were requested. Did you mean to use <var>page</var> instead of <var>title</var>?", + "apiwarn-redirectsandrevids": "Redirect resolution cannot be used together with the <var>revids</var> parameter. Any redirects the <var>revids</var> point to have not been resolved.", + "apiwarn-tokennotallowed": "Action \"$1\" is not allowed for the current user.", + "apiwarn-tokens-origin": "Tokens may not be obtained when the same-origin policy is not applied.", + "apiwarn-toomanyvalues": "Too many values supplied for parameter <var>$1</var>: the limit is $2.", + "apiwarn-truncatedresult": "This result was truncated because it would otherwise be larger than the limit of $1 bytes.", + "apiwarn-unclearnowtimestamp": "Passing \"$2\" for timestamp parameter <var>$1</var> has been deprecated. If for some reason you need to explicitly specify the current time without calculating it client-side, use <kbd>now<kbd>.", + "apiwarn-unrecognizedvalues": "Unrecognized {{PLURAL:$3|value|values}} for parameter <var>$1</var>: $2.", + "apiwarn-unsupportedarray": "Parameter <var>$1</var> uses unsupported PHP array syntax.", + "apiwarn-urlparamwidth": "Ignoring width value set in <var>$1urlparam</var> ($2) in favor of width value derived from <var>$1urlwidth</var>/<var>$1urlheight</var> ($3).", + "apiwarn-validationfailed-badchars": "invalid characters in key (only <code>a-z</code>, <code>A-Z</code>, <code>0-9</code>, <code>_</code>, and <code>-</code> are allowed).", + "apiwarn-validationfailed-badpref": "not a valid preference.", + "apiwarn-validationfailed-cannotset": "cannot be set by this module.", + "apiwarn-validationfailed-keytoolong": "key too long (no more than $1 bytes allowed).", + "apiwarn-validationfailed": "Validation error for <kbd>$1</kbd>: $2", + "apiwarn-wgDebugAPI": "<strong>Security Warning</strong>: <var>$wgDebugAPI</var> is enabled.", + + "api-feed-error-title": "Error ($1)", + "api-usage-docref": "See $1 for API usage.", + "api-exception-trace": "$1 at $2($3)\n$4", "api-credits-header": "Credits", "api-credits": "API developers:\n* Yuri Astrakhan (creator, lead developer Sep 2006–Sep 2007)\n* Roan Kattouw (lead developer Sep 2007–2009)\n* Victor Vasiliev\n* Bryan Tong Minh\n* Sam Reed\n* Brad Jorsch (lead developer 2013–present)\n\nPlease send your comments, suggestions and questions to mediawiki-api@lists.wikimedia.org\nor file a bug report at https://phabricator.wikimedia.org/." } diff --git a/includes/api/i18n/qqq.json b/includes/api/i18n/qqq.json index fd6a4dd609d0..c5d9bc041e8a 100644 --- a/includes/api/i18n/qqq.json +++ b/includes/api/i18n/qqq.json @@ -29,6 +29,10 @@ "apihelp-main-param-curtimestamp": "{{doc-apihelp-param|main|curtimestamp}}", "apihelp-main-param-origin": "{{doc-apihelp-param|main|origin}}", "apihelp-main-param-uselang": "{{doc-apihelp-param|main|uselang}}", + "apihelp-main-param-errorformat": "{{doc-apihelp-param|main|errorformat}}", + "apihelp-main-param-errorlang": "{{doc-apihelp-param|main|errorlang}}", + "apihelp-main-param-errorsuselocal": "{{doc-apihelp-param|main|errorsuselocal}}", + "apihelp-main-param-responselanginfo": "{{doc-apihelp-param|main|responselanginfo}}", "apihelp-block-description": "{{doc-apihelp-description|block}}", "apihelp-block-param-user": "{{doc-apihelp-param|block|user}}", "apihelp-block-param-expiry": "{{doc-apihelp-param|block|expiry}}\n{{doc-important|Do not translate \"5 months\", \"2 weeks\", \"infinite\", \"indefinite\" or \"never\"!}}", @@ -1420,6 +1424,236 @@ "api-help-authmanagerhelper-returnurl": "{{doc-apihelp-param|description=the \"returnurl\" parameter for AuthManager-using API modules|noseealso=1}}", "api-help-authmanagerhelper-continue": "{{doc-apihelp-param|description=the \"continue\" parameter for AuthManager-using API modules|noseealso=1}}", "api-help-authmanagerhelper-additional-params": "Message to display for AuthManager modules that take additional parameters to populate AuthenticationRequests. Parameters:\n* $1 - AuthManager action used by this module\n* $2 - Module parameter prefix, e.g. \"login\"\n* $3 - Module name, e.g. \"clientlogin\"\n* $4 - Module path, e.g. \"clientlogin\"", + "apierror-allimages-redirect": "{{doc-apierror}}", + "apierror-allpages-generator-redirects": "{{doc-apierror}}", + "apierror-appendnotsupported": "{{doc-apierror}}\n\nParameters:\n* $1 - Content model", + "apierror-articleexists": "{{doc-apierror}}", + "apierror-assertbotfailed": "{{doc-apierror}}", + "apierror-assertnameduserfailed": "{{doc-apierror}}\n\nParameters:\n* $1 - User name passed in.", + "apierror-assertuserfailed": "{{doc-apierror}}", + "apierror-autoblocked": "{{doc-apierror}}", + "apierror-bad-watchlist-token": "{{doc-apierror}}", + "apierror-badconfig-resulttoosmall": "{{doc-apierror}}", + "apierror-badcontinue": "{{doc-apierror}}", + "apierror-baddiff": "{{doc-apierror}}", + "apierror-baddiffto": "{{doc-apierror}}\n\nParameters:\n* $1 - Module parameter prefix, e.g. \"bl\".", + "apierror-badformat": "{{doc-apierror}}\n\nParameters:\n* $1 - Content format.\n* $2 - Content model.\n* $3 - Title using the model.", + "apierror-badformat-generic": "{{doc-apierror}}\n\nParameters:\n* $1 - Content format.\n* $2 - Content model.", + "apierror-badgenerator-notgenerator": "{{doc-apierror}}\n\nParameters:\n* $1 - Generator module name.", + "apierror-badgenerator-unknown": "{{doc-apierror}}\n\nParameters:\n* $1 - Generator module name.", + "apierror-badip": "{{doc-apierror}}", + "apierror-badmd5": "{{doc-apierror}}", + "apierror-badmodule-badsubmodule": "{{doc-apierror}}\n\nParameters:\n* $1 - Module path.\n* $2 - Submodule name.", + "apierror-badmodule-nosubmodules": "{{doc-apierror}}\n\nParameters:\n* $1 - Module path.", + "apierror-badparameter": "{{doc-apierror}}\n\nParameters:\n* $1 - Parameter name.", + "apierror-badquery": "{{doc-apierror}}", + "apierror-badtimestamp": "{{doc-apierror}}\n\nParameters:\n* $1 - Parameter name.\n* $2 - Value of the parameter.", + "apierror-badtoken": "{{doc-apierror}}", + "apierror-badupload": "{{doc-apierror}}\n\nParameters:\n* $1 - Parameter name.", + "apierror-badurl": "{{doc-apierror}}\n\nParameters:\n* $1 - Parameter name.\n* $2 - Value of the parameter.", + "apierror-baduser": "{{doc-apierror}}\n\nParameters:\n* $1 - Parameter name.\n* $2 - Value of the parameter.", + "apierror-badvalue-notmultivalue": "{{doc-apierror}}", + "apierror-blocked": "{{doc-apierror}}", + "apierror-blockedfrommail": "{{doc-apierror}}", + "apierror-botsnotsupported": "{{doc-apierror}}", + "apierror-cannotreauthenticate": "{{doc-apierror}}", + "apierror-cannotviewtitle": "{{doc-apierror}}\n\nParameters:\n* $1 - Title.", + "apierror-cantblock": "{{doc-apierror}}", + "apierror-cantblock-email": "{{doc-apierror}}", + "apierror-cantchangecontentmodel": "{{doc-apierror}}", + "apierror-canthide": "{{doc-apierror}}", + "apierror-cantimport": "{{doc-apierror}}", + "apierror-cantimport-upload": "{{doc-apierror}}", + "apierror-cantoverwrite-sharedfile": "{{doc-apierror}}", + "apierror-cantsend": "{{doc-apierror}}", + "apierror-cantundelete": "{{doc-apierror}}", + "apierror-changeauth-norequest": "{{doc-apierror}}", + "apierror-chunk-too-small": "{{doc-apierror}}\n\nParameters:\n* $1 - Minimum size in bytes.", + "apierror-cidrtoobroad": "{{doc-apierror}}\n\nParameters:\n* $1 - \"IPv4\" or \"IPv6\"\n* $2 - Minimum CIDR mask length.", + "apierror-compare-inputneeded": "{{doc-apierror}}", + "apierror-contentserializationexception": "{{doc-apierror}}\n\nParameters:\n* $1 - Exception text, may end with punctuation. Currently this is probably English, hopefully we'll fix that in the future.", + "apierror-contenttoobig": "{{doc-apierror}}\n\nParameters:\n* $1 - Maximum article size in kilobytes.", + "apierror-copyuploadbaddomain": "{{doc-apierror}}", + "apierror-copyuploadbadurl": "{{doc-apierror}}", + "apierror-create-titleexists": "{{doc-apierror}}", + "apierror-csp-report": "{{doc-apierror}}\n\nParameters:\n* $1 - Error code, e.g. \"toobig\".", + "apierror-databaseerror": "{{doc-apierror}}\n\nParameters:\n* $1 - Exception log ID code. This is meaningless to the end user, but can be used by people with access to the logs to easily find the logged error.", + "apierror-deletedrevs-param-not-1-2": "{{doc-apierror}}\n\nParameters:\n* $1 - Parameter name.\n\nSee also:\n* {{msg-mw|apihelp-query+deletedrevs-description}}", + "apierror-deletedrevs-param-not-3": "{{doc-apierror}}\n\nParameters:\n* $1 - Parameter name.\n\nSee also:\n* {{msg-mw|apihelp-query+deletedrevs-description}}", + "apierror-emptynewsection": "{{doc-apierror}}", + "apierror-emptypage": "{{doc-apierror}}", + "apierror-exceptioncaught": "{{doc-apierror}}\n\nParameters:\n* $1 - Exception log ID code. This is meaningless to the end user, but can be used by people with access to the logs to easily find the logged error.\n* $2 - Exception message, which may end with punctuation. Probably in English.", + "apierror-filedoesnotexist": "{{doc-apierror}}", + "apierror-fileexists-sharedrepo-perm": "{{doc-apierror}}", + "apierror-filenopath": "{{doc-apierror}}", + "apierror-filetypecannotberotated": "{{doc-apierror}}", + "apierror-formatphp": "{{doc-apierror}}", + "apierror-imageusage-badtitle": "{{doc-apierror}}\n\nParameters:\n* $1 - Module name.", + "apierror-import-unknownerror": "{{doc-apierror}}\n\nParameters:\n* $1 - Error message returned by the import, probably in English.", + "apierror-integeroutofrange-abovebotmax": "{{doc-apierror}}\n\nParameters:\n* $1 - Parameter name\n* $2 - Maximum allowed value\n* $3 - Supplied value", + "apierror-integeroutofrange-abovemax": "{{doc-apierror}}\n\nParameters:\n* $1 - Parameter name\n* $2 - Maximum allowed value\n* $3 - Supplied value", + "apierror-integeroutofrange-belowminimum": "{{doc-apierror}}\n\nParameters:\n* $1 - Parameter name\n* $2 - Minimum allowed value\n* $3 - Supplied value", + "apierror-invalid-chunk": "{{doc-apierror}}", + "apierror-invalid-file-key": "{{doc-apierror}}", + "apierror-invalidcategory": "{{doc-apierror}}", + "apierror-invalidexpiry": "{{doc-apierror}}\n\nParameters:\n* $1 - Value provided.", + "apierror-invalidlang": "{{doc-apierror}}\n\nParameters:\n* $1 - Parameter name.", + "apierror-invalidoldimage": "{{doc-apierror}}", + "apierror-invalidparammix": "{{doc-apierror}}\n\nParameters:\n* $1 - List of parameter names or \"parameter=value\" text.\n* $2 - Number of parameters.", + "apierror-invalidparammix-cannotusewith": "{{doc-apierror}}\n\nParameters:\n* $1 - Parameter name or \"parameter=value\" text.\n* $2 - Parameter name or \"parameter=value\" text.", + "apierror-invalidparammix-mustusewith": "{{doc-apierror}}\n\nParameters:\n* $1 - Parameter name or \"parameter=value\" text.\n* $2 - Parameter name or \"parameter=value\" text.", + "apierror-invalidparammix-parse-new-section": "{{doc-apierror}}", + "apierror-invalidsection": "{{doc-apierror}}", + "apierror-invalidsha1base36hash": "{{doc-apierror}}", + "apierror-invalidsha1hash": "{{doc-apierror}}", + "apierror-invalidtitle": "{{doc-apierror}}\n\nParameters:\n* $1 - Title that is invalid", + "apierror-invalidurlparam": "{{doc-apierror}}\n\nParameters:\n* $1 - Module parameter prefix, e.g. \"bl\".\n* $2 - Key\n* $3 - Value.", + "apierror-invaliduser": "{{doc-apierror}}\n\nParameters:\n* $1 - User name that is invalid.", + "apierror-maxlag": "{{doc-apierror}}\n\nParameters:\n* $1 - Database lag in seconds.\n* $2 - Database server that is lagged.", + "apierror-maxlag-generic": "{{doc-apierror}}\n\nParameters:\n* $1 - Database is lag in seconds.", + "apierror-mimesearchdisabled": "{{doc-apierror}}", + "apierror-missingcontent-pageid": "{{doc-apierror}}\n\nParameters:\n* $1 - Page ID number.", + "apierror-missingparam": "{{doc-apierror}}\n\nParameters:\n* $1 - Parameter name.", + "apierror-missingparam-at-least-one-of": "{{doc-apierror}}\n\nParameters:\n* $1 - List of parameter names.\n* $2 - Number of parameters.", + "apierror-missingparam-one-of": "{{doc-apierror}}\n\nParameters:\n* $1 - List of parameter names.\n* $2 - Number of parameters.", + "apierror-missingrev-pageid": "{{doc-apierror}}\n\nParameters:\n* $1 - Page ID number.", + "apierror-missingtitle": "{{doc-apierror}}", + "apierror-missingtitle-byname": "{{doc-apierror}}", + "apierror-missingtitle-createonly": "{{doc-apierror}}", + "apierror-moduledisabled": "{{doc-apierror}}\n\nParameters:\n* $1 - Name of the module.", + "apierror-multival-only-one": "{{doc-apierror}}\n\nParameters:\n* $1 - Parameter name.", + "apierror-multival-only-one-of": "{{doc-apierror}}\n\nParameters:\n* $1 - Parameter name.\n* $2 - Possible values for the parameter.\n* $3 - Number of values.", + "apierror-multpages": "{{doc-apierror}}\n\nParameters:\n* $1 - Parameter name", + "apierror-mustbeloggedin": "{{doc-apierror}}\n\nParameters:\n* $1 - One of the action-* messages (for example {{msg-mw|action-edit}}) or other such messages tagged with {{tl|doc-action}} in their documentation\n\nPlease report at [[Support]] if you are unable to properly translate this message. Also see [[phab:T16246]] (now closed) for background.\n\nSee also:\n* {{msg-mw|apierror-permissiondenied}}\n* {{msg-mw|permissionserrorstext-withaction}}", + "apierror-mustbeloggedin-changeauth": "{{doc-apierror}}", + "apierror-mustbeloggedin-generic": "{{doc-apierror}}", + "apierror-mustbeloggedin-linkaccounts": "{{doc-apierror}}", + "apierror-mustbeloggedin-removeauth": "{{doc-apierror}}", + "apierror-mustbeloggedin-uploadstash": "{{doc-apierror}}", + "apierror-mustbeposted": "{{doc-apierror}}\n\nParameters:\n* $1 - Module name.", + "apierror-mustpostparams": "{{doc-apierror}}\n\nParameters:\n* $1 - Parameter names.\n* $2 - Number of parameters.", + "apierror-no-direct-editing": "{{doc-apierror}}\n\nParameters:\n* $1 - Content model.\n* $2 - Title using the model.", + "apierror-noapiwrite": "{{doc-apierror}}", + "apierror-nochanges": "{{doc-apierror}}", + "apierror-nodeleteablefile": "{{doc-apierror}}", + "apierror-noedit": "{{doc-apierror}}", + "apierror-noedit-anon": "{{doc-apierror}}", + "apierror-noimageredirect": "{{doc-apierror}}", + "apierror-noimageredirect-anon": "{{doc-apierror}}", + "apierror-nosuchlogid": "{{doc-apierror}}\n\nParameters:\n* $1 - Log ID number.", + "apierror-nosuchpageid": "{{doc-apierror}}\n\nParameters:\n* $1 - Page ID number.", + "apierror-nosuchrcid": "{{doc-apierror}}\n\nParameters:\n* $1 - RecentChanges ID number.", + "apierror-nosuchrevid": "{{doc-apierror}}\n\nParameters:\n* $1 - Revision ID number.", + "apierror-nosuchsection": "{{doc-apierror}}\n\nParameters:\n* $1 - Section identifier. Probably a number or \"T-\" followed by a number.", + "apierror-nosuchsection-what": "{{doc-apierror}}\n\nParameters:\n* $1 - Section identifier. Probably a number or \"T-\" followed by a number.\n* $2 - Page title, revision ID formatted with {{msg-mw|revid}}, or page ID formatted with {{msg-mw|pageid}}.", + "apierror-notarget": "{{doc-apierror}}", + "apierror-notpatrollable": "{{doc-apierror}}\n\nParameters:\n* $1 - Revision ID number.", + "apierror-nouploadmodule": "{{doc-apierror}}", + "apierror-opensearch-json-warnings": "{{doc-apierror}}", + "apierror-pagecannotexist": "{{doc-apierror}}", + "apierror-pagedeleted": "{{doc-apierror}}", + "apierror-paramempty": "{{doc-apierror}}\n\nParameters:\n* $1 - Parameter name.", + "apierror-parsetree-notwikitext": "{{doc-apierror}}", + "apierror-parsetree-notwikitext-title": "{{doc-apierror}}\n\nParameters:\n* $1 - Page title.\n* $2 - Content model.", + "apierror-pastexpiry": "{{doc-apierror}}\n\nParameters:\n* $1 - Supplied expiry time.", + "apierror-permissiondenied-generic": "{{doc-apierror}}", + "apierror-permissiondenied-patrolflag": "{{doc-apierror}}\n\nSee also:\n* {{msg-mw|apierror-permissiondenied}}", + "apierror-permissiondenied-unblock": "{{doc-apierror}}\n\nSee also:\n* {{msg-mw|apierror-permissiondenied}}", + "apierror-permissiondenied": "{{doc-apierror}}\n\nParameters:\n* $1 - One of the action-* messages (for example {{msg-mw|action-edit}}) or other such messages tagged with {{tl|doc-action}} in their documentation\n\nPlease report at [[Support]] if you are unable to properly translate this message. Also see [[phab:T16246]] (now closed) for background.\n\nSee also:\n* {{msg-mw|permissionserrorstext-withaction}}", + "apierror-prefixsearchdisabled": "{{doc-apierror}}", + "apierror-promised-nonwrite-api": "{{doc-apierror}}", + "apierror-protect-invalidaction": "{{doc-apierror}}\n\nParameters:\n* $1 - Supplied protection type.", + "apierror-protect-invalidlevel": "{{doc-apierror}}\n\nParameters:\n* $1 - Supplied protection level.", + "apierror-ratelimited": "{{doc-apierror}}", + "apierror-readapidenied": "{{doc-apierror}}", + "apierror-readonly": "{{doc-apierror}}", + "apierror-reauthenticate": "{{doc-apierror}}", + "apierror-redirect-appendonly": "{{doc-apierror}}", + "apierror-revdel-mutuallyexclusive": "{{doc-apierror}}", + "apierror-revdel-needtarget": "{{doc-apierror}}", + "apierror-revdel-paramneeded": "{{doc-apierror}}", + "apierror-revisions-norevids": "{{doc-apierror}}\n\nParameters:\n* $1 - Module parameter prefix, e.g. \"bl\".", + "apierror-revisions-singlepage": "{{doc-apierror}}\n\nParameters:\n* $1 - Module parameter prefix, e.g. \"bl\".", + "apierror-revwrongpage": "{{doc-apierror}}\n\nParameters:\n* $1 - Revision ID number.\n* $2 - Page title.", + "apierror-searchdisabled": "{{doc-apierror}}\n\nParameters:\n* $1 - Search parameter that is disabled.", + "apierror-sectionreplacefailed": "{{doc-apierror}}", + "apierror-sectionsnotsupported": "{{doc-apierror}}\n\nParameters:\n* $1 - Content model that doesn't support sections.", + "apierror-sectionsnotsupported-what": "{{doc-apierror}}\n\nParameters:\n* $1 - Page title, revision ID formatted with {{msg-mw|revid}}, or page ID formatted with {{msg-mw|pageid}}.", + "apierror-show": "{{doc-apierror}}", + "apierror-siteinfo-includealldenied": "{{doc-apierror}}", + "apierror-sizediffdisabled": "{{doc-apierror}}", + "apierror-spamdetected": "{{doc-apierror}}\n\nParameters:\n* $1 - Matching \"spam filter\".\n\nSee also:\n* {{msg-mw|spamprotectionmatch}}", + "apierror-specialpage-cantexecute": "{{doc-apierror}}", + "apierror-stashedfilenotfound": "{{doc-apierror}}\n\nParameters:\n* $1 - Exception text. Currently this is probably English, hopefully we'll fix that in the future.", + "apierror-stashedit-missingtext": "{{doc-apierror}}", + "apierror-stashfailed-complete": "{{doc-apierror}}", + "apierror-stashfailed-nosession": "{{doc-apierror}}", + "apierror-stashfilestorage": "{{doc-apierror}}\n\nParameters:\n* $1 - Exception text, which may already end with punctuation. Currently this is probably English, hopefully we'll fix that in the future.", + "apierror-stashnosuchfilekey": "{{doc-apierror}}\n\nParameters:\n* $1 - Exception text. Currently this is probably English, hopefully we'll fix that in the future.", + "apierror-stashpathinvalid": "{{doc-apierror}}\n\nParameters:\n* $1 - Exception text. Currently this is probably English, hopefully we'll fix that in the future.", + "apierror-stashwrongowner": "{{doc-apierror}}\n\nParameters:\n* $1 - Exception text, which should already end with punctuation. Currently this is probably English, hopefully we'll fix that in the future.", + "apierror-stashzerolength": "{{doc-apierror}}\n\nParameters:\n* $1 - Exception text. Currently this is probably English, hopefully we'll fix that in the future.", + "apierror-templateexpansion-notwikitext": "{{doc-apierror}}\n\nParameters:\n* $1 - Page title.\n* $2 - Content model.", + "apierror-toofewexpiries": "{{doc-apierror}}\n\nParameters:\n* $1 - Number provided.\n* $2 - Number needed.", + "apierror-unknownaction": "{{doc-apierror}}\n\nParameters:\n* $1 - Action provided.", + "apierror-unknownerror": "{{doc-apierror}}\n\nParameters:\n* $1 - Error code (possibly a message key) not handled by ApiBase::parseMsg().", + "apierror-unknownerror-editpage": "{{doc-apierror}}\n\nParameters:\n* $1 - Error code (an integer).", + "apierror-unknownerror-nocode": "{{doc-apierror}}", + "apierror-unknownformat": "{{doc-apierror}}\n\nParameters:\n* $1 - Format provided.", + "apierror-unrecognizedparams": "{{doc-apierror}}\n\nParameters:\n* $1 - List of parameters.\n* $2 - Number of parameters.", + "apierror-unrecognizedvalue": "{{doc-apierror}}\n\nParameters:\n* $1 - Parameter name.\n* $2 - Parameter value.", + "apierror-unsupportedrepo": "{{doc-apierror}}", + "apierror-upload-filekeyneeded": "{{doc-apierror}}", + "apierror-upload-filekeynotallowed": "{{doc-apierror}}", + "apierror-upload-inprogress": "{{doc-apierror}}", + "apierror-upload-missingresult": "{{doc-apierror}}", + "apierror-urlparamnormal": "{{doc-apierror}}\n\nParameters:\n* $1 - Image title.", + "apierror-writeapidenied": "{{doc-apierror}}", + "apiwarn-alldeletedrevisions-performance": "{{doc-apierror}}\n\nParameters:\n* $1 - Module parameter prefix, e.g. \"bl\".", + "apiwarn-badurlparam": "{{doc-apierror}}\n\nParameters:\n* $1 - Module parameter prefix, e.g. \"bl\".\n* $2 - Image title.", + "apiwarn-badutf8": "{{doc-apierror}}\n\nParameters:\n* $1 - Parameter name.", + "apiwarn-checktoken-percentencoding": "{{doc-apierror}}", + "apiwarn-deprecation-deletedrevs": "{{doc-apierror}}", + "apiwarn-deprecation-expandtemplates-prop": "{{doc-apierror}}", + "apiwarn-deprecation-httpsexpected": "{{doc-apierror}}", + "apiwarn-deprecation-login-botpw": "{{doc-apierror}}", + "apiwarn-deprecation-login-nobotpw": "{{doc-apierror}}", + "apiwarn-deprecation-login-token": "{{doc-apierror}}", + "apiwarn-deprecation-parameter": "{{doc-apierror}}\n\nParameters:\n* $1 - Parameter name.", + "apiwarn-deprecation-parse-headitems": "{{doc-apierror}}", + "apiwarn-deprecation-purge-get": "{{doc-apierror}}", + "apiwarn-deprecation-withreplacement": "{{doc-apierror}}\n\nParameters:\n* $1 - Query string fragment that is deprecated, e.g. \"action=tokens\".\n* $2 - Query string fragment to use instead, e.g. \"action=tokens\".", + "apiwarn-difftohidden": "{{doc-apierror}}\n\nParameters:\n* $1 - Revision ID number.", + "apiwarn-errorprinterfailed": "{{doc-apierror}}", + "apiwarn-errorprinterfailed-ex": "{{doc-apierror}}\n\nParameters:\n* $1 - Exception message, which may already end in punctuation. Probably in English.", + "apiwarn-invalidcategory": "{{doc-apierror}}\n\nParameters:\n* $1 - Supplied category name.", + "apiwarn-invalidtitle": "{{doc-apierror}}\n\nParameters:\n* $1 - Supplied title.", + "apiwarn-invalidxmlstylesheet": "{{doc-apierror}}", + "apiwarn-invalidxmlstylesheetext": "{{doc-apierror}}", + "apiwarn-invalidxmlstylesheetns": "{{doc-apierror}}", + "apiwarn-moduleswithoutvars": "{{doc-apierror}}", + "apiwarn-notfile": "{{doc-apierror}}\n\nParameters:\n* $1 - Supplied file name.", + "apiwarn-nothumb-noimagehandler": "{{doc-apierror}}\n\nParameters:\n* $1 - File name.", + "apiwarn-parse-nocontentmodel": "{{doc-apierror}}\n\nParameters:\n* $1 - Content model being assumed.", + "apiwarn-parse-titlewithouttext": "{{doc-apierror}}", + "apiwarn-redirectsandrevids": "{{doc-apierror}}", + "apiwarn-tokennotallowed": "{{doc-apierror}}\n\nParameters:\n* $1 - Token type being requested, typically named after the action requiring the token.", + "apiwarn-tokens-origin": "{{doc-apierror}}", + "apiwarn-toomanyvalues": "{{doc-apierror}}\n\nParameters:\n* $1 - Parameter name.\n* $2 - Maximum number of values allowed.", + "apiwarn-truncatedresult": "{{doc-apierror}}\n\nParameters:\n* $1 - Size limit in bytes.", + "apiwarn-unclearnowtimestamp": "{{doc-apierror}}\n\nParameters:\n* $1 - Parameter name.\n* $2 - Supplied value.", + "apiwarn-unrecognizedvalues": "{{doc-apierror}}\n\nParameters:\n* $1 - Parameter name.\n* $2 - List of unknown values supplied.\n* $3 - Number of unknown values.", + "apiwarn-unsupportedarray": "{{doc-apierror}}\n\nParameters:\n* $1 - Parameter name.", + "apiwarn-urlparamwidth": "{{doc-apierror}}\n\nParameters:\n* $1 - Module parameter prefix, e.g. \"bl\".\n* $2 - Width being ignored.\n* $3 - Width being used.", + "apiwarn-validationfailed": "{{doc-apierror}}\n\nParameters:\n* $1 - User preference name.\n* $2 - Failure message, such as {{msg-mw|apiwarn-validationfailed-badpref}}. Probably already ends with punctuation", + "apiwarn-validationfailed-badchars": "{{doc-apierror}}\n\nUsed with {{msg-mw|apiwarn-validationfailed}}.", + "apiwarn-validationfailed-badpref": "{{doc-apierror}}\n\nUsed with {{msg-mw|apiwarn-validationfailed}}.", + "apiwarn-validationfailed-cannotset": "{{doc-apierror}}\n\nUsed with {{msg-mw|apiwarn-validationfailed}}.", + "apiwarn-validationfailed-keytoolong": "{{doc-apierror}}\n\nUsed with {{msg-mw|apiwarn-validationfailed}}.\n\nParameters:\n* $1 - Maximum allowed key length in bytes.", + "apiwarn-wgDebugAPI": "{{doc-apierror}}", + "api-feed-error-title": "Used as a feed item title when an error occurs in <kbd>action=feedwatchlist</kbd>.\n\nParameters:\n* $1 - API error code", + "api-usage-docref": "\n\nParameters:\n* $1 - URL of the API auto-generated documentation.", + "api-exception-trace": "\n\nParameters:\n* $1 - Exception class.\n* $2 - File from which the exception was thrown.\n* $3 - Line number from which the exception was thrown.\n* $4 - Exception backtrace.", "api-credits-header": "Header for the API credits section in the API help output\n{{Identical|Credit}}", "api-credits": "API credits text, displayed in the API help output" } diff --git a/includes/specials/SpecialApiHelp.php b/includes/specials/SpecialApiHelp.php index 74b474ace46d..54480132e4ca 100644 --- a/includes/specials/SpecialApiHelp.php +++ b/includes/specials/SpecialApiHelp.php @@ -77,6 +77,11 @@ class SpecialApiHelp extends UnlistedSpecialPage { $main = new ApiMain( $this->getContext(), false ); try { $module = $main->getModuleFromPath( $moduleName ); + } catch ( ApiUsageException $ex ) { + $this->getOutput()->addHTML( Html::rawElement( 'span', [ 'class' => 'error' ], + $this->msg( 'apihelp-no-such-module', $moduleName )->inContentLanguage()->parse() + ) ); + return; } catch ( UsageException $ex ) { $this->getOutput()->addHTML( Html::rawElement( 'span', [ 'class' => 'error' ], $this->msg( 'apihelp-no-such-module', $moduleName )->inContentLanguage()->parse() diff --git a/includes/specials/SpecialEmailuser.php b/includes/specials/SpecialEmailuser.php index a550e8853bae..c0f1c3dfe0e0 100644 --- a/includes/specials/SpecialEmailuser.php +++ b/includes/specials/SpecialEmailuser.php @@ -306,7 +306,7 @@ class SpecialEmailUser extends UnlistedSpecialPage { * @since 1.20 * @param array $data * @param HTMLForm $form - * @return Status|string|bool + * @return Status|bool */ public static function uiSubmit( array $data, HTMLForm $form ) { return self::submit( $data, $form->getContext() ); @@ -319,8 +319,7 @@ class SpecialEmailUser extends UnlistedSpecialPage { * * @param array $data * @param IContextSource $context - * @return Status|string|bool Status object, or potentially a String on error - * or maybe even true on success if anything uses the EmailUser hook. + * @return Status|bool */ public static function submit( array $data, IContextSource $context ) { $config = $context->getConfig(); @@ -328,7 +327,7 @@ class SpecialEmailUser extends UnlistedSpecialPage { $target = self::getTarget( $data['Target'] ); if ( !$target instanceof User ) { // Messages used here: notargettext, noemailtext, nowikiemailtext - return $context->msg( $target . 'text' )->parseAsBlock(); + return Status::newFatal( $target . 'text' ); } $to = MailAddress::newFromUser( $target ); @@ -341,9 +340,33 @@ class SpecialEmailUser extends UnlistedSpecialPage { $text .= $context->msg( 'emailuserfooter', $from->name, $to->name )->inContentLanguage()->text(); - $error = ''; + $error = false; if ( !Hooks::run( 'EmailUser', [ &$to, &$from, &$subject, &$text, &$error ] ) ) { - return $error; + if ( $error instanceof Status ) { + return $error; + } elseif ( $error === false || $error === '' || $error === [] ) { + // Possibly to tell HTMLForm to pretend there was no submission? + return false; + } elseif ( $error === true ) { + // Hook sent the mail itself and indicates success? + return Status::newGood(); + } elseif ( is_array( $error ) ) { + $status = Status::newGood(); + foreach ( $error as $e ) { + $status->fatal( $e ); + } + return $status; + } elseif ( $error instanceof MessageSpecifier ) { + return Status::newFatal( $error ); + } else { + // Ugh. Either a raw HTML string, or something that's supposed + // to be treated like one. + $type = is_object( $error ) ? get_class( $error ) : gettype( $error ); + wfDeprecated( "EmailUser hook returning a $type as \$error", '1.29' ); + return Status::newFatal( new ApiRawMessage( + [ '$1', Message::rawParam( (string)$error ) ], 'hookaborted' + ) ); + } } if ( $config->get( 'UserEmailUseReplyTo' ) ) { diff --git a/includes/specials/SpecialUnblock.php b/includes/specials/SpecialUnblock.php index cff8bf463abe..a8b5e5e5ed76 100644 --- a/includes/specials/SpecialUnblock.php +++ b/includes/specials/SpecialUnblock.php @@ -175,7 +175,7 @@ class SpecialUnblock extends SpecialPage { * @param array $data * @param IContextSource $context * @throws ErrorPageError - * @return array|bool Array(message key, parameters) on failure, True on success + * @return array|bool Array( Array( message key, parameters ) ) on failure, True on success */ public static function processUnblock( array $data, IContextSource $context ) { $performer = $context->getUser(); @@ -211,7 +211,7 @@ class SpecialUnblock extends SpecialPage { # Delete block if ( !$block->delete() ) { - return [ 'ipb_cant_unblock', htmlspecialchars( $block->getTarget() ) ]; + return [ [ 'ipb_cant_unblock', htmlspecialchars( $block->getTarget() ) ] ]; } # Unset _deleted fields as needed diff --git a/languages/i18n/en.json b/languages/i18n/en.json index afd13f0016d3..dad1737df9ad 100644 --- a/languages/i18n/en.json +++ b/languages/i18n/en.json @@ -1303,11 +1303,13 @@ "action-upload_by_url": "upload this file from a URL", "action-writeapi": "use the write API", "action-delete": "delete this page", - "action-deleterevision": "delete this revision", - "action-deletedhistory": "view this page's deleted history", + "action-deleterevision": "delete revisions", + "action-deletelogentry": "delete log entries", + "action-deletedhistory": "view a page's deleted history", + "action-deletedtext": "view deleted revision text", "action-browsearchive": "search deleted pages", - "action-undelete": "undelete this page", - "action-suppressrevision": "review and restore this hidden revision", + "action-undelete": "undelete pages", + "action-suppressrevision": "review and restore hidden revisions", "action-suppressionlog": "view this private log", "action-block": "block this user from editing", "action-protect": "change protection levels for this page", @@ -1322,6 +1324,7 @@ "action-userrights-interwiki": "edit user rights of users on other wikis", "action-siteadmin": "lock or unlock the database", "action-sendemail": "send emails", + "action-editmyoptions": "edit your preferences", "action-editmywatchlist": "edit your watchlist", "action-viewmywatchlist": "view your watchlist", "action-viewmyprivateinfo": "view your private information", @@ -4244,5 +4247,7 @@ "usercssispublic": "Please note: CSS subpages should not contain confidential data as they are viewable by other users.", "restrictionsfield-badip": "Invalid IP address or range: $1", "restrictionsfield-label": "Allowed IP ranges:", - "restrictionsfield-help": "One IP address or CIDR range per line. To enable everything, use<br><code>0.0.0.0/0</code><br><code>::/0</code>" + "restrictionsfield-help": "One IP address or CIDR range per line. To enable everything, use<br><code>0.0.0.0/0</code><br><code>::/0</code>", + "revid": "r$1", + "pageid": "page ID $1" } diff --git a/languages/i18n/qqq.json b/languages/i18n/qqq.json index 936fd8b8acfb..bc4bc4ced473 100644 --- a/languages/i18n/qqq.json +++ b/languages/i18n/qqq.json @@ -1489,6 +1489,8 @@ "action-delete": "{{Doc-action|delete}}", "action-deleterevision": "{{Doc-action|deleterevision}}", "action-deletedhistory": "{{Doc-action|deletedhistory}}", + "action-deletedtext": "{{Doc-action|deletedtext}}", + "action-deletelogentry": "{{Doc-action|deletelogentry}}", "action-browsearchive": "{{Doc-action|browsearchive}}", "action-undelete": "{{Doc-action|undelete}}", "action-suppressrevision": "{{Doc-action|suppressrevision}}", @@ -1508,6 +1510,7 @@ "action-sendemail": "{{doc-action|sendemail}}\n{{Identical|E-mail}}", "action-editmywatchlist": "{{doc-action|editmywatchlist}}\n{{Identical|Edit your watchlist}}", "action-viewmywatchlist": "{{doc-action|viewmywatchlist}}\n{{Identical|View your watchlist}}", + "action-editmyoptions": "{{Doc-action|editmyoptions}}", "action-viewmyprivateinfo": "{{doc-action|viewmyprivateinfo}}", "action-editmyprivateinfo": "{{doc-action|editmyprivateinfo}}", "action-editcontentmodel": "{{doc-action|editcontentmodel}}", @@ -4428,5 +4431,7 @@ "usercssispublic": "A reminder to users that CSS subpages are not preferences but normal pages, and thus can be viewed by other users and the general public. This message is shown to a user whenever they are editing a subpage in their own user-space that ends in .css. See also {{msg-mw|userjsispublic}}", "restrictionsfield-badip": "An error message shown when one entered an invalid IP address or range in a restrictions field (such as Special:BotPassword). $1 is the IP address.", "restrictionsfield-label": "Field label shown for restriction fields (e.g. on Special:BotPassword).", - "restrictionsfield-help": "Placeholder text displayed in restriction fields (e.g. on Special:BotPassword)." + "restrictionsfield-help": "Placeholder text displayed in restriction fields (e.g. on Special:BotPassword).", + "revid": "Used to format a revision ID number in text. Parameters:\n* $1 - Revision ID number.", + "pageid": "Used to format a page ID number in text. Parameters:\n* $1 - Page ID number." } diff --git a/resources/src/mediawiki/page/rollback.js b/resources/src/mediawiki/page/rollback.js index cb46b110dc40..d94b158538b7 100644 --- a/resources/src/mediawiki/page/rollback.js +++ b/resources/src/mediawiki/page/rollback.js @@ -30,6 +30,7 @@ $spinner = $.createSpinner( { size: 'small', type: 'inline' } ); $link.hide().after( $spinner ); + // @todo: data.messageHtml is no more. Convert to using errorformat=html. api = new mw.Api(); api.rollback( page, user ) .then( function ( data ) { diff --git a/tests/phpunit/includes/WatchedItemQueryServiceUnitTest.php b/tests/phpunit/includes/WatchedItemQueryServiceUnitTest.php index 93687df2d5bf..bdec0a50d90f 100644 --- a/tests/phpunit/includes/WatchedItemQueryServiceUnitTest.php +++ b/tests/phpunit/includes/WatchedItemQueryServiceUnitTest.php @@ -1233,7 +1233,7 @@ class WatchedItemQueryServiceUnitTest extends PHPUnit_Framework_TestCase { ->with( 'watchlisttoken' ) ->willReturn( '0123456789abcdef' ); - $this->setExpectedException( UsageException::class, 'Incorrect watchlist token provided' ); + $this->setExpectedException( ApiUsageException::class, 'Incorrect watchlist token provided' ); $queryService->getWatchedItemsWithRecentChangeInfo( $user, [ 'watchlistOwner' => $otherUser, 'watchlistOwnerToken' => $token ] diff --git a/tests/phpunit/includes/api/ApiBaseTest.php b/tests/phpunit/includes/api/ApiBaseTest.php index 8b75d562816f..96f3e44f0f21 100644 --- a/tests/phpunit/includes/api/ApiBaseTest.php +++ b/tests/phpunit/includes/api/ApiBaseTest.php @@ -20,7 +20,7 @@ class ApiBaseTest extends ApiTestCase { } /** - * @expectedException UsageException + * @expectedException ApiUsageException * @covers ApiBase::requireOnlyOneParameter */ public function testRequireOnlyOneParameterZero() { @@ -32,7 +32,7 @@ class ApiBaseTest extends ApiTestCase { } /** - * @expectedException UsageException + * @expectedException ApiUsageException * @covers ApiBase::requireOnlyOneParameter */ public function testRequireOnlyOneParameterTrue() { @@ -58,10 +58,10 @@ class ApiBaseTest extends ApiTestCase { $context->setRequest( new FauxRequest( $input !== null ? [ 'foo' => $input ] : [] ) ); $wrapper->mMainModule = new ApiMain( $context ); - if ( $expected instanceof UsageException ) { + if ( $expected instanceof ApiUsageException ) { try { $wrapper->getParameterFromSettings( 'foo', $paramSettings, true ); - } catch ( UsageException $ex ) { + } catch ( ApiUsageException $ex ) { $this->assertEquals( $expected, $ex ); } } else { @@ -73,9 +73,7 @@ class ApiBaseTest extends ApiTestCase { public static function provideGetParameterFromSettings() { $warnings = [ - 'The value passed for \'foo\' contains invalid or non-normalized data. Textual data should ' . - 'be valid, NFC-normalized Unicode without C0 control characters other than ' . - 'HT (\\t), LF (\\n), and CR (\\r).' + [ 'apiwarn-badutf8', 'foo' ], ]; $c0 = ''; @@ -96,7 +94,7 @@ class ApiBaseTest extends ApiTestCase { 'String param, required, empty' => [ '', [ ApiBase::PARAM_DFLT => 'default', ApiBase::PARAM_REQUIRED => true ], - new UsageException( 'The foo parameter must be set', 'nofoo' ), + ApiUsageException::newWithMessage( null, [ 'apierror-missingparam', 'foo' ] ), [] ], 'Multi-valued parameter' => [ @@ -126,4 +124,48 @@ class ApiBaseTest extends ApiTestCase { ]; } + public function testErrorArrayToStatus() { + $mock = new MockApi(); + + // Sanity check empty array + $expect = Status::newGood(); + $this->assertEquals( $expect, $mock->errorArrayToStatus( [] ) ); + + // No blocked $user, so no special block handling + $expect = Status::newGood(); + $expect->fatal( 'blockedtext' ); + $expect->fatal( 'autoblockedtext' ); + $expect->fatal( 'mainpage' ); + $expect->fatal( 'parentheses', 'foobar' ); + $this->assertEquals( $expect, $mock->errorArrayToStatus( [ + [ 'blockedtext' ], + [ 'autoblockedtext' ], + 'mainpage', + [ 'parentheses', 'foobar' ], + ] ) ); + + // Has a blocked $user, so special block handling + $user = $this->getMutableTestUser()->getUser(); + $block = new \Block( [ + 'address' => $user->getName(), + 'user' => $user->getID(), + 'reason' => __METHOD__, + 'expiry' => time() + 100500, + ] ); + $block->insert(); + $blockinfo = [ 'blockinfo' => ApiQueryUserInfo::getBlockInfo( $block ) ]; + + $expect = Status::newGood(); + $expect->fatal( ApiMessage::create( 'apierror-blocked', 'blocked', $blockinfo ) ); + $expect->fatal( ApiMessage::create( 'apierror-autoblocked', 'autoblocked', $blockinfo ) ); + $expect->fatal( 'mainpage' ); + $expect->fatal( 'parentheses', 'foobar' ); + $this->assertEquals( $expect, $mock->errorArrayToStatus( [ + [ 'blockedtext' ], + [ 'autoblockedtext' ], + 'mainpage', + [ 'parentheses', 'foobar' ], + ], $user ) ); + } + } diff --git a/tests/phpunit/includes/api/ApiBlockTest.php b/tests/phpunit/includes/api/ApiBlockTest.php index d2dccf997d27..08fc1286cf1f 100644 --- a/tests/phpunit/includes/api/ApiBlockTest.php +++ b/tests/phpunit/includes/api/ApiBlockTest.php @@ -65,8 +65,8 @@ class ApiBlockTest extends ApiTestCase { } /** - * @expectedException UsageException - * @expectedExceptionMessage The token parameter must be set + * @expectedException ApiUsageException + * @expectedExceptionMessage The "token" parameter must be set */ public function testBlockingActionWithNoToken() { $this->doApiRequest( diff --git a/tests/phpunit/includes/api/ApiContinuationManagerTest.php b/tests/phpunit/includes/api/ApiContinuationManagerTest.php index 3ad16d132224..bb4ea7589373 100644 --- a/tests/phpunit/includes/api/ApiContinuationManagerTest.php +++ b/tests/phpunit/includes/api/ApiContinuationManagerTest.php @@ -160,10 +160,8 @@ class ApiContinuationManagerTest extends MediaWikiTestCase { try { self::getManager( 'foo', $allModules, [ 'mock1', 'mock2' ] ); $this->fail( 'Expected exception not thrown' ); - } catch ( UsageException $ex ) { - $this->assertSame( - 'Invalid continue param. You should pass the original value returned by the previous query', - $ex->getMessage(), + } catch ( ApiUsageException $ex ) { + $this->assertTrue( ApiTestCase::apiExceptionHasCode( $ex, 'badcontinue' ), 'Expected exception' ); } diff --git a/tests/phpunit/includes/api/ApiEditPageTest.php b/tests/phpunit/includes/api/ApiEditPageTest.php index 02d0a0dc57af..0ffcbca762d6 100644 --- a/tests/phpunit/includes/api/ApiEditPageTest.php +++ b/tests/phpunit/includes/api/ApiEditPageTest.php @@ -195,9 +195,9 @@ class ApiEditPageTest extends ApiTestCase { 'section' => '9999', 'text' => 'text', ] ); - $this->fail( "Should have raised a UsageException" ); - } catch ( UsageException $e ) { - $this->assertEquals( 'nosuchsection', $e->getCodeString() ); + $this->fail( "Should have raised an ApiUsageException" ); + } catch ( ApiUsageException $e ) { + $this->assertTrue( self::apiExceptionHasCode( $e, 'nosuchsection' ) ); } } @@ -333,8 +333,8 @@ class ApiEditPageTest extends ApiTestCase { ], null, self::$users['sysop']->getUser() ); $this->fail( 'redirect-appendonly error expected' ); - } catch ( UsageException $ex ) { - $this->assertEquals( 'redirect-appendonly', $ex->getCodeString() ); + } catch ( ApiUsageException $ex ) { + $this->assertTrue( self::apiExceptionHasCode( $ex, 'redirect-appendonly' ) ); } } @@ -369,8 +369,8 @@ class ApiEditPageTest extends ApiTestCase { ], null, self::$users['sysop']->getUser() ); $this->fail( 'edit conflict expected' ); - } catch ( UsageException $ex ) { - $this->assertEquals( 'editconflict', $ex->getCodeString() ); + } catch ( ApiUsageException $ex ) { + $this->assertTrue( self::apiExceptionHasCode( $ex, 'editconflict' ) ); } } @@ -474,7 +474,7 @@ class ApiEditPageTest extends ApiTestCase { public function testCheckDirectApiEditingDisallowed_forNonTextContent() { $this->setExpectedException( - 'UsageException', + 'ApiUsageException', 'Direct editing via API is not supported for content model ' . 'testing used by Dummy:ApiEditPageTest_nonTextPageEdit' ); diff --git a/tests/phpunit/includes/api/ApiErrorFormatterTest.php b/tests/phpunit/includes/api/ApiErrorFormatterTest.php index d13b00be2e1a..1b7f6bff5737 100644 --- a/tests/phpunit/includes/api/ApiErrorFormatterTest.php +++ b/tests/phpunit/includes/api/ApiErrorFormatterTest.php @@ -7,6 +7,30 @@ class ApiErrorFormatterTest extends MediaWikiLangTestCase { /** * @covers ApiErrorFormatter + */ + public function testErrorFormatterBasics() { + $result = new ApiResult( 8388608 ); + $formatter = new ApiErrorFormatter( $result, Language::factory( 'de' ), 'wikitext', false ); + $this->assertSame( 'de', $formatter->getLanguage()->getCode() ); + + $formatter->addMessagesFromStatus( null, Status::newGood() ); + $this->assertSame( + [ ApiResult::META_TYPE => 'assoc' ], + $result->getResultData() + ); + + $this->assertSame( [], $formatter->arrayFromStatus( Status::newGood() ) ); + + $wrappedFormatter = TestingAccessWrapper::newFromObject( $formatter ); + $this->assertSame( + 'Blah "kbd" <X> 😊', + $wrappedFormatter->stripMarkup( 'Blah <kbd>kbd</kbd> <b><X></b> 😊' ), + 'stripMarkup' + ); + } + + /** + * @covers ApiErrorFormatter * @dataProvider provideErrorFormatter */ public function testErrorFormatter( $format, $lang, $useDB, @@ -22,7 +46,7 @@ class ApiErrorFormatterTest extends MediaWikiLangTestCase { $formatter->addWarning( 'string', 'mainpage' ); $formatter->addError( 'err', 'mainpage' ); - $this->assertSame( $expect1, $result->getResultData(), 'Simple test' ); + $this->assertEquals( $expect1, $result->getResultData(), 'Simple test' ); $result->reset(); $formatter->addWarning( 'foo', 'mainpage' ); @@ -35,6 +59,17 @@ class ApiErrorFormatterTest extends MediaWikiLangTestCase { $formatter->addError( 'errWithData', $msg2 ); $this->assertSame( $expect2, $result->getResultData(), 'Complex test' ); + $this->assertEquals( + $this->removeModuleTag( $expect2['warnings'][2] ), + $formatter->formatMessage( $msg1 ), + 'formatMessage test 1' + ); + $this->assertEquals( + $this->removeModuleTag( $expect2['warnings'][3] ), + $formatter->formatMessage( $msg2 ), + 'formatMessage test 2' + ); + $result->reset(); $status = Status::newGood(); $status->warning( 'mainpage' ); @@ -47,245 +82,256 @@ class ApiErrorFormatterTest extends MediaWikiLangTestCase { $this->assertSame( $expect3, $result->getResultData(), 'Status test' ); $this->assertSame( - $expect3['errors']['status'], + array_map( [ $this, 'removeModuleTag' ], $expect3['errors'] ), $formatter->arrayFromStatus( $status, 'error' ), 'arrayFromStatus test for error' ); $this->assertSame( - $expect3['warnings']['status'], + array_map( [ $this, 'removeModuleTag' ], $expect3['warnings'] ), $formatter->arrayFromStatus( $status, 'warning' ), 'arrayFromStatus test for warning' ); } + private function removeModuleTag( $s ) { + if ( is_array( $s ) ) { + unset( $s['module'] ); + } + return $s; + } + public static function provideErrorFormatter() { - $mainpagePlain = wfMessage( 'mainpage' )->useDatabase( false )->plain(); - $parensPlain = wfMessage( 'parentheses', 'foobar' )->useDatabase( false )->plain(); - $mainpageText = wfMessage( 'mainpage' )->inLanguage( 'de' )->text(); - $parensText = wfMessage( 'parentheses', 'foobar' )->inLanguage( 'de' )->text(); + $mainpageText = wfMessage( 'mainpage' )->inLanguage( 'de' )->useDatabase( false )->text(); + $parensText = wfMessage( 'parentheses', 'foobar' )->inLanguage( 'de' ) + ->useDatabase( false )->text(); + $mainpageHTML = wfMessage( 'mainpage' )->inLanguage( 'en' )->parse(); + $parensHTML = wfMessage( 'parentheses', 'foobar' )->inLanguage( 'en' )->parse(); $C = ApiResult::META_CONTENT; $I = ApiResult::META_INDEXED_TAG_NAME; + $overriddenData = [ 'overriddenData' => true, ApiResult::META_TYPE => 'assoc' ]; return [ - [ 'wikitext', 'de', true, + $tmp = [ 'wikitext', 'de', false, [ 'errors' => [ - 'err' => [ - [ 'code' => 'mainpage', 'text' => $mainpageText, $C => 'text' ], - $I => 'error', - ], + [ 'code' => 'mainpage', 'text' => $mainpageText, 'module' => 'err', $C => 'text' ], + $I => 'error', ], 'warnings' => [ - 'string' => [ - [ 'code' => 'mainpage', 'text' => $mainpageText, $C => 'text' ], - $I => 'warning', - ], + [ 'code' => 'mainpage', 'text' => $mainpageText, 'module' => 'string', $C => 'text' ], + $I => 'warning', ], ], [ 'errors' => [ - 'errWithData' => [ - [ 'code' => 'overriddenCode', 'text' => $mainpageText, - 'overriddenData' => true, $C => 'text' ], - $I => 'error', - ], + [ 'code' => 'overriddenCode', 'text' => $mainpageText, + 'data' => $overriddenData, 'module' => 'errWithData', $C => 'text' ], + $I => 'error', ], 'warnings' => [ - 'messageWithData' => [ - [ 'code' => 'overriddenCode', 'text' => $mainpageText, - 'overriddenData' => true, $C => 'text' ], - $I => 'warning', - ], - 'message' => [ - [ 'code' => 'mainpage', 'text' => $mainpageText, $C => 'text' ], - $I => 'warning', - ], - 'foo' => [ - [ 'code' => 'mainpage', 'text' => $mainpageText, $C => 'text' ], - [ 'code' => 'parentheses', 'text' => $parensText, $C => 'text' ], - $I => 'warning', - ], + [ 'code' => 'mainpage', 'text' => $mainpageText, 'module' => 'foo', $C => 'text' ], + [ 'code' => 'parentheses', 'text' => $parensText, 'module' => 'foo', $C => 'text' ], + [ 'code' => 'mainpage', 'text' => $mainpageText, 'module' => 'message', $C => 'text' ], + [ 'code' => 'overriddenCode', 'text' => $mainpageText, + 'data' => $overriddenData, 'module' => 'messageWithData', $C => 'text' ], + $I => 'warning', ], ], [ 'errors' => [ - 'status' => [ - [ 'code' => 'mainpage', 'text' => $mainpageText, $C => 'text' ], - [ 'code' => 'parentheses', 'text' => $parensText, $C => 'text' ], - $I => 'error', - ], + [ 'code' => 'mainpage', 'text' => $mainpageText, 'module' => 'status', $C => 'text' ], + [ 'code' => 'parentheses', 'text' => $parensText, 'module' => 'status', $C => 'text' ], + $I => 'error', ], 'warnings' => [ - 'status' => [ - [ 'code' => 'mainpage', 'text' => $mainpageText, $C => 'text' ], - [ 'code' => 'parentheses', 'text' => $parensText, $C => 'text' ], - [ 'code' => 'overriddenCode', 'text' => $mainpageText, - 'overriddenData' => true, $C => 'text' ], - $I => 'warning', - ], + [ 'code' => 'mainpage', 'text' => $mainpageText, 'module' => 'status', $C => 'text' ], + [ 'code' => 'parentheses', 'text' => $parensText, 'module' => 'status', $C => 'text' ], + [ 'code' => 'overriddenCode', 'text' => $mainpageText, + 'data' => $overriddenData, 'module' => 'status', $C => 'text' ], + $I => 'warning', + ], + ], + ], + [ 'plaintext' ] + $tmp, // For these messages, plaintext and wikitext are the same + [ 'html', 'en', true, + [ + 'errors' => [ + [ 'code' => 'mainpage', 'html' => $mainpageHTML, 'module' => 'err', $C => 'html' ], + $I => 'error', + ], + 'warnings' => [ + [ 'code' => 'mainpage', 'html' => $mainpageHTML, 'module' => 'string', $C => 'html' ], + $I => 'warning', + ], + ], + [ + 'errors' => [ + [ 'code' => 'overriddenCode', 'html' => $mainpageHTML, + 'data' => $overriddenData, 'module' => 'errWithData', $C => 'html' ], + $I => 'error', + ], + 'warnings' => [ + [ 'code' => 'mainpage', 'html' => $mainpageHTML, 'module' => 'foo', $C => 'html' ], + [ 'code' => 'parentheses', 'html' => $parensHTML, 'module' => 'foo', $C => 'html' ], + [ 'code' => 'mainpage', 'html' => $mainpageHTML, 'module' => 'message', $C => 'html' ], + [ 'code' => 'overriddenCode', 'html' => $mainpageHTML, + 'data' => $overriddenData, 'module' => 'messageWithData', $C => 'html' ], + $I => 'warning', + ], + ], + [ + 'errors' => [ + [ 'code' => 'mainpage', 'html' => $mainpageHTML, 'module' => 'status', $C => 'html' ], + [ 'code' => 'parentheses', 'html' => $parensHTML, 'module' => 'status', $C => 'html' ], + $I => 'error', + ], + 'warnings' => [ + [ 'code' => 'mainpage', 'html' => $mainpageHTML, 'module' => 'status', $C => 'html' ], + [ 'code' => 'parentheses', 'html' => $parensHTML, 'module' => 'status', $C => 'html' ], + [ 'code' => 'overriddenCode', 'html' => $mainpageHTML, + 'data' => $overriddenData, 'module' => 'status', $C => 'html' ], + $I => 'warning', ], ], ], [ 'raw', 'fr', true, [ 'errors' => [ - 'err' => [ - [ - 'code' => 'mainpage', - 'key' => 'mainpage', - 'params' => [ $I => 'param' ] - ], - $I => 'error', + [ + 'code' => 'mainpage', + 'key' => 'mainpage', + 'params' => [ $I => 'param' ], + 'module' => 'err', ], + $I => 'error', ], 'warnings' => [ - 'string' => [ - [ - 'code' => 'mainpage', - 'key' => 'mainpage', - 'params' => [ $I => 'param' ] - ], - $I => 'warning', + [ + 'code' => 'mainpage', + 'key' => 'mainpage', + 'params' => [ $I => 'param' ], + 'module' => 'string', ], + $I => 'warning', ], ], [ 'errors' => [ - 'errWithData' => [ - [ - 'code' => 'overriddenCode', - 'key' => 'mainpage', - 'params' => [ $I => 'param' ], - 'overriddenData' => true - ], - $I => 'error', + [ + 'code' => 'overriddenCode', + 'key' => 'mainpage', + 'params' => [ $I => 'param' ], + 'data' => $overriddenData, + 'module' => 'errWithData', ], + $I => 'error', ], 'warnings' => [ - 'messageWithData' => [ - [ - 'code' => 'overriddenCode', - 'key' => 'mainpage', - 'params' => [ $I => 'param' ], - 'overriddenData' => true - ], - $I => 'warning', + [ + 'code' => 'mainpage', + 'key' => 'mainpage', + 'params' => [ $I => 'param' ], + 'module' => 'foo', + ], + [ + 'code' => 'parentheses', + 'key' => 'parentheses', + 'params' => [ 'foobar', $I => 'param' ], + 'module' => 'foo', ], - 'message' => [ - [ - 'code' => 'mainpage', - 'key' => 'mainpage', - 'params' => [ $I => 'param' ] - ], - $I => 'warning', + [ + 'code' => 'mainpage', + 'key' => 'mainpage', + 'params' => [ $I => 'param' ], + 'module' => 'message', ], - 'foo' => [ - [ - 'code' => 'mainpage', - 'key' => 'mainpage', - 'params' => [ $I => 'param' ] - ], - [ - 'code' => 'parentheses', - 'key' => 'parentheses', - 'params' => [ 'foobar', $I => 'param' ] - ], - $I => 'warning', + [ + 'code' => 'overriddenCode', + 'key' => 'mainpage', + 'params' => [ $I => 'param' ], + 'data' => $overriddenData, + 'module' => 'messageWithData', ], + $I => 'warning', ], ], [ 'errors' => [ - 'status' => [ - [ - 'code' => 'mainpage', - 'key' => 'mainpage', - 'params' => [ $I => 'param' ] - ], - [ - 'code' => 'parentheses', - 'key' => 'parentheses', - 'params' => [ 'foobar', $I => 'param' ] - ], - $I => 'error', + [ + 'code' => 'mainpage', + 'key' => 'mainpage', + 'params' => [ $I => 'param' ], + 'module' => 'status', + ], + [ + 'code' => 'parentheses', + 'key' => 'parentheses', + 'params' => [ 'foobar', $I => 'param' ], + 'module' => 'status', ], + $I => 'error', ], 'warnings' => [ - 'status' => [ - [ - 'code' => 'mainpage', - 'key' => 'mainpage', - 'params' => [ $I => 'param' ] - ], - [ - 'code' => 'parentheses', - 'key' => 'parentheses', - 'params' => [ 'foobar', $I => 'param' ] - ], - [ - 'code' => 'overriddenCode', - 'key' => 'mainpage', - 'params' => [ $I => 'param' ], - 'overriddenData' => true - ], - $I => 'warning', + [ + 'code' => 'mainpage', + 'key' => 'mainpage', + 'params' => [ $I => 'param' ], + 'module' => 'status', ], + [ + 'code' => 'parentheses', + 'key' => 'parentheses', + 'params' => [ 'foobar', $I => 'param' ], + 'module' => 'status', + ], + [ + 'code' => 'overriddenCode', + 'key' => 'mainpage', + 'params' => [ $I => 'param' ], + 'data' => $overriddenData, + 'module' => 'status', + ], + $I => 'warning', ], ], ], [ 'none', 'fr', true, [ 'errors' => [ - 'err' => [ - [ 'code' => 'mainpage' ], - $I => 'error', - ], + [ 'code' => 'mainpage', 'module' => 'err' ], + $I => 'error', ], 'warnings' => [ - 'string' => [ - [ 'code' => 'mainpage' ], - $I => 'warning', - ], + [ 'code' => 'mainpage', 'module' => 'string' ], + $I => 'warning', ], ], [ 'errors' => [ - 'errWithData' => [ - [ 'code' => 'overriddenCode', 'overriddenData' => true ], - $I => 'error', - ], + [ 'code' => 'overriddenCode', 'data' => $overriddenData, + 'module' => 'errWithData' ], + $I => 'error', ], 'warnings' => [ - 'messageWithData' => [ - [ 'code' => 'overriddenCode', 'overriddenData' => true ], - $I => 'warning', - ], - 'message' => [ - [ 'code' => 'mainpage' ], - $I => 'warning', - ], - 'foo' => [ - [ 'code' => 'mainpage' ], - [ 'code' => 'parentheses' ], - $I => 'warning', - ], + [ 'code' => 'mainpage', 'module' => 'foo' ], + [ 'code' => 'parentheses', 'module' => 'foo' ], + [ 'code' => 'mainpage', 'module' => 'message' ], + [ 'code' => 'overriddenCode', 'data' => $overriddenData, + 'module' => 'messageWithData' ], + $I => 'warning', ], ], [ 'errors' => [ - 'status' => [ - [ 'code' => 'mainpage' ], - [ 'code' => 'parentheses' ], - $I => 'error', - ], + [ 'code' => 'mainpage', 'module' => 'status' ], + [ 'code' => 'parentheses', 'module' => 'status' ], + $I => 'error', ], 'warnings' => [ - 'status' => [ - [ 'code' => 'mainpage' ], - [ 'code' => 'parentheses' ], - [ 'code' => 'overriddenCode', 'overriddenData' => true ], - $I => 'warning', - ], + [ 'code' => 'mainpage', 'module' => 'status' ], + [ 'code' => 'parentheses', 'module' => 'status' ], + [ 'code' => 'overriddenCode', 'data' => $overriddenData, 'module' => 'status' ], + $I => 'warning', ], ], ], @@ -302,7 +348,14 @@ class ApiErrorFormatterTest extends MediaWikiLangTestCase { $result = new ApiResult( 8388608 ); $formatter = new ApiErrorFormatter_BackCompat( $result ); + $this->assertSame( 'en', $formatter->getLanguage()->getCode() ); + + $this->assertSame( [], $formatter->arrayFromStatus( Status::newGood() ) ); + $formatter->addWarning( 'string', 'mainpage' ); + $formatter->addWarning( 'raw', + new RawMessage( 'Blah <kbd>kbd</kbd> <b><X></b> 😞' ) + ); $formatter->addError( 'err', 'mainpage' ); $this->assertSame( [ 'error' => [ @@ -310,6 +363,10 @@ class ApiErrorFormatterTest extends MediaWikiLangTestCase { 'info' => $mainpagePlain, ], 'warnings' => [ + 'raw' => [ + 'warnings' => 'Blah "kbd" <X> 😞', + ApiResult::META_CONTENT => 'warnings', + ], 'string' => [ 'warnings' => $mainpagePlain, ApiResult::META_CONTENT => 'warnings', @@ -321,12 +378,13 @@ class ApiErrorFormatterTest extends MediaWikiLangTestCase { $result->reset(); $formatter->addWarning( 'foo', 'mainpage' ); $formatter->addWarning( 'foo', 'mainpage' ); - $formatter->addWarning( 'foo', [ 'parentheses', 'foobar' ] ); + $formatter->addWarning( 'xxx+foo', [ 'parentheses', 'foobar' ] ); $msg1 = wfMessage( 'mainpage' ); $formatter->addWarning( 'message', $msg1 ); $msg2 = new ApiMessage( 'mainpage', 'overriddenCode', [ 'overriddenData' => true ] ); $formatter->addWarning( 'messageWithData', $msg2 ); $formatter->addError( 'errWithData', $msg2 ); + $formatter->addWarning( null, 'mainpage' ); $this->assertSame( [ 'error' => [ 'code' => 'overriddenCode', @@ -334,6 +392,10 @@ class ApiErrorFormatterTest extends MediaWikiLangTestCase { 'overriddenData' => true, ], 'warnings' => [ + 'unknown' => [ + 'warnings' => $mainpagePlain, + ApiResult::META_CONTENT => 'warnings', + ], 'messageWithData' => [ 'warnings' => $mainpagePlain, ApiResult::META_CONTENT => 'warnings', @@ -350,6 +412,22 @@ class ApiErrorFormatterTest extends MediaWikiLangTestCase { ApiResult::META_TYPE => 'assoc', ], $result->getResultData(), 'Complex test' ); + $this->assertSame( + [ + 'code' => 'mainpage', + 'info' => 'Main Page', + ], + $formatter->formatMessage( $msg1 ) + ); + $this->assertSame( + [ + 'code' => 'overriddenCode', + 'info' => 'Main Page', + 'overriddenData' => true, + ], + $formatter->formatMessage( $msg2 ) + ); + $result->reset(); $status = Status::newGood(); $status->warning( 'mainpage' ); @@ -377,14 +455,16 @@ class ApiErrorFormatterTest extends MediaWikiLangTestCase { $this->assertSame( [ [ - 'type' => 'error', 'message' => 'mainpage', - 'params' => [ $I => 'param' ] + 'params' => [ $I => 'param' ], + 'code' => 'mainpage', + 'type' => 'error', ], [ - 'type' => 'error', 'message' => 'parentheses', - 'params' => [ 'foobar', $I => 'param' ] + 'params' => [ 'foobar', $I => 'param' ], + 'code' => 'parentheses', + 'type' => 'error', ], $I => 'error', ], @@ -394,24 +474,28 @@ class ApiErrorFormatterTest extends MediaWikiLangTestCase { $this->assertSame( [ [ - 'type' => 'warning', 'message' => 'mainpage', - 'params' => [ $I => 'param' ] + 'params' => [ $I => 'param' ], + 'code' => 'mainpage', + 'type' => 'warning', ], [ - 'type' => 'warning', 'message' => 'parentheses', - 'params' => [ 'foobar', $I => 'param' ] + 'params' => [ 'foobar', $I => 'param' ], + 'code' => 'parentheses', + 'type' => 'warning', ], [ 'message' => 'mainpage', 'params' => [ $I => 'param' ], - 'type' => 'warning' + 'code' => 'mainpage', + 'type' => 'warning', ], [ 'message' => 'mainpage', 'params' => [ $I => 'param' ], - 'type' => 'warning' + 'code' => 'overriddenCode', + 'type' => 'warning', ], $I => 'warning', ], diff --git a/tests/phpunit/includes/api/ApiMainTest.php b/tests/phpunit/includes/api/ApiMainTest.php index c111949d2fae..c9a3428da159 100644 --- a/tests/phpunit/includes/api/ApiMainTest.php +++ b/tests/phpunit/includes/api/ApiMainTest.php @@ -53,8 +53,8 @@ class ApiMainTest extends ApiTestCase { 'assert' => $assert, ], null, null, $user ); $this->assertFalse( $error ); // That no error was expected - } catch ( UsageException $e ) { - $this->assertEquals( $e->getCodeString(), $error ); + } catch ( ApiUsageException $e ) { + $this->assertTrue( self::apiExceptionHasCode( $e, $error ) ); } } @@ -76,8 +76,8 @@ class ApiMainTest extends ApiTestCase { 'assertuser' => $user->getName() . 'X', ], null, null, $user ); $this->fail( 'Expected exception not thrown' ); - } catch ( UsageException $e ) { - $this->assertEquals( $e->getCodeString(), 'assertnameduserfailed' ); + } catch ( ApiUsageException $e ) { + $this->assertTrue( self::apiExceptionHasCode( $e, 'assertnameduserfailed' ) ); } } @@ -305,4 +305,274 @@ class ApiMainTest extends ApiTestCase { $main = new ApiMain( new FauxRequest( [ 'action' => 'query', 'meta' => 'siteinfo' ] ) ); $this->assertTrue( $main->lacksSameOriginSecurity(), 'Hook, should lack security' ); } + + /** + * Test proper creation of the ApiErrorFormatter + * @covers ApiMain::__construct + * @dataProvider provideApiErrorFormatterCreation + * @param array $request Request parameters + * @param array $expect Expected data + * - uselang: ApiMain language + * - class: ApiErrorFormatter class + * - lang: ApiErrorFormatter language + * - format: ApiErrorFormatter format + * - usedb: ApiErrorFormatter use-database flag + */ + public function testApiErrorFormatterCreation( array $request, array $expect ) { + $context = new RequestContext(); + $context->setRequest( new FauxRequest( $request ) ); + $context->setLanguage( 'ru' ); + + $main = new ApiMain( $context ); + $formatter = $main->getErrorFormatter(); + $wrappedFormatter = TestingAccessWrapper::newFromObject( $formatter ); + + $this->assertSame( $expect['uselang'], $main->getLanguage()->getCode() ); + $this->assertInstanceOf( $expect['class'], $formatter ); + $this->assertSame( $expect['lang'], $formatter->getLanguage()->getCode() ); + $this->assertSame( $expect['format'], $wrappedFormatter->format ); + $this->assertSame( $expect['usedb'], $wrappedFormatter->useDB ); + } + + public static function provideApiErrorFormatterCreation() { + global $wgContLang; + + return [ + 'Default (BC)' => [ [], [ + 'uselang' => 'ru', + 'class' => ApiErrorFormatter_BackCompat::class, + 'lang' => 'en', + 'format' => 'none', + 'usedb' => false, + ] ], + 'BC ignores fields' => [ [ 'errorlang' => 'de', 'errorsuselocal' => 1 ], [ + 'uselang' => 'ru', + 'class' => ApiErrorFormatter_BackCompat::class, + 'lang' => 'en', + 'format' => 'none', + 'usedb' => false, + ] ], + 'Explicit BC' => [ [ 'errorformat' => 'bc' ], [ + 'uselang' => 'ru', + 'class' => ApiErrorFormatter_BackCompat::class, + 'lang' => 'en', + 'format' => 'none', + 'usedb' => false, + ] ], + 'Basic' => [ [ 'errorformat' => 'wikitext' ], [ + 'uselang' => 'ru', + 'class' => ApiErrorFormatter::class, + 'lang' => 'ru', + 'format' => 'wikitext', + 'usedb' => false, + ] ], + 'Follows uselang' => [ [ 'uselang' => 'fr', 'errorformat' => 'plaintext' ], [ + 'uselang' => 'fr', + 'class' => ApiErrorFormatter::class, + 'lang' => 'fr', + 'format' => 'plaintext', + 'usedb' => false, + ] ], + 'Explicitly follows uselang' => [ + [ 'uselang' => 'fr', 'errorlang' => 'uselang', 'errorformat' => 'plaintext' ], + [ + 'uselang' => 'fr', + 'class' => ApiErrorFormatter::class, + 'lang' => 'fr', + 'format' => 'plaintext', + 'usedb' => false, + ] + ], + 'uselang=content' => [ + [ 'uselang' => 'content', 'errorformat' => 'plaintext' ], + [ + 'uselang' => $wgContLang->getCode(), + 'class' => ApiErrorFormatter::class, + 'lang' => $wgContLang->getCode(), + 'format' => 'plaintext', + 'usedb' => false, + ] + ], + 'errorlang=content' => [ + [ 'errorlang' => 'content', 'errorformat' => 'plaintext' ], + [ + 'uselang' => 'ru', + 'class' => ApiErrorFormatter::class, + 'lang' => $wgContLang->getCode(), + 'format' => 'plaintext', + 'usedb' => false, + ] + ], + 'Explicit parameters' => [ + [ 'errorlang' => 'de', 'errorformat' => 'html', 'errorsuselocal' => 1 ], + [ + 'uselang' => 'ru', + 'class' => ApiErrorFormatter::class, + 'lang' => 'de', + 'format' => 'html', + 'usedb' => true, + ] + ], + 'Explicit parameters override uselang' => [ + [ 'errorlang' => 'de', 'uselang' => 'fr', 'errorformat' => 'raw' ], + [ + 'uselang' => 'fr', + 'class' => ApiErrorFormatter::class, + 'lang' => 'de', + 'format' => 'raw', + 'usedb' => false, + ] + ], + 'Bogus language doesn\'t explode' => [ + [ 'errorlang' => '<bogus1>', 'uselang' => '<bogus2>', 'errorformat' => 'none' ], + [ + 'uselang' => 'en', + 'class' => ApiErrorFormatter::class, + 'lang' => 'en', + 'format' => 'none', + 'usedb' => false, + ] + ], + 'Bogus format doesn\'t explode' => [ [ 'errorformat' => 'bogus' ], [ + 'uselang' => 'ru', + 'class' => ApiErrorFormatter_BackCompat::class, + 'lang' => 'en', + 'format' => 'none', + 'usedb' => false, + ] ], + ]; + } + + /** + * @covers ApiMain::errorMessagesFromException + * @covers ApiMain::substituteResultWithError + * @dataProvider provideExceptionErrors + * @param Exception $exception + * @param array $expectReturn + * @param array $expectResult + */ + public function testExceptionErrors( $error, $expectReturn, $expectResult ) { + $context = new RequestContext(); + $context->setRequest( new FauxRequest( [ 'errorformat' => 'plaintext' ] ) ); + $context->setLanguage( 'en' ); + $context->setConfig( new MultiConfig( [ + new HashConfig( [ 'ShowHostnames' => true, 'ShowSQLErrors' => false ] ), + $context->getConfig() + ] ) ); + + $main = new ApiMain( $context ); + $main->addWarning( new RawMessage( 'existing warning' ), 'existing-warning' ); + $main->addError( new RawMessage( 'existing error' ), 'existing-error' ); + + $ret = TestingAccessWrapper::newFromObject( $main )->substituteResultWithError( $error ); + $this->assertSame( $expectReturn, $ret ); + + // PHPUnit sometimes adds some SplObjectStorage garbage to the arrays, + // so let's try ->assertEquals(). + $this->assertEquals( + $expectResult, + $main->getResult()->getResultData( [], [ 'Strip' => 'all' ] ) + ); + } + + // Not static so $this->getMock() can be used + public function provideExceptionErrors() { + $reqId = WebRequest::getRequestId(); + $doclink = wfExpandUrl( wfScript( 'api' ) ); + + $ex = new InvalidArgumentException( 'Random exception' ); + $trace = wfMessage( 'api-exception-trace', + get_class( $ex ), + $ex->getFile(), + $ex->getLine(), + MWExceptionHandler::getRedactedTraceAsString( $ex ) + )->inLanguage( 'en' )->useDatabase( false )->text(); + + $dbex = new DBQueryError( $this->getMock( 'IDatabase' ), 'error', 1234, 'SELECT 1', __METHOD__ ); + $dbtrace = wfMessage( 'api-exception-trace', + get_class( $dbex ), + $dbex->getFile(), + $dbex->getLine(), + MWExceptionHandler::getRedactedTraceAsString( $dbex ) + )->inLanguage( 'en' )->useDatabase( false )->text(); + + $apiEx1 = new ApiUsageException( null, + StatusValue::newFatal( new ApiRawMessage( 'An error', 'sv-error1' ) ) ); + TestingAccessWrapper::newFromObject( $apiEx1 )->modulePath = 'foo+bar'; + $apiEx1->getStatusValue()->warning( new ApiRawMessage( 'A warning', 'sv-warn1' ) ); + $apiEx1->getStatusValue()->warning( new ApiRawMessage( 'Another warning', 'sv-warn2' ) ); + $apiEx1->getStatusValue()->fatal( new ApiRawMessage( 'Another error', 'sv-error2' ) ); + + return [ + [ + $ex, + [ 'existing-error', 'internal_api_error_InvalidArgumentException' ], + [ + 'warnings' => [ + [ 'code' => 'existing-warning', 'text' => 'existing warning', 'module' => 'main' ], + ], + 'errors' => [ + [ 'code' => 'existing-error', 'text' => 'existing error', 'module' => 'main' ], + [ + 'code' => 'internal_api_error_InvalidArgumentException', + 'text' => "[$reqId] Exception caught: Random exception", + ] + ], + 'trace' => $trace, + 'servedby' => wfHostname(), + ] + ], + [ + $dbex, + [ 'existing-error', 'internal_api_error_DBQueryError' ], + [ + 'warnings' => [ + [ 'code' => 'existing-warning', 'text' => 'existing warning', 'module' => 'main' ], + ], + 'errors' => [ + [ 'code' => 'existing-error', 'text' => 'existing error', 'module' => 'main' ], + [ + 'code' => 'internal_api_error_DBQueryError', + 'text' => "[$reqId] Database query error.", + ] + ], + 'trace' => $dbtrace, + 'servedby' => wfHostname(), + ] + ], + [ + new UsageException( 'Usage exception!', 'ue', 0, [ 'foo' => 'bar' ] ), + [ 'existing-error', 'ue' ], + [ + 'warnings' => [ + [ 'code' => 'existing-warning', 'text' => 'existing warning', 'module' => 'main' ], + ], + 'errors' => [ + [ 'code' => 'existing-error', 'text' => 'existing error', 'module' => 'main' ], + [ 'code' => 'ue', 'text' => "Usage exception!", 'data' => [ 'foo' => 'bar' ] ] + ], + 'docref' => "See $doclink for API usage.", + 'servedby' => wfHostname(), + ] + ], + [ + $apiEx1, + [ 'existing-error', 'sv-error1', 'sv-error2' ], + [ + 'warnings' => [ + [ 'code' => 'existing-warning', 'text' => 'existing warning', 'module' => 'main' ], + [ 'code' => 'sv-warn1', 'text' => 'A warning', 'module' => 'foo+bar' ], + [ 'code' => 'sv-warn2', 'text' => 'Another warning', 'module' => 'foo+bar' ], + ], + 'errors' => [ + [ 'code' => 'existing-error', 'text' => 'existing error', 'module' => 'main' ], + [ 'code' => 'sv-error1', 'text' => 'An error', 'module' => 'foo+bar' ], + [ 'code' => 'sv-error2', 'text' => 'Another error', 'module' => 'foo+bar' ], + ], + 'docref' => "See $doclink for API usage.", + 'servedby' => wfHostname(), + ] + ], + ]; + } } diff --git a/tests/phpunit/includes/api/ApiMessageTest.php b/tests/phpunit/includes/api/ApiMessageTest.php index 8764b4194f5b..e405b3b89592 100644 --- a/tests/phpunit/includes/api/ApiMessageTest.php +++ b/tests/phpunit/includes/api/ApiMessageTest.php @@ -24,6 +24,56 @@ class ApiMessageTest extends MediaWikiTestCase { } /** + * @covers ApiMessageTrait + */ + public function testCodeDefaults() { + $msg = new ApiMessage( 'foo' ); + $this->assertSame( 'foo', $msg->getApiCode() ); + + $msg = new ApiMessage( 'apierror-bar' ); + $this->assertSame( 'bar', $msg->getApiCode() ); + + $msg = new ApiMessage( 'apiwarn-baz' ); + $this->assertSame( 'baz', $msg->getApiCode() ); + + // BC case + $msg = new ApiMessage( 'actionthrottledtext' ); + $this->assertSame( 'ratelimited', $msg->getApiCode() ); + + $msg = new ApiMessage( [ 'apierror-missingparam', 'param' ] ); + $this->assertSame( 'noparam', $msg->getApiCode() ); + } + + /** + * @covers ApiMessageTrait + * @dataProvider provideInvalidCode + * @param mixed $code + */ + public function testInvalidCode( $code ) { + $msg = new ApiMessage( 'foo' ); + try { + $msg->setApiCode( $code ); + $this->fail( 'Expected exception not thrown' ); + } catch ( InvalidArgumentException $ex ) { + $this->assertTrue( true ); + } + + try { + new ApiMessage( 'foo', $code ); + $this->fail( 'Expected exception not thrown' ); + } catch ( InvalidArgumentException $ex ) { + $this->assertTrue( true ); + } + } + + public static function provideInvalidCode() { + return [ + [ '' ], + [ 42 ], + ]; + } + + /** * @covers ApiMessage * @covers ApiMessageTrait */ @@ -105,14 +155,32 @@ class ApiMessageTest extends MediaWikiTestCase { * @covers ApiMessage::create */ public function testApiMessageCreate() { - $this->assertInstanceOf( 'ApiMessage', ApiMessage::create( new Message( 'mainpage' ) ) ); - $this->assertInstanceOf( 'ApiRawMessage', ApiMessage::create( new RawMessage( 'mainpage' ) ) ); - $this->assertInstanceOf( 'ApiMessage', ApiMessage::create( 'mainpage' ) ); + $this->assertInstanceOf( ApiMessage::class, ApiMessage::create( new Message( 'mainpage' ) ) ); + $this->assertInstanceOf( + ApiRawMessage::class, ApiMessage::create( new RawMessage( 'mainpage' ) ) + ); + $this->assertInstanceOf( ApiMessage::class, ApiMessage::create( 'mainpage' ) ); + + $msg = new ApiMessage( [ 'parentheses', 'foobar' ] ); + $msg2 = new Message( 'parentheses', [ 'foobar' ] ); - $msg = new ApiMessage( 'mainpage' ); $this->assertSame( $msg, ApiMessage::create( $msg ) ); + $this->assertEquals( $msg, ApiMessage::create( $msg2 ) ); + $this->assertEquals( $msg, ApiMessage::create( [ 'parentheses', 'foobar' ] ) ); + $this->assertEquals( $msg, + ApiMessage::create( [ 'message' => 'parentheses', 'params' => [ 'foobar' ] ] ) + ); + $this->assertSame( $msg, + ApiMessage::create( [ 'message' => $msg, 'params' => [ 'xxx' ] ] ) + ); + $this->assertEquals( $msg, + ApiMessage::create( [ 'message' => $msg2, 'params' => [ 'xxx' ] ] ) + ); + $this->assertSame( $msg, + ApiMessage::create( [ 'message' => $msg ] ) + ); - $msg = new ApiRawMessage( 'mainpage' ); + $msg = new ApiRawMessage( [ 'parentheses', 'foobar' ] ); $this->assertSame( $msg, ApiMessage::create( $msg ) ); } diff --git a/tests/phpunit/includes/api/ApiOptionsTest.php b/tests/phpunit/includes/api/ApiOptionsTest.php index 0a577c1cb692..ef70626120fe 100644 --- a/tests/phpunit/includes/api/ApiOptionsTest.php +++ b/tests/phpunit/includes/api/ApiOptionsTest.php @@ -30,7 +30,7 @@ class ApiOptionsTest extends MediaWikiLangTestCase { $this->mUserMock->expects( $this->any() ) ->method( 'getEffectiveGroups' )->will( $this->returnValue( [ '*', 'user' ] ) ); $this->mUserMock->expects( $this->any() ) - ->method( 'isAllowed' )->will( $this->returnValue( true ) ); + ->method( 'isAllowedAny' )->will( $this->returnValue( true ) ); // Set up callback for User::getOptionKinds $this->mUserMock->expects( $this->any() ) @@ -146,7 +146,7 @@ class ApiOptionsTest extends MediaWikiLangTestCase { } /** - * @expectedException UsageException + * @expectedException ApiUsageException */ public function testNoToken() { $request = $this->getSampleRequest( [ 'token' => null ] ); @@ -163,13 +163,11 @@ class ApiOptionsTest extends MediaWikiLangTestCase { $request = $this->getSampleRequest(); $this->executeQuery( $request ); - } catch ( UsageException $e ) { - $this->assertEquals( 'notloggedin', $e->getCodeString() ); - $this->assertEquals( 'Anonymous users cannot change preferences', $e->getMessage() ); - + } catch ( ApiUsageException $e ) { + $this->assertTrue( ApiTestCase::apiExceptionHasCode( $e, 'notloggedin' ) ); return; } - $this->fail( "UsageException was not thrown" ); + $this->fail( "ApiUsageException was not thrown" ); } public function testNoOptionname() { @@ -177,13 +175,11 @@ class ApiOptionsTest extends MediaWikiLangTestCase { $request = $this->getSampleRequest( [ 'optionvalue' => '1' ] ); $this->executeQuery( $request ); - } catch ( UsageException $e ) { - $this->assertEquals( 'nooptionname', $e->getCodeString() ); - $this->assertEquals( 'The optionname parameter must be set', $e->getMessage() ); - + } catch ( ApiUsageException $e ) { + $this->assertTrue( ApiTestCase::apiExceptionHasCode( $e, 'nooptionname' ) ); return; } - $this->fail( "UsageException was not thrown" ); + $this->fail( "ApiUsageException was not thrown" ); } public function testNoChanges() { @@ -200,13 +196,11 @@ class ApiOptionsTest extends MediaWikiLangTestCase { $request = $this->getSampleRequest(); $this->executeQuery( $request ); - } catch ( UsageException $e ) { - $this->assertEquals( 'nochanges', $e->getCodeString() ); - $this->assertEquals( 'No changes were requested', $e->getMessage() ); - + } catch ( ApiUsageException $e ) { + $this->assertTrue( ApiTestCase::apiExceptionHasCode( $e, 'nochanges' ) ); return; } - $this->fail( "UsageException was not thrown" ); + $this->fail( "ApiUsageException was not thrown" ); } public function testReset() { @@ -400,7 +394,7 @@ class ApiOptionsTest extends MediaWikiLangTestCase { 'options' => 'success', 'warnings' => [ 'options' => [ - 'warnings' => "Validation error for 'special': cannot be set by this module" + 'warnings' => "Validation error for \"special\": cannot be set by this module." ] ] ], $response ); @@ -423,7 +417,7 @@ class ApiOptionsTest extends MediaWikiLangTestCase { 'options' => 'success', 'warnings' => [ 'options' => [ - 'warnings' => "Validation error for 'unknownOption': not a valid preference" + 'warnings' => "Validation error for \"unknownOption\": not a valid preference." ] ] ], $response ); diff --git a/tests/phpunit/includes/api/ApiParseTest.php b/tests/phpunit/includes/api/ApiParseTest.php index b72a4f8a8b8b..f01a670b7115 100644 --- a/tests/phpunit/includes/api/ApiParseTest.php +++ b/tests/phpunit/includes/api/ApiParseTest.php @@ -23,12 +23,10 @@ class ApiParseTest extends ApiTestCase { 'page' => $somePage ] ); $this->fail( "API did not return an error when parsing a nonexistent page" ); - } catch ( UsageException $ex ) { - $this->assertEquals( - 'missingtitle', - $ex->getCodeString(), + } catch ( ApiUsageException $ex ) { + $this->assertTrue( ApiTestCase::apiExceptionHasCode( $ex, 'missingtitle' ), "Parse request for nonexistent page must give 'missingtitle' error: " - . var_export( $ex->getMessageArray(), true ) + . var_export( self::getErrorFormatter()->arrayFromStatus( $ex->getStatusValue() ), true ) ); } } diff --git a/tests/phpunit/includes/api/ApiQueryWatchlistIntegrationTest.php b/tests/phpunit/includes/api/ApiQueryWatchlistIntegrationTest.php index eaeb3ae925c0..0a2cd83dd7a7 100644 --- a/tests/phpunit/includes/api/ApiQueryWatchlistIntegrationTest.php +++ b/tests/phpunit/includes/api/ApiQueryWatchlistIntegrationTest.php @@ -1498,7 +1498,7 @@ class ApiQueryWatchlistIntegrationTest extends ApiTestCase { $otherUser->setOption( 'watchlisttoken', '1234567890' ); $otherUser->saveSettings(); - $this->setExpectedException( UsageException::class, 'Incorrect watchlist token provided' ); + $this->setExpectedException( ApiUsageException::class, 'Incorrect watchlist token provided' ); $this->doListWatchlistRequest( [ 'wlowner' => $otherUser->getName(), @@ -1507,7 +1507,7 @@ class ApiQueryWatchlistIntegrationTest extends ApiTestCase { } public function testOwnerAndTokenParams_noWatchlistTokenSet() { - $this->setExpectedException( UsageException::class, 'Incorrect watchlist token provided' ); + $this->setExpectedException( ApiUsageException::class, 'Incorrect watchlist token provided' ); $this->doListWatchlistRequest( [ 'wlowner' => $this->getNonLoggedInTestUser()->getName(), diff --git a/tests/phpunit/includes/api/ApiQueryWatchlistRawIntegrationTest.php b/tests/phpunit/includes/api/ApiQueryWatchlistRawIntegrationTest.php index d6f315d5b37e..0f01664e720a 100644 --- a/tests/phpunit/includes/api/ApiQueryWatchlistRawIntegrationTest.php +++ b/tests/phpunit/includes/api/ApiQueryWatchlistRawIntegrationTest.php @@ -503,7 +503,7 @@ class ApiQueryWatchlistRawIntegrationTest extends ApiTestCase { $otherUser->setOption( 'watchlisttoken', '1234567890' ); $otherUser->saveSettings(); - $this->setExpectedException( UsageException::class, 'Incorrect watchlist token provided' ); + $this->setExpectedException( ApiUsageException::class, 'Incorrect watchlist token provided' ); $this->doListWatchlistRawRequest( [ 'wrowner' => $otherUser->getName(), @@ -512,7 +512,7 @@ class ApiQueryWatchlistRawIntegrationTest extends ApiTestCase { } public function testOwnerAndTokenParams_userHasNoWatchlistToken() { - $this->setExpectedException( UsageException::class, 'Incorrect watchlist token provided' ); + $this->setExpectedException( ApiUsageException::class, 'Incorrect watchlist token provided' ); $this->doListWatchlistRawRequest( [ 'wrowner' => $this->getNotLoggedInTestUser()->getName(), diff --git a/tests/phpunit/includes/api/ApiTestCase.php b/tests/phpunit/includes/api/ApiTestCase.php index 7e1f9d8775ef..6b299c98c80f 100644 --- a/tests/phpunit/includes/api/ApiTestCase.php +++ b/tests/phpunit/includes/api/ApiTestCase.php @@ -3,6 +3,8 @@ abstract class ApiTestCase extends MediaWikiLangTestCase { protected static $apiUrl; + protected static $errorFormatter = null; + /** * @var ApiTestContext */ @@ -196,6 +198,26 @@ abstract class ApiTestCase extends MediaWikiLangTestCase { return $data[0]['tokens']; } + protected static function getErrorFormatter() { + if ( self::$errorFormatter === null ) { + self::$errorFormatter = new ApiErrorFormatter( + new ApiResult( false ), + Language::factory( 'en' ), + 'none' + ); + } + return self::$errorFormatter; + } + + public static function apiExceptionHasCode( ApiUsageException $ex, $code ) { + return (bool)array_filter( + self::getErrorFormatter()->arrayFromStatus( $ex->getStatusValue() ), + function ( $e ) use ( $code ) { + return is_array( $e ) && $e['code'] === $code; + } + ); + } + public function testApiTestGroup() { $groups = PHPUnit_Util_Test::getGroups( get_class( $this ) ); $constraint = PHPUnit_Framework_Assert::logicalOr( diff --git a/tests/phpunit/includes/api/ApiUnblockTest.php b/tests/phpunit/includes/api/ApiUnblockTest.php index b63bf2ea3131..971b63c3d4bc 100644 --- a/tests/phpunit/includes/api/ApiUnblockTest.php +++ b/tests/phpunit/includes/api/ApiUnblockTest.php @@ -14,7 +14,7 @@ class ApiUnblockTest extends ApiTestCase { } /** - * @expectedException UsageException + * @expectedException ApiUsageException */ public function testWithNoToken() { $this->doApiRequest( diff --git a/tests/phpunit/includes/api/ApiUploadTest.php b/tests/phpunit/includes/api/ApiUploadTest.php index de2b56bde3f4..9b79e6c538a1 100644 --- a/tests/phpunit/includes/api/ApiUploadTest.php +++ b/tests/phpunit/includes/api/ApiUploadTest.php @@ -67,9 +67,9 @@ class ApiUploadTest extends ApiTestCaseUpload { $this->doApiRequest( [ 'action' => 'upload' ] ); - } catch ( UsageException $e ) { + } catch ( ApiUsageException $e ) { $exception = true; - $this->assertEquals( "The token parameter must be set", $e->getMessage() ); + $this->assertEquals( 'The "token" parameter must be set', $e->getMessage() ); } $this->assertTrue( $exception, "Got exception" ); } @@ -83,7 +83,7 @@ class ApiUploadTest extends ApiTestCaseUpload { $this->doApiRequestWithToken( [ 'action' => 'upload', ], $session, self::$users['uploader']->getUser() ); - } catch ( UsageException $e ) { + } catch ( ApiUsageException $e ) { $exception = true; $this->assertEquals( "One of the parameters filekey, file, url is required", $e->getMessage() ); @@ -129,7 +129,7 @@ class ApiUploadTest extends ApiTestCaseUpload { try { list( $result, , ) = $this->doApiRequestWithToken( $params, $session, self::$users['uploader']->getUser() ); - } catch ( UsageException $e ) { + } catch ( ApiUsageException $e ) { $exception = true; } $this->assertTrue( isset( $result['upload'] ) ); @@ -168,7 +168,7 @@ class ApiUploadTest extends ApiTestCaseUpload { $exception = false; try { $this->doApiRequestWithToken( $params, $session, self::$users['uploader']->getUser() ); - } catch ( UsageException $e ) { + } catch ( ApiUsageException $e ) { $this->assertContains( 'The file you submitted was empty', $e->getMessage() ); $exception = true; } @@ -218,7 +218,7 @@ class ApiUploadTest extends ApiTestCaseUpload { try { list( $result, , $session ) = $this->doApiRequestWithToken( $params, $session, self::$users['uploader']->getUser() ); - } catch ( UsageException $e ) { + } catch ( ApiUsageException $e ) { $exception = true; } $this->assertTrue( isset( $result['upload'] ) ); @@ -235,7 +235,7 @@ class ApiUploadTest extends ApiTestCaseUpload { try { list( $result, , ) = $this->doApiRequestWithToken( $params, $session, self::$users['uploader']->getUser() ); // FIXME: leaks a temporary file - } catch ( UsageException $e ) { + } catch ( ApiUsageException $e ) { $exception = true; } $this->assertTrue( isset( $result['upload'] ) ); @@ -289,7 +289,7 @@ class ApiUploadTest extends ApiTestCaseUpload { try { list( $result, , $session ) = $this->doApiRequestWithToken( $params, $session, self::$users['uploader']->getUser() ); - } catch ( UsageException $e ) { + } catch ( ApiUsageException $e ) { $exception = true; } $this->assertTrue( isset( $result['upload'] ) ); @@ -314,7 +314,7 @@ class ApiUploadTest extends ApiTestCaseUpload { try { list( $result ) = $this->doApiRequestWithToken( $params, $session, self::$users['uploader']->getUser() ); // FIXME: leaks a temporary file - } catch ( UsageException $e ) { + } catch ( ApiUsageException $e ) { $exception = true; } $this->assertTrue( isset( $result['upload'] ) ); @@ -371,7 +371,7 @@ class ApiUploadTest extends ApiTestCaseUpload { try { list( $result, , $session ) = $this->doApiRequestWithToken( $params, $session, self::$users['uploader']->getUser() ); // FIXME: leaks a temporary file - } catch ( UsageException $e ) { + } catch ( ApiUsageException $e ) { $exception = true; } $this->assertFalse( $exception ); @@ -400,12 +400,12 @@ class ApiUploadTest extends ApiTestCaseUpload { try { list( $result ) = $this->doApiRequestWithToken( $params, $session, self::$users['uploader']->getUser() ); - } catch ( UsageException $e ) { + } catch ( ApiUsageException $e ) { $exception = true; } $this->assertTrue( isset( $result['upload'] ) ); $this->assertEquals( 'Success', $result['upload']['result'] ); - $this->assertFalse( $exception, "No UsageException exception." ); + $this->assertFalse( $exception, "No ApiUsageException exception." ); // clean up $this->deleteFileByFileName( $fileName ); @@ -476,7 +476,7 @@ class ApiUploadTest extends ApiTestCaseUpload { try { list( $result, , $session ) = $this->doApiRequestWithToken( $params, $session, self::$users['uploader']->getUser() ); - } catch ( UsageException $e ) { + } catch ( ApiUsageException $e ) { $this->markTestIncomplete( $e->getMessage() ); } // Make sure we got a valid chunk continue: @@ -504,7 +504,7 @@ class ApiUploadTest extends ApiTestCaseUpload { try { list( $result, , $session ) = $this->doApiRequestWithToken( $params, $session, self::$users['uploader']->getUser() ); - } catch ( UsageException $e ) { + } catch ( ApiUsageException $e ) { $this->markTestIncomplete( $e->getMessage() ); } // Make sure we got a valid chunk continue: @@ -544,7 +544,7 @@ class ApiUploadTest extends ApiTestCaseUpload { try { list( $result ) = $this->doApiRequestWithToken( $params, $session, self::$users['uploader']->getUser() ); - } catch ( UsageException $e ) { + } catch ( ApiUsageException $e ) { $exception = true; } $this->assertTrue( isset( $result['upload'] ) ); diff --git a/tests/phpunit/includes/api/ApiWatchTest.php b/tests/phpunit/includes/api/ApiWatchTest.php index 19afc148b7ca..0cd270764002 100644 --- a/tests/phpunit/includes/api/ApiWatchTest.php +++ b/tests/phpunit/includes/api/ApiWatchTest.php @@ -146,11 +146,11 @@ class ApiWatchTest extends ApiTestCase { $this->assertArrayHasKey( 'rollback', $data[0] ); $this->assertArrayHasKey( 'title', $data[0]['rollback'] ); - } catch ( UsageException $ue ) { - if ( $ue->getCodeString() == 'onlyauthor' ) { + } catch ( ApiUsageException $ue ) { + if ( self::apiExceptionHasCode( $ue, 'onlyauthor' ) ) { $this->markTestIncomplete( "Only one author to 'Help:UTPage', cannot test rollback" ); } else { - $this->fail( "Received error '" . $ue->getCodeString() . "'" ); + $this->fail( "Received error '" . $ue->getMessage() . "'" ); } } } diff --git a/tests/phpunit/includes/api/MockApi.php b/tests/phpunit/includes/api/MockApi.php index d7db538273fb..1407c10d9314 100644 --- a/tests/phpunit/includes/api/MockApi.php +++ b/tests/phpunit/includes/api/MockApi.php @@ -9,7 +9,11 @@ class MockApi extends ApiBase { public function __construct() { } - public function setWarning( $warning ) { + public function getModulePath() { + return $this->getModuleName(); + } + + public function addWarning( $warning, $code = null, $data = null ) { $this->warnings[] = $warning; } diff --git a/tests/phpunit/includes/api/MockApiQueryBase.php b/tests/phpunit/includes/api/MockApiQueryBase.php index f5b50e5a5919..9915a38d0a45 100644 --- a/tests/phpunit/includes/api/MockApiQueryBase.php +++ b/tests/phpunit/includes/api/MockApiQueryBase.php @@ -12,4 +12,8 @@ class MockApiQueryBase extends ApiQueryBase { public function getModuleName() { return $this->name; } + + public function getModulePath() { + return 'query+' . $this->getModuleName(); + } } diff --git a/tests/phpunit/includes/api/format/ApiFormatPhpTest.php b/tests/phpunit/includes/api/format/ApiFormatPhpTest.php index 0028bbb0ace8..3aa1db301057 100644 --- a/tests/phpunit/includes/api/format/ApiFormatPhpTest.php +++ b/tests/phpunit/includes/api/format/ApiFormatPhpTest.php @@ -133,12 +133,10 @@ class ApiFormatPhpTest extends ApiFormatTestBase { $printer->closePrinter(); ob_end_clean(); $this->fail( 'Expected exception not thrown' ); - } catch ( UsageException $ex ) { + } catch ( ApiUsageException $ex ) { ob_end_clean(); - $this->assertSame( - 'This response cannot be represented using format=php. ' . - 'See https://phabricator.wikimedia.org/T68776', - $ex->getMessage(), + $this->assertTrue( + $ex->getStatusValue()->hasMessage( 'apierror-formatphp' ), 'Expected exception' ); } diff --git a/tests/phpunit/includes/api/format/ApiFormatXmlTest.php b/tests/phpunit/includes/api/format/ApiFormatXmlTest.php index 3fef0b0027cc..0f8c8ee6c125 100644 --- a/tests/phpunit/includes/api/format/ApiFormatXmlTest.php +++ b/tests/phpunit/includes/api/format/ApiFormatXmlTest.php @@ -105,11 +105,11 @@ class ApiFormatXmlTest extends ApiFormatTestBase { [ 'includexmlnamespace' => 1 ] ], // xslt param - [ [], '<?xml version="1.0"?><api><warnings><xml xml:space="preserve">Invalid or non-existent stylesheet specified</xml></warnings></api>', + [ [], '<?xml version="1.0"?><api><warnings><xml xml:space="preserve">Invalid or non-existent stylesheet specified.</xml></warnings></api>', [ 'xslt' => 'DoesNotExist' ] ], [ [], '<?xml version="1.0"?><api><warnings><xml xml:space="preserve">Stylesheet should be in the MediaWiki namespace.</xml></warnings></api>', [ 'xslt' => 'ApiFormatXmlTest' ] ], - [ [], '<?xml version="1.0"?><api><warnings><xml xml:space="preserve">Stylesheet should have .xsl extension.</xml></warnings></api>', + [ [], '<?xml version="1.0"?><api><warnings><xml xml:space="preserve">Stylesheet should have ".xsl" extension.</xml></warnings></api>', [ 'xslt' => 'MediaWiki:ApiFormatXmlTest' ] ], [ [], '<?xml version="1.0"?><?xml-stylesheet href="' . diff --git a/tests/phpunit/includes/api/query/ApiQueryTest.php b/tests/phpunit/includes/api/query/ApiQueryTest.php index 8cb2327dfbed..9407edfffa8f 100644 --- a/tests/phpunit/includes/api/query/ApiQueryTest.php +++ b/tests/phpunit/includes/api/query/ApiQueryTest.php @@ -99,11 +99,11 @@ class ApiQueryTest extends ApiTestCase { $exceptionCaught = false; try { $this->assertEquals( $expected, $api->titlePartToKey( $titlePart, $namespace ) ); - } catch ( UsageException $e ) { + } catch ( ApiUsageException $e ) { $exceptionCaught = true; } $this->assertEquals( $expectException, $exceptionCaught, - 'UsageException thrown by titlePartToKey' ); + 'ApiUsageException thrown by titlePartToKey' ); } function provideTestTitlePartToKey() { diff --git a/tests/phpunit/includes/upload/UploadFromUrlTest.php b/tests/phpunit/includes/upload/UploadFromUrlTest.php index 6d17a68c7df0..62081aa35da3 100644 --- a/tests/phpunit/includes/upload/UploadFromUrlTest.php +++ b/tests/phpunit/includes/upload/UploadFromUrlTest.php @@ -58,7 +58,7 @@ class UploadFromUrlTest extends ApiTestCase { $this->doApiRequest( [ 'action' => 'upload', ] ); - } catch ( UsageException $e ) { + } catch ( ApiUsageException $e ) { $exception = true; $this->assertEquals( "The token parameter must be set", $e->getMessage() ); } @@ -70,7 +70,7 @@ class UploadFromUrlTest extends ApiTestCase { 'action' => 'upload', 'token' => $token, ], $data ); - } catch ( UsageException $e ) { + } catch ( ApiUsageException $e ) { $exception = true; $this->assertEquals( "One of the parameters sessionkey, file, url is required", $e->getMessage() ); @@ -84,7 +84,7 @@ class UploadFromUrlTest extends ApiTestCase { 'url' => 'http://www.example.com/test.png', 'token' => $token, ], $data ); - } catch ( UsageException $e ) { + } catch ( ApiUsageException $e ) { $exception = true; $this->assertEquals( "The filename parameter must be set", $e->getMessage() ); } @@ -99,7 +99,7 @@ class UploadFromUrlTest extends ApiTestCase { 'filename' => 'UploadFromUrlTest.png', 'token' => $token, ], $data ); - } catch ( UsageException $e ) { + } catch ( ApiUsageException $e ) { $exception = true; $this->assertEquals( "Permission denied", $e->getMessage() ); } |