API: i18n for warnings and errors
authorBrad Jorsch <bjorsch@wikimedia.org>
Wed, 19 Oct 2016 16:54:25 +0000 (12:54 -0400)
committerBrad Jorsch <bjorsch@wikimedia.org>
Tue, 6 Dec 2016 15:20:48 +0000 (10:20 -0500)
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

131 files changed:
RELEASE-NOTES-1.29
autoload.php
docs/hooks.txt
includes/FileDeleteForm.php
includes/WatchedItemQueryService.php
includes/api/ApiAMCreateAccount.php
includes/api/ApiAuthManagerHelper.php
includes/api/ApiBase.php
includes/api/ApiBlock.php
includes/api/ApiCSPReport.php
includes/api/ApiChangeAuthenticationData.php
includes/api/ApiCheckToken.php
includes/api/ApiClientLogin.php
includes/api/ApiComparePages.php
includes/api/ApiContinuationManager.php
includes/api/ApiDelete.php
includes/api/ApiDisabled.php
includes/api/ApiEditPage.php
includes/api/ApiEmailUser.php
includes/api/ApiErrorFormatter.php
includes/api/ApiExpandTemplates.php
includes/api/ApiFeedContributions.php
includes/api/ApiFeedRecentChanges.php
includes/api/ApiFeedWatchlist.php
includes/api/ApiFileRevert.php
includes/api/ApiFormatJson.php
includes/api/ApiFormatPhp.php
includes/api/ApiFormatRaw.php
includes/api/ApiFormatXml.php
includes/api/ApiImageRotate.php
includes/api/ApiImport.php
includes/api/ApiLinkAccount.php
includes/api/ApiLogin.php
includes/api/ApiLogout.php
includes/api/ApiMain.php
includes/api/ApiManageTags.php
includes/api/ApiMergeHistory.php
includes/api/ApiMessage.php
includes/api/ApiMove.php
includes/api/ApiOpenSearch.php
includes/api/ApiOptions.php
includes/api/ApiPageSet.php
includes/api/ApiParamInfo.php
includes/api/ApiParse.php
includes/api/ApiPatrol.php
includes/api/ApiProtect.php
includes/api/ApiPurge.php
includes/api/ApiQuery.php
includes/api/ApiQueryAllDeletedRevisions.php
includes/api/ApiQueryAllImages.php
includes/api/ApiQueryAllLinks.php
includes/api/ApiQueryAllMessages.php
includes/api/ApiQueryAllPages.php
includes/api/ApiQueryAllUsers.php
includes/api/ApiQueryBacklinks.php
includes/api/ApiQueryBacklinksprop.php
includes/api/ApiQueryBase.php
includes/api/ApiQueryBlocks.php
includes/api/ApiQueryCategories.php
includes/api/ApiQueryCategoryMembers.php
includes/api/ApiQueryDeletedRevisions.php
includes/api/ApiQueryDeletedrevs.php
includes/api/ApiQueryDisabled.php
includes/api/ApiQueryFilearchive.php
includes/api/ApiQueryIWBacklinks.php
includes/api/ApiQueryIWLinks.php
includes/api/ApiQueryImageInfo.php
includes/api/ApiQueryImages.php
includes/api/ApiQueryInfo.php
includes/api/ApiQueryLangBacklinks.php
includes/api/ApiQueryLangLinks.php
includes/api/ApiQueryLinks.php
includes/api/ApiQueryLogEvents.php
includes/api/ApiQueryMyStashedFiles.php
includes/api/ApiQueryQueryPage.php
includes/api/ApiQueryRecentChanges.php
includes/api/ApiQueryRevisions.php
includes/api/ApiQueryRevisionsBase.php
includes/api/ApiQuerySearch.php
includes/api/ApiQuerySiteinfo.php
includes/api/ApiQueryStashImageInfo.php
includes/api/ApiQueryTokens.php
includes/api/ApiQueryUserContributions.php
includes/api/ApiQueryUserInfo.php
includes/api/ApiQueryUsers.php
includes/api/ApiQueryWatchlist.php
includes/api/ApiQueryWatchlistRaw.php
includes/api/ApiRemoveAuthenticationData.php
includes/api/ApiResetPassword.php
includes/api/ApiResult.php
includes/api/ApiRevisionDelete.php
includes/api/ApiRollback.php
includes/api/ApiSetNotificationTimestamp.php
includes/api/ApiStashEdit.php
includes/api/ApiTag.php
includes/api/ApiTokens.php
includes/api/ApiUnblock.php
includes/api/ApiUndelete.php
includes/api/ApiUpload.php
includes/api/ApiUsageException.php [new file with mode: 0644]
includes/api/ApiWatch.php
includes/api/i18n/en.json
includes/api/i18n/qqq.json
includes/specials/SpecialApiHelp.php
includes/specials/SpecialEmailuser.php
includes/specials/SpecialUnblock.php
languages/i18n/en.json
languages/i18n/qqq.json
resources/src/mediawiki/page/rollback.js
tests/phpunit/includes/WatchedItemQueryServiceUnitTest.php
tests/phpunit/includes/api/ApiBaseTest.php
tests/phpunit/includes/api/ApiBlockTest.php
tests/phpunit/includes/api/ApiContinuationManagerTest.php
tests/phpunit/includes/api/ApiEditPageTest.php
tests/phpunit/includes/api/ApiErrorFormatterTest.php
tests/phpunit/includes/api/ApiMainTest.php
tests/phpunit/includes/api/ApiMessageTest.php
tests/phpunit/includes/api/ApiOptionsTest.php
tests/phpunit/includes/api/ApiParseTest.php
tests/phpunit/includes/api/ApiQueryWatchlistIntegrationTest.php
tests/phpunit/includes/api/ApiQueryWatchlistRawIntegrationTest.php
tests/phpunit/includes/api/ApiTestCase.php
tests/phpunit/includes/api/ApiUnblockTest.php
tests/phpunit/includes/api/ApiUploadTest.php
tests/phpunit/includes/api/ApiWatchTest.php
tests/phpunit/includes/api/MockApi.php
tests/phpunit/includes/api/MockApiQueryBase.php
tests/phpunit/includes/api/format/ApiFormatPhpTest.php
tests/phpunit/includes/api/format/ApiFormatXmlTest.php
tests/phpunit/includes/api/query/ApiQueryTest.php
tests/phpunit/includes/upload/UploadFromUrlTest.php

index 21a94c5..b055ade 100644 (file)
@@ -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 ===
 
index 0d6407b..bf36f9f 100644 (file)
@@ -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',
index a73d50f..b88a87a 100644 (file)
@@ -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
index e6223e8..f850152 100644 (file)
@@ -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 = []
index 0c3d52a..cd78b49 100644 (file)
@@ -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();
                }
index 2511e3b..5d12590 100644 (file)
@@ -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}"
                                );
                        }
index 6fafebf..5327d7a 100644 (file)
@@ -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\"" );
index 0cd46e4..a40593f 100644 (file)
@@ -545,7 +545,7 @@ abstract class ApiBase extends ContextSource {
         * @since 1.25
         * @param string $path
         * @return ApiBase|null
-        * @throws UsageException
+        * @throws ApiUsageException
         */
        public function getModuleFromPath( $path ) {
                $module = $this->getMain();
@@ -565,14 +565,14 @@ abstract class ApiBase extends ContextSource {
                        $manager = $parent->getModuleManager();
                        if ( $manager === null ) {
                                $errorPath = implode( '+', array_slice( $parts, 0, $i ) );
-                               $this->dieUsage( "The module \"$errorPath\" has no submodules", 'badmodule' );
+                               $this->dieWithError( [ 'apierror-badmodule-nosubmodules', $errorPath ], 'badmodule' );
                        }
                        $module = $manager->getModule( $parts[$i] );
 
                        if ( $module === null ) {
                                $errorPath = $i ? implode( '+', array_slice( $parts, 0, $i ) ) : $parent->getModuleName();
-                               $this->dieUsage(
-                                       "The module \"$errorPath\" does not have a submodule \"{$parts[$i]}\"",
+                               $this->dieWithError(
+                                       [ 'apierror-badmodule-badsubmodule', $errorPath, wfEscapeWikiText( $parts[$i] ) ],
                                        'badmodule'
                                );
                        }
@@ -670,11 +670,18 @@ abstract class ApiBase extends ContextSource {
        /**
         * This method mangles parameter name based on the prefix supplied to the constructor.
         * Override this method to change parameter name during runtime
-        * @param string $paramName Parameter name
-        * @return string Prefixed parameter name
+        * @param string|string[] $paramName Parameter name
+        * @return string|string[] Prefixed parameter name
+        * @since 1.29 accepts an array of strings
         */
        public function encodeParamName( $paramName ) {
-               return $this->mModulePrefix . $paramName;
+               if ( is_array( $paramName ) ) {
+                       return array_map( function ( $name ) {
+                               return $this->mModulePrefix . $name;
+                       }, $paramName );
+               } else {
+                       return $this->mModulePrefix . $paramName;
+               }
        }
 
        /**
@@ -725,20 +732,32 @@ abstract class ApiBase extends ContextSource {
        public function requireOnlyOneParameter( $params, $required /*...*/ ) {
                $required = func_get_args();
                array_shift( $required );
-               $p = $this->getModulePrefix();
 
                $intersection = array_intersect( array_keys( array_filter( $params,
                        [ $this, 'parameterNotEmpty' ] ) ), $required );
 
                if ( count( $intersection ) > 1 ) {
-                       $this->dieUsage(
-                               "The parameters {$p}" . implode( ", {$p}", $intersection ) . ' can not be used together',
-                               'invalidparammix' );
+                       $this->dieWithError( [
+                               'apierror-invalidparammix',
+                               Message::listParam( array_map(
+                                       function ( $p ) {
+                                               return '<var>' . $this->encodeParamName( $p ) . '</var>';
+                                       },
+                                       array_values( $intersection )
+                               ) ),
+                               count( $intersection ),
+                       ] );
                } elseif ( count( $intersection ) == 0 ) {
-                       $this->dieUsage(
-                               "One of the parameters {$p}" . implode( ", {$p}", $required ) . ' is required',
-                               'missingparam'
-                       );
+                       $this->dieWithError( [
+                               'apierror-missingparam-one-of',
+                               Message::listParam( array_map(
+                                       function ( $p ) {
+                                               return '<var>' . $this->encodeParamName( $p ) . '</var>';
+                                       },
+                                       array_values( $required )
+                               ) ),
+                               count( $required ),
+                       ], 'missingparam' );
                }
        }
 
@@ -751,16 +770,21 @@ abstract class ApiBase extends ContextSource {
        public function requireMaxOneParameter( $params, $required /*...*/ ) {
                $required = func_get_args();
                array_shift( $required );
-               $p = $this->getModulePrefix();
 
                $intersection = array_intersect( array_keys( array_filter( $params,
                        [ $this, 'parameterNotEmpty' ] ) ), $required );
 
                if ( count( $intersection ) > 1 ) {
-                       $this->dieUsage(
-                               "The parameters {$p}" . implode( ", {$p}", $intersection ) . ' can not be used together',
-                               'invalidparammix'
-                       );
+                       $this->dieWithError( [
+                               'apierror-invalidparammix',
+                               Message::listParam( array_map(
+                                       function ( $p ) {
+                                               return '<var>' . $this->encodeParamName( $p ) . '</var>';
+                                       },
+                                       array_values( $intersection )
+                               ) ),
+                               count( $intersection ),
+                       ] );
                }
        }
 
@@ -774,7 +798,6 @@ abstract class ApiBase extends ContextSource {
        public function requireAtLeastOneParameter( $params, $required /*...*/ ) {
                $required = func_get_args();
                array_shift( $required );
-               $p = $this->getModulePrefix();
 
                $intersection = array_intersect(
                        array_keys( array_filter( $params, [ $this, 'parameterNotEmpty' ] ) ),
@@ -782,8 +805,16 @@ abstract class ApiBase extends ContextSource {
                );
 
                if ( count( $intersection ) == 0 ) {
-                       $this->dieUsage( "At least one of the parameters {$p}" .
-                               implode( ", {$p}", $required ) . ' is required', "{$p}missingparam" );
+                       $this->dieWithError( [
+                               'apierror-missingparam-at-least-one-of',
+                               Message::listParam( array_map(
+                                       function ( $p ) {
+                                               return '<var>' . $this->encodeParamName( $p ) . '</var>';
+                                       },
+                                       array_values( $required )
+                               ) ),
+                               count( $required ),
+                       ], 'missingparam' );
                }
        }
 
@@ -812,10 +843,8 @@ abstract class ApiBase extends ContextSource {
                }
 
                if ( $badParams ) {
-                       $this->dieUsage(
-                               'The following parameters were found in the query string, but must be in the POST body: '
-                                       . join( ', ', $badParams ),
-                               'mustpostparams'
+                       $this->dieWithError(
+                               [ 'apierror-mustpostparams', join( ', ', $badParams ), count( $badParams ) ]
                        );
                }
        }
@@ -848,10 +877,10 @@ abstract class ApiBase extends ContextSource {
                if ( isset( $params['title'] ) ) {
                        $titleObj = Title::newFromText( $params['title'] );
                        if ( !$titleObj || $titleObj->isExternal() ) {
-                               $this->dieUsageMsg( [ 'invalidtitle', $params['title'] ] );
+                               $this->dieWithError( [ 'apierror-invalidtitle', wfEscapeWikiText( $params['title'] ) ] );
                        }
                        if ( !$titleObj->canExist() ) {
-                               $this->dieUsage( "Namespace doesn't allow actual pages", 'pagecannotexist' );
+                               $this->dieWithError( 'apierror-pagecannotexist' );
                        }
                        $pageObj = WikiPage::factory( $titleObj );
                        if ( $load !== false ) {
@@ -863,7 +892,7 @@ abstract class ApiBase extends ContextSource {
                        }
                        $pageObj = WikiPage::newFromID( $params['pageid'], $load );
                        if ( !$pageObj ) {
-                               $this->dieUsageMsg( [ 'nosuchpageid', $params['pageid'] ] );
+                               $this->dieWithError( [ 'apierror-nosuchpageid', $params['pageid'] ] );
                        }
                }
 
@@ -994,10 +1023,8 @@ abstract class ApiBase extends ContextSource {
                                // accidentally uploaded as a field fails spectacularly)
                                $value = $this->getMain()->getRequest()->unsetVal( $encParamName );
                                if ( $value !== null ) {
-                                       $this->dieUsage(
-                                               "File upload param $encParamName is not a file upload; " .
-                                                       'be sure to use multipart/form-data for your POST and include ' .
-                                                       'a filename in the Content-Disposition header.',
+                                       $this->dieWithError(
+                                               [ 'apierror-badupload', $encParamName ],
                                                "badupload_{$encParamName}"
                                        );
                                }
@@ -1032,10 +1059,7 @@ abstract class ApiBase extends ContextSource {
                                        // done by WebRequest for $_GET. Let's call that a feature.
                                        $value = join( "\x1f", $request->normalizeUnicode( explode( "\x1f", $rawValue ) ) );
                                } else {
-                                       $this->dieUsage(
-                                               "U+001F multi-value separation may only be used for multi-valued parameters.",
-                                               'badvalue_notmultivalue'
-                                       );
+                                       $this->dieWithError( 'apierror-badvalue-notmultivalue', 'badvalue_notmultivalue' );
                                }
                        }
 
@@ -1072,7 +1096,7 @@ abstract class ApiBase extends ContextSource {
                                        case 'text':
                                        case 'password':
                                                if ( $required && $value === '' ) {
-                                                       $this->dieUsageMsg( [ 'missingparam', $paramName ] );
+                                                       $this->dieWithError( [ 'apierror-missingparam', $paramName ] );
                                                }
                                                break;
                                        case 'integer': // Force everything using intval() and optionally validate limits
@@ -1175,8 +1199,6 @@ abstract class ApiBase extends ContextSource {
 
                        // Set a warning if a deprecated parameter has been passed
                        if ( $deprecated && $value !== false ) {
-                               $this->setWarning( "The $encParamName parameter has been deprecated." );
-
                                $feature = $encParamName;
                                $m = $this;
                                while ( !$m->isMain() ) {
@@ -1186,10 +1208,10 @@ abstract class ApiBase extends ContextSource {
                                        $feature = "{$param}={$name}&{$feature}";
                                        $m = $p;
                                }
-                               $this->logFeatureUsage( $feature );
+                               $this->addDeprecation( [ 'apiwarn-deprecation-parameter', $encParamName ], $feature );
                        }
                } elseif ( $required ) {
-                       $this->dieUsageMsg( [ 'missingparam', $paramName ] );
+                       $this->dieWithError( [ 'apierror-missingparam', $paramName ] );
                }
 
                return $value;
@@ -1204,11 +1226,7 @@ abstract class ApiBase extends ContextSource {
         */
        protected function handleParamNormalization( $paramName, $value, $rawValue ) {
                $encParamName = $this->encodeParamName( $paramName );
-               $this->setWarning(
-                       "The value passed for '$encParamName' contains invalid or non-normalized data. "
-                       . 'Textual data should be valid, NFC-normalized Unicode without '
-                       . 'C0 control characters other than HT (\\t), LF (\\n), and CR (\\r).'
-               );
+               $this->addWarning( [ 'apiwarn-badutf8', $encParamName ] );
        }
 
        /**
@@ -1265,9 +1283,10 @@ abstract class ApiBase extends ContextSource {
                }
 
                if ( self::truncateArray( $valuesList, $sizeLimit ) ) {
-                       $this->logFeatureUsage( "too-many-$valueName-for-{$this->getModulePath()}" );
-                       $this->setWarning( "Too many values supplied for parameter '$valueName': " .
-                               "the limit is $sizeLimit" );
+                       $this->addDeprecation(
+                               [ 'apiwarn-toomanyvalues', $valueName, $sizeLimit ],
+                               "too-many-$valueName-for-{$this->getModulePath()}"
+                       );
                }
 
                if ( !$allowMultiple && count( $valuesList ) != 1 ) {
@@ -1276,26 +1295,38 @@ abstract class ApiBase extends ContextSource {
                                return $value;
                        }
 
-                       $possibleValues = is_array( $allowedValues )
-                               ? "of '" . implode( "', '", $allowedValues ) . "'"
-                               : '';
-                       $this->dieUsage(
-                               "Only one $possibleValues is allowed for parameter '$valueName'",
-                               "multival_$valueName"
-                       );
+                       if ( is_array( $allowedValues ) ) {
+                               $values = array_map( function ( $v ) {
+                                       return '<kbd>' . wfEscapeWikiText( $v ) . '</kbd>';
+                               }, $allowedValues );
+                               $this->dieWithError( [
+                                       'apierror-multival-only-one-of',
+                                       $valueName,
+                                       Message::listParam( $values ),
+                                       count( $values ),
+                               ], "multival_$valueName" );
+                       } else {
+                               $this->dieWithError( [
+                                       'apierror-multival-only-one',
+                                       $valueName,
+                               ], "multival_$valueName" );
+                       }
                }
 
                if ( is_array( $allowedValues ) ) {
                        // Check for unknown values
-                       $unknown = array_diff( $valuesList, $allowedValues );
+                       $unknown = array_map( 'wfEscapeWikiText', array_diff( $valuesList, $allowedValues ) );
                        if ( count( $unknown ) ) {
                                if ( $allowMultiple ) {
-                                       $s = count( $unknown ) > 1 ? 's' : '';
-                                       $vals = implode( ', ', $unknown );
-                                       $this->setWarning( "Unrecognized value$s for parameter '$valueName': $vals" );
+                                       $this->addWarning( [
+                                               'apiwarn-unrecognizedvalues',
+                                               $valueName,
+                                               Message::listParam( $unknown, 'comma' ),
+                                               count( $unknown ),
+                                       ] );
                                } else {
-                                       $this->dieUsage(
-                                               "Unrecognized value for parameter '$valueName': {$valuesList[0]}",
+                                       $this->dieWithError(
+                                               [ 'apierror-unrecognizedvalue', $valueName, wfEscapeWikiText( $valuesList[0] ) ],
                                                "unknown_$valueName"
                                        );
                                }
@@ -1321,7 +1352,12 @@ abstract class ApiBase extends ContextSource {
                $enforceLimits = false
        ) {
                if ( !is_null( $min ) && $value < $min ) {
-                       $msg = $this->encodeParamName( $paramName ) . " may not be less than $min (set to $value)";
+                       $msg = ApiMessage::create(
+                               [ 'apierror-integeroutofrange-belowminimum',
+                                       $this->encodeParamName( $paramName ), $min, $value ],
+                               'integeroutofrange',
+                               [ 'min' => $min, 'max' => $max, 'botMax' => $botMax ?: $max ]
+                       );
                        $this->warnOrDie( $msg, $enforceLimits );
                        $value = $min;
                }
@@ -1337,13 +1373,22 @@ abstract class ApiBase extends ContextSource {
                if ( !is_null( $max ) && $value > $max ) {
                        if ( !is_null( $botMax ) && $this->getMain()->canApiHighLimits() ) {
                                if ( $value > $botMax ) {
-                                       $msg = $this->encodeParamName( $paramName ) .
-                                               " may not be over $botMax (set to $value) for bots or sysops";
+                                       $msg = ApiMessage::create(
+                                               [ 'apierror-integeroutofrange-abovebotmax',
+                                                       $this->encodeParamName( $paramName ), $botMax, $value ],
+                                               'integeroutofrange',
+                                               [ 'min' => $min, 'max' => $max, 'botMax' => $botMax ?: $max ]
+                                       );
                                        $this->warnOrDie( $msg, $enforceLimits );
                                        $value = $botMax;
                                }
                        } else {
-                               $msg = $this->encodeParamName( $paramName ) . " may not be over $max (set to $value) for users";
+                               $msg = ApiMessage::create(
+                                       [ 'apierror-integeroutofrange-abovemax',
+                                               $this->encodeParamName( $paramName ), $max, $value ],
+                                       'integeroutofrange',
+                                       [ 'min' => $min, 'max' => $max, 'botMax' => $botMax ?: $max ]
+                               );
                                $this->warnOrDie( $msg, $enforceLimits );
                                $value = $max;
                        }
@@ -1361,11 +1406,9 @@ abstract class ApiBase extends ContextSource {
                // (wfTimestamp() also accepts various non-strings and the string of 14
                // ASCII NUL bytes, but those can't get here)
                if ( !$value ) {
-                       $this->logFeatureUsage( 'unclear-"now"-timestamp' );
-                       $this->setWarning(
-                               "Passing '$value' for timestamp parameter $encParamName has been deprecated." .
-                                       ' If for some reason you need to explicitly specify the current time without' .
-                                       ' calculating it client-side, use "now".'
+                       $this->addDeprecation(
+                               [ 'apiwarn-unclearnowtimestamp', $encParamName, wfEscapeWikiText( $value ) ],
+                               'unclear-"now"-timestamp'
                        );
                        return wfTimestamp( TS_MW );
                }
@@ -1377,8 +1420,8 @@ abstract class ApiBase extends ContextSource {
 
                $unixTimestamp = wfTimestamp( TS_UNIX, $value );
                if ( $unixTimestamp === false ) {
-                       $this->dieUsage(
-                               "Invalid value '$value' for timestamp parameter $encParamName",
+                       $this->dieWithError(
+                               [ 'apierror-badtimestamp', $encParamName, wfEscapeWikiText( $value ) ],
                                "badtimestamp_{$encParamName}"
                        );
                }
@@ -1433,8 +1476,8 @@ abstract class ApiBase extends ContextSource {
        private function validateUser( $value, $encParamName ) {
                $title = Title::makeTitleSafe( NS_USER, $value );
                if ( $title === null || $title->hasFragment() ) {
-                       $this->dieUsage(
-                               "Invalid value '$value' for user parameter $encParamName",
+                       $this->dieWithError(
+                               [ 'apierror-baduser', $encParamName, wfEscapeWikiText( $value ) ],
                                "baduser_{$encParamName}"
                        );
                }
@@ -1490,22 +1533,19 @@ abstract class ApiBase extends ContextSource {
                if ( !is_null( $params['owner'] ) && !is_null( $params['token'] ) ) {
                        $user = User::newFromName( $params['owner'], false );
                        if ( !( $user && $user->getId() ) ) {
-                               $this->dieUsage( 'Specified user does not exist', 'bad_wlowner' );
+                               $this->dieWithError(
+                                       [ 'nosuchusershort', wfEscapeWikiText( $params['owner'] ) ], 'bad_wlowner'
+                               );
                        }
                        $token = $user->getOption( 'watchlisttoken' );
                        if ( $token == '' || !hash_equals( $token, $params['token'] ) ) {
-                               $this->dieUsage(
-                                       'Incorrect watchlist token provided -- please set a correct token in Special:Preferences',
-                                       'bad_wltoken'
-                               );
+                               $this->dieWithError( 'apierror-bad-watchlist-token', 'bad_wltoken' );
                        }
                } else {
                        if ( !$this->getUser()->isLoggedIn() ) {
-                               $this->dieUsage( 'You must be logged-in to have a watchlist', 'notloggedin' );
-                       }
-                       if ( !$this->getUser()->isAllowed( 'viewmywatchlist' ) ) {
-                               $this->dieUsage( 'You don\'t have permission to view your watchlist', 'permissiondenied' );
+                               $this->dieWithError( 'watchlistanontext', 'notloggedin' );
                        }
+                       $this->checkUserRightsAny( 'viewmywatchlist' );
                        $user = $this->getUser();
                }
 
@@ -1561,6 +1601,39 @@ abstract class ApiBase extends ContextSource {
                return $msg;
        }
 
+       /**
+        * Turn an array of message keys or key+param arrays into a Status
+        * @since 1.29
+        * @param array $errors
+        * @param User|null $user
+        * @return Status
+        */
+       public function errorArrayToStatus( array $errors, User $user = null ) {
+               if ( $user === null ) {
+                       $user = $this->getUser();
+               }
+
+               $status = Status::newGood();
+               foreach ( $errors as $error ) {
+                       if ( is_array( $error ) && $error[0] === 'blockedtext' && $user->getBlock() ) {
+                               $status->fatal( ApiMessage::create(
+                                       'apierror-blocked',
+                                       'blocked',
+                                       [ 'blockinfo' => ApiQueryUserInfo::getBlockInfo( $user->getBlock() ) ]
+                               ) );
+                       } elseif ( is_array( $error ) && $error[0] === 'autoblockedtext' && $user->getBlock() ) {
+                               $status->fatal( ApiMessage::create(
+                                       'apierror-autoblocked',
+                                       'autoblocked',
+                                       [ 'blockinfo' => ApiQueryUserInfo::getBlockInfo( $user->getBlock() ) ]
+                               ) );
+                       } else {
+                               call_user_func_array( [ $status, 'fatal' ], (array)$error );
+                       }
+               }
+               return $status;
+       }
+
        /**@}*/
 
        /************************************************************************//**
@@ -1569,745 +1642,227 @@ abstract class ApiBase extends ContextSource {
         */
 
        /**
-        * Set warning section for this module. Users should monitor this
-        * section to notice any changes in API. Multiple calls to this
-        * function will result in the warning messages being separated by
-        * newlines
-        * @param string $warning Warning message
+        * Add a warning for this module.
+        *
+        * Users should monitor this section to notice any changes in API. Multiple
+        * calls to this function will result in multiple warning messages.
+        *
+        * If $msg is not an ApiMessage, the message code will be derived from the
+        * message key by stripping any "apiwarn-" or "apierror-" prefix.
+        *
+        * @since 1.29
+        * @param string|array|Message $msg See ApiErrorFormatter::addWarning()
+        * @param string|null $code See ApiErrorFormatter::addWarning()
+        * @param array|null $data See ApiErrorFormatter::addWarning()
         */
-       public function setWarning( $warning ) {
-               $msg = new ApiRawMessage( $warning, 'warning' );
-               $this->getErrorFormatter()->addWarning( $this->getModuleName(), $msg );
+       public function addWarning( $msg, $code = null, $data = null ) {
+               $this->getErrorFormatter()->addWarning( $this->getModulePath(), $msg, $code, $data );
        }
 
        /**
-        * Adds a warning to the output, else dies
+        * Add a deprecation warning for this module.
         *
-        * @param string $msg Message to show as a warning, or error message if dying
-        * @param bool $enforceLimits Whether this is an enforce (die)
+        * A combination of $this->addWarning() and $this->logFeatureUsage()
+        *
+        * @since 1.29
+        * @param string|array|Message $msg See ApiErrorFormatter::addWarning()
+        * @param string|null $feature See ApiBase::logFeatureUsage()
+        * @param array|null $data See ApiErrorFormatter::addWarning()
         */
-       private function warnOrDie( $msg, $enforceLimits = false ) {
-               if ( $enforceLimits ) {
-                       $this->dieUsage( $msg, 'integeroutofrange' );
+       public function addDeprecation( $msg, $feature, $data = [] ) {
+               $data = (array)$data;
+               if ( $feature !== null ) {
+                       $data['feature'] = $feature;
+                       $this->logFeatureUsage( $feature );
                }
+               $this->addWarning( $msg, 'deprecation', $data );
+       }
 
-               $this->setWarning( $msg );
+       /**
+        * Add an error for this module without aborting
+        *
+        * If $msg is not an ApiMessage, the message code will be derived from the
+        * message key by stripping any "apiwarn-" or "apierror-" prefix.
+        *
+        * @note If you want to abort processing, use self::dieWithError() instead.
+        * @since 1.29
+        * @param string|array|Message $msg See ApiErrorFormatter::addError()
+        * @param string|null $code See ApiErrorFormatter::addError()
+        * @param array|null $data See ApiErrorFormatter::addError()
+        */
+       public function addError( $msg, $code = null, $data = null ) {
+               $this->getErrorFormatter()->addError( $this->getModulePath(), $msg, $code, $data );
        }
 
        /**
-        * Throw a UsageException, which will (if uncaught) call the main module's
-        * error handler and die with an error message.
+        * Add warnings and/or errors from a Status
         *
-        * @param string $description One-line human-readable description of the
-        *   error condition, e.g., "The API requires a valid action parameter"
-        * @param string $errorCode Brief, arbitrary, stable string to allow easy
-        *   automated identification of the error, e.g., 'unknown_action'
-        * @param int $httpRespCode HTTP response code
-        * @param array|null $extradata Data to add to the "<error>" element; array in ApiResult format
-        * @throws UsageException always
+        * @note If you want to abort processing, use self::dieStatus() instead.
+        * @since 1.29
+        * @param StatusValue $status
+        * @param string[] $types 'warning' and/or 'error'
         */
-       public function dieUsage( $description, $errorCode, $httpRespCode = 0, $extradata = null ) {
-               throw new UsageException(
-                       $description,
-                       $this->encodeParamName( $errorCode ),
-                       $httpRespCode,
-                       $extradata
-               );
+       public function addMessagesFromStatus( StatusValue $status, $types = [ 'warning', 'error' ] ) {
+               $this->getErrorFormatter()->addMessagesFromStatus( $this->getModulePath(), $status, $types );
+       }
+
+       /**
+        * Abort execution with an error
+        *
+        * If $msg is not an ApiMessage, the message code will be derived from the
+        * message key by stripping any "apiwarn-" or "apierror-" prefix.
+        *
+        * @since 1.29
+        * @param string|array|Message $msg See ApiErrorFormatter::addError()
+        * @param string|null $code See ApiErrorFormatter::addError()
+        * @param array|null $data See ApiErrorFormatter::addError()
+        * @param int|null $httpCode HTTP error code to use
+        * @throws ApiUsageException always
+        */
+       public function dieWithError( $msg, $code = null, $data = null, $httpCode = null ) {
+               throw ApiUsageException::newWithMessage( $this, $msg, $code, $data, $httpCode );
        }
 
        /**
-        * Throw a UsageException, which will (if uncaught) call the main module's
+        * Adds a warning to the output, else dies
+        *
+        * @param ApiMessage $msg Message to show as a warning, or error message if dying
+        * @param bool $enforceLimits Whether this is an enforce (die)
+        */
+       private function warnOrDie( ApiMessage $msg, $enforceLimits = false ) {
+               if ( $enforceLimits ) {
+                       $this->dieWithError( $msg );
+               } else {
+                       $this->addWarning( $msg );
+               }
+       }
+
+       /**
+        * Throw an ApiUsageException, which will (if uncaught) call the main module's
         * error handler and die with an error message including block info.
         *
         * @since 1.27
-        * @param Block $block The block used to generate the UsageException
-        * @throws UsageException always
+        * @param Block $block The block used to generate the ApiUsageException
+        * @throws ApiUsageException always
         */
        public function dieBlocked( Block $block ) {
                // Die using the appropriate message depending on block type
                if ( $block->getType() == Block::TYPE_AUTO ) {
-                       $this->dieUsage(
-                               'Your IP address has been blocked automatically, because it was used by a blocked user',
+                       $this->dieWithError(
+                               'apierror-autoblocked',
                                'autoblocked',
-                               0,
                                [ 'blockinfo' => ApiQueryUserInfo::getBlockInfo( $block ) ]
                        );
                } else {
-                       $this->dieUsage(
-                               'You have been blocked from editing',
+                       $this->dieWithError(
+                               'apierror-blocked',
                                'blocked',
-                               0,
                                [ 'blockinfo' => ApiQueryUserInfo::getBlockInfo( $block ) ]
                        );
                }
        }
 
        /**
-        * Get error (as code, string) from a Status object.
+        * Throw an ApiUsageException based on the Status object.
         *
-        * @since 1.23
-        * @param Status $status
-        * @param array|null &$extraData Set if extra data from IApiMessage is available (since 1.27)
-        * @return array Array of code and error string
-        * @throws MWException
+        * @since 1.22
+        * @since 1.29 Accepts a StatusValue
+        * @param StatusValue $status
+        * @throws ApiUsageException always
         */
-       public function getErrorFromStatus( $status, &$extraData = null ) {
+       public function dieStatus( StatusValue $status ) {
                if ( $status->isGood() ) {
                        throw new MWException( 'Successful status passed to ApiBase::dieStatus' );
                }
 
-               $errors = $status->getErrorsByType( 'error' );
-               if ( !$errors ) {
-                       // No errors? Assume the warnings should be treated as errors
-                       $errors = $status->getErrorsByType( 'warning' );
-               }
-               if ( !$errors ) {
-                       // Still no errors? Punt
-                       $errors = [ [ 'message' => 'unknownerror-nocode', 'params' => [] ] ];
-               }
-
-               // Cannot use dieUsageMsg() because extensions might return custom
-               // error messages.
-               if ( $errors[0]['message'] instanceof Message ) {
-                       $msg = $errors[0]['message'];
-                       if ( $msg instanceof IApiMessage ) {
-                               $extraData = $msg->getApiData();
-                               $code = $msg->getApiCode();
-                       } else {
-                               $code = $msg->getKey();
-                       }
-               } else {
-                       $code = $errors[0]['message'];
-                       $msg = wfMessage( $code, $errors[0]['params'] );
-               }
-               if ( isset( ApiBase::$messageMap[$code] ) ) {
-                       // Translate message to code, for backwards compatibility
-                       $code = ApiBase::$messageMap[$code]['code'];
-               }
-
-               return [ $code, $msg->inLanguage( 'en' )->useDatabase( false )->plain() ];
+               throw new ApiUsageException( $this, $status );
        }
 
-       /**
-        * Throw a UsageException based on the errors in the Status object.
-        *
-        * @since 1.22
-        * @param Status $status
-        * @throws UsageException always
-        */
-       public function dieStatus( $status ) {
-               $extraData = null;
-               list( $code, $msg ) = $this->getErrorFromStatus( $status, $extraData );
-               $this->dieUsage( $msg, $code, 0, $extraData );
-       }
-
-       // @codingStandardsIgnoreStart Allow long lines. Cannot split these.
-       /**
-        * Array that maps message keys to error messages. $1 and friends are replaced.
-        */
-       public static $messageMap = [
-               // This one MUST be present, or dieUsageMsg() will recurse infinitely
-               'unknownerror' => [ 'code' => 'unknownerror', 'info' => "Unknown error: \"\$1\"" ],
-               'unknownerror-nocode' => [ 'code' => 'unknownerror', 'info' => 'Unknown error' ],
-
-               // Messages from Title::getUserPermissionsErrors()
-               'ns-specialprotected' => [
-                       'code' => 'unsupportednamespace',
-                       'info' => "Pages in the Special namespace can't be edited"
-               ],
-               'protectedinterface' => [
-                       'code' => 'protectednamespace-interface',
-                       'info' => "You're not allowed to edit interface messages"
-               ],
-               'namespaceprotected' => [
-                       'code' => 'protectednamespace',
-                       'info' => "You're not allowed to edit pages in the \"\$1\" namespace"
-               ],
-               'customcssprotected' => [
-                       'code' => 'customcssprotected',
-                       'info' => "You're not allowed to edit custom CSS pages"
-               ],
-               'customjsprotected' => [
-                       'code' => 'customjsprotected',
-                       'info' => "You're not allowed to edit custom JavaScript pages"
-               ],
-               'cascadeprotected' => [
-                       'code' => 'cascadeprotected',
-                       'info' => "The page you're trying to edit is protected because it's included in a cascade-protected page"
-               ],
-               'protectedpagetext' => [
-                       'code' => 'protectedpage',
-                       'info' => "The \"\$1\" right is required to edit this page"
-               ],
-               'protect-cantedit' => [
-                       'code' => 'cantedit',
-                       'info' => "You can't protect this page because you can't edit it"
-               ],
-               'deleteprotected' => [
-                       'code' => 'cantedit',
-                       'info' => "You can't delete this page because it has been protected"
-               ],
-               'badaccess-group0' => [
-                       'code' => 'permissiondenied',
-                       'info' => 'Permission denied'
-               ], // Generic permission denied message
-               'badaccess-groups' => [
-                       'code' => 'permissiondenied',
-                       'info' => 'Permission denied'
-               ],
-               'titleprotected' => [
-                       'code' => 'protectedtitle',
-                       'info' => 'This title has been protected from creation'
-               ],
-               'nocreate-loggedin' => [
-                       'code' => 'cantcreate',
-                       'info' => "You don't have permission to create new pages"
-               ],
-               'nocreatetext' => [
-                       'code' => 'cantcreate-anon',
-                       'info' => "Anonymous users can't create new pages"
-               ],
-               'movenologintext' => [
-                       'code' => 'cantmove-anon',
-                       'info' => "Anonymous users can't move pages"
-               ],
-               'movenotallowed' => [
-                       'code' => 'cantmove',
-                       'info' => "You don't have permission to move pages"
-               ],
-               'confirmedittext' => [
-                       'code' => 'confirmemail',
-                       'info' => 'You must confirm your email address before you can edit'
-               ],
-               'blockedtext' => [
-                       'code' => 'blocked',
-                       'info' => 'You have been blocked from editing'
-               ],
-               'autoblockedtext' => [
-                       'code' => 'autoblocked',
-                       'info' => 'Your IP address has been blocked automatically, because it was used by a blocked user'
-               ],
-
-               // Miscellaneous interface messages
-               'actionthrottledtext' => [
-                       'code' => 'ratelimited',
-                       'info' => "You've exceeded your rate limit. Please wait some time and try again"
-               ],
-               'alreadyrolled' => [
-                       'code' => 'alreadyrolled',
-                       'info' => 'The page you tried to rollback was already rolled back'
-               ],
-               'cantrollback' => [
-                       'code' => 'onlyauthor',
-                       'info' => 'The page you tried to rollback only has one author'
-               ],
-               'readonlytext' => [
-                       'code' => 'readonly',
-                       'info' => 'The wiki is currently in read-only mode'
-               ],
-               'sessionfailure' => [
-                       'code' => 'badtoken',
-                       'info' => 'Invalid token' ],
-               'cannotdelete' => [
-                       'code' => 'cantdelete',
-                       'info' => "Couldn't delete \"\$1\". Maybe it was deleted already by someone else"
-               ],
-               'notanarticle' => [
-                       'code' => 'missingtitle',
-                       'info' => "The page you requested doesn't exist"
-               ],
-               'selfmove' => [ 'code' => 'selfmove', 'info' => "Can't move a page to itself"
-               ],
-               'immobile_namespace' => [
-                       'code' => 'immobilenamespace',
-                       'info' => 'You tried to move pages from or to a namespace that is protected from moving'
-               ],
-               'articleexists' => [
-                       'code' => 'articleexists',
-                       'info' => 'The destination article already exists and is not a redirect to the source article'
-               ],
-               'protectedpage' => [
-                       'code' => 'protectedpage',
-                       'info' => "You don't have permission to perform this move"
-               ],
-               'hookaborted' => [
-                       'code' => 'hookaborted',
-                       'info' => 'The modification you tried to make was aborted by an extension hook'
-               ],
-               'cantmove-titleprotected' => [
-                       'code' => 'protectedtitle',
-                       'info' => 'The destination article has been protected from creation'
-               ],
-               'imagenocrossnamespace' => [
-                       'code' => 'nonfilenamespace',
-                       'info' => "Can't move a file to a non-file namespace"
-               ],
-               'imagetypemismatch' => [
-                       'code' => 'filetypemismatch',
-                       'info' => "The new file extension doesn't match its type"
-               ],
-               // 'badarticleerror' => shouldn't happen
-               // 'badtitletext' => shouldn't happen
-               'ip_range_invalid' => [ 'code' => 'invalidrange', 'info' => 'Invalid IP range' ],
-               'range_block_disabled' => [
-                       'code' => 'rangedisabled',
-                       'info' => 'Blocking IP ranges has been disabled'
-               ],
-               'nosuchusershort' => [
-                       'code' => 'nosuchuser',
-                       'info' => "The user you specified doesn't exist"
-               ],
-               'badipaddress' => [ 'code' => 'invalidip', 'info' => 'Invalid IP address specified' ],
-               'ipb_expiry_invalid' => [ 'code' => 'invalidexpiry', 'info' => 'Invalid expiry time' ],
-               'ipb_already_blocked' => [
-                       'code' => 'alreadyblocked',
-                       'info' => 'The user you tried to block was already blocked'
-               ],
-               'ipb_blocked_as_range' => [
-                       'code' => 'blockedasrange',
-                       'info' => "IP address \"\$1\" was blocked as part of range \"\$2\". You can't unblock the IP individually, but you can unblock the range as a whole."
-               ],
-               'ipb_cant_unblock' => [
-                       'code' => 'cantunblock',
-                       'info' => 'The block you specified was not found. It may have been unblocked already'
-               ],
-               'mailnologin' => [
-                       'code' => 'cantsend',
-                       'info' => 'You are not logged in, you do not have a confirmed email address, or you are not allowed to send email to other users, so you cannot send email'
-               ],
-               'ipbblocked' => [
-                       'code' => 'ipbblocked',
-                       'info' => 'You cannot block or unblock users while you are yourself blocked'
-               ],
-               'ipbnounblockself' => [
-                       'code' => 'ipbnounblockself',
-                       'info' => 'You are not allowed to unblock yourself'
-               ],
-               'usermaildisabled' => [
-                       'code' => 'usermaildisabled',
-                       'info' => 'User email has been disabled'
-               ],
-               'blockedemailuser' => [
-                       'code' => 'blockedfrommail',
-                       'info' => 'You have been blocked from sending email'
-               ],
-               'notarget' => [
-                       'code' => 'notarget',
-                       'info' => 'You have not specified a valid target for this action'
-               ],
-               'noemail' => [
-                       'code' => 'noemail',
-                       'info' => 'The user has not specified a valid email address, or has chosen not to receive email from other users'
-               ],
-               'rcpatroldisabled' => [
-                       'code' => 'patroldisabled',
-                       'info' => 'Patrolling is disabled on this wiki'
-               ],
-               'markedaspatrollederror-noautopatrol' => [
-                       'code' => 'noautopatrol',
-                       'info' => "You don't have permission to patrol your own changes"
-               ],
-               'delete-toobig' => [
-                       'code' => 'bigdelete',
-                       'info' => "You can't delete this page because it has more than \$1 revisions"
-               ],
-               'movenotallowedfile' => [
-                       'code' => 'cantmovefile',
-                       'info' => "You don't have permission to move files"
-               ],
-               'userrights-no-interwiki' => [
-                       'code' => 'nointerwikiuserrights',
-                       'info' => "You don't have permission to change user rights on other wikis"
-               ],
-               'userrights-nodatabase' => [
-                       'code' => 'nosuchdatabase',
-                       'info' => "Database \"\$1\" does not exist or is not local"
-               ],
-               'nouserspecified' => [ 'code' => 'invaliduser', 'info' => "Invalid username \"\$1\"" ],
-               'noname' => [ 'code' => 'invaliduser', 'info' => "Invalid username \"\$1\"" ],
-               'summaryrequired' => [ 'code' => 'summaryrequired', 'info' => 'Summary required' ],
-               'import-rootpage-invalid' => [
-                       'code' => 'import-rootpage-invalid',
-                       'info' => 'Root page is an invalid title'
-               ],
-               'import-rootpage-nosubpage' => [
-                       'code' => 'import-rootpage-nosubpage',
-                       'info' => 'Namespace "$1" of the root page does not allow subpages'
-               ],
-
-               // API-specific messages
-               'readrequired' => [
-                       'code' => 'readapidenied',
-                       'info' => 'You need read permission to use this module'
-               ],
-               'writedisabled' => [
-                       'code' => 'noapiwrite',
-                       'info' => "Editing of this wiki through the API is disabled. Make sure the \$wgEnableWriteAPI=true; statement is included in the wiki's LocalSettings.php file"
-               ],
-               'writerequired' => [
-                       'code' => 'writeapidenied',
-                       'info' => "You're not allowed to edit this wiki through the API"
-               ],
-               'missingparam' => [ 'code' => 'no$1', 'info' => "The \$1 parameter must be set" ],
-               'invalidtitle' => [ 'code' => 'invalidtitle', 'info' => "Bad title \"\$1\"" ],
-               'nosuchpageid' => [ 'code' => 'nosuchpageid', 'info' => "There is no page with ID \$1" ],
-               'nosuchrevid' => [ 'code' => 'nosuchrevid', 'info' => "There is no revision with ID \$1" ],
-               'nosuchuser' => [ 'code' => 'nosuchuser', 'info' => "User \"\$1\" doesn't exist" ],
-               'invaliduser' => [ 'code' => 'invaliduser', 'info' => "Invalid username \"\$1\"" ],
-               'invalidexpiry' => [ 'code' => 'invalidexpiry', 'info' => "Invalid expiry time \"\$1\"" ],
-               'pastexpiry' => [ 'code' => 'pastexpiry', 'info' => "Expiry time \"\$1\" is in the past" ],
-               'create-titleexists' => [
-                       'code' => 'create-titleexists',
-                       'info' => "Existing titles can't be protected with 'create'"
-               ],
-               'missingtitle-createonly' => [
-                       'code' => 'missingtitle-createonly',
-                       'info' => "Missing titles can only be protected with 'create'"
-               ],
-               'cantblock' => [ 'code' => 'cantblock',
-                       'info' => "You don't have permission to block users"
-               ],
-               'canthide' => [
-                       'code' => 'canthide',
-                       'info' => "You don't have permission to hide user names from the block log"
-               ],
-               'cantblock-email' => [
-                       'code' => 'cantblock-email',
-                       'info' => "You don't have permission to block users from sending email through the wiki"
-               ],
-               'unblock-notarget' => [
-                       'code' => 'notarget',
-                       'info' => 'Either the id or the user parameter must be set'
-               ],
-               'unblock-idanduser' => [
-                       'code' => 'idanduser',
-                       'info' => "The id and user parameters can't be used together"
-               ],
-               'cantunblock' => [
-                       'code' => 'permissiondenied',
-                       'info' => "You don't have permission to unblock users"
-               ],
-               'cannotundelete' => [
-                       'code' => 'cantundelete',
-                       'info' => "Couldn't undelete: the requested revisions may not exist, or may have been undeleted already"
-               ],
-               'permdenied-undelete' => [
-                       'code' => 'permissiondenied',
-                       'info' => "You don't have permission to restore deleted revisions"
-               ],
-               'createonly-exists' => [
-                       'code' => 'articleexists',
-                       'info' => 'The article you tried to create has been created already'
-               ],
-               'nocreate-missing' => [
-                       'code' => 'missingtitle',
-                       'info' => "The article you tried to edit doesn't exist"
-               ],
-               'cantchangecontentmodel' => [
-                       'code' => 'cantchangecontentmodel',
-                       'info' => "You don't have permission to change the content model of a page"
-               ],
-               'nosuchrcid' => [
-                       'code' => 'nosuchrcid',
-                       'info' => "There is no change with rcid \"\$1\""
-               ],
-               'nosuchlogid' => [
-                       'code' => 'nosuchlogid',
-                       'info' => "There is no log entry with ID \"\$1\""
-               ],
-               'protect-invalidaction' => [
-                       'code' => 'protect-invalidaction',
-                       'info' => "Invalid protection type \"\$1\""
-               ],
-               'protect-invalidlevel' => [
-                       'code' => 'protect-invalidlevel',
-                       'info' => "Invalid protection level \"\$1\""
-               ],
-               'toofewexpiries' => [
-                       'code' => 'toofewexpiries',
-                       'info' => "\$1 expiry timestamps were provided where \$2 were needed"
-               ],
-               'cantimport' => [
-                       'code' => 'cantimport',
-                       'info' => "You don't have permission to import pages"
-               ],
-               'cantimport-upload' => [
-                       'code' => 'cantimport-upload',
-                       'info' => "You don't have permission to import uploaded pages"
-               ],
-               'importnofile' => [ 'code' => 'nofile', 'info' => "You didn't upload a file" ],
-               'importuploaderrorsize' => [
-                       'code' => 'filetoobig',
-                       'info' => 'The file you uploaded is bigger than the maximum upload size'
-               ],
-               'importuploaderrorpartial' => [
-                       'code' => 'partialupload',
-                       'info' => 'The file was only partially uploaded'
-               ],
-               'importuploaderrortemp' => [
-                       'code' => 'notempdir',
-                       'info' => 'The temporary upload directory is missing'
-               ],
-               'importcantopen' => [
-                       'code' => 'cantopenfile',
-                       'info' => "Couldn't open the uploaded file"
-               ],
-               'import-noarticle' => [
-                       'code' => 'badinterwiki',
-                       'info' => 'Invalid interwiki title specified'
-               ],
-               'importbadinterwiki' => [
-                       'code' => 'badinterwiki',
-                       'info' => 'Invalid interwiki title specified'
-               ],
-               'import-unknownerror' => [
-                       'code' => 'import-unknownerror',
-                       'info' => "Unknown error on import: \"\$1\""
-               ],
-               'cantoverwrite-sharedfile' => [
-                       'code' => 'cantoverwrite-sharedfile',
-                       'info' => 'The target file exists on a shared repository and you do not have permission to override it'
-               ],
-               'sharedfile-exists' => [
-                       'code' => 'fileexists-sharedrepo-perm',
-                       'info' => 'The target file exists on a shared repository. Use the ignorewarnings parameter to override it.'
-               ],
-               'mustbeposted' => [
-                       'code' => 'mustbeposted',
-                       'info' => "The \$1 module requires a POST request"
-               ],
-               'show' => [
-                       'code' => 'show',
-                       'info' => 'Incorrect parameter - mutually exclusive values may not be supplied'
-               ],
-               'specialpage-cantexecute' => [
-                       'code' => 'specialpage-cantexecute',
-                       'info' => "You don't have permission to view the results of this special page"
-               ],
-               'invalidoldimage' => [
-                       'code' => 'invalidoldimage',
-                       'info' => 'The oldimage parameter has invalid format'
-               ],
-               'nodeleteablefile' => [
-                       'code' => 'nodeleteablefile',
-                       'info' => 'No such old version of the file'
-               ],
-               'fileexists-forbidden' => [
-                       'code' => 'fileexists-forbidden',
-                       'info' => 'A file with name "$1" already exists, and cannot be overwritten.'
-               ],
-               'fileexists-shared-forbidden' => [
-                       'code' => 'fileexists-shared-forbidden',
-                       'info' => 'A file with name "$1" already exists in the shared file repository, and cannot be overwritten.'
-               ],
-               'filerevert-badversion' => [
-                       'code' => 'filerevert-badversion',
-                       'info' => 'There is no previous local version of this file with the provided timestamp.'
-               ],
-
-               // ApiEditPage messages
-               'noimageredirect-anon' => [
-                       'code' => 'noimageredirect-anon',
-                       'info' => "Anonymous users can't create image redirects"
-               ],
-               'noimageredirect-logged' => [
-                       'code' => 'noimageredirect',
-                       'info' => "You don't have permission to create image redirects"
-               ],
-               'spamdetected' => [
-                       'code' => 'spamdetected',
-                       'info' => "Your edit was refused because it contained a spam fragment: \"\$1\""
-               ],
-               'contenttoobig' => [
-                       'code' => 'contenttoobig',
-                       'info' => "The content you supplied exceeds the article size limit of \$1 kilobytes"
-               ],
-               'noedit-anon' => [ 'code' => 'noedit-anon', 'info' => "Anonymous users can't edit pages" ],
-               'noedit' => [ 'code' => 'noedit', 'info' => "You don't have permission to edit pages" ],
-               'wasdeleted' => [
-                       'code' => 'pagedeleted',
-                       'info' => 'The page has been deleted since you fetched its timestamp'
-               ],
-               'blankpage' => [
-                       'code' => 'emptypage',
-                       'info' => 'Creating new, empty pages is not allowed'
-               ],
-               'editconflict' => [ 'code' => 'editconflict', 'info' => 'Edit conflict detected' ],
-               'hashcheckfailed' => [ 'code' => 'badmd5', 'info' => 'The supplied MD5 hash was incorrect' ],
-               'missingtext' => [
-                       'code' => 'notext',
-                       'info' => 'One of the text, appendtext, prependtext and undo parameters must be set'
-               ],
-               'emptynewsection' => [
-                       'code' => 'emptynewsection',
-                       'info' => 'Creating empty new sections is not possible.'
-               ],
-               'revwrongpage' => [
-                       'code' => 'revwrongpage',
-                       'info' => "r\$1 is not a revision of \"\$2\""
-               ],
-               'undo-failure' => [
-                       'code' => 'undofailure',
-                       'info' => 'Undo failed due to conflicting intermediate edits'
-               ],
-               'content-not-allowed-here' => [
-                       'code' => 'contentnotallowedhere',
-                       'info' => 'Content model "$1" is not allowed at title "$2"'
-               ],
-
-               // Messages from WikiPage::doEit(]
-               'edit-hook-aborted' => [
-                       'code' => 'edit-hook-aborted',
-                       'info' => 'Your edit was aborted by an ArticleSave hook'
-               ],
-               'edit-gone-missing' => [
-                       'code' => 'edit-gone-missing',
-                       'info' => "The page you tried to edit doesn't seem to exist anymore"
-               ],
-               'edit-conflict' => [ 'code' => 'editconflict', 'info' => 'Edit conflict detected' ],
-               'edit-already-exists' => [
-                       'code' => 'edit-already-exists',
-                       'info' => 'It seems the page you tried to create already exist'
-               ],
-
-               // uploadMsgs
-               'invalid-file-key' => [ 'code' => 'invalid-file-key', 'info' => 'Not a valid file key' ],
-               'nouploadmodule' => [ 'code' => 'nouploadmodule', 'info' => 'No upload module set' ],
-               'uploaddisabled' => [
-                       'code' => 'uploaddisabled',
-                       'info' => 'Uploads are not enabled. Make sure $wgEnableUploads is set to true in LocalSettings.php and the PHP ini setting file_uploads is true'
-               ],
-               'copyuploaddisabled' => [
-                       'code' => 'copyuploaddisabled',
-                       'info' => 'Uploads by URL is not enabled. Make sure $wgAllowCopyUploads is set to true in LocalSettings.php.'
-               ],
-               'copyuploadbaddomain' => [
-                       'code' => 'copyuploadbaddomain',
-                       'info' => 'Uploads by URL are not allowed from this domain.'
-               ],
-               'copyuploadbadurl' => [
-                       'code' => 'copyuploadbadurl',
-                       'info' => 'Upload not allowed from this URL.'
-               ],
-
-               'filename-tooshort' => [
-                       'code' => 'filename-tooshort',
-                       'info' => 'The filename is too short'
-               ],
-               'filename-toolong' => [ 'code' => 'filename-toolong', 'info' => 'The filename is too long' ],
-               'illegal-filename' => [
-                       'code' => 'illegal-filename',
-                       'info' => 'The filename is not allowed'
-               ],
-               'filetype-missing' => [
-                       'code' => 'filetype-missing',
-                       'info' => 'The file is missing an extension'
-               ],
-
-               'mustbeloggedin' => [ 'code' => 'mustbeloggedin', 'info' => 'You must be logged in to $1.' ]
-       ];
-       // @codingStandardsIgnoreEnd
-
        /**
         * Helper function for readonly errors
         *
-        * @throws UsageException always
+        * @throws ApiUsageException always
         */
        public function dieReadOnly() {
-               $parsed = $this->parseMsg( [ 'readonlytext' ] );
-               $this->dieUsage( $parsed['info'], $parsed['code'], /* http error */ 0,
-                       [ 'readonlyreason' => wfReadOnlyReason() ] );
+               $this->dieWithError(
+                       'apierror-readonly',
+                       'readonly',
+                       [ 'readonlyreason' => wfReadOnlyReason() ]
+               );
        }
 
        /**
-        * Output the error message related to a certain array
-        * @param array|string|MessageSpecifier $error Element of a getUserPermissionsErrors()-style array
-        * @throws UsageException always
+        * Helper function for permission-denied errors
+        * @since 1.29
+        * @param string|string[] $rights
+        * @param User|null $user
+        * @throws ApiUsageException if the user doesn't have any of the rights.
+        *  The error message is based on $rights[0].
         */
-       public function dieUsageMsg( $error ) {
-               # most of the time we send a 1 element, so we might as well send it as
-               # a string and make this an array here.
-               if ( is_string( $error ) ) {
-                       $error = [ $error ];
+       public function checkUserRightsAny( $rights, $user = null ) {
+               if ( !$user ) {
+                       $user = $this->getUser();
+               }
+               $rights = (array)$rights;
+               if ( !call_user_func_array( [ $user, 'isAllowedAny' ], $rights ) ) {
+                       $this->dieWithError( [ 'apierror-permissiondenied', $this->msg( "action-{$rights[0]}" ) ] );
                }
-               $parsed = $this->parseMsg( $error );
-               $extraData = isset( $parsed['data'] ) ? $parsed['data'] : null;
-               $this->dieUsage( $parsed['info'], $parsed['code'], 0, $extraData );
        }
 
        /**
-        * Will only set a warning instead of failing if the global $wgDebugAPI
-        * is set to true. Otherwise behaves exactly as dieUsageMsg().
-        * @param array|string|MessageSpecifier $error Element of a getUserPermissionsErrors()-style array
-        * @throws UsageException
-        * @since 1.21
+        * Helper function for permission-denied errors
+        * @since 1.29
+        * @param Title $title
+        * @param string|string[] $actions
+        * @param User|null $user
+        * @throws ApiUsageException if the user doesn't have all of the rights.
         */
-       public function dieUsageMsgOrDebug( $error ) {
-               if ( $this->getConfig()->get( 'DebugAPI' ) !== true ) {
-                       $this->dieUsageMsg( $error );
+       public function checkTitleUserPermissions( Title $title, $actions, $user = null ) {
+               if ( !$user ) {
+                       $user = $this->getUser();
                }
 
-               if ( is_string( $error ) ) {
-                       $error = [ $error ];
+               $errors = [];
+               foreach ( (array)$actions as $action ) {
+                       $errors = array_merge( $errors, $title->getUserPermissionsErrors( $action, $user ) );
+               }
+               if ( $errors ) {
+                       $this->dieStatus( $this->errorArrayToStatus( $errors, $user ) );
                }
-               $parsed = $this->parseMsg( $error );
-               $this->setWarning( '$wgDebugAPI: ' . $parsed['code'] . ' - ' . $parsed['info'] );
        }
 
        /**
-        * Die with the $prefix.'badcontinue' error. This call is common enough to
-        * make it into the base method.
-        * @param bool $condition Will only die if this value is true
-        * @throws UsageException
-        * @since 1.21
+        * Will only set a warning instead of failing if the global $wgDebugAPI
+        * is set to true. Otherwise behaves exactly as self::dieWithError().
+        *
+        * @since 1.29
+        * @param string|array|Message $msg
+        * @param string|null $code
+        * @param array|null $data
+        * @param int|null $httpCode
+        * @throws ApiUsageException
         */
-       protected function dieContinueUsageIf( $condition ) {
-               if ( $condition ) {
-                       $this->dieUsage(
-                               'Invalid continue param. You should pass the original value returned by the previous query',
-                               'badcontinue' );
+       public function dieWithErrorOrDebug( $msg, $code = null, $data = null, $httpCode = null ) {
+               if ( $this->getConfig()->get( 'DebugAPI' ) !== true ) {
+                       $this->dieWithError( $msg, $code, $data, $httpCode );
+               } else {
+                       $this->addWarning( $msg, $code, $data );
                }
        }
 
        /**
-        * Return the error message related to a certain array
-        * @param array|string|MessageSpecifier $error Element of a getUserPermissionsErrors()-style array
-        * @return [ 'code' => code, 'info' => info ]
+        * Die with the 'badcontinue' error.
+        *
+        * This call is common enough to make it into the base method.
+        *
+        * @param bool $condition Will only die if this value is true
+        * @throws ApiUsageException
+        * @since 1.21
         */
-       public function parseMsg( $error ) {
-               // Check whether someone passed the whole array, instead of one element as
-               // documented. This breaks if it's actually an array of fallback keys, but
-               // that's long-standing misbehavior introduced in r87627 to incorrectly
-               // fix T30797.
-               if ( is_array( $error ) ) {
-                       $first = reset( $error );
-                       if ( is_array( $first ) ) {
-                               wfDebug( __METHOD__ . ' was passed an array of arrays. ' . wfGetAllCallers( 5 ) );
-                               $error = $first;
-                       }
-               }
-
-               $msg = Message::newFromSpecifier( $error );
-
-               if ( $msg instanceof IApiMessage ) {
-                       return [
-                               'code' => $msg->getApiCode(),
-                               'info' => $msg->inLanguage( 'en' )->useDatabase( false )->text(),
-                               'data' => $msg->getApiData()
-                       ];
-               }
-
-               $key = $msg->getKey();
-               if ( isset( self::$messageMap[$key] ) ) {
-                       $params = $msg->getParams();
-                       return [
-                               'code' => wfMsgReplaceArgs( self::$messageMap[$key]['code'], $params ),
-                               'info' => wfMsgReplaceArgs( self::$messageMap[$key]['info'], $params )
-                       ];
+       protected function dieContinueUsageIf( $condition ) {
+               if ( $condition ) {
+                       $this->dieWithError( 'apierror-badcontinue' );
                }
-
-               // If the key isn't present, throw an "unknown error"
-               return $this->parseMsg( [ 'unknownerror', $key ] );
        }
 
        /**
@@ -2323,6 +1878,7 @@ abstract class ApiBase extends ContextSource {
        /**
         * Write logging information for API features to a debug log, for usage
         * analysis.
+        * @note Consider using $this->addDeprecation() instead to both warn and log.
         * @param string $feature Feature being used.
         */
        public function logFeatureUsage( $feature ) {
@@ -2790,6 +2346,300 @@ abstract class ApiBase extends ContextSource {
                }
        }
 
+       /**
+        * @deprecated since 1.29, use ApiBase::addWarning() instead
+        * @param string $warning Warning message
+        */
+       public function setWarning( $warning ) {
+               $msg = new ApiRawMessage( $warning, 'warning' );
+               $this->getErrorFormatter()->addWarning( $this->getModulePath(), $msg );
+       }
+
+       /**
+        * Throw an ApiUsageException, which will (if uncaught) call the main module's
+        * error handler and die with an error message.
+        *
+        * @deprecated since 1.29, use self::dieWithError() instead
+        * @param string $description One-line human-readable description of the
+        *   error condition, e.g., "The API requires a valid action parameter"
+        * @param string $errorCode Brief, arbitrary, stable string to allow easy
+        *   automated identification of the error, e.g., 'unknown_action'
+        * @param int $httpRespCode HTTP response code
+        * @param array|null $extradata Data to add to the "<error>" element; array in ApiResult format
+        * @throws ApiUsageException always
+        */
+       public function dieUsage( $description, $errorCode, $httpRespCode = 0, $extradata = null ) {
+               $this->dieWithError(
+                       new RawMessage( '$1', [ $description ] ),
+                       $errorCode,
+                       $extradata,
+                       $httpRespCode
+               );
+       }
+
+       /**
+        * Get error (as code, string) from a Status object.
+        *
+        * @since 1.23
+        * @deprecated since 1.29, use ApiErrorFormatter::arrayFromStatus instead
+        * @param Status $status
+        * @param array|null &$extraData Set if extra data from IApiMessage is available (since 1.27)
+        * @return array Array of code and error string
+        * @throws MWException
+        */
+       public function getErrorFromStatus( $status, &$extraData = null ) {
+               if ( $status->isGood() ) {
+                       throw new MWException( 'Successful status passed to ApiBase::dieStatus' );
+               }
+
+               $errors = $status->getErrorsByType( 'error' );
+               if ( !$errors ) {
+                       // No errors? Assume the warnings should be treated as errors
+                       $errors = $status->getErrorsByType( 'warning' );
+               }
+               if ( !$errors ) {
+                       // Still no errors? Punt
+                       $errors = [ [ 'message' => 'unknownerror-nocode', 'params' => [] ] ];
+               }
+
+               if ( $errors[0]['message'] instanceof MessageSpecifier ) {
+                       $msg = $errors[0]['message'];
+               } else {
+                       $msg = new Message( $errors[0]['message'], $errors[0]['params'] );
+               }
+               if ( !$msg instanceof IApiMessage ) {
+                       $key = $msg->getKey();
+                       $params = $msg->getParams();
+                       array_unshift( $params, isset( self::$messageMap[$key] ) ? self::$messageMap[$key] : $key );
+                       $msg = ApiMessage::create( $params );
+               }
+
+               return [
+                       $msg->getApiCode(),
+                       ApiErrorFormatter::stripMarkup( $msg->inLanguage( 'en' )->useDatabase( false )->text() )
+               ];
+       }
+
+       /**
+        * @deprecated since 1.29. Prior to 1.29, this was a public mapping from
+        *  arbitrary strings (often message keys used elsewhere in MediaWiki) to
+        *  API codes and message texts, and a few interfaces required poking
+        *  something in here. Now we're repurposing it to map those same strings
+        *  to i18n messages, and declaring that any interface that requires poking
+        *  at this is broken and needs replacing ASAP.
+        */
+       private static $messageMap = [
+               'unknownerror' => 'apierror-unknownerror',
+               'unknownerror-nocode' => 'apierror-unknownerror-nocode',
+               'ns-specialprotected' => 'ns-specialprotected',
+               'protectedinterface' => 'protectedinterface',
+               'namespaceprotected' => 'namespaceprotected',
+               'customcssprotected' => 'customcssprotected',
+               'customjsprotected' => 'customjsprotected',
+               'cascadeprotected' => 'cascadeprotected',
+               'protectedpagetext' => 'protectedpagetext',
+               'protect-cantedit' => 'protect-cantedit',
+               'deleteprotected' => 'deleteprotected',
+               'badaccess-group0' => 'badaccess-group0',
+               'badaccess-groups' => 'badaccess-groups',
+               'titleprotected' => 'titleprotected',
+               'nocreate-loggedin' => 'nocreate-loggedin',
+               'nocreatetext' => 'nocreatetext',
+               'movenologintext' => 'movenologintext',
+               'movenotallowed' => 'movenotallowed',
+               'confirmedittext' => 'confirmedittext',
+               'blockedtext' => 'apierror-blocked',
+               'autoblockedtext' => 'apierror-autoblocked',
+               'actionthrottledtext' => 'apierror-ratelimited',
+               'alreadyrolled' => 'alreadyrolled',
+               'cantrollback' => 'cantrollback',
+               'readonlytext' => 'readonlytext',
+               'sessionfailure' => 'sessionfailure',
+               'cannotdelete' => 'cannotdelete',
+               'notanarticle' => 'apierror-missingtitle',
+               'selfmove' => 'selfmove',
+               'immobile_namespace' => 'apierror-immobilenamespace',
+               'articleexists' => 'articleexists',
+               'hookaborted' => 'hookaborted',
+               'cantmove-titleprotected' => 'cantmove-titleprotected',
+               'imagenocrossnamespace' => 'imagenocrossnamespace',
+               'imagetypemismatch' => 'imagetypemismatch',
+               'ip_range_invalid' => 'ip_range_invalid',
+               'range_block_disabled' => 'range_block_disabled',
+               'nosuchusershort' => 'nosuchusershort',
+               'badipaddress' => 'badipaddress',
+               'ipb_expiry_invalid' => 'ipb_expiry_invalid',
+               'ipb_already_blocked' => 'ipb_already_blocked',
+               'ipb_blocked_as_range' => 'ipb_blocked_as_range',
+               'ipb_cant_unblock' => 'ipb_cant_unblock',
+               'mailnologin' => 'apierror-cantsend',
+               'ipbblocked' => 'ipbblocked',
+               'ipbnounblockself' => 'ipbnounblockself',
+               'usermaildisabled' => 'usermaildisabled',
+               'blockedemailuser' => 'apierror-blockedfrommail',
+               'notarget' => 'apierror-notarget',
+               'noemail' => 'noemail',
+               'rcpatroldisabled' => 'rcpatroldisabled',
+               'markedaspatrollederror-noautopatrol' => 'markedaspatrollederror-noautopatrol',
+               'delete-toobig' => 'delete-toobig',
+               'movenotallowedfile' => 'movenotallowedfile',
+               'userrights-no-interwiki' => 'userrights-no-interwiki',
+               'userrights-nodatabase' => 'userrights-nodatabase',
+               'nouserspecified' => 'nouserspecified',
+               'noname' => 'noname',
+               'summaryrequired' => 'apierror-summaryrequired',
+               'import-rootpage-invalid' => 'import-rootpage-invalid',
+               'import-rootpage-nosubpage' => 'import-rootpage-nosubpage',
+               'readrequired' => 'apierror-readapidenied',
+               'writedisabled' => 'apierror-noapiwrite',
+               'writerequired' => 'apierror-writeapidenied',
+               'missingparam' => 'apierror-missingparam',
+               'invalidtitle' => 'apierror-invalidtitle',
+               'nosuchpageid' => 'apierror-nosuchpageid',
+               'nosuchrevid' => 'apierror-nosuchrevid',
+               'nosuchuser' => 'nosuchusershort',
+               'invaliduser' => 'apierror-invaliduser',
+               'invalidexpiry' => 'apierror-invalidexpiry',
+               'pastexpiry' => 'apierror-pastexpiry',
+               'create-titleexists' => 'apierror-create-titleexists',
+               'missingtitle-createonly' => 'apierror-missingtitle-createonly',
+               'cantblock' => 'apierror-cantblock',
+               'canthide' => 'apierror-canthide',
+               'cantblock-email' => 'apierror-cantblock-email',
+               'cantunblock' => 'apierror-permissiondenied-generic',
+               'cannotundelete' => 'cannotundelete',
+               'permdenied-undelete' => 'apierror-permissiondenied-generic',
+               'createonly-exists' => 'apierror-articleexists',
+               'nocreate-missing' => 'apierror-missingtitle',
+               'cantchangecontentmodel' => 'apierror-cantchangecontentmodel',
+               'nosuchrcid' => 'apierror-nosuchrcid',
+               'nosuchlogid' => 'apierror-nosuchlogid',
+               'protect-invalidaction' => 'apierror-protect-invalidaction',
+               'protect-invalidlevel' => 'apierror-protect-invalidlevel',
+               'toofewexpiries' => 'apierror-toofewexpiries',
+               'cantimport' => 'apierror-cantimport',
+               'cantimport-upload' => 'apierror-cantimport-upload',
+               'importnofile' => 'importnofile',
+               'importuploaderrorsize' => 'importuploaderrorsize',
+               'importuploaderrorpartial' => 'importuploaderrorpartial',
+               'importuploaderrortemp' => 'importuploaderrortemp',
+               'importcantopen' => 'importcantopen',
+               'import-noarticle' => 'import-noarticle',
+               'importbadinterwiki' => 'importbadinterwiki',
+               'import-unknownerror' => 'apierror-import-unknownerror',
+               'cantoverwrite-sharedfile' => 'apierror-cantoverwrite-sharedfile',
+               'sharedfile-exists' => 'apierror-fileexists-sharedrepo-perm',
+               'mustbeposted' => 'apierror-mustbeposted',
+               'show' => 'apierror-show',
+               'specialpage-cantexecute' => 'apierror-specialpage-cantexecute',
+               'invalidoldimage' => 'apierror-invalidoldimage',
+               'nodeleteablefile' => 'apierror-nodeleteablefile',
+               'fileexists-forbidden' => 'fileexists-forbidden',
+               'fileexists-shared-forbidden' => 'fileexists-shared-forbidden',
+               'filerevert-badversion' => 'filerevert-badversion',
+               'noimageredirect-anon' => 'apierror-noimageredirect-anon',
+               'noimageredirect-logged' => 'apierror-noimageredirect',
+               'spamdetected' => 'apierror-spamdetected',
+               'contenttoobig' => 'apierror-contenttoobig',
+               'noedit-anon' => 'apierror-noedit-anon',
+               'noedit' => 'apierror-noedit',
+               'wasdeleted' => 'apierror-pagedeleted',
+               'blankpage' => 'apierror-emptypage',
+               'editconflict' => 'editconflict',
+               'hashcheckfailed' => 'apierror-badmd5',
+               'missingtext' => 'apierror-notext',
+               'emptynewsection' => 'apierror-emptynewsection',
+               'revwrongpage' => 'apierror-revwrongpage',
+               'undo-failure' => 'undo-failure',
+               'content-not-allowed-here' => 'content-not-allowed-here',
+               'edit-hook-aborted' => 'edit-hook-aborted',
+               'edit-gone-missing' => 'edit-gone-missing',
+               'edit-conflict' => 'edit-conflict',
+               'edit-already-exists' => 'edit-already-exists',
+               'invalid-file-key' => 'apierror-invalid-file-key',
+               'nouploadmodule' => 'apierror-nouploadmodule',
+               'uploaddisabled' => 'uploaddisabled',
+               'copyuploaddisabled' => 'copyuploaddisabled',
+               'copyuploadbaddomain' => 'apierror-copyuploadbaddomain',
+               'copyuploadbadurl' => 'apierror-copyuploadbadurl',
+               'filename-tooshort' => 'filename-tooshort',
+               'filename-toolong' => 'filename-toolong',
+               'illegal-filename' => 'illegal-filename',
+               'filetype-missing' => 'filetype-missing',
+               'mustbeloggedin' => 'apierror-mustbeloggedin',
+       ];
+
+       /**
+        * @deprecated do not use
+        * @param array|string|MessageSpecifier $error Element of a getUserPermissionsErrors()-style array
+        * @return ApiMessage
+        */
+       private function parseMsgInternal( $error ) {
+               $msg = Message::newFromSpecifier( $error );
+               if ( !$msg instanceof IApiMessage ) {
+                       $key = $msg->getKey();
+                       if ( isset( self::$messageMap[$key] ) ) {
+                               $params = $msg->getParams();
+                               array_unshift( $params, self::$messageMap[$key] );
+                       } else {
+                               $params = [ 'apierror-unknownerror', wfEscapeWikiText( $key ) ];
+                       }
+                       $msg = ApiMessage::create( $params );
+               }
+               return $msg;
+       }
+
+       /**
+        * Return the error message related to a certain array
+        * @deprecated since 1.29
+        * @param array|string|MessageSpecifier $error Element of a getUserPermissionsErrors()-style array
+        * @return [ 'code' => code, 'info' => info ]
+        */
+       public function parseMsg( $error ) {
+               // Check whether someone passed the whole array, instead of one element as
+               // documented. This breaks if it's actually an array of fallback keys, but
+               // that's long-standing misbehavior introduced in r87627 to incorrectly
+               // fix T30797.
+               if ( is_array( $error ) ) {
+                       $first = reset( $error );
+                       if ( is_array( $first ) ) {
+                               wfDebug( __METHOD__ . ' was passed an array of arrays. ' . wfGetAllCallers( 5 ) );
+                               $error = $first;
+                       }
+               }
+
+               $msg = $this->parseMsgInternal( $error );
+               return [
+                       'code' => $msg->getApiCode(),
+                       'info' => ApiErrorFormatter::stripMarkup(
+                               $msg->inLanguage( 'en' )->useDatabase( false )->text()
+                       ),
+                       'data' => $msg->getApiData()
+               ];
+       }
+
+       /**
+        * Output the error message related to a certain array
+        * @deprecated since 1.29, use ApiBase::dieWithError() instead
+        * @param array|string|MessageSpecifier $error Element of a getUserPermissionsErrors()-style array
+        * @throws ApiUsageException always
+        */
+       public function dieUsageMsg( $error ) {
+               $this->dieWithError( $this->parseMsgInternal( $error ) );
+       }
+
+       /**
+        * Will only set a warning instead of failing if the global $wgDebugAPI
+        * is set to true. Otherwise behaves exactly as dieUsageMsg().
+        * @deprecated since 1.29, use ApiBase::dieWithErrorOrDebug() instead
+        * @param array|string|MessageSpecifier $error Element of a getUserPermissionsErrors()-style array
+        * @throws ApiUsageException
+        * @since 1.21
+        */
+       public function dieUsageMsgOrDebug( $error ) {
+               $this->dieWithErrorOrDebug( $this->parseMsgInternal( $error ) );
+       }
+
        /**@}*/
 }
 
index e4c9d0a..a4ea385 100644 (file)
@@ -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'] );
index 5a0edfc..4139019 100644 (file)
@@ -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() {
index aea2819..c25920e 100644 (file)
@@ -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 );
 
index dd88b5f..3cc7a8a 100644 (file)
@@ -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 ) ) {
index cbb1524..3f5bc0c 100644 (file)
@@ -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}"
                                );
                        }
index 7eb0bf3..d6867eb 100644 (file)
@@ -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() {
index 19e2453..7da8ed9 100644 (file)
@@ -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] );
index 993c23e..50c24ae 100644 (file)
@@ -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' );
                        }
                }
 
index fc97522..41bf9b6 100644 (file)
@@ -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() {
index d6de834..6b56870 100644 (file)
@@ -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 );
index 192378e..8aff6f8 100644 (file)
@@ -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 );
        }
index 6d9184f..4fb19b8 100644 (file)
@@ -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( '!</?(var|kbd|samp|code)>!', '"', $text );
+
+               // Strip tags and decode.
+               $ret = html_entity_decode( strip_tags( $ret ), ENT_QUOTES | ENT_HTML5 );
+
+               return $ret;
+       }
+
+       /**
+        * Format a Message object for raw format
+        * @param MessageSpecifier $msg
+        * @return array
+        */
+       private function formatRawMessage( MessageSpecifier $msg ) {
+               $ret = [
+                       'key' => $msg->getKey(),
+                       'params' => $msg->getParams(),
+               ];
+               ApiResult::setIndexedTagName( $ret['params'], 'param' );
+
+               // Transform Messages as parameters in the style of Message::fooParam().
+               foreach ( $ret['params'] as $i => $param ) {
+                       if ( $param instanceof MessageSpecifier ) {
+                               $ret['params'][$i] = [ 'message' => $this->formatRawMessage( $param ) ];
+                       }
+               }
+               return $ret;
+       }
+
+       /**
+        * Format a message as an array
+        * @since 1.29
         * @param ApiMessage|ApiRawMessage $msg
+        * @param string|null $format
+        * @return array
         */
-       protected function addWarningOrError( $tag, $moduleName, $msg ) {
+       protected function formatMessageInternal( $msg, $format ) {
                $value = [ 'code' => $msg->getApiCode() ];
-               switch ( $this->format ) {
+               switch ( $format ) {
+                       case 'plaintext':
+                               $value += [
+                                       'text' => self::stripMarkup( $msg->text() ),
+                                       ApiResult::META_CONTENT => 'text',
+                               ];
+                               break;
+
                        case 'wikitext':
                                $value += [
                                        'text' => $msg->text(),
@@ -201,19 +250,34 @@ class ApiErrorFormatter {
                                break;
 
                        case 'raw':
-                               $value += [
-                                       'key' => $msg->getKey(),
-                                       'params' => $msg->getParams(),
-                               ];
-                               ApiResult::setIndexedTagName( $value['params'], 'param' );
+                               $value += $this->formatRawMessage( $msg );
                                break;
 
                        case 'none':
                                break;
                }
-               $value += $msg->getApiData();
+               $data = $msg->getApiData();
+               if ( $data ) {
+                       $value['data'] = $msg->getApiData() + [
+                               ApiResult::META_TYPE => 'assoc',
+                       ];
+               }
+               return $value;
+       }
 
-               $path = [ $tag . 's', $moduleName ];
+       /**
+        * Actually add the warning or error to the result
+        * @param string $tag 'warning' or 'error'
+        * @param string|null $modulePath
+        * @param ApiMessage|ApiRawMessage $msg
+        */
+       protected function addWarningOrError( $tag, $modulePath, $msg ) {
+               $value = $this->formatMessageInternal( $msg, $this->format );
+               if ( $modulePath !== null ) {
+                       $value += [ 'module' => $modulePath ];
+               }
+
+               $path = [ $tag . 's' ];
                $existing = $this->result->getResultData( $path );
                if ( $existing === null || !in_array( $value, $existing ) ) {
                        $flags = ApiResult::NO_SIZE_CHECK;
@@ -243,19 +307,19 @@ class ApiErrorFormatter_BackCompat extends ApiErrorFormatter {
                parent::__construct( $result, Language::factory( 'en' ), 'none', false );
        }
 
-       public function arrayFromStatus( Status $status, $type = 'error', $format = null ) {
-               if ( $status->isGood() || !$status->errors ) {
+       public function arrayFromStatus( StatusValue $status, $type = 'error', $format = null ) {
+               if ( $status->isGood() || !$status->getErrors() ) {
                        return [];
                }
 
                $result = [];
                foreach ( $status->getErrorsByType( $type ) as $error ) {
-                       if ( $error['message'] instanceof Message ) {
-                               $error = [
-                                       'message' => $error['message']->getKey(),
-                                       'params' => $error['message']->getParams(),
-                               ] + $error;
-                       }
+                       $msg = ApiMessage::create( $error );
+                       $error = [
+                               'message' => $msg->getKey(),
+                               'params' => $msg->getParams(),
+                               'code' => $msg->getApiCode(),
+                       ] + $error;
                        ApiResult::setIndexedTagName( $error['params'], 'param' );
                        $result[] = $error;
                }
@@ -264,24 +328,32 @@ class ApiErrorFormatter_BackCompat extends ApiErrorFormatter {
                return $result;
        }
 
-       protected function addWarningOrError( $tag, $moduleName, $msg ) {
-               $value = $msg->plain();
+       protected function formatMessageInternal( $msg, $format ) {
+               return [
+                       'code' => $msg->getApiCode(),
+                       'info' => $msg->text(),
+               ] + $msg->getApiData();
+       }
+
+       protected function addWarningOrError( $tag, $modulePath, $msg ) {
+               $value = self::stripMarkup( $msg->text() );
 
                if ( $tag === 'error' ) {
                        // In BC mode, only one error
-                       $code = $msg->getApiCode();
-                       if ( isset( ApiBase::$messageMap[$code] ) ) {
-                               // Backwards compatibility
-                               $code = ApiBase::$messageMap[$code]['code'];
-                       }
-
                        $value = [
-                               'code' => $code,
+                               'code' => $msg->getApiCode(),
                                'info' => $value,
                        ] + $msg->getApiData();
                        $this->result->addValue( null, 'error', $value,
                                ApiResult::OVERRIDE | ApiResult::ADD_ON_TOP | ApiResult::NO_SIZE_CHECK );
                } else {
+                       if ( $modulePath === null ) {
+                               $moduleName = 'unknown';
+                       } else {
+                               $i = strrpos( $modulePath, '+' );
+                               $moduleName = $i === false ? $modulePath : substr( $modulePath, $i + 1 );
+                       }
+
                        // Don't add duplicate warnings
                        $tag .= 's';
                        $path = [ $tag, $moduleName ];
index 10fb182..6f7cf65 100644 (file)
@@ -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' );
                                }
                        }
                }
index c7dc303..97720c6 100644 (file)
@@ -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();
index 813ac01..e0e50ed 100644 (file)
@@ -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 );
index af5b1af..b9bb761 100644 (file)
@@ -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 );
                }
        }
index 97464d6..736898e 100644 (file)
@@ -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' );
                }
        }
 
index 2e917e1..8ebfe48 100644 (file)
@@ -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 );
index fc25f47..a744f57 100644 (file)
@@ -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 );
index 9da040c..228b47e 100644 (file)
@@ -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;
                }
index a45dbeb..e4dfda0 100644 (file)
@@ -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;
                }
index 37cb80a..72fb16d 100644 (file)
@@ -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;
        }
index 10106ff..3f48f38 100644 (file)
@@ -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();
index 1017607..9a21e76 100644 (file)
@@ -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}"
                                );
                        }
index 6ac261d..723dc33 100644 (file)
@@ -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':
index 6a26e2e..d5c28f1 100644 (file)
@@ -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'
                        );
                }
index 38299b4..fe6ed41 100644 (file)
@@ -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
index 617db22..3299f73 100644 (file)
@@ -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();
index 276f1c0..357698e 100644 (file)
@@ -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'] ] );
                        }
                }
 
index ae66778..9d69a77 100644 (file)
@@ -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 );
        }
 }
index 29e67b0..7c8aa90 100644 (file)
@@ -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;
index ace776c..e6fe27c 100644 (file)
@@ -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 ]
                                );
                        }
index 8bfe447..466d186 100644 (file)
@@ -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 ] );
                        }
                }
 
index 853a805..4cf896f 100644 (file)
@@ -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'] );
index ffc3fc2..a9b3dde 100644 (file)
@@ -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';
index 0cad5de..2263b8f 100644 (file)
@@ -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;
                }
 
index 6252882..c33542f 100644 (file)
@@ -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' ) ) ];
index d289060..746dc9a 100644 (file)
@@ -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;
                        }
index 8bbd88d..324d030 100644 (file)
@@ -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;
                                }
                        }
index 16bd725..8196cfa 100644 (file)
@@ -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'] ) {
index 3073a95..b09b977 100644 (file)
@@ -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;
index 8734f38..e3e5ed6 100644 (file)
@@ -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 = [];
index ac90605..c3636c6 100644 (file)
@@ -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' );
index e0ba4ea..244effc 100644 (file)
@@ -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' );
index 6a0f124..7460bd5 100644 (file)
@@ -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' ) {
index b7ed9dd..2e2ac32 100644 (file)
@@ -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
index fb502e4..4c32320 100644 (file)
@@ -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'
                        );
                }
index 8e89c32..ef7b9af 100644 (file)
@@ -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(
index bba5375..af2aed5 100644 (file)
@@ -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 ) ];
index 5d7c664..ef79efd 100644 (file)
@@ -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;
        }
index 63d0f6d..f2498ca 100644 (file)
@@ -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' );
index 4865ad5..02961aa 100644 (file)
@@ -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 {
index cfd0653..d0b8214 100644 (file)
@@ -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'];
index d58efa1..6a259cd 100644 (file)
@@ -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;
index e1c97e1..9476066 100644 (file)
@@ -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() {
index 03be491..116dbb3 100644 (file)
@@ -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 ) {
index 7568107..6e2fb67 100644 (file)
@@ -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'] ) ) {
index 6d9c2ca..cfd990b 100644 (file)
@@ -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
index d1fcfa3..0bbfad3 100644 (file)
@@ -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() ) ] );
                }
        }
 
index e04d8c8..ae6f5bf 100644 (file)
@@ -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();
                                }
index d287020..fd65038 100644 (file)
@@ -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;
                                }
index a6153de..8d5b5f3 100644 (file)
@@ -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'] ) ) {
index 67f2c9e..55e3c85 100644 (file)
@@ -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
index 6e5239f..e9ae132 100644 (file)
@@ -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 );
                                }
index 122594d..2dcd0b4 100644 (file)
@@ -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() ) );
index 0c70a8a..1324f2f 100644 (file)
@@ -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
index 9ba757c..908cdee 100644 (file)
@@ -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'] ];
index 8b11dc2..8d14927 100644 (file)
@@ -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;
                                }
index 3259927..48f6046 100644 (file)
@@ -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;
                                                }
index 266d699..696ec87 100644 (file)
@@ -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 {
index 9962d5e..64bc43f 100644 (file)
@@ -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 ) {
index 19e0c93..6fc6aa3 100644 (file)
@@ -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();
index b039a1e..981cb09 100644 (file)
@@ -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() ) ] );
                }
        }
 
index de5a377..5b700db 100644 (file)
@@ -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;
                }
 
index b85bec4..b6d871b 100644 (file)
@@ -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
index d3cd0c4..3b60478 100644 (file)
@@ -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'] ) &&
index 9b45b91..65d3797 100644 (file)
@@ -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;
                                                        }
index 42ea55d..6b5ceb7 100644 (file)
@@ -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'];
                }
index 806861e..a1078a5 100644 (file)
@@ -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 = [];
index d72c8a4..359d045 100644 (file)
@@ -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 );
 
index 2d7f5df..b5fa8ed 100644 (file)
@@ -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() + [
index 6e27fc8..61a4394 100644 (file)
@@ -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;
index ed9fba2..0251bdb 100644 (file)
@@ -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(
index b9911da..c802087 100644 (file)
@@ -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;
index 3412f38..5769ff6 100644 (file)
@@ -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 ) {
index 92cbe90..e29fda5 100644 (file)
@@ -51,7 +51,7 @@ class ApiStashEdit extends ApiBase {
                $params = $this->extractRequestParams();
 
                if ( $user->isBot() ) { // sanity
-                       $this->dieUsage( 'This interface is not supported for bots', 'botsnotsupported' );
+                       $this->dieWithError( 'apierror-botsnotsupported' );
                }
 
                $cache = ObjectCache::getLocalClusterInstance();
@@ -61,9 +61,14 @@ class ApiStashEdit extends ApiBase {
                if ( !ContentHandler::getForModelID( $params['contentmodel'] )
                        ->isSupportedFormat( $params['contentformat'] )
                ) {
-                       $this->dieUsage( 'Unsupported content model/format', 'badmodelformat' );
+                       $this->dieWithError(
+                               [ 'apierror-badformat-generic', $params['contentformat'], $params['contentmodel'] ],
+                               'badmodelformat'
+                       );
                }
 
+               $this->requireAtLeastOneParameter( $params, 'stashedtexthash', 'text' );
+
                $text = null;
                $textHash = null;
                if ( strlen( $params['stashedtexthash'] ) ) {
@@ -72,15 +77,18 @@ class ApiStashEdit extends ApiBase {
                        $textKey = $cache->makeKey( 'stashedit', 'text', $textHash );
                        $text = $cache->get( $textKey );
                        if ( !is_string( $text ) ) {
-                               $this->dieUsage( 'No stashed text found with the given hash', 'missingtext' );
+                               $this->dieWithError( 'apierror-stashedit-missingtext', 'missingtext' );
                        }
                } elseif ( $params['text'] !== null ) {
                        // Trim and fix newlines so the key SHA1's match (see WebRequest::getText())
                        $text = rtrim( str_replace( "\r\n", "\n", $params['text'] ) );
                        $textHash = sha1( $text );
                } else {
-                       $this->dieUsage(
-                               'The text or stashedtexthash parameter must be given', 'missingtextparam' );
+                       $this->dieWithError( [
+                               'apierror-missingparam-at-least-one-of',
+                               Message::listParam( [ '<var>stashedtexthash</var>', '<var>text</var>' ] ),
+                               2,
+                       ], 'missingparam' );
                }
 
                $textContent = ContentHandler::makeContent(
@@ -91,11 +99,11 @@ class ApiStashEdit extends ApiBase {
                        // Page exists: get the merged content with the proposed change
                        $baseRev = Revision::newFromPageId( $page->getId(), $params['baserevid'] );
                        if ( !$baseRev ) {
-                               $this->dieUsage( "No revision ID {$params['baserevid']}", 'missingrev' );
+                               $this->dieWithError( [ 'apierror-nosuchrevid', $params['baserevid'] ] );
                        }
                        $currentRev = $page->getRevision();
                        if ( !$currentRev ) {
-                               $this->dieUsage( "No current revision of page ID {$page->getId()}", 'missingrev' );
+                               $this->dieWithError( [ 'apierror-missingrev-pageid', $page->getId() ], 'missingrev' );
                        }
                        // Merge in the new version of the section to get the proposed version
                        $editContent = $page->replaceSectionAtRev(
@@ -105,7 +113,7 @@ class ApiStashEdit extends ApiBase {
                                $baseRev->getId()
                        );
                        if ( !$editContent ) {
-                               $this->dieUsage( 'Could not merge updated section.', 'replacefailed' );
+                               $this->dieWithError( 'apierror-sectionreplacefailed', 'replacefailed' );
                        }
                        if ( $currentRev->getId() == $baseRev->getId() ) {
                                // Base revision was still the latest; nothing to merge
@@ -115,7 +123,7 @@ class ApiStashEdit extends ApiBase {
                                $baseContent = $baseRev->getContent();
                                $currentContent = $currentRev->getContent();
                                if ( !$baseContent || !$currentContent ) {
-                                       $this->dieUsage( "Missing content for page ID {$page->getId()}", 'missingrev' );
+                                       $this->dieWithError( [ 'apierror-missingcontent-pageid', $page->getId() ], 'missingrev' );
                                }
                                $handler = ContentHandler::getForModelID( $baseContent->getModel() );
                                $content = $handler->merge3( $baseContent, $editContent, $currentContent );
index f88c2db..f6c0584 100644 (file)
@@ -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;
                }
 
index 4940394..fc2951a 100644 (file)
 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;
                        }
index ace41a4..523a888 100644 (file)
@@ -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();
index e24f2ce..7fda1ea 100644 (file)
@@ -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] ) {
index 7b44f40..6bdd68f 100644 (file)
@@ -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 (file)
index 0000000..7e21ab5
--- /dev/null
@@ -0,0 +1,217 @@
+<?php
+/**
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ * http://www.gnu.org/copyleft/gpl.html
+ *
+ * @file
+ * @defgroup API API
+ */
+
+/**
+ * This exception will be thrown when dieUsage is called to stop module execution.
+ *
+ * @ingroup API
+ * @deprecated since 1.29, use ApiUsageException instead
+ */
+class UsageException extends MWException {
+
+       private $mCodestr;
+
+       /**
+        * @var null|array
+        */
+       private $mExtraData;
+
+       /**
+        * @param string $message
+        * @param string $codestr
+        * @param int $code
+        * @param array|null $extradata
+        */
+       public function __construct( $message, $codestr, $code = 0, $extradata = null ) {
+               parent::__construct( $message, $code );
+               $this->mCodestr = $codestr;
+               $this->mExtraData = $extradata;
+
+               // This should never happen, so throw an exception about it that will
+               // hopefully get logged with a backtrace (T138585)
+               if ( !is_string( $codestr ) || $codestr === '' ) {
+                       throw new InvalidArgumentException( 'Invalid $codestr, was ' .
+                               ( $codestr === '' ? 'empty string' : gettype( $codestr ) )
+                       );
+               }
+       }
+
+       /**
+        * @return string
+        */
+       public function getCodeString() {
+               return $this->mCodestr;
+       }
+
+       /**
+        * @return array
+        */
+       public function getMessageArray() {
+               $result = [
+                       'code' => $this->mCodestr,
+                       'info' => $this->getMessage()
+               ];
+               if ( is_array( $this->mExtraData ) ) {
+                       $result = array_merge( $result, $this->mExtraData );
+               }
+
+               return $result;
+       }
+
+       /**
+        * @return string
+        */
+       public function __toString() {
+               return "{$this->getCodeString()}: {$this->getMessage()}";
+       }
+}
+
+/**
+ * Exception used to abort API execution with an error
+ *
+ * If possible, use ApiBase::dieWithError() instead of throwing this directly.
+ *
+ * @ingroup API
+ * @note This currently extends UsageException for backwards compatibility, so
+ *  all the existing code that catches UsageException won't break when stuff
+ *  starts throwing ApiUsageException. Eventually UsageException will go away
+ *  and this will (probably) extend MWException directly.
+ */
+class ApiUsageException extends UsageException {
+
+       protected $modulePath;
+       protected $status;
+
+       /**
+        * @param ApiBase|null $module API module responsible for the error, if known
+        * @param StatusValue $status Status holding errors
+        * @param int $httpCode HTTP error code to use
+        */
+       public function __construct(
+               ApiBase $module = null, StatusValue $status, $httpCode = 0
+       ) {
+               if ( $status->isOK() ) {
+                       throw new InvalidArgumentException( __METHOD__ . ' requires a fatal Status' );
+               }
+
+               $this->modulePath = $module ? $module->getModulePath() : null;
+               $this->status = $status;
+
+               // Bug T46111: Messages in the log files should be in English and not
+               // customized by the local wiki.
+               $enMsg = clone $this->getApiMessage();
+               $enMsg->inLanguage( 'en' )->useDatabase( false );
+               parent::__construct(
+                       ApiErrorFormatter::stripMarkup( $enMsg->text() ),
+                       $enMsg->getApiCode(),
+                       $httpCode,
+                       $enMsg->getApiData()
+               );
+       }
+
+       /**
+        * @param ApiBase|null $module API module responsible for the error, if known
+        * @param string|array|Message $msg See ApiMessage::create()
+        * @param string|null $code See ApiMessage::create()
+        * @param array|null $data See ApiMessage::create()
+        * @param int $httpCode HTTP error code to use
+        * @return static
+        */
+       public static function newWithMessage(
+               ApiBase $module = null, $msg, $code = null, $data = null, $httpCode = 0
+       ) {
+               return new static(
+                       $module,
+                       StatusValue::newFatal( ApiMessage::create( $msg, $code, $data ) ),
+                       $httpCode
+               );
+       }
+
+       /**
+        * @returns ApiMessage
+        */
+       private function getApiMessage() {
+               $errors = $this->status->getErrorsByType( 'error' );
+               if ( !$errors ) {
+                       $errors = $this->status->getErrors();
+               }
+               if ( !$errors ) {
+                       $msg = new ApiMessage( 'apierror-unknownerror-nocode', 'unknownerror' );
+               } else {
+                       $msg = ApiMessage::create( $errors[0] );
+               }
+               return $msg;
+       }
+
+       /**
+        * Fetch the responsible module name
+        * @return string|null
+        */
+       public function getModulePath() {
+               return $this->modulePath;
+       }
+
+       /**
+        * Fetch the error status
+        * @return StatusValue
+        */
+       public function getStatusValue() {
+               return $this->status;
+       }
+
+       /**
+        * @deprecated Do not use. This only exists here because UsageException is in
+        *  the inheritance chain for backwards compatibility.
+        * @inheritdoc
+        */
+       public function getCodeString() {
+               return $this->getApiMessage()->getApiCode();
+       }
+
+       /**
+        * @deprecated Do not use. This only exists here because UsageException is in
+        *  the inheritance chain for backwards compatibility.
+        * @inheritdoc
+        */
+       public function getMessageArray() {
+               $enMsg = clone $this->getApiMessage();
+               $enMsg->inLanguage( 'en' )->useDatabase( false );
+
+               return [
+                       'code' => $enMsg->getApiCode(),
+                       'info' => ApiErrorFormatter::stripMarkup( $enMsg->text() ),
+               ] + $enMsg->getApiData();
+       }
+
+       /**
+        * @return string
+        */
+       public function __toString() {
+               $enMsg = clone $this->getApiMessage();
+               $enMsg->inLanguage( 'en' )->useDatabase( false );
+               $text = ApiErrorFormatter::stripMarkup( $enMsg->text() );
+
+               return get_class( $this ) . ": {$enMsg->getApiCode()}: {$text} "
+                       . "in {$this->getFile()}:{$this->getLine()}\n"
+                       . "Stack trace:\n{$this->getTraceAsString()}";
+       }
+
+}
index 3a7a082..d257e90 100644 (file)
@@ -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;
index 28cd746..442bdf4 100644 (file)
        "apihelp-main-param-requestid": "Any value given here will be included in the response. May be used to distinguish requests.",
        "apihelp-main-param-servedby": "Include the hostname that served the request in the results.",
        "apihelp-main-param-curtimestamp": "Include the current timestamp in the result.",
+       "apihelp-main-param-responselanginfo": "Include the languages used for <var>uselang</var> and <var>errorlang</var> in the result.",
        "apihelp-main-param-origin": "When accessing the API using a cross-domain AJAX request (CORS), set this to the originating domain. This must be included in any pre-flight request, and therefore must be part of the request URI (not the POST body).\n\nFor authenticated requests, this must match one of the origins in the <code>Origin</code> header exactly, so it has to be set to something like <kbd>https://en.wikipedia.org</kbd> or <kbd>https://meta.wikimedia.org</kbd>. If this parameter does not match the <code>Origin</code> header, a 403 response will be returned. If this parameter matches the <code>Origin</code> header and the origin is whitelisted, the <code>Access-Control-Allow-Origin</code> and <code>Access-Control-Allow-Credentials</code> headers will be set.\n\nFor non-authenticated requests, specify the value <kbd>*</kbd>. This will cause the <code>Access-Control-Allow-Origin</code> header to be set, but <code>Access-Control-Allow-Credentials</code> will be <code>false</code> and all user-specific data will be restricted.",
        "apihelp-main-param-uselang": "Language to use for message translations. <kbd>[[Special:ApiHelp/query+siteinfo|action=query&meta=siteinfo]]</kbd> with <kbd>siprop=languages</kbd> returns a list of language codes, or specify <kbd>user</kbd> to use the current user's language preference, or specify <kbd>content</kbd> to use this wiki's content language.",
+       "apihelp-main-param-errorformat": "Format to use for warning and error text output.\n; plaintext: Wikitext with HTML tags removed and entities replaced.\n; wikitext: Unparsed wikitext.\n; html: HTML.\n; raw: Message key and parameters.\n; none: No text output, only the error codes.\n; bc: Format used prior to MediaWiki 1.29. <var>errorlang</var> and <var>errorsusedb</var> are ignored.",
+       "apihelp-main-param-errorlang": "Language to use for warnings and errors. <kbd>[[Special:ApiHelp/query+siteinfo|action=query&meta=siteinfo]]</kbd> with <kbd>siprop=languages</kbd> returns a list of language codes, or specify <kbd>content</kbd> to use this wiki's content language, or specify <kbd>uselang</kbd> to use the same value as the <var>uselang</var> parameter.",
+       "apihelp-main-param-errorsuselocal": "If given, error texts will use locally-customized messages from the {{ns:MediaWiki}} namespace.",
 
        "apihelp-block-description": "Block a user.",
        "apihelp-block-param-user": "Username, IP address, or IP address range to block.",
        "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.",
        "apihelp-phpfm-description": "Output data in serialized PHP format (pretty-print in HTML).",
        "apihelp-rawfm-description": "Output data, including debugging elements, in JSON format (pretty-print in HTML).",
        "apihelp-xml-description": "Output data in XML format.",
-       "apihelp-xml-param-xslt": "If specified, adds the named page as an XSL stylesheet. The value must be a title in the {{ns:mediawiki}} namespace ending in <code>.xsl</code>.",
+       "apihelp-xml-param-xslt": "If specified, adds the named page as an XSL stylesheet. The value must be a title in the {{ns:MediaWiki}} namespace ending in <code>.xsl</code>.",
        "apihelp-xml-param-includexmlnamespace": "If specified, adds an XML namespace.",
        "apihelp-xmlfm-description": "Output data in XML format (pretty-print in HTML).",
 
        "api-help-authmanagerhelper-continue": "This request is a continuation after an earlier <samp>UI</samp> or <samp>REDIRECT</samp> response. Either this or <var>$1returnurl</var> is required.",
        "api-help-authmanagerhelper-additional-params": "This module accepts additional parameters depending on the available authentication requests. Use <kbd>[[Special:ApiHelp/query+authmanagerinfo|action=query&meta=authmanagerinfo]]</kbd> with <kbd>amirequestsfor=$1</kbd> (or a previous response from this module, if applicable) to determine the requests available and the fields that they use.",
 
+       "apierror-allimages-redirect": "Use <kbd>gaifilterredir=nonredirects</kbd> instead of <var>redirects</var> when using <kbd>allimages</kbd> as a generator.",
+       "apierror-allpages-generator-redirects": "Use <kbd>gapfilterredir=nonredirects</kbd> instead of <var>redirects</var> when using <kbd>allpages</kbd> as a generator.",
+       "apierror-appendnotsupported": "Can't append to pages using content model $1.",
+       "apierror-articleexists": "The article you tried to create has been created already.",
+       "apierror-assertbotfailed": "Assertion that the user has the <code>bot</code> right failed.",
+       "apierror-assertnameduserfailed": "Assertion that the user is \"$1\" failed.",
+       "apierror-assertuserfailed": "Assertion that the user is logged in failed.",
+       "apierror-autoblocked": "Your IP address has been blocked automatically, because it was used by a blocked user.",
+       "apierror-badconfig-resulttoosmall": "The value of <code>$wgAPIMaxResultSize</code> on this wiki is too small to hold basic result information.",
+       "apierror-badcontinue": "Invalid continue param. You should pass the original value returned by the previous query.",
+       "apierror-baddiff": "The diff cannot be retrieved, one or both revisions do not exist or you do not have permission to view them.",
+       "apierror-baddiffto": "<var>$1diffto</var> must be set to a non-negative number, <kbd>prev</kbd>, <kbd>next</kbd> or <kbd>cur</kbd>.",
+       "apierror-badformat-generic": "The requested format $1 is not supported for content model $2.",
+       "apierror-badformat": "The requested format $1 is not supported for content model $2 used by $3.",
+       "apierror-badgenerator-notgenerator": "Module <kbd>$1</kbd> cannot be used as a generator.",
+       "apierror-badgenerator-unknown": "Unknown <kbd>generator=$1</kbd>.",
+       "apierror-badip": "IP parameter is not valid.",
+       "apierror-badmd5": "The supplied MD5 hash was incorrect.",
+       "apierror-badmodule-badsubmodule": "The module <kbd>$1</kbd> does not have a submodule \"$2\".",
+       "apierror-badmodule-nosubmodules": "The module <kbd>$1</kbd> has no submodules.",
+       "apierror-badparameter": "Invalid value for parameter <var>$1</var>.",
+       "apierror-badquery": "Invalid query.",
+       "apierror-badtimestamp": "Invalid value \"$2\" for timestamp parameter <var>$1</var>.",
+       "apierror-badtoken": "Invalid CSRF token.",
+       "apierror-badupload": "File upload parameter <var>$1</var> is not a file upload; be sure to use <code>multipart/form-data</code> for your POST and include a filename in the <code>Content-Disposition</code> header.",
+       "apierror-badurl": "Invalid value \"$2\" for URL parameter <var>$1</var>.",
+       "apierror-baduser": "Invalid value \"$2\" for user parameter <var>$1</var>.",
+       "apierror-badvalue-notmultivalue": "U+001F multi-value separation may only be used for multi-valued parameters.",
+       "apierror-bad-watchlist-token": "Incorrect watchlist token provided. Please set a correct token in [[Special:Preferences]].",
+       "apierror-blockedfrommail": "You have been blocked from sending email.",
+       "apierror-blocked": "You have been blocked from editing.",
+       "apierror-botsnotsupported": "This interface is not supported for bots.",
+       "apierror-cannotreauthenticate": "This action is not available as your identity cannot be verified.",
+       "apierror-cannotviewtitle": "You are not allowed to view $1.",
+       "apierror-cantblock-email": "You don't have permission to block users from sending email through the wiki.",
+       "apierror-cantblock": "You don't have permission to block users.",
+       "apierror-cantchangecontentmodel": "You don't have permission to change the content model of a page.",
+       "apierror-canthide": "You don't have permission to hide user names from the block log.",
+       "apierror-cantimport-upload": "You don't have permission to import uploaded pages.",
+       "apierror-cantimport": "You don't have permission to import pages.",
+       "apierror-cantoverwrite-sharedfile": "The target file exists on a shared repository and you do not have permission to override it.",
+       "apierror-cantsend": "You are not logged in, you do not have a confirmed email address, or you are not allowed to send email to other users, so you cannot send email.",
+       "apierror-cantundelete": "Couldn't undelete: the requested revisions may not exist, or may have been undeleted already.",
+       "apierror-changeauth-norequest": "Failed to create change request.",
+       "apierror-chunk-too-small": "Minimum chunk size is $1 {{PLURAL:$1|byte|bytes}} for non-final chunks.",
+       "apierror-cidrtoobroad": "$1 CIDR ranges broader than /$2 are not accepted.",
+       "apierror-compare-inputneeded": "A title, a page ID, or a revision number is needed for both the <var>from</var> and the <var>to</var> parameters.",
+       "apierror-contentserializationexception": "Content serialization failed: $1",
+       "apierror-contenttoobig": "The content you supplied exceeds the article size limit of $1 {{PLURAL:$1|kilobyte|kilobytes}}.",
+       "apierror-copyuploadbaddomain": "Uploads by URL are not allowed from this domain.",
+       "apierror-copyuploadbadurl": "Upload not allowed from this URL.",
+       "apierror-create-titleexists": "Existing titles can't be protected with <kbd>create</kbd>.",
+       "apierror-csp-report": "Error processing CSP report: $1.",
+       "apierror-databaseerror": "[$1] Database query error.",
+       "apierror-deletedrevs-param-not-1-2": "The <var>$1</var> parameter cannot be used in modes 1 or 2.",
+       "apierror-deletedrevs-param-not-3": "The <var>$1</var> parameter cannot be used in mode 3.",
+       "apierror-emptynewsection": "Creating empty new sections is not possible.",
+       "apierror-emptypage": "Creating new, empty pages is not allowed.",
+       "apierror-exceptioncaught": "[$1] Exception caught: $2",
+       "apierror-filedoesnotexist": "File does not exist.",
+       "apierror-fileexists-sharedrepo-perm": "The target file exists on a shared repository. Use the <var>ignorewarnings</var> parameter to override it.",
+       "apierror-filenopath": "Cannot get local file path.",
+       "apierror-filetypecannotberotated": "File type cannot be rotated.",
+       "apierror-formatphp": "This response cannot be represented using <kbd>format=php</kbd>. See https://phabricator.wikimedia.org/T68776.",
+       "apierror-imageusage-badtitle": "The title for <kbd>$1</kbd> must be a file.",
+       "apierror-import-unknownerror": "Unknown error on import: $1.",
+       "apierror-integeroutofrange-abovebotmax": "<var>$1</var> may not be over $2 (set to $3) for bots or sysops.",
+       "apierror-integeroutofrange-abovemax": "<var>$1</var> may not be over $2 (set to $3) for users.",
+       "apierror-integeroutofrange-belowminimum": "<var>$1</var> may not be less than $2 (set to $3).",
+       "apierror-invalidcategory": "The category name you entered is not valid.",
+       "apierror-invalid-chunk": "Offset plus current chunk is greater than claimed file size.",
+       "apierror-invalidexpiry": "Invalid expiry time \"$1\".",
+       "apierror-invalid-file-key": "Not a valid file key.",
+       "apierror-invalidlang": "Invalid language code for parameter <var>$1</var>.",
+       "apierror-invalidoldimage": "The oldimage parameter has invalid format.",
+       "apierror-invalidparammix-cannotusewith": "The <kbd>$1</kbd> parameter cannot be used with <kbd>$2</kbd>.",
+       "apierror-invalidparammix-mustusewith": "The <kbd>$1</kbd> parameter may only be used with <kbd>$2</kbd>.",
+       "apierror-invalidparammix-parse-new-section": "<kbd>section=new</kbd> cannot be combined with the <var>oldid</var>, <var>pageid</var> or <var>page</var> parameters. Please use <var>title</var> and <var>text</var>.",
+       "apierror-invalidparammix": "The {{PLURAL:$2|parameters}} $1 can not be used together.",
+       "apierror-invalidsection": "The section parameter must be a valid section ID or <kbd>new</kbd>.",
+       "apierror-invalidsha1base36hash": "The SHA1Base36 hash provided is not valid.",
+       "apierror-invalidsha1hash": "The SHA1 hash provided is not valid.",
+       "apierror-invalidtitle": "Bad title \"$1\".",
+       "apierror-invalidurlparam": "Invalid value for <var>$1urlparam</var> (<kbd>$2=$3</kbd>).",
+       "apierror-invaliduser": "Invalid username \"$1\".",
+       "apierror-maxlag-generic": "Waiting for a database server: $1 {{PLURAL:$1|second|seconds}} lagged.",
+       "apierror-maxlag": "Waiting for $2: $1 {{PLURAL:$1|second|seconds}} lagged.",
+       "apierror-mimesearchdisabled": "MIME search is disabled in Miser Mode.",
+       "apierror-missingcontent-pageid": "Missing content for page ID $1.",
+       "apierror-missingparam-at-least-one-of": "{{PLURAL:$2|The parameter|At least one of the parameters}} $1 is required.",
+       "apierror-missingparam-one-of": "{{PLURAL:$2|The parameter|One of the parameters}} $1 is required.",
+       "apierror-missingparam": "The <var>$1</var> parameter must be set.",
+       "apierror-missingrev-pageid": "No current revision of page ID $1.",
+       "apierror-missingtitle-createonly": "Missing titles can only be protected with <kbd>create</kbd>.",
+       "apierror-missingtitle": "The page you specified doesn't exist.",
+       "apierror-missingtitle-byname": "The page $1 doesn't exist.",
+       "apierror-moduledisabled": "The <kbd>$1</kbd> module has been disabled.",
+       "apierror-multival-only-one-of": "{{PLURAL:$3|Only|Only one of}} $2 is allowed for parameter <var>$1</var>.",
+       "apierror-multival-only-one": "Only one value is allowed for parameter <var>$1</var>.",
+       "apierror-multpages": "<var>$1</var> may only be used with a single page.",
+       "apierror-mustbeloggedin-changeauth": "You must be logged in to change authentication data.",
+       "apierror-mustbeloggedin-generic": "You must be logged in.",
+       "apierror-mustbeloggedin-linkaccounts": "You must be logged in to link accounts.",
+       "apierror-mustbeloggedin-removeauth": "You must be logged in to remove authentication data.",
+       "apierror-mustbeloggedin-uploadstash": "The upload stash is only available to logged-in users.",
+       "apierror-mustbeloggedin": "You must be logged in to $1.",
+       "apierror-mustbeposted": "The <kbd>$1</kbd> module requires a POST request.",
+       "apierror-mustpostparams": "The following {{PLURAL:$2|parameter was|parameters were}} found in the query string, but must be in the POST body: $1.",
+       "apierror-noapiwrite": "Editing of this wiki through the API is disabled. Make sure the <code>$wgEnableWriteAPI=true;</code> statement is included in the wiki's <code>LocalSettings.php</code> file.",
+       "apierror-nochanges": "No changes were requested.",
+       "apierror-nodeleteablefile": "No such old version of the file.",
+       "apierror-no-direct-editing": "Direct editing via API is not supported for content model $1 used by $2.",
+       "apierror-noedit-anon": "Anonymous users can't edit pages.",
+       "apierror-noedit": "You don't have permission to edit pages.",
+       "apierror-noimageredirect-anon": "Anonymous users can't create image redirects.",
+       "apierror-noimageredirect": "You don't have permission to create image redirects.",
+       "apierror-nosuchlogid": "There is no log entry with ID $1.",
+       "apierror-nosuchpageid": "There is no page with ID $1.",
+       "apierror-nosuchrcid": "There is no recent change with ID $1.",
+       "apierror-nosuchrevid": "There is no revision with ID $1.",
+       "apierror-nosuchsection": "There is no section $1.",
+       "apierror-nosuchsection-what": "There is no section $1 in $2.",
+       "apierror-notarget": "You have not specified a valid target for this action.",
+       "apierror-notpatrollable": "The revision r$1 can't be patrolled as it's too old.",
+       "apierror-nouploadmodule": "No upload module set.",
+       "apierror-opensearch-json-warnings": "Warnings cannot be represented in OpenSearch JSON format.",
+       "apierror-pagecannotexist": "Namespace doesn't allow actual pages.",
+       "apierror-pagedeleted": "The page has been deleted since you fetched its timestamp.",
+       "apierror-paramempty": "The parameter <var>$1</var> may not be empty.",
+       "apierror-parsetree-notwikitext": "<kbd>prop=parsetree</kbd> is only supported for wikitext content.",
+       "apierror-parsetree-notwikitext-title": "<kbd>prop=parsetree</kbd> is only supported for wikitext content. $1 uses content model $2.",
+       "apierror-pastexpiry": "Expiry time \"$1\" is in the past.",
+       "apierror-permissiondenied": "You don't have permission to $1.",
+       "apierror-permissiondenied-generic": "Permission denied.",
+       "apierror-permissiondenied-patrolflag": "You need the <code>patrol</code> or <code>patrolmarks</code> right to request the patrolled flag.",
+       "apierror-permissiondenied-unblock": "You don't have permission to unblock users.",
+       "apierror-prefixsearchdisabled": "Prefix search is disabled in Miser Mode.",
+       "apierror-promised-nonwrite-api": "The <code>Promise-Non-Write-API-Action</code> HTTP header cannot be sent to write-mode API modules.",
+       "apierror-protect-invalidaction": "Invalid protection type \"$1\".",
+       "apierror-protect-invalidlevel": "Invalid protection level \"$1\".",
+       "apierror-ratelimited": "You've exceeded your rate limit. Please wait some time and try again.",
+       "apierror-readapidenied": "You need read permission to use this module.",
+       "apierror-readonly": "The wiki is currently in read-only mode.",
+       "apierror-reauthenticate": "You have not authenticated recently in this session, please reauthenticate.",
+       "apierror-redirect-appendonly": "You have attempted to edit using the redirect-following mode, which must be used in conjuction with <kbd>section=new</kbd>, <var>prependtext</var>, or <var>appendtext</var>.",
+       "apierror-revdel-mutuallyexclusive": "The same field cannot be used in both <var>hide</var> and <var>show</var>.",
+       "apierror-revdel-needtarget": "A target title is required for this RevDel type.",
+       "apierror-revdel-paramneeded": "At least one value is required for <var>hide</var> and/or <var>show</var>.",
+       "apierror-revisions-norevids": "The <var>revids</var> parameter may not be used with the list options (<var>$1limit</var>, <var>$1startid</var>, <var>$1endid</var>, <kbd>$1dir=newer</kbd>, <var>$1user</var>, <var>$1excludeuser</var>, <var>$1start</var>, and <var>$1end</var>).",
+       "apierror-revisions-singlepage": "<var>titles</var>, <var>pageids</var> or a generator was used to supply multiple pages, but the <var>$1limit</var>, <var>$1startid</var>, <var>$1endid</var>, <kbd>$1dir=newer</kbd>, <var>$1user</var>, <var>$1excludeuser</var>, <var>$1start</var>, and <var>$1end</var> parameters may only be used on a single page.",
+       "apierror-revwrongpage": "r$1 is not a revision of $2.",
+       "apierror-searchdisabled": "<var>$1</var> search is disabled.",
+       "apierror-sectionreplacefailed": "Could not merge updated section.",
+       "apierror-sectionsnotsupported": "Sections are not supported for content model $1.",
+       "apierror-sectionsnotsupported-what": "Sections are not supported by $1.",
+       "apierror-show": "Incorrect parameter - mutually exclusive values may not be supplied.",
+       "apierror-siteinfo-includealldenied": "Cannot view all servers' info unless <var>$wgShowHostNames</var> is true.",
+       "apierror-sizediffdisabled": "Size difference is disabled in Miser Mode.",
+       "apierror-spamdetected": "Your edit was refused because it contained a spam fragment: <code>$1</code>.",
+       "apierror-specialpage-cantexecute": "You don't have permission to view the results of this special page.",
+       "apierror-stashedfilenotfound": "Could not find the file in the stash: $1.",
+       "apierror-stashedit-missingtext": "No stashed text found with the given hash.",
+       "apierror-stashfailed-complete": "Chunked upload is already completed, check status for details.",
+       "apierror-stashfailed-nosession": "No chunked upload session with this key.",
+       "apierror-stashfilestorage": "Could not store upload in the stash: $1",
+       "apierror-stashnosuchfilekey": "No such filekey: $1.",
+       "apierror-stashpathinvalid": "File key of improper format or otherwise invalid: $1.",
+       "apierror-stashwrongowner": "Wrong owner: $1",
+       "apierror-stashzerolength": "File is of zero length, and could not be stored in the stash: $1.",
+       "apierror-templateexpansion-notwikitext": "Template expansion is only supported for wikitext content. $1 uses content model $2.",
+       "apierror-toofewexpiries": "$1 expiry {{PLURAL:$1|timestamp was|timestamps were}} provided where $2 {{PLURAL:$2|was|were}} needed.",
+       "apierror-unknownaction": "The action specified, <kbd>$1</kbd>, is not recognized.",
+       "apierror-unknownerror-editpage": "Unknown EditPage error: $1.",
+       "apierror-unknownerror-nocode": "Unknown error.",
+       "apierror-unknownerror": "Unknown error: \"$1\".",
+       "apierror-unknownformat": "Unrecognized format \"$1\".",
+       "apierror-unrecognizedparams": "Unrecognized {{PLURAL:$2|parameter|parameters}}: $1.",
+       "apierror-unrecognizedvalue": "Unrecognized value for parameter <var>$1</var>: $2.",
+       "apierror-unsupportedrepo": "Local file repository does not support querying all images.",
+       "apierror-upload-filekeyneeded": "Must supply a <var>filekey</var> when <var>offset</var> is non-zero.",
+       "apierror-upload-filekeynotallowed": "Cannot supply a <var>filekey</var> when <var>offset</var> is 0.",
+       "apierror-upload-inprogress": "Upload from stash already in progress.",
+       "apierror-upload-missingresult": "No result in status data.",
+       "apierror-urlparamnormal": "Could not normalize image parameters for $1.",
+       "apierror-writeapidenied": "You're not allowed to edit this wiki through the API.",
+
+       "apiwarn-alldeletedrevisions-performance": "For better performance when generating titles, set <kbd>$1dir=newer</kbd>.",
+       "apiwarn-badurlparam": "Could not parse <var>$1urlparam</var> for $2. Using only width and height.",
+       "apiwarn-badutf8": "The value passed for <var>$1</var> contains invalid or non-normalized data. Textual data should be valid, NFC-normalized Unicode without C0 control characters other than HT (\\t), LF (\\n), and CR (\\r).",
+       "apiwarn-checktoken-percentencoding": "Check that symbols such as \"+\" in the token are properly percent-encoded in the URL.",
+       "apiwarn-deprecation-deletedrevs": "<kbd>list=deletedrevs</kbd> has been deprecated. Please use <kbd>prop=deletedrevisions</kbd> or <kbd>list=alldeletedrevisions</kbd> instead.",
+       "apiwarn-deprecation-expandtemplates-prop": "Because no values have been specified for the <var>prop</var> parameter, a legacy format has been used for the output. This format is deprecated, and in the future, a default value will be set for the <var>prop</var> parameter, causing the new format to always be used.",
+       "apiwarn-deprecation-httpsexpected": "HTTP used when HTTPS was expected.",
+       "apiwarn-deprecation-login-botpw": "Main-account login via <kbd>action=login</kbd> is deprecated and may stop working without warning. To continue login with <kbd>action=login</kbd>, see [[Special:BotPasswords]]. To safely continue using main-account login, see <kbd>action=clientlogin</kbd>.",
+       "apiwarn-deprecation-login-nobotpw": "Main-account login via <kbd>action=login</kbd> is deprecated and may stop working without warning. To safely log in, see <kbd>action=clientlogin</kbd>.",
+       "apiwarn-deprecation-login-token": "Fetching a token via <kbd>action=login</kbd> is deprecated. Use <kbd>action=query&meta=tokens&type=login</kbd> instead.",
+       "apiwarn-deprecation-parameter": "The parameter <var>$1</var> has been deprecated.",
+       "apiwarn-deprecation-parse-headitems": "<kbd>prop=headitems</kbd> is deprecated since MediaWiki 1.28. Use <kbd>prop=headhtml</kbd> when creating new HTML documents, or <kbd>prop=modules|jsconfigvars</kbd> when updating a document client-side.",
+       "apiwarn-deprecation-purge-get": "Use of <kbd>action=purge</kbd> via GET is deprecated. Use POST instead.",
+       "apiwarn-deprecation-withreplacement": "<kbd>$1</kbd> has been deprecated. Please use <kbd>$2</kbd> instead.",
+       "apiwarn-difftohidden": "Couldn't diff to r$1: content is hidden.",
+       "apiwarn-errorprinterfailed": "Error printer failed. Will retry without params.",
+       "apiwarn-errorprinterfailed-ex": "Error printer failed (will retry without params): $1",
+       "apiwarn-invalidcategory": "\"$1\" is not a category.",
+       "apiwarn-invalidtitle": "\"$1\" is not a valid title.",
+       "apiwarn-invalidxmlstylesheetext": "Stylesheet should have <code>.xsl</code> extension.",
+       "apiwarn-invalidxmlstylesheet": "Invalid or non-existent stylesheet specified.",
+       "apiwarn-invalidxmlstylesheetns": "Stylesheet should be in the {{ns:MediaWiki}} namespace.",
+       "apiwarn-moduleswithoutvars": "Property <kbd>modules</kbd> was set but not <kbd>jsconfigvars</kbd> or <kbd>encodedjsconfigvars</kbd>. Configuration variables are necessary for proper module usage.",
+       "apiwarn-notfile": "\"$1\" is not a file.",
+       "apiwarn-nothumb-noimagehandler": "Could not create thumbnail because $1 does not have an associated image handler.",
+       "apiwarn-parse-nocontentmodel": "No <var>title</var> or <var>contentmodel</var> was given, assuming $1.",
+       "apiwarn-parse-titlewithouttext": "<var>title</var> used without <var>text</var>, and parsed page properties were requested. Did you mean to use <var>page</var> instead of <var>title</var>?",
+       "apiwarn-redirectsandrevids": "Redirect resolution cannot be used together with the <var>revids</var> parameter. Any redirects the <var>revids</var> point to have not been resolved.",
+       "apiwarn-tokennotallowed": "Action \"$1\" is not allowed for the current user.",
+       "apiwarn-tokens-origin": "Tokens may not be obtained when the same-origin policy is not applied.",
+       "apiwarn-toomanyvalues": "Too many values supplied for parameter <var>$1</var>: the limit is $2.",
+       "apiwarn-truncatedresult": "This result was truncated because it would otherwise be larger than the limit of $1 bytes.",
+       "apiwarn-unclearnowtimestamp": "Passing \"$2\" for timestamp parameter <var>$1</var> has been deprecated. If for some reason you need to explicitly specify the current time without calculating it client-side, use <kbd>now<kbd>.",
+       "apiwarn-unrecognizedvalues": "Unrecognized {{PLURAL:$3|value|values}} for parameter <var>$1</var>: $2.",
+       "apiwarn-unsupportedarray": "Parameter <var>$1</var> uses unsupported PHP array syntax.",
+       "apiwarn-urlparamwidth": "Ignoring width value set in <var>$1urlparam</var> ($2) in favor of width value derived from <var>$1urlwidth</var>/<var>$1urlheight</var> ($3).",
+       "apiwarn-validationfailed-badchars": "invalid characters in key (only <code>a-z</code>, <code>A-Z</code>, <code>0-9</code>, <code>_</code>, and <code>-</code> are allowed).",
+       "apiwarn-validationfailed-badpref": "not a valid preference.",
+       "apiwarn-validationfailed-cannotset": "cannot be set by this module.",
+       "apiwarn-validationfailed-keytoolong": "key too long (no more than $1 bytes allowed).",
+       "apiwarn-validationfailed": "Validation error for <kbd>$1</kbd>: $2",
+       "apiwarn-wgDebugAPI": "<strong>Security Warning</strong>: <var>$wgDebugAPI</var> is enabled.",
+
+       "api-feed-error-title": "Error ($1)",
+       "api-usage-docref": "See $1 for API usage.",
+       "api-exception-trace": "$1 at $2($3)\n$4",
        "api-credits-header": "Credits",
        "api-credits": "API developers:\n* Yuri Astrakhan (creator, lead developer Sep 2006–Sep 2007)\n* Roan Kattouw (lead developer Sep 2007–2009)\n* Victor Vasiliev\n* Bryan Tong Minh\n* Sam Reed\n* Brad Jorsch (lead developer 2013–present)\n\nPlease send your comments, suggestions and questions to mediawiki-api@lists.wikimedia.org\nor file a bug report at https://phabricator.wikimedia.org/."
 }
index fd6a4dd..c5d9bc0 100644 (file)
        "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\"!}}",
        "api-help-authmanagerhelper-returnurl": "{{doc-apihelp-param|description=the \"returnurl\" parameter for AuthManager-using API modules|noseealso=1}}",
        "api-help-authmanagerhelper-continue": "{{doc-apihelp-param|description=the \"continue\" parameter for AuthManager-using API modules|noseealso=1}}",
        "api-help-authmanagerhelper-additional-params": "Message to display for AuthManager modules that take additional parameters to populate AuthenticationRequests. Parameters:\n* $1 - AuthManager action used by this module\n* $2 - Module parameter prefix, e.g. \"login\"\n* $3 - Module name, e.g. \"clientlogin\"\n* $4 - Module path, e.g. \"clientlogin\"",
+       "apierror-allimages-redirect": "{{doc-apierror}}",
+       "apierror-allpages-generator-redirects": "{{doc-apierror}}",
+       "apierror-appendnotsupported": "{{doc-apierror}}\n\nParameters:\n* $1 - Content model",
+       "apierror-articleexists": "{{doc-apierror}}",
+       "apierror-assertbotfailed": "{{doc-apierror}}",
+       "apierror-assertnameduserfailed": "{{doc-apierror}}\n\nParameters:\n* $1 - User name passed in.",
+       "apierror-assertuserfailed": "{{doc-apierror}}",
+       "apierror-autoblocked": "{{doc-apierror}}",
+       "apierror-bad-watchlist-token": "{{doc-apierror}}",
+       "apierror-badconfig-resulttoosmall": "{{doc-apierror}}",
+       "apierror-badcontinue": "{{doc-apierror}}",
+       "apierror-baddiff": "{{doc-apierror}}",
+       "apierror-baddiffto": "{{doc-apierror}}\n\nParameters:\n* $1 - Module parameter prefix, e.g. \"bl\".",
+       "apierror-badformat": "{{doc-apierror}}\n\nParameters:\n* $1 - Content format.\n* $2 - Content model.\n* $3 - Title using the model.",
+       "apierror-badformat-generic": "{{doc-apierror}}\n\nParameters:\n* $1 - Content format.\n* $2 - Content model.",
+       "apierror-badgenerator-notgenerator": "{{doc-apierror}}\n\nParameters:\n* $1 - Generator module name.",
+       "apierror-badgenerator-unknown": "{{doc-apierror}}\n\nParameters:\n* $1 - Generator module name.",
+       "apierror-badip": "{{doc-apierror}}",
+       "apierror-badmd5": "{{doc-apierror}}",
+       "apierror-badmodule-badsubmodule": "{{doc-apierror}}\n\nParameters:\n* $1 - Module path.\n* $2 - Submodule name.",
+       "apierror-badmodule-nosubmodules": "{{doc-apierror}}\n\nParameters:\n* $1 - Module path.",
+       "apierror-badparameter": "{{doc-apierror}}\n\nParameters:\n* $1 - Parameter name.",
+       "apierror-badquery": "{{doc-apierror}}",
+       "apierror-badtimestamp": "{{doc-apierror}}\n\nParameters:\n* $1 - Parameter name.\n* $2 - Value of the parameter.",
+       "apierror-badtoken": "{{doc-apierror}}",
+       "apierror-badupload": "{{doc-apierror}}\n\nParameters:\n* $1 - Parameter name.",
+       "apierror-badurl": "{{doc-apierror}}\n\nParameters:\n* $1 - Parameter name.\n* $2 - Value of the parameter.",
+       "apierror-baduser": "{{doc-apierror}}\n\nParameters:\n* $1 - Parameter name.\n* $2 - Value of the parameter.",
+       "apierror-badvalue-notmultivalue": "{{doc-apierror}}",
+       "apierror-blocked": "{{doc-apierror}}",
+       "apierror-blockedfrommail": "{{doc-apierror}}",
+       "apierror-botsnotsupported": "{{doc-apierror}}",
+       "apierror-cannotreauthenticate": "{{doc-apierror}}",
+       "apierror-cannotviewtitle": "{{doc-apierror}}\n\nParameters:\n* $1 - Title.",
+       "apierror-cantblock": "{{doc-apierror}}",
+       "apierror-cantblock-email": "{{doc-apierror}}",
+       "apierror-cantchangecontentmodel": "{{doc-apierror}}",
+       "apierror-canthide": "{{doc-apierror}}",
+       "apierror-cantimport": "{{doc-apierror}}",
+       "apierror-cantimport-upload": "{{doc-apierror}}",
+       "apierror-cantoverwrite-sharedfile": "{{doc-apierror}}",
+       "apierror-cantsend": "{{doc-apierror}}",
+       "apierror-cantundelete": "{{doc-apierror}}",
+       "apierror-changeauth-norequest": "{{doc-apierror}}",
+       "apierror-chunk-too-small": "{{doc-apierror}}\n\nParameters:\n* $1 - Minimum size in bytes.",
+       "apierror-cidrtoobroad": "{{doc-apierror}}\n\nParameters:\n* $1 - \"IPv4\" or \"IPv6\"\n* $2 - Minimum CIDR mask length.",
+       "apierror-compare-inputneeded": "{{doc-apierror}}",
+       "apierror-contentserializationexception": "{{doc-apierror}}\n\nParameters:\n* $1 - Exception text, may end with punctuation. Currently this is probably English, hopefully we'll fix that in the future.",
+       "apierror-contenttoobig": "{{doc-apierror}}\n\nParameters:\n* $1 - Maximum article size in kilobytes.",
+       "apierror-copyuploadbaddomain": "{{doc-apierror}}",
+       "apierror-copyuploadbadurl": "{{doc-apierror}}",
+       "apierror-create-titleexists": "{{doc-apierror}}",
+       "apierror-csp-report": "{{doc-apierror}}\n\nParameters:\n* $1 - Error code, e.g. \"toobig\".",
+       "apierror-databaseerror": "{{doc-apierror}}\n\nParameters:\n* $1 - Exception log ID code. This is meaningless to the end user, but can be used by people with access to the logs to easily find the logged error.",
+       "apierror-deletedrevs-param-not-1-2": "{{doc-apierror}}\n\nParameters:\n* $1 - Parameter name.\n\nSee also:\n* {{msg-mw|apihelp-query+deletedrevs-description}}",
+       "apierror-deletedrevs-param-not-3": "{{doc-apierror}}\n\nParameters:\n* $1 - Parameter name.\n\nSee also:\n* {{msg-mw|apihelp-query+deletedrevs-description}}",
+       "apierror-emptynewsection": "{{doc-apierror}}",
+       "apierror-emptypage": "{{doc-apierror}}",
+       "apierror-exceptioncaught": "{{doc-apierror}}\n\nParameters:\n* $1 - Exception log ID code. This is meaningless to the end user, but can be used by people with access to the logs to easily find the logged error.\n* $2 - Exception message, which may end with punctuation. Probably in English.",
+       "apierror-filedoesnotexist": "{{doc-apierror}}",
+       "apierror-fileexists-sharedrepo-perm": "{{doc-apierror}}",
+       "apierror-filenopath": "{{doc-apierror}}",
+       "apierror-filetypecannotberotated": "{{doc-apierror}}",
+       "apierror-formatphp": "{{doc-apierror}}",
+       "apierror-imageusage-badtitle": "{{doc-apierror}}\n\nParameters:\n* $1 - Module name.",
+       "apierror-import-unknownerror": "{{doc-apierror}}\n\nParameters:\n* $1 - Error message returned by the import, probably in English.",
+       "apierror-integeroutofrange-abovebotmax": "{{doc-apierror}}\n\nParameters:\n* $1 - Parameter name\n* $2 - Maximum allowed value\n* $3 - Supplied value",
+       "apierror-integeroutofrange-abovemax": "{{doc-apierror}}\n\nParameters:\n* $1 - Parameter name\n* $2 - Maximum allowed value\n* $3 - Supplied value",
+       "apierror-integeroutofrange-belowminimum": "{{doc-apierror}}\n\nParameters:\n* $1 - Parameter name\n* $2 - Minimum allowed value\n* $3 - Supplied value",
+       "apierror-invalid-chunk": "{{doc-apierror}}",
+       "apierror-invalid-file-key": "{{doc-apierror}}",
+       "apierror-invalidcategory": "{{doc-apierror}}",
+       "apierror-invalidexpiry": "{{doc-apierror}}\n\nParameters:\n* $1 - Value provided.",
+       "apierror-invalidlang": "{{doc-apierror}}\n\nParameters:\n* $1 - Parameter name.",
+       "apierror-invalidoldimage": "{{doc-apierror}}",
+       "apierror-invalidparammix": "{{doc-apierror}}\n\nParameters:\n* $1 - List of parameter names or \"parameter=value\" text.\n* $2 - Number of parameters.",
+       "apierror-invalidparammix-cannotusewith": "{{doc-apierror}}\n\nParameters:\n* $1 - Parameter name or \"parameter=value\" text.\n* $2 - Parameter name or \"parameter=value\" text.",
+       "apierror-invalidparammix-mustusewith": "{{doc-apierror}}\n\nParameters:\n* $1 - Parameter name or \"parameter=value\" text.\n* $2 - Parameter name or \"parameter=value\" text.",
+       "apierror-invalidparammix-parse-new-section": "{{doc-apierror}}",
+       "apierror-invalidsection": "{{doc-apierror}}",
+       "apierror-invalidsha1base36hash": "{{doc-apierror}}",
+       "apierror-invalidsha1hash": "{{doc-apierror}}",
+       "apierror-invalidtitle": "{{doc-apierror}}\n\nParameters:\n* $1 - Title that is invalid",
+       "apierror-invalidurlparam": "{{doc-apierror}}\n\nParameters:\n* $1 - Module parameter prefix, e.g. \"bl\".\n* $2 - Key\n* $3 - Value.",
+       "apierror-invaliduser": "{{doc-apierror}}\n\nParameters:\n* $1 - User name that is invalid.",
+       "apierror-maxlag": "{{doc-apierror}}\n\nParameters:\n* $1 - Database lag in seconds.\n* $2 - Database server that is lagged.",
+       "apierror-maxlag-generic": "{{doc-apierror}}\n\nParameters:\n* $1 - Database is lag in seconds.",
+       "apierror-mimesearchdisabled": "{{doc-apierror}}",
+       "apierror-missingcontent-pageid": "{{doc-apierror}}\n\nParameters:\n* $1 - Page ID number.",
+       "apierror-missingparam": "{{doc-apierror}}\n\nParameters:\n* $1 - Parameter name.",
+       "apierror-missingparam-at-least-one-of": "{{doc-apierror}}\n\nParameters:\n* $1 - List of parameter names.\n* $2 - Number of parameters.",
+       "apierror-missingparam-one-of": "{{doc-apierror}}\n\nParameters:\n* $1 - List of parameter names.\n* $2 - Number of parameters.",
+       "apierror-missingrev-pageid": "{{doc-apierror}}\n\nParameters:\n* $1 - Page ID number.",
+       "apierror-missingtitle": "{{doc-apierror}}",
+       "apierror-missingtitle-byname": "{{doc-apierror}}",
+       "apierror-missingtitle-createonly": "{{doc-apierror}}",
+       "apierror-moduledisabled": "{{doc-apierror}}\n\nParameters:\n* $1 - Name of the module.",
+       "apierror-multival-only-one": "{{doc-apierror}}\n\nParameters:\n* $1 - Parameter name.",
+       "apierror-multival-only-one-of": "{{doc-apierror}}\n\nParameters:\n* $1 - Parameter name.\n* $2 - Possible values for the parameter.\n* $3 - Number of values.",
+       "apierror-multpages": "{{doc-apierror}}\n\nParameters:\n* $1 - Parameter name",
+       "apierror-mustbeloggedin": "{{doc-apierror}}\n\nParameters:\n* $1 - One of the action-* messages (for example {{msg-mw|action-edit}}) or other such messages tagged with {{tl|doc-action}} in their documentation\n\nPlease report at [[Support]] if you are unable to properly translate this message. Also see [[phab:T16246]] (now closed) for background.\n\nSee also:\n* {{msg-mw|apierror-permissiondenied}}\n* {{msg-mw|permissionserrorstext-withaction}}",
+       "apierror-mustbeloggedin-changeauth": "{{doc-apierror}}",
+       "apierror-mustbeloggedin-generic": "{{doc-apierror}}",
+       "apierror-mustbeloggedin-linkaccounts": "{{doc-apierror}}",
+       "apierror-mustbeloggedin-removeauth": "{{doc-apierror}}",
+       "apierror-mustbeloggedin-uploadstash": "{{doc-apierror}}",
+       "apierror-mustbeposted": "{{doc-apierror}}\n\nParameters:\n* $1 - Module name.",
+       "apierror-mustpostparams": "{{doc-apierror}}\n\nParameters:\n* $1 - Parameter names.\n* $2 - Number of parameters.",
+       "apierror-no-direct-editing": "{{doc-apierror}}\n\nParameters:\n* $1 - Content model.\n* $2 - Title using the model.",
+       "apierror-noapiwrite": "{{doc-apierror}}",
+       "apierror-nochanges": "{{doc-apierror}}",
+       "apierror-nodeleteablefile": "{{doc-apierror}}",
+       "apierror-noedit": "{{doc-apierror}}",
+       "apierror-noedit-anon": "{{doc-apierror}}",
+       "apierror-noimageredirect": "{{doc-apierror}}",
+       "apierror-noimageredirect-anon": "{{doc-apierror}}",
+       "apierror-nosuchlogid": "{{doc-apierror}}\n\nParameters:\n* $1 - Log ID number.",
+       "apierror-nosuchpageid": "{{doc-apierror}}\n\nParameters:\n* $1 - Page ID number.",
+       "apierror-nosuchrcid": "{{doc-apierror}}\n\nParameters:\n* $1 - RecentChanges ID number.",
+       "apierror-nosuchrevid": "{{doc-apierror}}\n\nParameters:\n* $1 - Revision ID number.",
+       "apierror-nosuchsection": "{{doc-apierror}}\n\nParameters:\n* $1 - Section identifier. Probably a number or \"T-\" followed by a number.",
+       "apierror-nosuchsection-what": "{{doc-apierror}}\n\nParameters:\n* $1 - Section identifier. Probably a number or \"T-\" followed by a number.\n* $2 - Page title, revision ID formatted with {{msg-mw|revid}}, or page ID formatted with {{msg-mw|pageid}}.",
+       "apierror-notarget": "{{doc-apierror}}",
+       "apierror-notpatrollable": "{{doc-apierror}}\n\nParameters:\n* $1 - Revision ID number.",
+       "apierror-nouploadmodule": "{{doc-apierror}}",
+       "apierror-opensearch-json-warnings": "{{doc-apierror}}",
+       "apierror-pagecannotexist": "{{doc-apierror}}",
+       "apierror-pagedeleted": "{{doc-apierror}}",
+       "apierror-paramempty": "{{doc-apierror}}\n\nParameters:\n* $1 - Parameter name.",
+       "apierror-parsetree-notwikitext": "{{doc-apierror}}",
+       "apierror-parsetree-notwikitext-title": "{{doc-apierror}}\n\nParameters:\n* $1 - Page title.\n* $2 - Content model.",
+       "apierror-pastexpiry": "{{doc-apierror}}\n\nParameters:\n* $1 - Supplied expiry time.",
+       "apierror-permissiondenied-generic": "{{doc-apierror}}",
+       "apierror-permissiondenied-patrolflag": "{{doc-apierror}}\n\nSee also:\n* {{msg-mw|apierror-permissiondenied}}",
+       "apierror-permissiondenied-unblock": "{{doc-apierror}}\n\nSee also:\n* {{msg-mw|apierror-permissiondenied}}",
+       "apierror-permissiondenied": "{{doc-apierror}}\n\nParameters:\n* $1 - One of the action-* messages (for example {{msg-mw|action-edit}}) or other such messages tagged with {{tl|doc-action}} in their documentation\n\nPlease report at [[Support]] if you are unable to properly translate this message. Also see [[phab:T16246]] (now closed) for background.\n\nSee also:\n* {{msg-mw|permissionserrorstext-withaction}}",
+       "apierror-prefixsearchdisabled": "{{doc-apierror}}",
+       "apierror-promised-nonwrite-api": "{{doc-apierror}}",
+       "apierror-protect-invalidaction": "{{doc-apierror}}\n\nParameters:\n* $1 - Supplied protection type.",
+       "apierror-protect-invalidlevel": "{{doc-apierror}}\n\nParameters:\n* $1 - Supplied protection level.",
+       "apierror-ratelimited": "{{doc-apierror}}",
+       "apierror-readapidenied": "{{doc-apierror}}",
+       "apierror-readonly": "{{doc-apierror}}",
+       "apierror-reauthenticate": "{{doc-apierror}}",
+       "apierror-redirect-appendonly": "{{doc-apierror}}",
+       "apierror-revdel-mutuallyexclusive": "{{doc-apierror}}",
+       "apierror-revdel-needtarget": "{{doc-apierror}}",
+       "apierror-revdel-paramneeded": "{{doc-apierror}}",
+       "apierror-revisions-norevids": "{{doc-apierror}}\n\nParameters:\n* $1 - Module parameter prefix, e.g. \"bl\".",
+       "apierror-revisions-singlepage": "{{doc-apierror}}\n\nParameters:\n* $1 - Module parameter prefix, e.g. \"bl\".",
+       "apierror-revwrongpage": "{{doc-apierror}}\n\nParameters:\n* $1 - Revision ID number.\n* $2 - Page title.",
+       "apierror-searchdisabled": "{{doc-apierror}}\n\nParameters:\n* $1 - Search parameter that is disabled.",
+       "apierror-sectionreplacefailed": "{{doc-apierror}}",
+       "apierror-sectionsnotsupported": "{{doc-apierror}}\n\nParameters:\n* $1 - Content model that doesn't support sections.",
+       "apierror-sectionsnotsupported-what": "{{doc-apierror}}\n\nParameters:\n* $1 - Page title, revision ID formatted with {{msg-mw|revid}}, or page ID formatted with {{msg-mw|pageid}}.",
+       "apierror-show": "{{doc-apierror}}",
+       "apierror-siteinfo-includealldenied": "{{doc-apierror}}",
+       "apierror-sizediffdisabled": "{{doc-apierror}}",
+       "apierror-spamdetected": "{{doc-apierror}}\n\nParameters:\n* $1 - Matching \"spam filter\".\n\nSee also:\n* {{msg-mw|spamprotectionmatch}}",
+       "apierror-specialpage-cantexecute": "{{doc-apierror}}",
+       "apierror-stashedfilenotfound": "{{doc-apierror}}\n\nParameters:\n* $1 - Exception text. Currently this is probably English, hopefully we'll fix that in the future.",
+       "apierror-stashedit-missingtext": "{{doc-apierror}}",
+       "apierror-stashfailed-complete": "{{doc-apierror}}",
+       "apierror-stashfailed-nosession": "{{doc-apierror}}",
+       "apierror-stashfilestorage": "{{doc-apierror}}\n\nParameters:\n* $1 - Exception text, which may already end with punctuation. Currently this is probably English, hopefully we'll fix that in the future.",
+       "apierror-stashnosuchfilekey": "{{doc-apierror}}\n\nParameters:\n* $1 - Exception text. Currently this is probably English, hopefully we'll fix that in the future.",
+       "apierror-stashpathinvalid": "{{doc-apierror}}\n\nParameters:\n* $1 - Exception text. Currently this is probably English, hopefully we'll fix that in the future.",
+       "apierror-stashwrongowner": "{{doc-apierror}}\n\nParameters:\n* $1 - Exception text, which should already end with punctuation. Currently this is probably English, hopefully we'll fix that in the future.",
+       "apierror-stashzerolength": "{{doc-apierror}}\n\nParameters:\n* $1 - Exception text. Currently this is probably English, hopefully we'll fix that in the future.",
+       "apierror-templateexpansion-notwikitext": "{{doc-apierror}}\n\nParameters:\n* $1 - Page title.\n* $2 - Content model.",
+       "apierror-toofewexpiries": "{{doc-apierror}}\n\nParameters:\n* $1 - Number provided.\n* $2 - Number needed.",
+       "apierror-unknownaction": "{{doc-apierror}}\n\nParameters:\n* $1 - Action provided.",
+       "apierror-unknownerror": "{{doc-apierror}}\n\nParameters:\n* $1 - Error code (possibly a message key) not handled by ApiBase::parseMsg().",
+       "apierror-unknownerror-editpage": "{{doc-apierror}}\n\nParameters:\n* $1 - Error code (an integer).",
+       "apierror-unknownerror-nocode": "{{doc-apierror}}",
+       "apierror-unknownformat": "{{doc-apierror}}\n\nParameters:\n* $1 - Format provided.",
+       "apierror-unrecognizedparams": "{{doc-apierror}}\n\nParameters:\n* $1 - List of parameters.\n* $2 - Number of parameters.",
+       "apierror-unrecognizedvalue": "{{doc-apierror}}\n\nParameters:\n* $1 - Parameter name.\n* $2 - Parameter value.",
+       "apierror-unsupportedrepo": "{{doc-apierror}}",
+       "apierror-upload-filekeyneeded": "{{doc-apierror}}",
+       "apierror-upload-filekeynotallowed": "{{doc-apierror}}",
+       "apierror-upload-inprogress": "{{doc-apierror}}",
+       "apierror-upload-missingresult": "{{doc-apierror}}",
+       "apierror-urlparamnormal": "{{doc-apierror}}\n\nParameters:\n* $1 - Image title.",
+       "apierror-writeapidenied": "{{doc-apierror}}",
+       "apiwarn-alldeletedrevisions-performance": "{{doc-apierror}}\n\nParameters:\n* $1 - Module parameter prefix, e.g. \"bl\".",
+       "apiwarn-badurlparam": "{{doc-apierror}}\n\nParameters:\n* $1 - Module parameter prefix, e.g. \"bl\".\n* $2 - Image title.",
+       "apiwarn-badutf8": "{{doc-apierror}}\n\nParameters:\n* $1 - Parameter name.",
+       "apiwarn-checktoken-percentencoding": "{{doc-apierror}}",
+       "apiwarn-deprecation-deletedrevs": "{{doc-apierror}}",
+       "apiwarn-deprecation-expandtemplates-prop": "{{doc-apierror}}",
+       "apiwarn-deprecation-httpsexpected": "{{doc-apierror}}",
+       "apiwarn-deprecation-login-botpw": "{{doc-apierror}}",
+       "apiwarn-deprecation-login-nobotpw": "{{doc-apierror}}",
+       "apiwarn-deprecation-login-token": "{{doc-apierror}}",
+       "apiwarn-deprecation-parameter": "{{doc-apierror}}\n\nParameters:\n* $1 - Parameter name.",
+       "apiwarn-deprecation-parse-headitems": "{{doc-apierror}}",
+       "apiwarn-deprecation-purge-get": "{{doc-apierror}}",
+       "apiwarn-deprecation-withreplacement": "{{doc-apierror}}\n\nParameters:\n* $1 - Query string fragment that is deprecated, e.g. \"action=tokens\".\n* $2 - Query string fragment to use instead, e.g. \"action=tokens\".",
+       "apiwarn-difftohidden": "{{doc-apierror}}\n\nParameters:\n* $1 - Revision ID number.",
+       "apiwarn-errorprinterfailed": "{{doc-apierror}}",
+       "apiwarn-errorprinterfailed-ex": "{{doc-apierror}}\n\nParameters:\n* $1 - Exception message, which may already end in punctuation. Probably in English.",
+       "apiwarn-invalidcategory": "{{doc-apierror}}\n\nParameters:\n* $1 - Supplied category name.",
+       "apiwarn-invalidtitle": "{{doc-apierror}}\n\nParameters:\n* $1 - Supplied title.",
+       "apiwarn-invalidxmlstylesheet": "{{doc-apierror}}",
+       "apiwarn-invalidxmlstylesheetext": "{{doc-apierror}}",
+       "apiwarn-invalidxmlstylesheetns": "{{doc-apierror}}",
+       "apiwarn-moduleswithoutvars": "{{doc-apierror}}",
+       "apiwarn-notfile": "{{doc-apierror}}\n\nParameters:\n* $1 - Supplied file name.",
+       "apiwarn-nothumb-noimagehandler": "{{doc-apierror}}\n\nParameters:\n* $1 - File name.",
+       "apiwarn-parse-nocontentmodel": "{{doc-apierror}}\n\nParameters:\n* $1 - Content model being assumed.",
+       "apiwarn-parse-titlewithouttext": "{{doc-apierror}}",
+       "apiwarn-redirectsandrevids": "{{doc-apierror}}",
+       "apiwarn-tokennotallowed": "{{doc-apierror}}\n\nParameters:\n* $1 - Token type being requested, typically named after the action requiring the token.",
+       "apiwarn-tokens-origin": "{{doc-apierror}}",
+       "apiwarn-toomanyvalues": "{{doc-apierror}}\n\nParameters:\n* $1 - Parameter name.\n* $2 - Maximum number of values allowed.",
+       "apiwarn-truncatedresult": "{{doc-apierror}}\n\nParameters:\n* $1 - Size limit in bytes.",
+       "apiwarn-unclearnowtimestamp": "{{doc-apierror}}\n\nParameters:\n* $1 - Parameter name.\n* $2 - Supplied value.",
+       "apiwarn-unrecognizedvalues": "{{doc-apierror}}\n\nParameters:\n* $1 - Parameter name.\n* $2 - List of unknown values supplied.\n* $3 - Number of unknown values.",
+       "apiwarn-unsupportedarray": "{{doc-apierror}}\n\nParameters:\n* $1 - Parameter name.",
+       "apiwarn-urlparamwidth": "{{doc-apierror}}\n\nParameters:\n* $1 - Module parameter prefix, e.g. \"bl\".\n* $2 - Width being ignored.\n* $3 - Width being used.",
+       "apiwarn-validationfailed": "{{doc-apierror}}\n\nParameters:\n* $1 - User preference name.\n* $2 - Failure message, such as {{msg-mw|apiwarn-validationfailed-badpref}}. Probably already ends with punctuation",
+       "apiwarn-validationfailed-badchars": "{{doc-apierror}}\n\nUsed with {{msg-mw|apiwarn-validationfailed}}.",
+       "apiwarn-validationfailed-badpref": "{{doc-apierror}}\n\nUsed with {{msg-mw|apiwarn-validationfailed}}.",
+       "apiwarn-validationfailed-cannotset": "{{doc-apierror}}\n\nUsed with {{msg-mw|apiwarn-validationfailed}}.",
+       "apiwarn-validationfailed-keytoolong": "{{doc-apierror}}\n\nUsed with {{msg-mw|apiwarn-validationfailed}}.\n\nParameters:\n* $1 - Maximum allowed key length in bytes.",
+       "apiwarn-wgDebugAPI": "{{doc-apierror}}",
+       "api-feed-error-title": "Used as a feed item title when an error occurs in <kbd>action=feedwatchlist</kbd>.\n\nParameters:\n* $1 - API error code",
+       "api-usage-docref": "\n\nParameters:\n* $1 - URL of the API auto-generated documentation.",
+       "api-exception-trace": "\n\nParameters:\n* $1 - Exception class.\n* $2 - File from which the exception was thrown.\n* $3 - Line number from which the exception was thrown.\n* $4 - Exception backtrace.",
        "api-credits-header": "Header for the API credits section in the API help output\n{{Identical|Credit}}",
        "api-credits": "API credits text, displayed in the API help output"
 }
index 74b474a..5448013 100644 (file)
@@ -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()
index a550e88..c0f1c3d 100644 (file)
@@ -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' ) ) {
index cff8bf4..a8b5e5e 100644 (file)
@@ -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
index afd13f0..dad1737 100644 (file)
        "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",
        "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",
        "usercssispublic": "Please note: CSS subpages should not contain confidential data as they are viewable by other users.",
        "restrictionsfield-badip": "Invalid IP address or range: $1",
        "restrictionsfield-label": "Allowed IP ranges:",
-       "restrictionsfield-help": "One IP address or CIDR range per line. To enable everything, use<br><code>0.0.0.0/0</code><br><code>::/0</code>"
+       "restrictionsfield-help": "One IP address or CIDR range per line. To enable everything, use<br><code>0.0.0.0/0</code><br><code>::/0</code>",
+       "revid": "r$1",
+       "pageid": "page ID $1"
 }
index 936fd8b..bc4bc4c 100644 (file)
        "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}}",
        "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}}",
        "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."
 }
index cb46b11..d94b158 100644 (file)
@@ -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 ) {
index 93687df..bdec0a5 100644 (file)
@@ -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 ]
index 8b75d56..96f3e44 100644 (file)
@@ -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 ) );
+       }
+
 }
index d2dccf9..08fc128 100644 (file)
@@ -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(
index 3ad16d1..bb4ea75 100644 (file)
@@ -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'
                        );
                }
index 02d0a0d..0ffcbca 100644 (file)
@@ -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'
                );
index d13b00b..1b7f6bf 100644 (file)
@@ -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" <X> ðŸ˜Š',
+                       $wrappedFormatter->stripMarkup( 'Blah <kbd>kbd</kbd> <b>&lt;X&gt;</b> &#x1f60a;' ),
+                       '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>kbd</kbd> <b>&lt;X&gt;</b> &#x1f61e;' )
+               );
                $formatter->addError( 'err', 'mainpage' );
                $this->assertSame( [
                        'error' => [
@@ -310,6 +363,10 @@ class ApiErrorFormatterTest extends MediaWikiLangTestCase {
                                'info' => $mainpagePlain,
                        ],
                        'warnings' => [
+                               'raw' => [
+                                       'warnings' => 'Blah "kbd" <X> ðŸ˜ž',
+                                       ApiResult::META_CONTENT => 'warnings',
+                               ],
                                'string' => [
                                        'warnings' => $mainpagePlain,
                                        ApiResult::META_CONTENT => 'warnings',
@@ -321,12 +378,13 @@ class ApiErrorFormatterTest extends MediaWikiLangTestCase {
                $result->reset();
                $formatter->addWarning( 'foo', 'mainpage' );
                $formatter->addWarning( 'foo', 'mainpage' );
-               $formatter->addWarning( 'foo', [ 'parentheses', 'foobar' ] );
+               $formatter->addWarning( 'xxx+foo', [ 'parentheses', 'foobar' ] );
                $msg1 = wfMessage( 'mainpage' );
                $formatter->addWarning( 'message', $msg1 );
                $msg2 = new ApiMessage( 'mainpage', 'overriddenCode', [ 'overriddenData' => true ] );
                $formatter->addWarning( 'messageWithData', $msg2 );
                $formatter->addError( 'errWithData', $msg2 );
+               $formatter->addWarning( null, 'mainpage' );
                $this->assertSame( [
                        'error' => [
                                'code' => 'overriddenCode',
@@ -334,6 +392,10 @@ class ApiErrorFormatterTest extends MediaWikiLangTestCase {
                                'overriddenData' => true,
                        ],
                        'warnings' => [
+                               'unknown' => [
+                                       'warnings' => $mainpagePlain,
+                                       ApiResult::META_CONTENT => 'warnings',
+                               ],
                                'messageWithData' => [
                                        'warnings' => $mainpagePlain,
                                        ApiResult::META_CONTENT => 'warnings',
@@ -350,6 +412,22 @@ class ApiErrorFormatterTest extends MediaWikiLangTestCase {
                        ApiResult::META_TYPE => 'assoc',
                ], $result->getResultData(), 'Complex test' );
 
+               $this->assertSame(
+                       [
+                               'code' => 'mainpage',
+                               'info' => 'Main Page',
+                       ],
+                       $formatter->formatMessage( $msg1 )
+               );
+               $this->assertSame(
+                       [
+                               'code' => 'overriddenCode',
+                               'info' => 'Main Page',
+                               'overriddenData' => true,
+                       ],
+                       $formatter->formatMessage( $msg2 )
+               );
+
                $result->reset();
                $status = Status::newGood();
                $status->warning( 'mainpage' );
@@ -377,14 +455,16 @@ class ApiErrorFormatterTest extends MediaWikiLangTestCase {
                $this->assertSame(
                        [
                                [
-                                       'type' => 'error',
                                        'message' => 'mainpage',
-                                       'params' => [ $I => 'param' ]
+                                       'params' => [ $I => 'param' ],
+                                       'code' => 'mainpage',
+                                       'type' => 'error',
                                ],
                                [
-                                       'type' => 'error',
                                        'message' => 'parentheses',
-                                       'params' => [ 'foobar', $I => 'param' ]
+                                       'params' => [ 'foobar', $I => 'param' ],
+                                       'code' => 'parentheses',
+                                       'type' => 'error',
                                ],
                                $I => 'error',
                        ],
@@ -394,24 +474,28 @@ class ApiErrorFormatterTest extends MediaWikiLangTestCase {
                $this->assertSame(
                        [
                                [
-                                       'type' => 'warning',
                                        'message' => 'mainpage',
-                                       'params' => [ $I => 'param' ]
+                                       'params' => [ $I => 'param' ],
+                                       'code' => 'mainpage',
+                                       'type' => 'warning',
                                ],
                                [
-                                       'type' => 'warning',
                                        'message' => 'parentheses',
-                                       'params' => [ 'foobar', $I => 'param' ]
+                                       'params' => [ 'foobar', $I => 'param' ],
+                                       'code' => 'parentheses',
+                                       'type' => 'warning',
                                ],
                                [
                                        'message' => 'mainpage',
                                        'params' => [ $I => 'param' ],
-                                       'type' => 'warning'
+                                       'code' => 'mainpage',
+                                       'type' => 'warning',
                                ],
                                [
                                        'message' => 'mainpage',
                                        'params' => [ $I => 'param' ],
-                                       'type' => 'warning'
+                                       'code' => 'overriddenCode',
+                                       'type' => 'warning',
                                ],
                                $I => 'warning',
                        ],
index c111949..c9a3428 100644 (file)
@@ -53,8 +53,8 @@ class ApiMainTest extends ApiTestCase {
                                'assert' => $assert,
                        ], null, null, $user );
                        $this->assertFalse( $error ); // That no error was expected
-               } catch ( UsageException $e ) {
-                       $this->assertEquals( $e->getCodeString(), $error );
+               } catch ( ApiUsageException $e ) {
+                       $this->assertTrue( self::apiExceptionHasCode( $e, $error ) );
                }
        }
 
@@ -76,8 +76,8 @@ class ApiMainTest extends ApiTestCase {
                                'assertuser' => $user->getName() . 'X',
                        ], null, null, $user );
                        $this->fail( 'Expected exception not thrown' );
-               } catch ( UsageException $e ) {
-                       $this->assertEquals( $e->getCodeString(), 'assertnameduserfailed' );
+               } catch ( ApiUsageException $e ) {
+                       $this->assertTrue( self::apiExceptionHasCode( $e, 'assertnameduserfailed' ) );
                }
        }
 
@@ -305,4 +305,274 @@ class ApiMainTest extends ApiTestCase {
                $main = new ApiMain( new FauxRequest( [ 'action' => 'query', 'meta' => 'siteinfo' ] ) );
                $this->assertTrue( $main->lacksSameOriginSecurity(), 'Hook, should lack security' );
        }
+
+       /**
+        * Test proper creation of the ApiErrorFormatter
+        * @covers ApiMain::__construct
+        * @dataProvider provideApiErrorFormatterCreation
+        * @param array $request Request parameters
+        * @param array $expect Expected data
+        *  - uselang: ApiMain language
+        *  - class: ApiErrorFormatter class
+        *  - lang: ApiErrorFormatter language
+        *  - format: ApiErrorFormatter format
+        *  - usedb: ApiErrorFormatter use-database flag
+        */
+       public function testApiErrorFormatterCreation( array $request, array $expect ) {
+               $context = new RequestContext();
+               $context->setRequest( new FauxRequest( $request ) );
+               $context->setLanguage( 'ru' );
+
+               $main = new ApiMain( $context );
+               $formatter = $main->getErrorFormatter();
+               $wrappedFormatter = TestingAccessWrapper::newFromObject( $formatter );
+
+               $this->assertSame( $expect['uselang'], $main->getLanguage()->getCode() );
+               $this->assertInstanceOf( $expect['class'], $formatter );
+               $this->assertSame( $expect['lang'], $formatter->getLanguage()->getCode() );
+               $this->assertSame( $expect['format'], $wrappedFormatter->format );
+               $this->assertSame( $expect['usedb'], $wrappedFormatter->useDB );
+       }
+
+       public static function provideApiErrorFormatterCreation() {
+               global $wgContLang;
+
+               return [
+                       'Default (BC)' => [ [], [
+                               'uselang' => 'ru',
+                               'class' => ApiErrorFormatter_BackCompat::class,
+                               'lang' => 'en',
+                               'format' => 'none',
+                               'usedb' => false,
+                       ] ],
+                       'BC ignores fields' => [ [ 'errorlang' => 'de', 'errorsuselocal' => 1 ], [
+                               'uselang' => 'ru',
+                               'class' => ApiErrorFormatter_BackCompat::class,
+                               'lang' => 'en',
+                               'format' => 'none',
+                               'usedb' => false,
+                       ] ],
+                       'Explicit BC' => [ [ 'errorformat' => 'bc' ], [
+                               'uselang' => 'ru',
+                               'class' => ApiErrorFormatter_BackCompat::class,
+                               'lang' => 'en',
+                               'format' => 'none',
+                               'usedb' => false,
+                       ] ],
+                       'Basic' => [ [ 'errorformat' => 'wikitext' ], [
+                               'uselang' => 'ru',
+                               'class' => ApiErrorFormatter::class,
+                               'lang' => 'ru',
+                               'format' => 'wikitext',
+                               'usedb' => false,
+                       ] ],
+                       'Follows uselang' => [ [ 'uselang' => 'fr', 'errorformat' => 'plaintext' ], [
+                               'uselang' => 'fr',
+                               'class' => ApiErrorFormatter::class,
+                               'lang' => 'fr',
+                               'format' => 'plaintext',
+                               'usedb' => false,
+                       ] ],
+                       'Explicitly follows uselang' => [
+                               [ 'uselang' => 'fr', 'errorlang' => 'uselang', 'errorformat' => 'plaintext' ],
+                               [
+                                       'uselang' => 'fr',
+                                       'class' => ApiErrorFormatter::class,
+                                       'lang' => 'fr',
+                                       'format' => 'plaintext',
+                                       'usedb' => false,
+                               ]
+                       ],
+                       'uselang=content' => [
+                               [ 'uselang' => 'content', 'errorformat' => 'plaintext' ],
+                               [
+                                       'uselang' => $wgContLang->getCode(),
+                                       'class' => ApiErrorFormatter::class,
+                                       'lang' => $wgContLang->getCode(),
+                                       'format' => 'plaintext',
+                                       'usedb' => false,
+                               ]
+                       ],
+                       'errorlang=content' => [
+                               [ 'errorlang' => 'content', 'errorformat' => 'plaintext' ],
+                               [
+                                       'uselang' => 'ru',
+                                       'class' => ApiErrorFormatter::class,
+                                       'lang' => $wgContLang->getCode(),
+                                       'format' => 'plaintext',
+                                       'usedb' => false,
+                               ]
+                       ],
+                       'Explicit parameters' => [
+                               [ 'errorlang' => 'de', 'errorformat' => 'html', 'errorsuselocal' => 1 ],
+                               [
+                                       'uselang' => 'ru',
+                                       'class' => ApiErrorFormatter::class,
+                                       'lang' => 'de',
+                                       'format' => 'html',
+                                       'usedb' => true,
+                               ]
+                       ],
+                       'Explicit parameters override uselang' => [
+                               [ 'errorlang' => 'de', 'uselang' => 'fr', 'errorformat' => 'raw' ],
+                               [
+                                       'uselang' => 'fr',
+                                       'class' => ApiErrorFormatter::class,
+                                       'lang' => 'de',
+                                       'format' => 'raw',
+                                       'usedb' => false,
+                               ]
+                       ],
+                       'Bogus language doesn\'t explode' => [
+                               [ 'errorlang' => '<bogus1>', 'uselang' => '<bogus2>', 'errorformat' => 'none' ],
+                               [
+                                       'uselang' => 'en',
+                                       'class' => ApiErrorFormatter::class,
+                                       'lang' => 'en',
+                                       'format' => 'none',
+                                       'usedb' => false,
+                               ]
+                       ],
+                       'Bogus format doesn\'t explode' => [ [ 'errorformat' => 'bogus' ], [
+                               'uselang' => 'ru',
+                               'class' => ApiErrorFormatter_BackCompat::class,
+                               'lang' => 'en',
+                               'format' => 'none',
+                               'usedb' => false,
+                       ] ],
+               ];
+       }
+
+       /**
+        * @covers ApiMain::errorMessagesFromException
+        * @covers ApiMain::substituteResultWithError
+        * @dataProvider provideExceptionErrors
+        * @param Exception $exception
+        * @param array $expectReturn
+        * @param array $expectResult
+        */
+       public function testExceptionErrors( $error, $expectReturn, $expectResult ) {
+               $context = new RequestContext();
+               $context->setRequest( new FauxRequest( [ 'errorformat' => 'plaintext' ] ) );
+               $context->setLanguage( 'en' );
+               $context->setConfig( new MultiConfig( [
+                       new HashConfig( [ 'ShowHostnames' => true, 'ShowSQLErrors' => false ] ),
+                       $context->getConfig()
+               ] ) );
+
+               $main = new ApiMain( $context );
+               $main->addWarning( new RawMessage( 'existing warning' ), 'existing-warning' );
+               $main->addError( new RawMessage( 'existing error' ), 'existing-error' );
+
+               $ret = TestingAccessWrapper::newFromObject( $main )->substituteResultWithError( $error );
+               $this->assertSame( $expectReturn, $ret );
+
+               // PHPUnit sometimes adds some SplObjectStorage garbage to the arrays,
+               // so let's try ->assertEquals().
+               $this->assertEquals(
+                       $expectResult,
+                       $main->getResult()->getResultData( [], [ 'Strip' => 'all' ] )
+               );
+       }
+
+       // Not static so $this->getMock() can be used
+       public function provideExceptionErrors() {
+               $reqId = WebRequest::getRequestId();
+               $doclink = wfExpandUrl( wfScript( 'api' ) );
+
+               $ex = new InvalidArgumentException( 'Random exception' );
+               $trace = wfMessage( 'api-exception-trace',
+                       get_class( $ex ),
+                       $ex->getFile(),
+                       $ex->getLine(),
+                       MWExceptionHandler::getRedactedTraceAsString( $ex )
+               )->inLanguage( 'en' )->useDatabase( false )->text();
+
+               $dbex = new DBQueryError( $this->getMock( 'IDatabase' ), 'error', 1234, 'SELECT 1', __METHOD__ );
+               $dbtrace = wfMessage( 'api-exception-trace',
+                       get_class( $dbex ),
+                       $dbex->getFile(),
+                       $dbex->getLine(),
+                       MWExceptionHandler::getRedactedTraceAsString( $dbex )
+               )->inLanguage( 'en' )->useDatabase( false )->text();
+
+               $apiEx1 = new ApiUsageException( null,
+                       StatusValue::newFatal( new ApiRawMessage( 'An error', 'sv-error1' ) ) );
+               TestingAccessWrapper::newFromObject( $apiEx1 )->modulePath = 'foo+bar';
+               $apiEx1->getStatusValue()->warning( new ApiRawMessage( 'A warning', 'sv-warn1' ) );
+               $apiEx1->getStatusValue()->warning( new ApiRawMessage( 'Another warning', 'sv-warn2' ) );
+               $apiEx1->getStatusValue()->fatal( new ApiRawMessage( 'Another error', 'sv-error2' ) );
+
+               return [
+                       [
+                               $ex,
+                               [ 'existing-error', 'internal_api_error_InvalidArgumentException' ],
+                               [
+                                       'warnings' => [
+                                               [ 'code' => 'existing-warning', 'text' => 'existing warning', 'module' => 'main' ],
+                                       ],
+                                       'errors' => [
+                                               [ 'code' => 'existing-error', 'text' => 'existing error', 'module' => 'main' ],
+                                               [
+                                                       'code' => 'internal_api_error_InvalidArgumentException',
+                                                       'text' => "[$reqId] Exception caught: Random exception",
+                                               ]
+                                       ],
+                                       'trace' => $trace,
+                                       'servedby' => wfHostname(),
+                               ]
+                       ],
+                       [
+                               $dbex,
+                               [ 'existing-error', 'internal_api_error_DBQueryError' ],
+                               [
+                                       'warnings' => [
+                                               [ 'code' => 'existing-warning', 'text' => 'existing warning', 'module' => 'main' ],
+                                       ],
+                                       'errors' => [
+                                               [ 'code' => 'existing-error', 'text' => 'existing error', 'module' => 'main' ],
+                                               [
+                                                       'code' => 'internal_api_error_DBQueryError',
+                                                       'text' => "[$reqId] Database query error.",
+                                               ]
+                                       ],
+                                       'trace' => $dbtrace,
+                                       'servedby' => wfHostname(),
+                               ]
+                       ],
+                       [
+                               new UsageException( 'Usage exception!', 'ue', 0, [ 'foo' => 'bar' ] ),
+                               [ 'existing-error', 'ue' ],
+                               [
+                                       'warnings' => [
+                                               [ 'code' => 'existing-warning', 'text' => 'existing warning', 'module' => 'main' ],
+                                       ],
+                                       'errors' => [
+                                               [ 'code' => 'existing-error', 'text' => 'existing error', 'module' => 'main' ],
+                                               [ 'code' => 'ue', 'text' => "Usage exception!", 'data' => [ 'foo' => 'bar' ] ]
+                                       ],
+                                       'docref' => "See $doclink for API usage.",
+                                       'servedby' => wfHostname(),
+                               ]
+                       ],
+                       [
+                               $apiEx1,
+                               [ 'existing-error', 'sv-error1', 'sv-error2' ],
+                               [
+                                       'warnings' => [
+                                               [ 'code' => 'existing-warning', 'text' => 'existing warning', 'module' => 'main' ],
+                                               [ 'code' => 'sv-warn1', 'text' => 'A warning', 'module' => 'foo+bar' ],
+                                               [ 'code' => 'sv-warn2', 'text' => 'Another warning', 'module' => 'foo+bar' ],
+                                       ],
+                                       'errors' => [
+                                               [ 'code' => 'existing-error', 'text' => 'existing error', 'module' => 'main' ],
+                                               [ 'code' => 'sv-error1', 'text' => 'An error', 'module' => 'foo+bar' ],
+                                               [ 'code' => 'sv-error2', 'text' => 'Another error', 'module' => 'foo+bar' ],
+                                       ],
+                                       'docref' => "See $doclink for API usage.",
+                                       'servedby' => wfHostname(),
+                               ]
+                       ],
+               ];
+       }
 }
index 8764b41..e405b3b 100644 (file)
@@ -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 ) );
        }
 
index 0a577c1..ef70626 100644 (file)
@@ -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 );
index b72a4f8..f01a670 100644 (file)
@@ -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 )
                        );
                }
        }
index eaeb3ae..0a2cd83 100644 (file)
@@ -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(),
index d6f315d..0f01664 100644 (file)
@@ -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(),
index 7e1f9d8..6b299c9 100644 (file)
@@ -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(
index b63bf2e..971b63c 100644 (file)
@@ -14,7 +14,7 @@ class ApiUnblockTest extends ApiTestCase {
        }
 
        /**
-        * @expectedException UsageException
+        * @expectedException ApiUsageException
         */
        public function testWithNoToken() {
                $this->doApiRequest(
index de2b56b..9b79e6c 100644 (file)
@@ -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'] ) );
index 19afc14..0cd2707 100644 (file)
@@ -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() . "'" );
                        }
                }
        }
index d7db538..1407c10 100644 (file)
@@ -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;
        }
 
index f5b50e5..9915a38 100644 (file)
@@ -12,4 +12,8 @@ class MockApiQueryBase extends ApiQueryBase {
        public function getModuleName() {
                return $this->name;
        }
+
+       public function getModulePath() {
+               return 'query+' . $this->getModuleName();
+       }
 }
index 0028bbb..3aa1db3 100644 (file)
@@ -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'
                        );
                }
index 3fef0b0..0f8c8ee 100644 (file)
@@ -105,11 +105,11 @@ class ApiFormatXmlTest extends ApiFormatTestBase {
                                [ 'includexmlnamespace' => 1 ] ],
 
                        // xslt param
-                       [ [], '<?xml version="1.0"?><api><warnings><xml xml:space="preserve">Invalid or non-existent stylesheet specified</xml></warnings></api>',
+                       [ [], '<?xml version="1.0"?><api><warnings><xml xml:space="preserve">Invalid or non-existent stylesheet specified.</xml></warnings></api>',
                                [ 'xslt' => 'DoesNotExist' ] ],
                        [ [], '<?xml version="1.0"?><api><warnings><xml xml:space="preserve">Stylesheet should be in the MediaWiki namespace.</xml></warnings></api>',
                                [ 'xslt' => 'ApiFormatXmlTest' ] ],
-                       [ [], '<?xml version="1.0"?><api><warnings><xml xml:space="preserve">Stylesheet should have .xsl extension.</xml></warnings></api>',
+                       [ [], '<?xml version="1.0"?><api><warnings><xml xml:space="preserve">Stylesheet should have &quot;.xsl&quot; extension.</xml></warnings></api>',
                                [ 'xslt' => 'MediaWiki:ApiFormatXmlTest' ] ],
                        [ [],
                                '<?xml version="1.0"?><?xml-stylesheet href="' .
index 8cb2327..9407edf 100644 (file)
@@ -99,11 +99,11 @@ class ApiQueryTest extends ApiTestCase {
                $exceptionCaught = false;
                try {
                        $this->assertEquals( $expected, $api->titlePartToKey( $titlePart, $namespace ) );
-               } catch ( UsageException $e ) {
+               } catch ( ApiUsageException $e ) {
                        $exceptionCaught = true;
                }
                $this->assertEquals( $expectException, $exceptionCaught,
-                       'UsageException thrown by titlePartToKey' );
+                       'ApiUsageException thrown by titlePartToKey' );
        }
 
        function provideTestTitlePartToKey() {
index 6d17a68..62081aa 100644 (file)
@@ -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() );
                }