From: Brad Jorsch Date: Wed, 19 Oct 2016 16:54:25 +0000 (-0400) Subject: API: i18n for warnings and errors X-Git-Tag: 1.31.0-rc.0~4660^2 X-Git-Url: http://git.cyclocoop.org/%24href?a=commitdiff_plain;h=4e6810e4a2c1d821d8d108c7974ac16917561764;p=lhc%2Fweb%2Fwiklou.git API: i18n for warnings and errors API warnings and error messages are currently hard-coded English strings. This patch changes that. With a few exceptions, this patch should be compatible with non-updated extensions: * The change to ApiBase::$messageMap will blow up anything trying to mess with it. * The changes to the 'ApiCheckCanExecute' hook will cause a wrong (probably unparsed) error message to be emitted for extensions not already using an ApiMessage. Unless they're currently broken like Wikibase. Bug: T37074 Bug: T47843 Depends-On: Ia2b66b57cd4eaddc30b3ffdd7b97d6ca3e02d898 Depends-On: I2e1bb975bb0045476c03ebe6cdec00259bae22ec Depends-On: I53987bf87c48f6c00deec17a8e957d24fcc3eaa6 Depends-On: Ibf93a459eb62d30f7c70d20e91ec9faeb80d10ed Depends-On: I3cf889811f44a15935e454dd42f081164d4a098c Depends-On: Ieae527de86735ddcba34724730e8730fb277b99b Depends-On: I535344c29d51521147c2a26c341dae38cec3e931 Change-Id: Iae0e2ce3bd42dd4776a9779664086119ac188412 --- diff --git a/RELEASE-NOTES-1.29 b/RELEASE-NOTES-1.29 index 21a94c5d4a..b055ade42b 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 0d6407bc20..bf36f9ffd9 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 a73d50f9bd..b88a87a701 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 e6223e81b8..f850152050 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 0c3d52a39f..cd78b499df 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 2511e3be99..5d12590fdf 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 6fafebff3b..5327d7a99b 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 0cd46e42b4..a40593f6bd 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 '' . $this->encodeParamName( $p ) . ''; + }, + 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 '' . $this->encodeParamName( $p ) . ''; + }, + 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 '' . $this->encodeParamName( $p ) . ''; + }, + 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 '' . $this->encodeParamName( $p ) . ''; + }, + 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 '' . wfEscapeWikiText( $v ) . ''; + }, $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 "" 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 "" 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 e4c9d0a80b..a4ea3857bb 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 5a0edfcd82..4139019ccf 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 aea28195f0..c25920e728 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 dd88b5fe3a..3cc7a8a058 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 cbb1524cc7..3f5bc0c0c8 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 7eb0bf3e81..d6867eb52d 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 19e2453944..7da8ed9a5b 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 993c23e582..50c24aeca8 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 fc9752205e..41bf9b69c7 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 d6de834301..6b568701fd 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 192378e8e8..8aff6f8afd 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 6d9184f781..4fb19b88d5 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 @@ -56,6 +58,15 @@ class ApiErrorFormatter { $this->format = $format; } + /** + * 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( '!!', '"', $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 10fb1824be..6f7cf652c3 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 c7dc303ada..97720c6e9f 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 813ac013a0..e0e50edd9c 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 af5b1afc28..b9bb761b3c 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 97464d61db..736898edcf 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( @@ -70,23 +70,6 @@ class ApiFileRevert extends ApiBase { $this->getResult()->addValue( null, $this->getModuleName(), $result ); } - /** - * 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 2e917e1a4f..8ebfe48cf8 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 fc25f47723..a744f57bec 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 9da040ca0b..228b47ea65 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 a45dbebfb5..e4dfda0f57 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 37cb80a9fc..72fb16d19b 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 10106ff02b..3f48f38a0e 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 1017607ce6..9a21e7620c 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 6ac261dd3a..723dc33c78 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 6a26e2e350..d5c28f1d6a 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 38299b4711..fe6ed417ca 100644 --- a/includes/api/ApiMain.php +++ b/includes/api/ApiMain.php @@ -46,6 +46,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 */ @@ -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() ); } @@ -1831,70 +1942,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 617db227df..3299f73b5b 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 276f1c0ebe..357698e13c 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 ae66778641..9d69a771d4 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 29e67b07cd..7c8aa90ce9 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 ace776c923..e6fe27ca2a 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 8bfe447df5..466d1865d6 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 853a8056be..4cf896f1dc 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 ffc3fc2e32..a9b3dde947 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 0cad5dee9c..2263b8f83c 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 62528825de..c33542f1c7 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 d28906069f..746dc9a16b 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 8bbd88dfec..324d030fdb 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 16bd725e3c..8196cfa2bb 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 3073a951b0..b09b97702d 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 8734f380bb..e3e5ed6c9f 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 ac906056a3..c3636c6bcf 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 e0ba4ea1c1..244effc523 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 6a0f124faf..7460bd5377 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 b7ed9dda00..2e2ac320fd 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 fb502e40e7..4c323206ef 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 8e89c32e50..ef7b9af986 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 bba53755c1..af2aed5504 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 5d7c664aac..ef79efd358 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 63d0f6da13..f2498cae20 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 4865ad56f9..02961aa299 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 cfd0653d9e..d0b8214469 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 d58efa1d5a..6a259cd00a 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 e1c97e149b..9476066dfc 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 03be491e7c..116dbb3d34 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 75681077de..6e2fb67b8d 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 6d9c2ca861..cfd990b213 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 d1fcfa3f07..0bbfad3a83 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 e04d8c888f..ae6f5bf564 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 d287020536..fd6503801f 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 a6153de961..8d5b5f3ea6 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 67f2c9ecea..55e3c85265 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 6e5239f7b9..e9ae132df7 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 122594d175..2dcd0b4f88 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 0c70a8a4ef..1324f2ff49 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 9ba757c078..908cdee667 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 8b11dc2a47..8d149274fd 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 3259927a23..48f604664f 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 266d6999ba..696ec87867 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 9962d5ec20..64bc43f07b 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 19e0c939e9..6fc6aa370c 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 b039a1ec45..981cb09483 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 de5a377417..5b700dbc9c 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 b85bec4899..b6d871b817 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 d3cd0c48c4..3b604786ab 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 9b45b91923..65d3797a74 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 42ea55dd70..6b5ceb703f 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 806861e800..a1078a5d48 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 d72c8a407e..359d045fdd 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 2d7f5dff23..b5fa8ed859 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 6e27fc8920..61a4394e74 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 ed9fba27c0..0251bdbddd 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 b9911da138..c802087231 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 3412f38ed3..5769ff6d39 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 92cbe9053a..e29fda536f 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( [ 'stashedtexthash', 'text' ] ), + 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 f88c2dbc62..f6c0584547 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 4940394fe8..fc2951a9db 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 ace41a4e36..523a888d12 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 e24f2ced59..7fda1ea01a 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 7b44f40993..6bdd68f937 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 0000000000..7e21ab5ba4 --- /dev/null +++ b/includes/api/ApiUsageException.php @@ -0,0 +1,217 @@ +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 3a7a082148..d257e9005f 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 28cd746adc..442bdf4cf2 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 uselang and errorlang 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 Origin header exactly, so it has to be set to something like https://en.wikipedia.org or https://meta.wikimedia.org. If this parameter does not match the Origin header, a 403 response will be returned. If this parameter matches the Origin header and the origin is whitelisted, the Access-Control-Allow-Origin and Access-Control-Allow-Credentials headers will be set.\n\nFor non-authenticated requests, specify the value *. This will cause the Access-Control-Allow-Origin header to be set, but Access-Control-Allow-Credentials will be false and all user-specific data will be restricted.", "apihelp-main-param-uselang": "Language to use for message translations. [[Special:ApiHelp/query+siteinfo|action=query&meta=siteinfo]] with siprop=languages returns a list of language codes, or specify user to use the current user's language preference, or specify content 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. errorlang and errorsusedb are ignored.", + "apihelp-main-param-errorlang": "Language to use for warnings and errors. [[Special:ApiHelp/query+siteinfo|action=query&meta=siteinfo]] with siprop=languages returns a list of language codes, or specify content to use this wiki's content language, or specify uselang to use the same value as the uselang 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 .xsl.", + "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 .xsl.", "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 UI or REDIRECT response. Either this or $1returnurl is required.", "api-help-authmanagerhelper-additional-params": "This module accepts additional parameters depending on the available authentication requests. Use [[Special:ApiHelp/query+authmanagerinfo|action=query&meta=authmanagerinfo]] with amirequestsfor=$1 (or a previous response from this module, if applicable) to determine the requests available and the fields that they use.", + "apierror-allimages-redirect": "Use gaifilterredir=nonredirects instead of redirects when using allimages as a generator.", + "apierror-allpages-generator-redirects": "Use gapfilterredir=nonredirects instead of redirects when using allpages 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 bot 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 $wgAPIMaxResultSize 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": "$1diffto must be set to a non-negative number, prev, next or cur.", + "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 $1 cannot be used as a generator.", + "apierror-badgenerator-unknown": "Unknown generator=$1.", + "apierror-badip": "IP parameter is not valid.", + "apierror-badmd5": "The supplied MD5 hash was incorrect.", + "apierror-badmodule-badsubmodule": "The module $1 does not have a submodule \"$2\".", + "apierror-badmodule-nosubmodules": "The module $1 has no submodules.", + "apierror-badparameter": "Invalid value for parameter $1.", + "apierror-badquery": "Invalid query.", + "apierror-badtimestamp": "Invalid value \"$2\" for timestamp parameter $1.", + "apierror-badtoken": "Invalid CSRF token.", + "apierror-badupload": "File upload parameter $1 is not a file upload; be sure to use multipart/form-data for your POST and include a filename in the Content-Disposition header.", + "apierror-badurl": "Invalid value \"$2\" for URL parameter $1.", + "apierror-baduser": "Invalid value \"$2\" for user parameter $1.", + "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 from and the to 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 create.", + "apierror-csp-report": "Error processing CSP report: $1.", + "apierror-databaseerror": "[$1] Database query error.", + "apierror-deletedrevs-param-not-1-2": "The $1 parameter cannot be used in modes 1 or 2.", + "apierror-deletedrevs-param-not-3": "The $1 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 ignorewarnings 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 format=php. See https://phabricator.wikimedia.org/T68776.", + "apierror-imageusage-badtitle": "The title for $1 must be a file.", + "apierror-import-unknownerror": "Unknown error on import: $1.", + "apierror-integeroutofrange-abovebotmax": "$1 may not be over $2 (set to $3) for bots or sysops.", + "apierror-integeroutofrange-abovemax": "$1 may not be over $2 (set to $3) for users.", + "apierror-integeroutofrange-belowminimum": "$1 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 $1.", + "apierror-invalidoldimage": "The oldimage parameter has invalid format.", + "apierror-invalidparammix-cannotusewith": "The $1 parameter cannot be used with $2.", + "apierror-invalidparammix-mustusewith": "The $1 parameter may only be used with $2.", + "apierror-invalidparammix-parse-new-section": "section=new cannot be combined with the oldid, pageid or page parameters. Please use title and text.", + "apierror-invalidparammix": "The {{PLURAL:$2|parameters}} $1 can not be used together.", + "apierror-invalidsection": "The section parameter must be a valid section ID or new.", + "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 $1urlparam ($2=$3).", + "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 $1 parameter must be set.", + "apierror-missingrev-pageid": "No current revision of page ID $1.", + "apierror-missingtitle-createonly": "Missing titles can only be protected with create.", + "apierror-missingtitle": "The page you specified doesn't exist.", + "apierror-missingtitle-byname": "The page $1 doesn't exist.", + "apierror-moduledisabled": "The $1 module has been disabled.", + "apierror-multival-only-one-of": "{{PLURAL:$3|Only|Only one of}} $2 is allowed for parameter $1.", + "apierror-multival-only-one": "Only one value is allowed for parameter $1.", + "apierror-multpages": "$1 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 $1 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 $wgEnableWriteAPI=true; statement is included in the wiki's LocalSettings.php 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 $1 may not be empty.", + "apierror-parsetree-notwikitext": "prop=parsetree is only supported for wikitext content.", + "apierror-parsetree-notwikitext-title": "prop=parsetree 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 patrol or patrolmarks 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 Promise-Non-Write-API-Action 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 section=new, prependtext, or appendtext.", + "apierror-revdel-mutuallyexclusive": "The same field cannot be used in both hide and show.", + "apierror-revdel-needtarget": "A target title is required for this RevDel type.", + "apierror-revdel-paramneeded": "At least one value is required for hide and/or show.", + "apierror-revisions-norevids": "The revids parameter may not be used with the list options ($1limit, $1startid, $1endid, $1dir=newer, $1user, $1excludeuser, $1start, and $1end).", + "apierror-revisions-singlepage": "titles, pageids or a generator was used to supply multiple pages, but the $1limit, $1startid, $1endid, $1dir=newer, $1user, $1excludeuser, $1start, and $1end parameters may only be used on a single page.", + "apierror-revwrongpage": "r$1 is not a revision of $2.", + "apierror-searchdisabled": "$1 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 $wgShowHostNames is true.", + "apierror-sizediffdisabled": "Size difference is disabled in Miser Mode.", + "apierror-spamdetected": "Your edit was refused because it contained a spam fragment: $1.", + "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, $1, 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 $1: $2.", + "apierror-unsupportedrepo": "Local file repository does not support querying all images.", + "apierror-upload-filekeyneeded": "Must supply a filekey when offset is non-zero.", + "apierror-upload-filekeynotallowed": "Cannot supply a filekey when offset 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 $1dir=newer.", + "apiwarn-badurlparam": "Could not parse $1urlparam for $2. Using only width and height.", + "apiwarn-badutf8": "The value passed for $1 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": "list=deletedrevs has been deprecated. Please use prop=deletedrevisions or list=alldeletedrevisions instead.", + "apiwarn-deprecation-expandtemplates-prop": "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.", + "apiwarn-deprecation-httpsexpected": "HTTP used when HTTPS was expected.", + "apiwarn-deprecation-login-botpw": "Main-account login via action=login is deprecated and may stop working without warning. To continue login with action=login, see [[Special:BotPasswords]]. To safely continue using main-account login, see action=clientlogin.", + "apiwarn-deprecation-login-nobotpw": "Main-account login via action=login is deprecated and may stop working without warning. To safely log in, see action=clientlogin.", + "apiwarn-deprecation-login-token": "Fetching a token via action=login is deprecated. Use action=query&meta=tokens&type=login instead.", + "apiwarn-deprecation-parameter": "The parameter $1 has been deprecated.", + "apiwarn-deprecation-parse-headitems": "prop=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.", + "apiwarn-deprecation-purge-get": "Use of action=purge via GET is deprecated. Use POST instead.", + "apiwarn-deprecation-withreplacement": "$1 has been deprecated. Please use $2 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 .xsl extension.", + "apiwarn-invalidxmlstylesheet": "Invalid or non-existent stylesheet specified.", + "apiwarn-invalidxmlstylesheetns": "Stylesheet should be in the {{ns:MediaWiki}} namespace.", + "apiwarn-moduleswithoutvars": "Property modules was set but not jsconfigvars or encodedjsconfigvars. 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 title or contentmodel was given, assuming $1.", + "apiwarn-parse-titlewithouttext": "title used without text, and parsed page properties were requested. Did you mean to use page instead of title?", + "apiwarn-redirectsandrevids": "Redirect resolution cannot be used together with the revids parameter. Any redirects the revids 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 $1: 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 $1 has been deprecated. If for some reason you need to explicitly specify the current time without calculating it client-side, use now.", + "apiwarn-unrecognizedvalues": "Unrecognized {{PLURAL:$3|value|values}} for parameter $1: $2.", + "apiwarn-unsupportedarray": "Parameter $1 uses unsupported PHP array syntax.", + "apiwarn-urlparamwidth": "Ignoring width value set in $1urlparam ($2) in favor of width value derived from $1urlwidth/$1urlheight ($3).", + "apiwarn-validationfailed-badchars": "invalid characters in key (only a-z, A-Z, 0-9, _, and - 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 $1: $2", + "apiwarn-wgDebugAPI": "Security Warning: $wgDebugAPI 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 fd6a4dd609..c5d9bc041e 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 action=feedwatchlist.\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 74b474ace4..54480132e4 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 a550e8853b..c0f1c3dfe0 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 cff8bf463a..a8b5e5e5ed 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 afd13f0016..dad1737df9 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
0.0.0.0/0
::/0" + "restrictionsfield-help": "One IP address or CIDR range per line. To enable everything, use
0.0.0.0/0
::/0", + "revid": "r$1", + "pageid": "page ID $1" } diff --git a/languages/i18n/qqq.json b/languages/i18n/qqq.json index 936fd8b8ac..bc4bc4ced4 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 cb46b110dc..d94b158538 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 93687df2d5..bdec0a50d9 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 8b75d56281..96f3e44f0f 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 d2dccf997d..08fc1286cf 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 3ad16d1322..bb4ea75893 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 02d0a0dc57..0ffcbca762 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 d13b00be2e..1b7f6bff57 100644 --- a/tests/phpunit/includes/api/ApiErrorFormatterTest.php +++ b/tests/phpunit/includes/api/ApiErrorFormatterTest.php @@ -5,6 +5,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" 😊', + $wrappedFormatter->stripMarkup( 'Blah kbd <X> 😊' ), + 'stripMarkup' + ); + } + /** * @covers ApiErrorFormatter * @dataProvider provideErrorFormatter @@ -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 <X> 😞' ) + ); $formatter->addError( 'err', 'mainpage' ); $this->assertSame( [ 'error' => [ @@ -310,6 +363,10 @@ class ApiErrorFormatterTest extends MediaWikiLangTestCase { 'info' => $mainpagePlain, ], 'warnings' => [ + 'raw' => [ + 'warnings' => 'Blah "kbd" 😞', + 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 c111949d2f..c9a3428da1 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' => '', 'uselang' => '', '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 8764b4194f..e405b3b895 100644 --- a/tests/phpunit/includes/api/ApiMessageTest.php +++ b/tests/phpunit/includes/api/ApiMessageTest.php @@ -23,6 +23,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 0a577c1cb6..ef70626120 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 b72a4f8a8b..f01a670b71 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 eaeb3ae925..0a2cd83dd7 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 d6f315d5b3..0f01664e72 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 7e1f9d8775..6b299c98c8 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 b63bf2ea31..971b63c3d4 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 de2b56bde3..9b79e6c538 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 19afc148b7..0cd2707640 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 d7db538273..1407c10d93 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 f5b50e5a59..9915a38d0a 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 0028bbb0ac..3aa1db3010 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 3fef0b0027..0f8c8ee6c1 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 - [ [], 'Invalid or non-existent stylesheet specified', + [ [], 'Invalid or non-existent stylesheet specified.', [ 'xslt' => 'DoesNotExist' ] ], [ [], 'Stylesheet should be in the MediaWiki namespace.', [ 'xslt' => 'ApiFormatXmlTest' ] ], - [ [], 'Stylesheet should have .xsl extension.', + [ [], 'Stylesheet should have ".xsl" extension.', [ 'xslt' => 'MediaWiki:ApiFormatXmlTest' ] ], [ [], '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 6d17a68c7d..62081aa35d 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() ); }