aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--RELEASE-NOTES-1.2944
-rw-r--r--autoload.php3
-rw-r--r--docs/hooks.txt10
-rw-r--r--includes/FileDeleteForm.php2
-rw-r--r--includes/WatchedItemQueryService.php5
-rw-r--r--includes/api/ApiAMCreateAccount.php4
-rw-r--r--includes/api/ApiAuthManagerHelper.php10
-rw-r--r--includes/api/ApiBase.php1362
-rw-r--r--includes/api/ApiBlock.php23
-rw-r--r--includes/api/ApiCSPReport.php13
-rw-r--r--includes/api/ApiChangeAuthenticationData.php4
-rw-r--r--includes/api/ApiCheckToken.php4
-rw-r--r--includes/api/ApiClientLogin.php4
-rw-r--r--includes/api/ApiComparePages.php18
-rw-r--r--includes/api/ApiContinuationManager.php7
-rw-r--r--includes/api/ApiDelete.php20
-rw-r--r--includes/api/ApiDisabled.php2
-rw-r--r--includes/api/ApiEditPage.php192
-rw-r--r--includes/api/ApiEmailUser.php38
-rw-r--r--includes/api/ApiErrorFormatter.php230
-rw-r--r--includes/api/ApiExpandTemplates.php16
-rw-r--r--includes/api/ApiFeedContributions.php6
-rw-r--r--includes/api/ApiFeedRecentChanges.php6
-rw-r--r--includes/api/ApiFeedWatchlist.php27
-rw-r--r--includes/api/ApiFileRevert.php27
-rw-r--r--includes/api/ApiFormatJson.php4
-rw-r--r--includes/api/ApiFormatPhp.php9
-rw-r--r--includes/api/ApiFormatRaw.php8
-rw-r--r--includes/api/ApiFormatXml.php6
-rw-r--r--includes/api/ApiImageRotate.php48
-rw-r--r--includes/api/ApiImport.php8
-rw-r--r--includes/api/ApiLinkAccount.php6
-rw-r--r--includes/api/ApiLogin.php24
-rw-r--r--includes/api/ApiLogout.php8
-rw-r--r--includes/api/ApiMain.php397
-rw-r--r--includes/api/ApiManageTags.php6
-rw-r--r--includes/api/ApiMergeHistory.php8
-rw-r--r--includes/api/ApiMessage.php117
-rw-r--r--includes/api/ApiMove.php26
-rw-r--r--includes/api/ApiOpenSearch.php10
-rw-r--r--includes/api/ApiOptions.php26
-rw-r--r--includes/api/ApiPageSet.php26
-rw-r--r--includes/api/ApiParamInfo.php15
-rw-r--r--includes/api/ApiParse.php82
-rw-r--r--includes/api/ApiPatrol.php11
-rw-r--r--includes/api/ApiProtect.php22
-rw-r--r--includes/api/ApiPurge.php9
-rw-r--r--includes/api/ApiQuery.php8
-rw-r--r--includes/api/ApiQueryAllDeletedRevisions.php30
-rw-r--r--includes/api/ApiQueryAllImages.php52
-rw-r--r--includes/api/ApiQueryAllLinks.php10
-rw-r--r--includes/api/ApiQueryAllMessages.php6
-rw-r--r--includes/api/ApiQueryAllPages.php10
-rw-r--r--includes/api/ApiQueryAllUsers.php4
-rw-r--r--includes/api/ApiQueryBacklinks.php4
-rw-r--r--includes/api/ApiQueryBacklinksprop.php2
-rw-r--r--includes/api/ApiQueryBase.php8
-rw-r--r--includes/api/ApiQueryBlocks.php19
-rw-r--r--includes/api/ApiQueryCategories.php4
-rw-r--r--includes/api/ApiQueryCategoryMembers.php8
-rw-r--r--includes/api/ApiQueryDeletedRevisions.php18
-rw-r--r--includes/api/ApiQueryDeletedrevs.php31
-rw-r--r--includes/api/ApiQueryDisabled.php2
-rw-r--r--includes/api/ApiQueryFilearchive.php13
-rw-r--r--includes/api/ApiQueryIWBacklinks.php9
-rw-r--r--includes/api/ApiQueryIWLinks.php9
-rw-r--r--includes/api/ApiQueryImageInfo.php19
-rw-r--r--includes/api/ApiQueryImages.php2
-rw-r--r--includes/api/ApiQueryInfo.php2
-rw-r--r--includes/api/ApiQueryLangBacklinks.php9
-rw-r--r--includes/api/ApiQueryLangLinks.php9
-rw-r--r--includes/api/ApiQueryLinks.php2
-rw-r--r--includes/api/ApiQueryLogEvents.php14
-rw-r--r--includes/api/ApiQueryMyStashedFiles.php2
-rw-r--r--includes/api/ApiQueryQueryPage.php2
-rw-r--r--includes/api/ApiQueryRecentChanges.php18
-rw-r--r--includes/api/ApiQueryRevisions.php40
-rw-r--r--includes/api/ApiQueryRevisionsBase.php46
-rw-r--r--includes/api/ApiQuerySearch.php14
-rw-r--r--includes/api/ApiQuerySiteinfo.php5
-rw-r--r--includes/api/ApiQueryStashImageInfo.php11
-rw-r--r--includes/api/ApiQueryTokens.php2
-rw-r--r--includes/api/ApiQueryUserContributions.php22
-rw-r--r--includes/api/ApiQueryUserInfo.php9
-rw-r--r--includes/api/ApiQueryUsers.php4
-rw-r--r--includes/api/ApiQueryWatchlist.php13
-rw-r--r--includes/api/ApiQueryWatchlistRaw.php2
-rw-r--r--includes/api/ApiRemoveAuthenticationData.php4
-rw-r--r--includes/api/ApiResetPassword.php2
-rw-r--r--includes/api/ApiResult.php8
-rw-r--r--includes/api/ApiRevisionDelete.php16
-rw-r--r--includes/api/ApiRollback.php26
-rw-r--r--includes/api/ApiSetNotificationTimestamp.php18
-rw-r--r--includes/api/ApiStashEdit.php26
-rw-r--r--includes/api/ApiTag.php8
-rw-r--r--includes/api/ApiTokens.php8
-rw-r--r--includes/api/ApiUnblock.php19
-rw-r--r--includes/api/ApiUndelete.php10
-rw-r--r--includes/api/ApiUpload.php237
-rw-r--r--includes/api/ApiUsageException.php217
-rw-r--r--includes/api/ApiWatch.php23
-rw-r--r--includes/api/i18n/en.json240
-rw-r--r--includes/api/i18n/qqq.json234
-rw-r--r--includes/specials/SpecialApiHelp.php5
-rw-r--r--includes/specials/SpecialEmailuser.php35
-rw-r--r--includes/specials/SpecialUnblock.php4
-rw-r--r--languages/i18n/en.json15
-rw-r--r--languages/i18n/qqq.json7
-rw-r--r--resources/src/mediawiki/page/rollback.js1
-rw-r--r--tests/phpunit/includes/WatchedItemQueryServiceUnitTest.php2
-rw-r--r--tests/phpunit/includes/api/ApiBaseTest.php58
-rw-r--r--tests/phpunit/includes/api/ApiBlockTest.php4
-rw-r--r--tests/phpunit/includes/api/ApiContinuationManagerTest.php6
-rw-r--r--tests/phpunit/includes/api/ApiEditPageTest.php16
-rw-r--r--tests/phpunit/includes/api/ApiErrorFormatterTest.php430
-rw-r--r--tests/phpunit/includes/api/ApiMainTest.php278
-rw-r--r--tests/phpunit/includes/api/ApiMessageTest.php78
-rw-r--r--tests/phpunit/includes/api/ApiOptionsTest.php32
-rw-r--r--tests/phpunit/includes/api/ApiParseTest.php8
-rw-r--r--tests/phpunit/includes/api/ApiQueryWatchlistIntegrationTest.php4
-rw-r--r--tests/phpunit/includes/api/ApiQueryWatchlistRawIntegrationTest.php4
-rw-r--r--tests/phpunit/includes/api/ApiTestCase.php22
-rw-r--r--tests/phpunit/includes/api/ApiUnblockTest.php2
-rw-r--r--tests/phpunit/includes/api/ApiUploadTest.php30
-rw-r--r--tests/phpunit/includes/api/ApiWatchTest.php6
-rw-r--r--tests/phpunit/includes/api/MockApi.php6
-rw-r--r--tests/phpunit/includes/api/MockApiQueryBase.php4
-rw-r--r--tests/phpunit/includes/api/format/ApiFormatPhpTest.php8
-rw-r--r--tests/phpunit/includes/api/format/ApiFormatXmlTest.php4
-rw-r--r--tests/phpunit/includes/api/query/ApiQueryTest.php4
-rw-r--r--tests/phpunit/includes/upload/UploadFromUrlTest.php8
131 files changed, 3382 insertions, 2242 deletions
diff --git a/RELEASE-NOTES-1.29 b/RELEASE-NOTES-1.29
index 21a94c5d4ae8..b055ade42b14 100644
--- a/RELEASE-NOTES-1.29
+++ b/RELEASE-NOTES-1.29
@@ -14,6 +14,14 @@ production.
will still be blocked.
* The resetpassword right and associated password reset capture feature has
been removed.
+* The $error parameter to the EmailUser hook should be set to a Status object
+ or boolean false. This should be compatible with at least MediaWiki 1.23 if
+ not earlier. Returning a raw HTML string is now deprecated.
+* The $message parameter to the ApiCheckCanExecute hook should be set to an
+ ApiMessage. This is compatible with MediaWiki 1.27 and later. Returning a
+ code for ApiBase::parseMsg() will no longer work.
+* ApiBase::$messageMap is no longer public. Code attempting to access it will
+ result in a PHP fatal error.
=== New features in 1.29 ===
* (T5233) A cookie can now be set when a user is autoblocked, to track that user if
@@ -37,8 +45,44 @@ production.
body instead.
* The capture option for action=resetpassword has been removed
* action=clearhasmsg now requires a POST.
+* (T47843) API errors and warnings may be requested in non-English languages
+ using the new 'errorformat', 'errorlang', and 'errorsuselocal' parameters.
+* API error codes may have changed. Most notably, errors from modules using
+ parameter prefixes (e.g. all query submodules) will no longer be prefixed.
+* action=emailuser may return a "Warnings" status, and now returns 'warnings' and
+ 'errors' subelements (as applicable) instead of 'message'.
+* action=imagerotate returns an 'errors' subelement rather than 'errormessage'.
+* action=move now reports errors when moving the talk page as an array under
+ key 'talkmove-errors', rather than using 'talkmove-error-code' and
+ 'talkmove-error-info'. The format for subpage move errors has also changed.
+* action=rollback no longer returns a "messageHtml" property on errors. Use
+ errorformat=html if you're wanting HTML formatting of messages.
+* action=upload now reports optional stash failures as an array under key
+ 'stasherrors' rather than a 'stashfailed' text string.
+* action=watch reports 'errors' and 'warnings' instead of a single 'error'.
=== Action API internal changes in 1.29 ===
+* New methods were added to ApiBase to handle errors and warnings using i18n
+ keys. Methods for using hard-coded English messages were deprecated:
+ * ApiBase::dieUsage() was deprecated
+ * ApiBase::dieUsageMsg() was deprecated
+ * ApiBase::dieUsageMsgOrDebug() was deprecated
+ * ApiBase::getErrorFromStatus() was deprecated
+ * ApiBase::parseMsg() was deprecated
+ * ApiBase::setWarning() was deprecated
+* ApiBase::$messageMap is no longer public. Code attempting to access it will
+ result in a PHP fatal error.
+* The $message parameter to the ApiCheckCanExecute hook should be set to an
+ ApiMessage. This is compatible with MediaWiki 1.27 and later. Returning a
+ code for ApiBase::parseMsg() will no longer work.
+* UsageException is deprecated in favor of ApiUsageException. For the time
+ being ApiUsageException is a subclass of UsageException to allow things that
+ catch only UsageException to still function properly.
+* If, for some strange reason, code was using an ApiErrorFormatter instead of
+ ApiErrorFormatter_BackCompat, note that the result format has changed and
+ various methods now take a module path rather than a module name.
+* ApiMessageTrait::getApiCode() now strips 'apierror-' and 'apiwarn-' prefixes
+ from the message key, and maps some message keys for backwards compatibility.
=== Languages updated in 1.29 ===
diff --git a/autoload.php b/autoload.php
index 0d6407bc20c0..bf36f9ffd96b 100644
--- a/autoload.php
+++ b/autoload.php
@@ -145,6 +145,7 @@ $wgAutoloadLocalClasses = [
'ApiUnblock' => __DIR__ . '/includes/api/ApiUnblock.php',
'ApiUndelete' => __DIR__ . '/includes/api/ApiUndelete.php',
'ApiUpload' => __DIR__ . '/includes/api/ApiUpload.php',
+ 'ApiUsageException' => __DIR__ . '/includes/api/ApiUsageException.php',
'ApiUserrights' => __DIR__ . '/includes/api/ApiUserrights.php',
'ApiWatch' => __DIR__ . '/includes/api/ApiWatch.php',
'ArchivedFile' => __DIR__ . '/includes/filerepo/file/ArchivedFile.php',
@@ -1503,7 +1504,7 @@ $wgAutoloadLocalClasses = [
'UploadStashWrongOwnerException' => __DIR__ . '/includes/upload/UploadStash.php',
'UploadStashZeroLengthFileException' => __DIR__ . '/includes/upload/UploadStash.php',
'UppercaseCollation' => __DIR__ . '/includes/collation/UppercaseCollation.php',
- 'UsageException' => __DIR__ . '/includes/api/ApiMain.php',
+ 'UsageException' => __DIR__ . '/includes/api/ApiUsageException.php',
'User' => __DIR__ . '/includes/user/User.php',
'UserArray' => __DIR__ . '/includes/user/UserArray.php',
'UserArrayFromResult' => __DIR__ . '/includes/user/UserArrayFromResult.php',
diff --git a/docs/hooks.txt b/docs/hooks.txt
index a73d50f9bd71..b88a87a70126 100644
--- a/docs/hooks.txt
+++ b/docs/hooks.txt
@@ -358,8 +358,12 @@ authenticate and authorize API clients before executing the module. Return
false and set a message to cancel the request.
$module: Module object
$user: Current user
-&$message: API usage message to die with, as a message key or array
- as accepted by ApiBase::dieUsageMsg.
+&$message: API message to die with. Specific values accepted depend on the
+ MediaWiki version:
+ * 1.29+: IApiMessage, Message, string message key, or key+parameters array to
+ pass to ApiBase::dieWithError().
+ * 1.27+: IApiMessage, or a key or key+parameters in ApiBase::$messageMap.
+ * Earlier: A key or key+parameters in ApiBase::$messageMap.
'APIEditBeforeSave': DEPRECATED! Use EditFilterMergedContent instead.
Before saving a page with api.php?action=edit, after
@@ -1459,7 +1463,7 @@ true to allow those checks to occur, and false if checking is done.
&$from: MailAddress object of sending user
&$subject: subject of the mail
&$text: text of the mail
-&$error: Out-param for an error
+&$error: Out-param for an error. Should be set to a Status object or boolean false.
'EmailUserCC': Before sending the copy of the email to the author.
&$to: MailAddress object of receiving user
diff --git a/includes/FileDeleteForm.php b/includes/FileDeleteForm.php
index e6223e81b84e..f850152050c0 100644
--- a/includes/FileDeleteForm.php
+++ b/includes/FileDeleteForm.php
@@ -152,7 +152,7 @@ class FileDeleteForm {
* @param User $user User object performing the request
* @param array $tags Tags to apply to the deletion action
* @throws MWException
- * @return bool|Status
+ * @return Status
*/
public static function doDelete( &$title, &$file, &$oldimage, $reason,
$suppress, User $user = null, $tags = []
diff --git a/includes/WatchedItemQueryService.php b/includes/WatchedItemQueryService.php
index 0c3d52a39fef..cd78b499df3a 100644
--- a/includes/WatchedItemQueryService.php
+++ b/includes/WatchedItemQueryService.php
@@ -422,10 +422,7 @@ class WatchedItemQueryService {
$ownersToken = $watchlistOwner->getOption( 'watchlisttoken' );
$token = $options['watchlistOwnerToken'];
if ( $ownersToken == '' || !hash_equals( $ownersToken, $token ) ) {
- throw new UsageException(
- 'Incorrect watchlist token provided -- please set a correct token in Special:Preferences',
- 'bad_wltoken'
- );
+ throw ApiUsageException::newWithMessage( null, 'apierror-bad-watchlist-token', 'bad_wltoken' );
}
return $watchlistOwner->getId();
}
diff --git a/includes/api/ApiAMCreateAccount.php b/includes/api/ApiAMCreateAccount.php
index 2511e3be99cb..5d12590fdf77 100644
--- a/includes/api/ApiAMCreateAccount.php
+++ b/includes/api/ApiAMCreateAccount.php
@@ -56,8 +56,8 @@ class ApiAMCreateAccount extends ApiBase {
$bits = wfParseUrl( $params['returnurl'] );
if ( !$bits || $bits['scheme'] === '' ) {
$encParamName = $this->encodeParamName( 'returnurl' );
- $this->dieUsage(
- "Invalid value '{$params['returnurl']}' for url parameter $encParamName",
+ $this->dieWithError(
+ [ 'apierror-badurl', $encParamName, wfEscapeWikiText( $params['returnurl'] ) ],
"badurl_{$encParamName}"
);
}
diff --git a/includes/api/ApiAuthManagerHelper.php b/includes/api/ApiAuthManagerHelper.php
index 6fafebff3b49..5327d7a99bca 100644
--- a/includes/api/ApiAuthManagerHelper.php
+++ b/includes/api/ApiAuthManagerHelper.php
@@ -93,7 +93,7 @@ class ApiAuthManagerHelper {
/**
* Call $manager->securitySensitiveOperationStatus()
* @param string $operation Operation being checked.
- * @throws UsageException
+ * @throws ApiUsageException
*/
public function securitySensitiveOperation( $operation ) {
$status = AuthManager::singleton()->securitySensitiveOperationStatus( $operation );
@@ -102,14 +102,10 @@ class ApiAuthManagerHelper {
return;
case AuthManager::SEC_REAUTH:
- $this->module->dieUsage(
- 'You have not authenticated recently in this session, please reauthenticate.', 'reauthenticate'
- );
+ $this->module->dieWithError( 'apierror-reauthenticate' );
case AuthManager::SEC_FAIL:
- $this->module->dieUsage(
- 'This action is not available as your identify cannot be verified.', 'cannotreauthenticate'
- );
+ $this->module->dieWithError( 'apierror-cannotreauthenticate' );
default:
throw new UnexpectedValueException( "Unknown status \"$status\"" );
diff --git a/includes/api/ApiBase.php b/includes/api/ApiBase.php
index 0cd46e42b492..a40593f6bd5a 100644
--- a/includes/api/ApiBase.php
+++ b/includes/api/ApiBase.php
@@ -545,7 +545,7 @@ abstract class ApiBase extends ContextSource {
* @since 1.25
* @param string $path
* @return ApiBase|null
- * @throws UsageException
+ * @throws ApiUsageException
*/
public function getModuleFromPath( $path ) {
$module = $this->getMain();
@@ -565,14 +565,14 @@ abstract class ApiBase extends ContextSource {
$manager = $parent->getModuleManager();
if ( $manager === null ) {
$errorPath = implode( '+', array_slice( $parts, 0, $i ) );
- $this->dieUsage( "The module \"$errorPath\" has no submodules", 'badmodule' );
+ $this->dieWithError( [ 'apierror-badmodule-nosubmodules', $errorPath ], 'badmodule' );
}
$module = $manager->getModule( $parts[$i] );
if ( $module === null ) {
$errorPath = $i ? implode( '+', array_slice( $parts, 0, $i ) ) : $parent->getModuleName();
- $this->dieUsage(
- "The module \"$errorPath\" does not have a submodule \"{$parts[$i]}\"",
+ $this->dieWithError(
+ [ 'apierror-badmodule-badsubmodule', $errorPath, wfEscapeWikiText( $parts[$i] ) ],
'badmodule'
);
}
@@ -670,11 +670,18 @@ abstract class ApiBase extends ContextSource {
/**
* This method mangles parameter name based on the prefix supplied to the constructor.
* Override this method to change parameter name during runtime
- * @param string $paramName Parameter name
- * @return string Prefixed parameter name
+ * @param string|string[] $paramName Parameter name
+ * @return string|string[] Prefixed parameter name
+ * @since 1.29 accepts an array of strings
*/
public function encodeParamName( $paramName ) {
- return $this->mModulePrefix . $paramName;
+ if ( is_array( $paramName ) ) {
+ return array_map( function ( $name ) {
+ return $this->mModulePrefix . $name;
+ }, $paramName );
+ } else {
+ return $this->mModulePrefix . $paramName;
+ }
}
/**
@@ -725,20 +732,32 @@ abstract class ApiBase extends ContextSource {
public function requireOnlyOneParameter( $params, $required /*...*/ ) {
$required = func_get_args();
array_shift( $required );
- $p = $this->getModulePrefix();
$intersection = array_intersect( array_keys( array_filter( $params,
[ $this, 'parameterNotEmpty' ] ) ), $required );
if ( count( $intersection ) > 1 ) {
- $this->dieUsage(
- "The parameters {$p}" . implode( ", {$p}", $intersection ) . ' can not be used together',
- 'invalidparammix' );
+ $this->dieWithError( [
+ 'apierror-invalidparammix',
+ Message::listParam( array_map(
+ function ( $p ) {
+ return '<var>' . $this->encodeParamName( $p ) . '</var>';
+ },
+ array_values( $intersection )
+ ) ),
+ count( $intersection ),
+ ] );
} elseif ( count( $intersection ) == 0 ) {
- $this->dieUsage(
- "One of the parameters {$p}" . implode( ", {$p}", $required ) . ' is required',
- 'missingparam'
- );
+ $this->dieWithError( [
+ 'apierror-missingparam-one-of',
+ Message::listParam( array_map(
+ function ( $p ) {
+ return '<var>' . $this->encodeParamName( $p ) . '</var>';
+ },
+ array_values( $required )
+ ) ),
+ count( $required ),
+ ], 'missingparam' );
}
}
@@ -751,16 +770,21 @@ abstract class ApiBase extends ContextSource {
public function requireMaxOneParameter( $params, $required /*...*/ ) {
$required = func_get_args();
array_shift( $required );
- $p = $this->getModulePrefix();
$intersection = array_intersect( array_keys( array_filter( $params,
[ $this, 'parameterNotEmpty' ] ) ), $required );
if ( count( $intersection ) > 1 ) {
- $this->dieUsage(
- "The parameters {$p}" . implode( ", {$p}", $intersection ) . ' can not be used together',
- 'invalidparammix'
- );
+ $this->dieWithError( [
+ 'apierror-invalidparammix',
+ Message::listParam( array_map(
+ function ( $p ) {
+ return '<var>' . $this->encodeParamName( $p ) . '</var>';
+ },
+ array_values( $intersection )
+ ) ),
+ count( $intersection ),
+ ] );
}
}
@@ -774,7 +798,6 @@ abstract class ApiBase extends ContextSource {
public function requireAtLeastOneParameter( $params, $required /*...*/ ) {
$required = func_get_args();
array_shift( $required );
- $p = $this->getModulePrefix();
$intersection = array_intersect(
array_keys( array_filter( $params, [ $this, 'parameterNotEmpty' ] ) ),
@@ -782,8 +805,16 @@ abstract class ApiBase extends ContextSource {
);
if ( count( $intersection ) == 0 ) {
- $this->dieUsage( "At least one of the parameters {$p}" .
- implode( ", {$p}", $required ) . ' is required', "{$p}missingparam" );
+ $this->dieWithError( [
+ 'apierror-missingparam-at-least-one-of',
+ Message::listParam( array_map(
+ function ( $p ) {
+ return '<var>' . $this->encodeParamName( $p ) . '</var>';
+ },
+ array_values( $required )
+ ) ),
+ count( $required ),
+ ], 'missingparam' );
}
}
@@ -812,10 +843,8 @@ abstract class ApiBase extends ContextSource {
}
if ( $badParams ) {
- $this->dieUsage(
- 'The following parameters were found in the query string, but must be in the POST body: '
- . join( ', ', $badParams ),
- 'mustpostparams'
+ $this->dieWithError(
+ [ 'apierror-mustpostparams', join( ', ', $badParams ), count( $badParams ) ]
);
}
}
@@ -848,10 +877,10 @@ abstract class ApiBase extends ContextSource {
if ( isset( $params['title'] ) ) {
$titleObj = Title::newFromText( $params['title'] );
if ( !$titleObj || $titleObj->isExternal() ) {
- $this->dieUsageMsg( [ 'invalidtitle', $params['title'] ] );
+ $this->dieWithError( [ 'apierror-invalidtitle', wfEscapeWikiText( $params['title'] ) ] );
}
if ( !$titleObj->canExist() ) {
- $this->dieUsage( "Namespace doesn't allow actual pages", 'pagecannotexist' );
+ $this->dieWithError( 'apierror-pagecannotexist' );
}
$pageObj = WikiPage::factory( $titleObj );
if ( $load !== false ) {
@@ -863,7 +892,7 @@ abstract class ApiBase extends ContextSource {
}
$pageObj = WikiPage::newFromID( $params['pageid'], $load );
if ( !$pageObj ) {
- $this->dieUsageMsg( [ 'nosuchpageid', $params['pageid'] ] );
+ $this->dieWithError( [ 'apierror-nosuchpageid', $params['pageid'] ] );
}
}
@@ -994,10 +1023,8 @@ abstract class ApiBase extends ContextSource {
// accidentally uploaded as a field fails spectacularly)
$value = $this->getMain()->getRequest()->unsetVal( $encParamName );
if ( $value !== null ) {
- $this->dieUsage(
- "File upload param $encParamName is not a file upload; " .
- 'be sure to use multipart/form-data for your POST and include ' .
- 'a filename in the Content-Disposition header.',
+ $this->dieWithError(
+ [ 'apierror-badupload', $encParamName ],
"badupload_{$encParamName}"
);
}
@@ -1032,10 +1059,7 @@ abstract class ApiBase extends ContextSource {
// done by WebRequest for $_GET. Let's call that a feature.
$value = join( "\x1f", $request->normalizeUnicode( explode( "\x1f", $rawValue ) ) );
} else {
- $this->dieUsage(
- "U+001F multi-value separation may only be used for multi-valued parameters.",
- 'badvalue_notmultivalue'
- );
+ $this->dieWithError( 'apierror-badvalue-notmultivalue', 'badvalue_notmultivalue' );
}
}
@@ -1072,7 +1096,7 @@ abstract class ApiBase extends ContextSource {
case 'text':
case 'password':
if ( $required && $value === '' ) {
- $this->dieUsageMsg( [ 'missingparam', $paramName ] );
+ $this->dieWithError( [ 'apierror-missingparam', $paramName ] );
}
break;
case 'integer': // Force everything using intval() and optionally validate limits
@@ -1175,8 +1199,6 @@ abstract class ApiBase extends ContextSource {
// Set a warning if a deprecated parameter has been passed
if ( $deprecated && $value !== false ) {
- $this->setWarning( "The $encParamName parameter has been deprecated." );
-
$feature = $encParamName;
$m = $this;
while ( !$m->isMain() ) {
@@ -1186,10 +1208,10 @@ abstract class ApiBase extends ContextSource {
$feature = "{$param}={$name}&{$feature}";
$m = $p;
}
- $this->logFeatureUsage( $feature );
+ $this->addDeprecation( [ 'apiwarn-deprecation-parameter', $encParamName ], $feature );
}
} elseif ( $required ) {
- $this->dieUsageMsg( [ 'missingparam', $paramName ] );
+ $this->dieWithError( [ 'apierror-missingparam', $paramName ] );
}
return $value;
@@ -1204,11 +1226,7 @@ abstract class ApiBase extends ContextSource {
*/
protected function handleParamNormalization( $paramName, $value, $rawValue ) {
$encParamName = $this->encodeParamName( $paramName );
- $this->setWarning(
- "The value passed for '$encParamName' contains invalid or non-normalized data. "
- . 'Textual data should be valid, NFC-normalized Unicode without '
- . 'C0 control characters other than HT (\\t), LF (\\n), and CR (\\r).'
- );
+ $this->addWarning( [ 'apiwarn-badutf8', $encParamName ] );
}
/**
@@ -1265,9 +1283,10 @@ abstract class ApiBase extends ContextSource {
}
if ( self::truncateArray( $valuesList, $sizeLimit ) ) {
- $this->logFeatureUsage( "too-many-$valueName-for-{$this->getModulePath()}" );
- $this->setWarning( "Too many values supplied for parameter '$valueName': " .
- "the limit is $sizeLimit" );
+ $this->addDeprecation(
+ [ 'apiwarn-toomanyvalues', $valueName, $sizeLimit ],
+ "too-many-$valueName-for-{$this->getModulePath()}"
+ );
}
if ( !$allowMultiple && count( $valuesList ) != 1 ) {
@@ -1276,26 +1295,38 @@ abstract class ApiBase extends ContextSource {
return $value;
}
- $possibleValues = is_array( $allowedValues )
- ? "of '" . implode( "', '", $allowedValues ) . "'"
- : '';
- $this->dieUsage(
- "Only one $possibleValues is allowed for parameter '$valueName'",
- "multival_$valueName"
- );
+ if ( is_array( $allowedValues ) ) {
+ $values = array_map( function ( $v ) {
+ return '<kbd>' . wfEscapeWikiText( $v ) . '</kbd>';
+ }, $allowedValues );
+ $this->dieWithError( [
+ 'apierror-multival-only-one-of',
+ $valueName,
+ Message::listParam( $values ),
+ count( $values ),
+ ], "multival_$valueName" );
+ } else {
+ $this->dieWithError( [
+ 'apierror-multival-only-one',
+ $valueName,
+ ], "multival_$valueName" );
+ }
}
if ( is_array( $allowedValues ) ) {
// Check for unknown values
- $unknown = array_diff( $valuesList, $allowedValues );
+ $unknown = array_map( 'wfEscapeWikiText', array_diff( $valuesList, $allowedValues ) );
if ( count( $unknown ) ) {
if ( $allowMultiple ) {
- $s = count( $unknown ) > 1 ? 's' : '';
- $vals = implode( ', ', $unknown );
- $this->setWarning( "Unrecognized value$s for parameter '$valueName': $vals" );
+ $this->addWarning( [
+ 'apiwarn-unrecognizedvalues',
+ $valueName,
+ Message::listParam( $unknown, 'comma' ),
+ count( $unknown ),
+ ] );
} else {
- $this->dieUsage(
- "Unrecognized value for parameter '$valueName': {$valuesList[0]}",
+ $this->dieWithError(
+ [ 'apierror-unrecognizedvalue', $valueName, wfEscapeWikiText( $valuesList[0] ) ],
"unknown_$valueName"
);
}
@@ -1321,7 +1352,12 @@ abstract class ApiBase extends ContextSource {
$enforceLimits = false
) {
if ( !is_null( $min ) && $value < $min ) {
- $msg = $this->encodeParamName( $paramName ) . " may not be less than $min (set to $value)";
+ $msg = ApiMessage::create(
+ [ 'apierror-integeroutofrange-belowminimum',
+ $this->encodeParamName( $paramName ), $min, $value ],
+ 'integeroutofrange',
+ [ 'min' => $min, 'max' => $max, 'botMax' => $botMax ?: $max ]
+ );
$this->warnOrDie( $msg, $enforceLimits );
$value = $min;
}
@@ -1337,13 +1373,22 @@ abstract class ApiBase extends ContextSource {
if ( !is_null( $max ) && $value > $max ) {
if ( !is_null( $botMax ) && $this->getMain()->canApiHighLimits() ) {
if ( $value > $botMax ) {
- $msg = $this->encodeParamName( $paramName ) .
- " may not be over $botMax (set to $value) for bots or sysops";
+ $msg = ApiMessage::create(
+ [ 'apierror-integeroutofrange-abovebotmax',
+ $this->encodeParamName( $paramName ), $botMax, $value ],
+ 'integeroutofrange',
+ [ 'min' => $min, 'max' => $max, 'botMax' => $botMax ?: $max ]
+ );
$this->warnOrDie( $msg, $enforceLimits );
$value = $botMax;
}
} else {
- $msg = $this->encodeParamName( $paramName ) . " may not be over $max (set to $value) for users";
+ $msg = ApiMessage::create(
+ [ 'apierror-integeroutofrange-abovemax',
+ $this->encodeParamName( $paramName ), $max, $value ],
+ 'integeroutofrange',
+ [ 'min' => $min, 'max' => $max, 'botMax' => $botMax ?: $max ]
+ );
$this->warnOrDie( $msg, $enforceLimits );
$value = $max;
}
@@ -1361,11 +1406,9 @@ abstract class ApiBase extends ContextSource {
// (wfTimestamp() also accepts various non-strings and the string of 14
// ASCII NUL bytes, but those can't get here)
if ( !$value ) {
- $this->logFeatureUsage( 'unclear-"now"-timestamp' );
- $this->setWarning(
- "Passing '$value' for timestamp parameter $encParamName has been deprecated." .
- ' If for some reason you need to explicitly specify the current time without' .
- ' calculating it client-side, use "now".'
+ $this->addDeprecation(
+ [ 'apiwarn-unclearnowtimestamp', $encParamName, wfEscapeWikiText( $value ) ],
+ 'unclear-"now"-timestamp'
);
return wfTimestamp( TS_MW );
}
@@ -1377,8 +1420,8 @@ abstract class ApiBase extends ContextSource {
$unixTimestamp = wfTimestamp( TS_UNIX, $value );
if ( $unixTimestamp === false ) {
- $this->dieUsage(
- "Invalid value '$value' for timestamp parameter $encParamName",
+ $this->dieWithError(
+ [ 'apierror-badtimestamp', $encParamName, wfEscapeWikiText( $value ) ],
"badtimestamp_{$encParamName}"
);
}
@@ -1433,8 +1476,8 @@ abstract class ApiBase extends ContextSource {
private function validateUser( $value, $encParamName ) {
$title = Title::makeTitleSafe( NS_USER, $value );
if ( $title === null || $title->hasFragment() ) {
- $this->dieUsage(
- "Invalid value '$value' for user parameter $encParamName",
+ $this->dieWithError(
+ [ 'apierror-baduser', $encParamName, wfEscapeWikiText( $value ) ],
"baduser_{$encParamName}"
);
}
@@ -1490,22 +1533,19 @@ abstract class ApiBase extends ContextSource {
if ( !is_null( $params['owner'] ) && !is_null( $params['token'] ) ) {
$user = User::newFromName( $params['owner'], false );
if ( !( $user && $user->getId() ) ) {
- $this->dieUsage( 'Specified user does not exist', 'bad_wlowner' );
+ $this->dieWithError(
+ [ 'nosuchusershort', wfEscapeWikiText( $params['owner'] ) ], 'bad_wlowner'
+ );
}
$token = $user->getOption( 'watchlisttoken' );
if ( $token == '' || !hash_equals( $token, $params['token'] ) ) {
- $this->dieUsage(
- 'Incorrect watchlist token provided -- please set a correct token in Special:Preferences',
- 'bad_wltoken'
- );
+ $this->dieWithError( 'apierror-bad-watchlist-token', 'bad_wltoken' );
}
} else {
if ( !$this->getUser()->isLoggedIn() ) {
- $this->dieUsage( 'You must be logged-in to have a watchlist', 'notloggedin' );
- }
- if ( !$this->getUser()->isAllowed( 'viewmywatchlist' ) ) {
- $this->dieUsage( 'You don\'t have permission to view your watchlist', 'permissiondenied' );
+ $this->dieWithError( 'watchlistanontext', 'notloggedin' );
}
+ $this->checkUserRightsAny( 'viewmywatchlist' );
$user = $this->getUser();
}
@@ -1561,6 +1601,39 @@ abstract class ApiBase extends ContextSource {
return $msg;
}
+ /**
+ * Turn an array of message keys or key+param arrays into a Status
+ * @since 1.29
+ * @param array $errors
+ * @param User|null $user
+ * @return Status
+ */
+ public function errorArrayToStatus( array $errors, User $user = null ) {
+ if ( $user === null ) {
+ $user = $this->getUser();
+ }
+
+ $status = Status::newGood();
+ foreach ( $errors as $error ) {
+ if ( is_array( $error ) && $error[0] === 'blockedtext' && $user->getBlock() ) {
+ $status->fatal( ApiMessage::create(
+ 'apierror-blocked',
+ 'blocked',
+ [ 'blockinfo' => ApiQueryUserInfo::getBlockInfo( $user->getBlock() ) ]
+ ) );
+ } elseif ( is_array( $error ) && $error[0] === 'autoblockedtext' && $user->getBlock() ) {
+ $status->fatal( ApiMessage::create(
+ 'apierror-autoblocked',
+ 'autoblocked',
+ [ 'blockinfo' => ApiQueryUserInfo::getBlockInfo( $user->getBlock() ) ]
+ ) );
+ } else {
+ call_user_func_array( [ $status, 'fatal' ], (array)$error );
+ }
+ }
+ return $status;
+ }
+
/**@}*/
/************************************************************************//**
@@ -1569,745 +1642,227 @@ abstract class ApiBase extends ContextSource {
*/
/**
- * Set warning section for this module. Users should monitor this
- * section to notice any changes in API. Multiple calls to this
- * function will result in the warning messages being separated by
- * newlines
- * @param string $warning Warning message
+ * Add a warning for this module.
+ *
+ * Users should monitor this section to notice any changes in API. Multiple
+ * calls to this function will result in multiple warning messages.
+ *
+ * If $msg is not an ApiMessage, the message code will be derived from the
+ * message key by stripping any "apiwarn-" or "apierror-" prefix.
+ *
+ * @since 1.29
+ * @param string|array|Message $msg See ApiErrorFormatter::addWarning()
+ * @param string|null $code See ApiErrorFormatter::addWarning()
+ * @param array|null $data See ApiErrorFormatter::addWarning()
*/
- public function setWarning( $warning ) {
- $msg = new ApiRawMessage( $warning, 'warning' );
- $this->getErrorFormatter()->addWarning( $this->getModuleName(), $msg );
+ public function addWarning( $msg, $code = null, $data = null ) {
+ $this->getErrorFormatter()->addWarning( $this->getModulePath(), $msg, $code, $data );
}
/**
- * Adds a warning to the output, else dies
+ * Add a deprecation warning for this module.
*
- * @param string $msg Message to show as a warning, or error message if dying
- * @param bool $enforceLimits Whether this is an enforce (die)
+ * A combination of $this->addWarning() and $this->logFeatureUsage()
+ *
+ * @since 1.29
+ * @param string|array|Message $msg See ApiErrorFormatter::addWarning()
+ * @param string|null $feature See ApiBase::logFeatureUsage()
+ * @param array|null $data See ApiErrorFormatter::addWarning()
*/
- private function warnOrDie( $msg, $enforceLimits = false ) {
- if ( $enforceLimits ) {
- $this->dieUsage( $msg, 'integeroutofrange' );
+ public function addDeprecation( $msg, $feature, $data = [] ) {
+ $data = (array)$data;
+ if ( $feature !== null ) {
+ $data['feature'] = $feature;
+ $this->logFeatureUsage( $feature );
}
+ $this->addWarning( $msg, 'deprecation', $data );
+ }
- $this->setWarning( $msg );
+ /**
+ * Add an error for this module without aborting
+ *
+ * If $msg is not an ApiMessage, the message code will be derived from the
+ * message key by stripping any "apiwarn-" or "apierror-" prefix.
+ *
+ * @note If you want to abort processing, use self::dieWithError() instead.
+ * @since 1.29
+ * @param string|array|Message $msg See ApiErrorFormatter::addError()
+ * @param string|null $code See ApiErrorFormatter::addError()
+ * @param array|null $data See ApiErrorFormatter::addError()
+ */
+ public function addError( $msg, $code = null, $data = null ) {
+ $this->getErrorFormatter()->addError( $this->getModulePath(), $msg, $code, $data );
}
/**
- * Throw a UsageException, which will (if uncaught) call the main module's
- * error handler and die with an error message.
+ * Add warnings and/or errors from a Status
*
- * @param string $description One-line human-readable description of the
- * error condition, e.g., "The API requires a valid action parameter"
- * @param string $errorCode Brief, arbitrary, stable string to allow easy
- * automated identification of the error, e.g., 'unknown_action'
- * @param int $httpRespCode HTTP response code
- * @param array|null $extradata Data to add to the "<error>" element; array in ApiResult format
- * @throws UsageException always
+ * @note If you want to abort processing, use self::dieStatus() instead.
+ * @since 1.29
+ * @param StatusValue $status
+ * @param string[] $types 'warning' and/or 'error'
*/
- public function dieUsage( $description, $errorCode, $httpRespCode = 0, $extradata = null ) {
- throw new UsageException(
- $description,
- $this->encodeParamName( $errorCode ),
- $httpRespCode,
- $extradata
- );
+ public function addMessagesFromStatus( StatusValue $status, $types = [ 'warning', 'error' ] ) {
+ $this->getErrorFormatter()->addMessagesFromStatus( $this->getModulePath(), $status, $types );
+ }
+
+ /**
+ * Abort execution with an error
+ *
+ * If $msg is not an ApiMessage, the message code will be derived from the
+ * message key by stripping any "apiwarn-" or "apierror-" prefix.
+ *
+ * @since 1.29
+ * @param string|array|Message $msg See ApiErrorFormatter::addError()
+ * @param string|null $code See ApiErrorFormatter::addError()
+ * @param array|null $data See ApiErrorFormatter::addError()
+ * @param int|null $httpCode HTTP error code to use
+ * @throws ApiUsageException always
+ */
+ public function dieWithError( $msg, $code = null, $data = null, $httpCode = null ) {
+ throw ApiUsageException::newWithMessage( $this, $msg, $code, $data, $httpCode );
}
/**
- * Throw a UsageException, which will (if uncaught) call the main module's
+ * Adds a warning to the output, else dies
+ *
+ * @param ApiMessage $msg Message to show as a warning, or error message if dying
+ * @param bool $enforceLimits Whether this is an enforce (die)
+ */
+ private function warnOrDie( ApiMessage $msg, $enforceLimits = false ) {
+ if ( $enforceLimits ) {
+ $this->dieWithError( $msg );
+ } else {
+ $this->addWarning( $msg );
+ }
+ }
+
+ /**
+ * Throw an ApiUsageException, which will (if uncaught) call the main module's
* error handler and die with an error message including block info.
*
* @since 1.27
- * @param Block $block The block used to generate the UsageException
- * @throws UsageException always
+ * @param Block $block The block used to generate the ApiUsageException
+ * @throws ApiUsageException always
*/
public function dieBlocked( Block $block ) {
// Die using the appropriate message depending on block type
if ( $block->getType() == Block::TYPE_AUTO ) {
- $this->dieUsage(
- 'Your IP address has been blocked automatically, because it was used by a blocked user',
+ $this->dieWithError(
+ 'apierror-autoblocked',
'autoblocked',
- 0,
[ 'blockinfo' => ApiQueryUserInfo::getBlockInfo( $block ) ]
);
} else {
- $this->dieUsage(
- 'You have been blocked from editing',
+ $this->dieWithError(
+ 'apierror-blocked',
'blocked',
- 0,
[ 'blockinfo' => ApiQueryUserInfo::getBlockInfo( $block ) ]
);
}
}
/**
- * Get error (as code, string) from a Status object.
+ * Throw an ApiUsageException based on the Status object.
*
- * @since 1.23
- * @param Status $status
- * @param array|null &$extraData Set if extra data from IApiMessage is available (since 1.27)
- * @return array Array of code and error string
- * @throws MWException
+ * @since 1.22
+ * @since 1.29 Accepts a StatusValue
+ * @param StatusValue $status
+ * @throws ApiUsageException always
*/
- public function getErrorFromStatus( $status, &$extraData = null ) {
+ public function dieStatus( StatusValue $status ) {
if ( $status->isGood() ) {
throw new MWException( 'Successful status passed to ApiBase::dieStatus' );
}
- $errors = $status->getErrorsByType( 'error' );
- if ( !$errors ) {
- // No errors? Assume the warnings should be treated as errors
- $errors = $status->getErrorsByType( 'warning' );
- }
- if ( !$errors ) {
- // Still no errors? Punt
- $errors = [ [ 'message' => 'unknownerror-nocode', 'params' => [] ] ];
- }
-
- // Cannot use dieUsageMsg() because extensions might return custom
- // error messages.
- if ( $errors[0]['message'] instanceof Message ) {
- $msg = $errors[0]['message'];
- if ( $msg instanceof IApiMessage ) {
- $extraData = $msg->getApiData();
- $code = $msg->getApiCode();
- } else {
- $code = $msg->getKey();
- }
- } else {
- $code = $errors[0]['message'];
- $msg = wfMessage( $code, $errors[0]['params'] );
- }
- if ( isset( ApiBase::$messageMap[$code] ) ) {
- // Translate message to code, for backwards compatibility
- $code = ApiBase::$messageMap[$code]['code'];
- }
-
- return [ $code, $msg->inLanguage( 'en' )->useDatabase( false )->plain() ];
+ throw new ApiUsageException( $this, $status );
}
/**
- * Throw a UsageException based on the errors in the Status object.
- *
- * @since 1.22
- * @param Status $status
- * @throws UsageException always
- */
- public function dieStatus( $status ) {
- $extraData = null;
- list( $code, $msg ) = $this->getErrorFromStatus( $status, $extraData );
- $this->dieUsage( $msg, $code, 0, $extraData );
- }
-
- // @codingStandardsIgnoreStart Allow long lines. Cannot split these.
- /**
- * Array that maps message keys to error messages. $1 and friends are replaced.
- */
- public static $messageMap = [
- // This one MUST be present, or dieUsageMsg() will recurse infinitely
- 'unknownerror' => [ 'code' => 'unknownerror', 'info' => "Unknown error: \"\$1\"" ],
- 'unknownerror-nocode' => [ 'code' => 'unknownerror', 'info' => 'Unknown error' ],
-
- // Messages from Title::getUserPermissionsErrors()
- 'ns-specialprotected' => [
- 'code' => 'unsupportednamespace',
- 'info' => "Pages in the Special namespace can't be edited"
- ],
- 'protectedinterface' => [
- 'code' => 'protectednamespace-interface',
- 'info' => "You're not allowed to edit interface messages"
- ],
- 'namespaceprotected' => [
- 'code' => 'protectednamespace',
- 'info' => "You're not allowed to edit pages in the \"\$1\" namespace"
- ],
- 'customcssprotected' => [
- 'code' => 'customcssprotected',
- 'info' => "You're not allowed to edit custom CSS pages"
- ],
- 'customjsprotected' => [
- 'code' => 'customjsprotected',
- 'info' => "You're not allowed to edit custom JavaScript pages"
- ],
- 'cascadeprotected' => [
- 'code' => 'cascadeprotected',
- 'info' => "The page you're trying to edit is protected because it's included in a cascade-protected page"
- ],
- 'protectedpagetext' => [
- 'code' => 'protectedpage',
- 'info' => "The \"\$1\" right is required to edit this page"
- ],
- 'protect-cantedit' => [
- 'code' => 'cantedit',
- 'info' => "You can't protect this page because you can't edit it"
- ],
- 'deleteprotected' => [
- 'code' => 'cantedit',
- 'info' => "You can't delete this page because it has been protected"
- ],
- 'badaccess-group0' => [
- 'code' => 'permissiondenied',
- 'info' => 'Permission denied'
- ], // Generic permission denied message
- 'badaccess-groups' => [
- 'code' => 'permissiondenied',
- 'info' => 'Permission denied'
- ],
- 'titleprotected' => [
- 'code' => 'protectedtitle',
- 'info' => 'This title has been protected from creation'
- ],
- 'nocreate-loggedin' => [
- 'code' => 'cantcreate',
- 'info' => "You don't have permission to create new pages"
- ],
- 'nocreatetext' => [
- 'code' => 'cantcreate-anon',
- 'info' => "Anonymous users can't create new pages"
- ],
- 'movenologintext' => [
- 'code' => 'cantmove-anon',
- 'info' => "Anonymous users can't move pages"
- ],
- 'movenotallowed' => [
- 'code' => 'cantmove',
- 'info' => "You don't have permission to move pages"
- ],
- 'confirmedittext' => [
- 'code' => 'confirmemail',
- 'info' => 'You must confirm your email address before you can edit'
- ],
- 'blockedtext' => [
- 'code' => 'blocked',
- 'info' => 'You have been blocked from editing'
- ],
- 'autoblockedtext' => [
- 'code' => 'autoblocked',
- 'info' => 'Your IP address has been blocked automatically, because it was used by a blocked user'
- ],
-
- // Miscellaneous interface messages
- 'actionthrottledtext' => [
- 'code' => 'ratelimited',
- 'info' => "You've exceeded your rate limit. Please wait some time and try again"
- ],
- 'alreadyrolled' => [
- 'code' => 'alreadyrolled',
- 'info' => 'The page you tried to rollback was already rolled back'
- ],
- 'cantrollback' => [
- 'code' => 'onlyauthor',
- 'info' => 'The page you tried to rollback only has one author'
- ],
- 'readonlytext' => [
- 'code' => 'readonly',
- 'info' => 'The wiki is currently in read-only mode'
- ],
- 'sessionfailure' => [
- 'code' => 'badtoken',
- 'info' => 'Invalid token' ],
- 'cannotdelete' => [
- 'code' => 'cantdelete',
- 'info' => "Couldn't delete \"\$1\". Maybe it was deleted already by someone else"
- ],
- 'notanarticle' => [
- 'code' => 'missingtitle',
- 'info' => "The page you requested doesn't exist"
- ],
- 'selfmove' => [ 'code' => 'selfmove', 'info' => "Can't move a page to itself"
- ],
- 'immobile_namespace' => [
- 'code' => 'immobilenamespace',
- 'info' => 'You tried to move pages from or to a namespace that is protected from moving'
- ],
- 'articleexists' => [
- 'code' => 'articleexists',
- 'info' => 'The destination article already exists and is not a redirect to the source article'
- ],
- 'protectedpage' => [
- 'code' => 'protectedpage',
- 'info' => "You don't have permission to perform this move"
- ],
- 'hookaborted' => [
- 'code' => 'hookaborted',
- 'info' => 'The modification you tried to make was aborted by an extension hook'
- ],
- 'cantmove-titleprotected' => [
- 'code' => 'protectedtitle',
- 'info' => 'The destination article has been protected from creation'
- ],
- 'imagenocrossnamespace' => [
- 'code' => 'nonfilenamespace',
- 'info' => "Can't move a file to a non-file namespace"
- ],
- 'imagetypemismatch' => [
- 'code' => 'filetypemismatch',
- 'info' => "The new file extension doesn't match its type"
- ],
- // 'badarticleerror' => shouldn't happen
- // 'badtitletext' => shouldn't happen
- 'ip_range_invalid' => [ 'code' => 'invalidrange', 'info' => 'Invalid IP range' ],
- 'range_block_disabled' => [
- 'code' => 'rangedisabled',
- 'info' => 'Blocking IP ranges has been disabled'
- ],
- 'nosuchusershort' => [
- 'code' => 'nosuchuser',
- 'info' => "The user you specified doesn't exist"
- ],
- 'badipaddress' => [ 'code' => 'invalidip', 'info' => 'Invalid IP address specified' ],
- 'ipb_expiry_invalid' => [ 'code' => 'invalidexpiry', 'info' => 'Invalid expiry time' ],
- 'ipb_already_blocked' => [
- 'code' => 'alreadyblocked',
- 'info' => 'The user you tried to block was already blocked'
- ],
- 'ipb_blocked_as_range' => [
- 'code' => 'blockedasrange',
- 'info' => "IP address \"\$1\" was blocked as part of range \"\$2\". You can't unblock the IP individually, but you can unblock the range as a whole."
- ],
- 'ipb_cant_unblock' => [
- 'code' => 'cantunblock',
- 'info' => 'The block you specified was not found. It may have been unblocked already'
- ],
- 'mailnologin' => [
- 'code' => 'cantsend',
- 'info' => 'You are not logged in, you do not have a confirmed email address, or you are not allowed to send email to other users, so you cannot send email'
- ],
- 'ipbblocked' => [
- 'code' => 'ipbblocked',
- 'info' => 'You cannot block or unblock users while you are yourself blocked'
- ],
- 'ipbnounblockself' => [
- 'code' => 'ipbnounblockself',
- 'info' => 'You are not allowed to unblock yourself'
- ],
- 'usermaildisabled' => [
- 'code' => 'usermaildisabled',
- 'info' => 'User email has been disabled'
- ],
- 'blockedemailuser' => [
- 'code' => 'blockedfrommail',
- 'info' => 'You have been blocked from sending email'
- ],
- 'notarget' => [
- 'code' => 'notarget',
- 'info' => 'You have not specified a valid target for this action'
- ],
- 'noemail' => [
- 'code' => 'noemail',
- 'info' => 'The user has not specified a valid email address, or has chosen not to receive email from other users'
- ],
- 'rcpatroldisabled' => [
- 'code' => 'patroldisabled',
- 'info' => 'Patrolling is disabled on this wiki'
- ],
- 'markedaspatrollederror-noautopatrol' => [
- 'code' => 'noautopatrol',
- 'info' => "You don't have permission to patrol your own changes"
- ],
- 'delete-toobig' => [
- 'code' => 'bigdelete',
- 'info' => "You can't delete this page because it has more than \$1 revisions"
- ],
- 'movenotallowedfile' => [
- 'code' => 'cantmovefile',
- 'info' => "You don't have permission to move files"
- ],
- 'userrights-no-interwiki' => [
- 'code' => 'nointerwikiuserrights',
- 'info' => "You don't have permission to change user rights on other wikis"
- ],
- 'userrights-nodatabase' => [
- 'code' => 'nosuchdatabase',
- 'info' => "Database \"\$1\" does not exist or is not local"
- ],
- 'nouserspecified' => [ 'code' => 'invaliduser', 'info' => "Invalid username \"\$1\"" ],
- 'noname' => [ 'code' => 'invaliduser', 'info' => "Invalid username \"\$1\"" ],
- 'summaryrequired' => [ 'code' => 'summaryrequired', 'info' => 'Summary required' ],
- 'import-rootpage-invalid' => [
- 'code' => 'import-rootpage-invalid',
- 'info' => 'Root page is an invalid title'
- ],
- 'import-rootpage-nosubpage' => [
- 'code' => 'import-rootpage-nosubpage',
- 'info' => 'Namespace "$1" of the root page does not allow subpages'
- ],
-
- // API-specific messages
- 'readrequired' => [
- 'code' => 'readapidenied',
- 'info' => 'You need read permission to use this module'
- ],
- 'writedisabled' => [
- 'code' => 'noapiwrite',
- 'info' => "Editing of this wiki through the API is disabled. Make sure the \$wgEnableWriteAPI=true; statement is included in the wiki's LocalSettings.php file"
- ],
- 'writerequired' => [
- 'code' => 'writeapidenied',
- 'info' => "You're not allowed to edit this wiki through the API"
- ],
- 'missingparam' => [ 'code' => 'no$1', 'info' => "The \$1 parameter must be set" ],
- 'invalidtitle' => [ 'code' => 'invalidtitle', 'info' => "Bad title \"\$1\"" ],
- 'nosuchpageid' => [ 'code' => 'nosuchpageid', 'info' => "There is no page with ID \$1" ],
- 'nosuchrevid' => [ 'code' => 'nosuchrevid', 'info' => "There is no revision with ID \$1" ],
- 'nosuchuser' => [ 'code' => 'nosuchuser', 'info' => "User \"\$1\" doesn't exist" ],
- 'invaliduser' => [ 'code' => 'invaliduser', 'info' => "Invalid username \"\$1\"" ],
- 'invalidexpiry' => [ 'code' => 'invalidexpiry', 'info' => "Invalid expiry time \"\$1\"" ],
- 'pastexpiry' => [ 'code' => 'pastexpiry', 'info' => "Expiry time \"\$1\" is in the past" ],
- 'create-titleexists' => [
- 'code' => 'create-titleexists',
- 'info' => "Existing titles can't be protected with 'create'"
- ],
- 'missingtitle-createonly' => [
- 'code' => 'missingtitle-createonly',
- 'info' => "Missing titles can only be protected with 'create'"
- ],
- 'cantblock' => [ 'code' => 'cantblock',
- 'info' => "You don't have permission to block users"
- ],
- 'canthide' => [
- 'code' => 'canthide',
- 'info' => "You don't have permission to hide user names from the block log"
- ],
- 'cantblock-email' => [
- 'code' => 'cantblock-email',
- 'info' => "You don't have permission to block users from sending email through the wiki"
- ],
- 'unblock-notarget' => [
- 'code' => 'notarget',
- 'info' => 'Either the id or the user parameter must be set'
- ],
- 'unblock-idanduser' => [
- 'code' => 'idanduser',
- 'info' => "The id and user parameters can't be used together"
- ],
- 'cantunblock' => [
- 'code' => 'permissiondenied',
- 'info' => "You don't have permission to unblock users"
- ],
- 'cannotundelete' => [
- 'code' => 'cantundelete',
- 'info' => "Couldn't undelete: the requested revisions may not exist, or may have been undeleted already"
- ],
- 'permdenied-undelete' => [
- 'code' => 'permissiondenied',
- 'info' => "You don't have permission to restore deleted revisions"
- ],
- 'createonly-exists' => [
- 'code' => 'articleexists',
- 'info' => 'The article you tried to create has been created already'
- ],
- 'nocreate-missing' => [
- 'code' => 'missingtitle',
- 'info' => "The article you tried to edit doesn't exist"
- ],
- 'cantchangecontentmodel' => [
- 'code' => 'cantchangecontentmodel',
- 'info' => "You don't have permission to change the content model of a page"
- ],
- 'nosuchrcid' => [
- 'code' => 'nosuchrcid',
- 'info' => "There is no change with rcid \"\$1\""
- ],
- 'nosuchlogid' => [
- 'code' => 'nosuchlogid',
- 'info' => "There is no log entry with ID \"\$1\""
- ],
- 'protect-invalidaction' => [
- 'code' => 'protect-invalidaction',
- 'info' => "Invalid protection type \"\$1\""
- ],
- 'protect-invalidlevel' => [
- 'code' => 'protect-invalidlevel',
- 'info' => "Invalid protection level \"\$1\""
- ],
- 'toofewexpiries' => [
- 'code' => 'toofewexpiries',
- 'info' => "\$1 expiry timestamps were provided where \$2 were needed"
- ],
- 'cantimport' => [
- 'code' => 'cantimport',
- 'info' => "You don't have permission to import pages"
- ],
- 'cantimport-upload' => [
- 'code' => 'cantimport-upload',
- 'info' => "You don't have permission to import uploaded pages"
- ],
- 'importnofile' => [ 'code' => 'nofile', 'info' => "You didn't upload a file" ],
- 'importuploaderrorsize' => [
- 'code' => 'filetoobig',
- 'info' => 'The file you uploaded is bigger than the maximum upload size'
- ],
- 'importuploaderrorpartial' => [
- 'code' => 'partialupload',
- 'info' => 'The file was only partially uploaded'
- ],
- 'importuploaderrortemp' => [
- 'code' => 'notempdir',
- 'info' => 'The temporary upload directory is missing'
- ],
- 'importcantopen' => [
- 'code' => 'cantopenfile',
- 'info' => "Couldn't open the uploaded file"
- ],
- 'import-noarticle' => [
- 'code' => 'badinterwiki',
- 'info' => 'Invalid interwiki title specified'
- ],
- 'importbadinterwiki' => [
- 'code' => 'badinterwiki',
- 'info' => 'Invalid interwiki title specified'
- ],
- 'import-unknownerror' => [
- 'code' => 'import-unknownerror',
- 'info' => "Unknown error on import: \"\$1\""
- ],
- 'cantoverwrite-sharedfile' => [
- 'code' => 'cantoverwrite-sharedfile',
- 'info' => 'The target file exists on a shared repository and you do not have permission to override it'
- ],
- 'sharedfile-exists' => [
- 'code' => 'fileexists-sharedrepo-perm',
- 'info' => 'The target file exists on a shared repository. Use the ignorewarnings parameter to override it.'
- ],
- 'mustbeposted' => [
- 'code' => 'mustbeposted',
- 'info' => "The \$1 module requires a POST request"
- ],
- 'show' => [
- 'code' => 'show',
- 'info' => 'Incorrect parameter - mutually exclusive values may not be supplied'
- ],
- 'specialpage-cantexecute' => [
- 'code' => 'specialpage-cantexecute',
- 'info' => "You don't have permission to view the results of this special page"
- ],
- 'invalidoldimage' => [
- 'code' => 'invalidoldimage',
- 'info' => 'The oldimage parameter has invalid format'
- ],
- 'nodeleteablefile' => [
- 'code' => 'nodeleteablefile',
- 'info' => 'No such old version of the file'
- ],
- 'fileexists-forbidden' => [
- 'code' => 'fileexists-forbidden',
- 'info' => 'A file with name "$1" already exists, and cannot be overwritten.'
- ],
- 'fileexists-shared-forbidden' => [
- 'code' => 'fileexists-shared-forbidden',
- 'info' => 'A file with name "$1" already exists in the shared file repository, and cannot be overwritten.'
- ],
- 'filerevert-badversion' => [
- 'code' => 'filerevert-badversion',
- 'info' => 'There is no previous local version of this file with the provided timestamp.'
- ],
-
- // ApiEditPage messages
- 'noimageredirect-anon' => [
- 'code' => 'noimageredirect-anon',
- 'info' => "Anonymous users can't create image redirects"
- ],
- 'noimageredirect-logged' => [
- 'code' => 'noimageredirect',
- 'info' => "You don't have permission to create image redirects"
- ],
- 'spamdetected' => [
- 'code' => 'spamdetected',
- 'info' => "Your edit was refused because it contained a spam fragment: \"\$1\""
- ],
- 'contenttoobig' => [
- 'code' => 'contenttoobig',
- 'info' => "The content you supplied exceeds the article size limit of \$1 kilobytes"
- ],
- 'noedit-anon' => [ 'code' => 'noedit-anon', 'info' => "Anonymous users can't edit pages" ],
- 'noedit' => [ 'code' => 'noedit', 'info' => "You don't have permission to edit pages" ],
- 'wasdeleted' => [
- 'code' => 'pagedeleted',
- 'info' => 'The page has been deleted since you fetched its timestamp'
- ],
- 'blankpage' => [
- 'code' => 'emptypage',
- 'info' => 'Creating new, empty pages is not allowed'
- ],
- 'editconflict' => [ 'code' => 'editconflict', 'info' => 'Edit conflict detected' ],
- 'hashcheckfailed' => [ 'code' => 'badmd5', 'info' => 'The supplied MD5 hash was incorrect' ],
- 'missingtext' => [
- 'code' => 'notext',
- 'info' => 'One of the text, appendtext, prependtext and undo parameters must be set'
- ],
- 'emptynewsection' => [
- 'code' => 'emptynewsection',
- 'info' => 'Creating empty new sections is not possible.'
- ],
- 'revwrongpage' => [
- 'code' => 'revwrongpage',
- 'info' => "r\$1 is not a revision of \"\$2\""
- ],
- 'undo-failure' => [
- 'code' => 'undofailure',
- 'info' => 'Undo failed due to conflicting intermediate edits'
- ],
- 'content-not-allowed-here' => [
- 'code' => 'contentnotallowedhere',
- 'info' => 'Content model "$1" is not allowed at title "$2"'
- ],
-
- // Messages from WikiPage::doEit(]
- 'edit-hook-aborted' => [
- 'code' => 'edit-hook-aborted',
- 'info' => 'Your edit was aborted by an ArticleSave hook'
- ],
- 'edit-gone-missing' => [
- 'code' => 'edit-gone-missing',
- 'info' => "The page you tried to edit doesn't seem to exist anymore"
- ],
- 'edit-conflict' => [ 'code' => 'editconflict', 'info' => 'Edit conflict detected' ],
- 'edit-already-exists' => [
- 'code' => 'edit-already-exists',
- 'info' => 'It seems the page you tried to create already exist'
- ],
-
- // uploadMsgs
- 'invalid-file-key' => [ 'code' => 'invalid-file-key', 'info' => 'Not a valid file key' ],
- 'nouploadmodule' => [ 'code' => 'nouploadmodule', 'info' => 'No upload module set' ],
- 'uploaddisabled' => [
- 'code' => 'uploaddisabled',
- 'info' => 'Uploads are not enabled. Make sure $wgEnableUploads is set to true in LocalSettings.php and the PHP ini setting file_uploads is true'
- ],
- 'copyuploaddisabled' => [
- 'code' => 'copyuploaddisabled',
- 'info' => 'Uploads by URL is not enabled. Make sure $wgAllowCopyUploads is set to true in LocalSettings.php.'
- ],
- 'copyuploadbaddomain' => [
- 'code' => 'copyuploadbaddomain',
- 'info' => 'Uploads by URL are not allowed from this domain.'
- ],
- 'copyuploadbadurl' => [
- 'code' => 'copyuploadbadurl',
- 'info' => 'Upload not allowed from this URL.'
- ],
-
- 'filename-tooshort' => [
- 'code' => 'filename-tooshort',
- 'info' => 'The filename is too short'
- ],
- 'filename-toolong' => [ 'code' => 'filename-toolong', 'info' => 'The filename is too long' ],
- 'illegal-filename' => [
- 'code' => 'illegal-filename',
- 'info' => 'The filename is not allowed'
- ],
- 'filetype-missing' => [
- 'code' => 'filetype-missing',
- 'info' => 'The file is missing an extension'
- ],
-
- 'mustbeloggedin' => [ 'code' => 'mustbeloggedin', 'info' => 'You must be logged in to $1.' ]
- ];
- // @codingStandardsIgnoreEnd
-
- /**
* Helper function for readonly errors
*
- * @throws UsageException always
+ * @throws ApiUsageException always
*/
public function dieReadOnly() {
- $parsed = $this->parseMsg( [ 'readonlytext' ] );
- $this->dieUsage( $parsed['info'], $parsed['code'], /* http error */ 0,
- [ 'readonlyreason' => wfReadOnlyReason() ] );
+ $this->dieWithError(
+ 'apierror-readonly',
+ 'readonly',
+ [ 'readonlyreason' => wfReadOnlyReason() ]
+ );
}
/**
- * Output the error message related to a certain array
- * @param array|string|MessageSpecifier $error Element of a getUserPermissionsErrors()-style array
- * @throws UsageException always
+ * Helper function for permission-denied errors
+ * @since 1.29
+ * @param string|string[] $rights
+ * @param User|null $user
+ * @throws ApiUsageException if the user doesn't have any of the rights.
+ * The error message is based on $rights[0].
*/
- public function dieUsageMsg( $error ) {
- # most of the time we send a 1 element, so we might as well send it as
- # a string and make this an array here.
- if ( is_string( $error ) ) {
- $error = [ $error ];
+ public function checkUserRightsAny( $rights, $user = null ) {
+ if ( !$user ) {
+ $user = $this->getUser();
+ }
+ $rights = (array)$rights;
+ if ( !call_user_func_array( [ $user, 'isAllowedAny' ], $rights ) ) {
+ $this->dieWithError( [ 'apierror-permissiondenied', $this->msg( "action-{$rights[0]}" ) ] );
}
- $parsed = $this->parseMsg( $error );
- $extraData = isset( $parsed['data'] ) ? $parsed['data'] : null;
- $this->dieUsage( $parsed['info'], $parsed['code'], 0, $extraData );
}
/**
- * Will only set a warning instead of failing if the global $wgDebugAPI
- * is set to true. Otherwise behaves exactly as dieUsageMsg().
- * @param array|string|MessageSpecifier $error Element of a getUserPermissionsErrors()-style array
- * @throws UsageException
- * @since 1.21
+ * Helper function for permission-denied errors
+ * @since 1.29
+ * @param Title $title
+ * @param string|string[] $actions
+ * @param User|null $user
+ * @throws ApiUsageException if the user doesn't have all of the rights.
*/
- public function dieUsageMsgOrDebug( $error ) {
- if ( $this->getConfig()->get( 'DebugAPI' ) !== true ) {
- $this->dieUsageMsg( $error );
+ public function checkTitleUserPermissions( Title $title, $actions, $user = null ) {
+ if ( !$user ) {
+ $user = $this->getUser();
}
- if ( is_string( $error ) ) {
- $error = [ $error ];
+ $errors = [];
+ foreach ( (array)$actions as $action ) {
+ $errors = array_merge( $errors, $title->getUserPermissionsErrors( $action, $user ) );
+ }
+ if ( $errors ) {
+ $this->dieStatus( $this->errorArrayToStatus( $errors, $user ) );
}
- $parsed = $this->parseMsg( $error );
- $this->setWarning( '$wgDebugAPI: ' . $parsed['code'] . ' - ' . $parsed['info'] );
}
/**
- * Die with the $prefix.'badcontinue' error. This call is common enough to
- * make it into the base method.
- * @param bool $condition Will only die if this value is true
- * @throws UsageException
- * @since 1.21
+ * Will only set a warning instead of failing if the global $wgDebugAPI
+ * is set to true. Otherwise behaves exactly as self::dieWithError().
+ *
+ * @since 1.29
+ * @param string|array|Message $msg
+ * @param string|null $code
+ * @param array|null $data
+ * @param int|null $httpCode
+ * @throws ApiUsageException
*/
- protected function dieContinueUsageIf( $condition ) {
- if ( $condition ) {
- $this->dieUsage(
- 'Invalid continue param. You should pass the original value returned by the previous query',
- 'badcontinue' );
+ public function dieWithErrorOrDebug( $msg, $code = null, $data = null, $httpCode = null ) {
+ if ( $this->getConfig()->get( 'DebugAPI' ) !== true ) {
+ $this->dieWithError( $msg, $code, $data, $httpCode );
+ } else {
+ $this->addWarning( $msg, $code, $data );
}
}
/**
- * Return the error message related to a certain array
- * @param array|string|MessageSpecifier $error Element of a getUserPermissionsErrors()-style array
- * @return [ 'code' => code, 'info' => info ]
+ * Die with the 'badcontinue' error.
+ *
+ * This call is common enough to make it into the base method.
+ *
+ * @param bool $condition Will only die if this value is true
+ * @throws ApiUsageException
+ * @since 1.21
*/
- public function parseMsg( $error ) {
- // Check whether someone passed the whole array, instead of one element as
- // documented. This breaks if it's actually an array of fallback keys, but
- // that's long-standing misbehavior introduced in r87627 to incorrectly
- // fix T30797.
- if ( is_array( $error ) ) {
- $first = reset( $error );
- if ( is_array( $first ) ) {
- wfDebug( __METHOD__ . ' was passed an array of arrays. ' . wfGetAllCallers( 5 ) );
- $error = $first;
- }
- }
-
- $msg = Message::newFromSpecifier( $error );
-
- if ( $msg instanceof IApiMessage ) {
- return [
- 'code' => $msg->getApiCode(),
- 'info' => $msg->inLanguage( 'en' )->useDatabase( false )->text(),
- 'data' => $msg->getApiData()
- ];
- }
-
- $key = $msg->getKey();
- if ( isset( self::$messageMap[$key] ) ) {
- $params = $msg->getParams();
- return [
- 'code' => wfMsgReplaceArgs( self::$messageMap[$key]['code'], $params ),
- 'info' => wfMsgReplaceArgs( self::$messageMap[$key]['info'], $params )
- ];
+ protected function dieContinueUsageIf( $condition ) {
+ if ( $condition ) {
+ $this->dieWithError( 'apierror-badcontinue' );
}
-
- // If the key isn't present, throw an "unknown error"
- return $this->parseMsg( [ 'unknownerror', $key ] );
}
/**
@@ -2323,6 +1878,7 @@ abstract class ApiBase extends ContextSource {
/**
* Write logging information for API features to a debug log, for usage
* analysis.
+ * @note Consider using $this->addDeprecation() instead to both warn and log.
* @param string $feature Feature being used.
*/
public function logFeatureUsage( $feature ) {
@@ -2790,6 +2346,300 @@ abstract class ApiBase extends ContextSource {
}
}
+ /**
+ * @deprecated since 1.29, use ApiBase::addWarning() instead
+ * @param string $warning Warning message
+ */
+ public function setWarning( $warning ) {
+ $msg = new ApiRawMessage( $warning, 'warning' );
+ $this->getErrorFormatter()->addWarning( $this->getModulePath(), $msg );
+ }
+
+ /**
+ * Throw an ApiUsageException, which will (if uncaught) call the main module's
+ * error handler and die with an error message.
+ *
+ * @deprecated since 1.29, use self::dieWithError() instead
+ * @param string $description One-line human-readable description of the
+ * error condition, e.g., "The API requires a valid action parameter"
+ * @param string $errorCode Brief, arbitrary, stable string to allow easy
+ * automated identification of the error, e.g., 'unknown_action'
+ * @param int $httpRespCode HTTP response code
+ * @param array|null $extradata Data to add to the "<error>" element; array in ApiResult format
+ * @throws ApiUsageException always
+ */
+ public function dieUsage( $description, $errorCode, $httpRespCode = 0, $extradata = null ) {
+ $this->dieWithError(
+ new RawMessage( '$1', [ $description ] ),
+ $errorCode,
+ $extradata,
+ $httpRespCode
+ );
+ }
+
+ /**
+ * Get error (as code, string) from a Status object.
+ *
+ * @since 1.23
+ * @deprecated since 1.29, use ApiErrorFormatter::arrayFromStatus instead
+ * @param Status $status
+ * @param array|null &$extraData Set if extra data from IApiMessage is available (since 1.27)
+ * @return array Array of code and error string
+ * @throws MWException
+ */
+ public function getErrorFromStatus( $status, &$extraData = null ) {
+ if ( $status->isGood() ) {
+ throw new MWException( 'Successful status passed to ApiBase::dieStatus' );
+ }
+
+ $errors = $status->getErrorsByType( 'error' );
+ if ( !$errors ) {
+ // No errors? Assume the warnings should be treated as errors
+ $errors = $status->getErrorsByType( 'warning' );
+ }
+ if ( !$errors ) {
+ // Still no errors? Punt
+ $errors = [ [ 'message' => 'unknownerror-nocode', 'params' => [] ] ];
+ }
+
+ if ( $errors[0]['message'] instanceof MessageSpecifier ) {
+ $msg = $errors[0]['message'];
+ } else {
+ $msg = new Message( $errors[0]['message'], $errors[0]['params'] );
+ }
+ if ( !$msg instanceof IApiMessage ) {
+ $key = $msg->getKey();
+ $params = $msg->getParams();
+ array_unshift( $params, isset( self::$messageMap[$key] ) ? self::$messageMap[$key] : $key );
+ $msg = ApiMessage::create( $params );
+ }
+
+ return [
+ $msg->getApiCode(),
+ ApiErrorFormatter::stripMarkup( $msg->inLanguage( 'en' )->useDatabase( false )->text() )
+ ];
+ }
+
+ /**
+ * @deprecated since 1.29. Prior to 1.29, this was a public mapping from
+ * arbitrary strings (often message keys used elsewhere in MediaWiki) to
+ * API codes and message texts, and a few interfaces required poking
+ * something in here. Now we're repurposing it to map those same strings
+ * to i18n messages, and declaring that any interface that requires poking
+ * at this is broken and needs replacing ASAP.
+ */
+ private static $messageMap = [
+ 'unknownerror' => 'apierror-unknownerror',
+ 'unknownerror-nocode' => 'apierror-unknownerror-nocode',
+ 'ns-specialprotected' => 'ns-specialprotected',
+ 'protectedinterface' => 'protectedinterface',
+ 'namespaceprotected' => 'namespaceprotected',
+ 'customcssprotected' => 'customcssprotected',
+ 'customjsprotected' => 'customjsprotected',
+ 'cascadeprotected' => 'cascadeprotected',
+ 'protectedpagetext' => 'protectedpagetext',
+ 'protect-cantedit' => 'protect-cantedit',
+ 'deleteprotected' => 'deleteprotected',
+ 'badaccess-group0' => 'badaccess-group0',
+ 'badaccess-groups' => 'badaccess-groups',
+ 'titleprotected' => 'titleprotected',
+ 'nocreate-loggedin' => 'nocreate-loggedin',
+ 'nocreatetext' => 'nocreatetext',
+ 'movenologintext' => 'movenologintext',
+ 'movenotallowed' => 'movenotallowed',
+ 'confirmedittext' => 'confirmedittext',
+ 'blockedtext' => 'apierror-blocked',
+ 'autoblockedtext' => 'apierror-autoblocked',
+ 'actionthrottledtext' => 'apierror-ratelimited',
+ 'alreadyrolled' => 'alreadyrolled',
+ 'cantrollback' => 'cantrollback',
+ 'readonlytext' => 'readonlytext',
+ 'sessionfailure' => 'sessionfailure',
+ 'cannotdelete' => 'cannotdelete',
+ 'notanarticle' => 'apierror-missingtitle',
+ 'selfmove' => 'selfmove',
+ 'immobile_namespace' => 'apierror-immobilenamespace',
+ 'articleexists' => 'articleexists',
+ 'hookaborted' => 'hookaborted',
+ 'cantmove-titleprotected' => 'cantmove-titleprotected',
+ 'imagenocrossnamespace' => 'imagenocrossnamespace',
+ 'imagetypemismatch' => 'imagetypemismatch',
+ 'ip_range_invalid' => 'ip_range_invalid',
+ 'range_block_disabled' => 'range_block_disabled',
+ 'nosuchusershort' => 'nosuchusershort',
+ 'badipaddress' => 'badipaddress',
+ 'ipb_expiry_invalid' => 'ipb_expiry_invalid',
+ 'ipb_already_blocked' => 'ipb_already_blocked',
+ 'ipb_blocked_as_range' => 'ipb_blocked_as_range',
+ 'ipb_cant_unblock' => 'ipb_cant_unblock',
+ 'mailnologin' => 'apierror-cantsend',
+ 'ipbblocked' => 'ipbblocked',
+ 'ipbnounblockself' => 'ipbnounblockself',
+ 'usermaildisabled' => 'usermaildisabled',
+ 'blockedemailuser' => 'apierror-blockedfrommail',
+ 'notarget' => 'apierror-notarget',
+ 'noemail' => 'noemail',
+ 'rcpatroldisabled' => 'rcpatroldisabled',
+ 'markedaspatrollederror-noautopatrol' => 'markedaspatrollederror-noautopatrol',
+ 'delete-toobig' => 'delete-toobig',
+ 'movenotallowedfile' => 'movenotallowedfile',
+ 'userrights-no-interwiki' => 'userrights-no-interwiki',
+ 'userrights-nodatabase' => 'userrights-nodatabase',
+ 'nouserspecified' => 'nouserspecified',
+ 'noname' => 'noname',
+ 'summaryrequired' => 'apierror-summaryrequired',
+ 'import-rootpage-invalid' => 'import-rootpage-invalid',
+ 'import-rootpage-nosubpage' => 'import-rootpage-nosubpage',
+ 'readrequired' => 'apierror-readapidenied',
+ 'writedisabled' => 'apierror-noapiwrite',
+ 'writerequired' => 'apierror-writeapidenied',
+ 'missingparam' => 'apierror-missingparam',
+ 'invalidtitle' => 'apierror-invalidtitle',
+ 'nosuchpageid' => 'apierror-nosuchpageid',
+ 'nosuchrevid' => 'apierror-nosuchrevid',
+ 'nosuchuser' => 'nosuchusershort',
+ 'invaliduser' => 'apierror-invaliduser',
+ 'invalidexpiry' => 'apierror-invalidexpiry',
+ 'pastexpiry' => 'apierror-pastexpiry',
+ 'create-titleexists' => 'apierror-create-titleexists',
+ 'missingtitle-createonly' => 'apierror-missingtitle-createonly',
+ 'cantblock' => 'apierror-cantblock',
+ 'canthide' => 'apierror-canthide',
+ 'cantblock-email' => 'apierror-cantblock-email',
+ 'cantunblock' => 'apierror-permissiondenied-generic',
+ 'cannotundelete' => 'cannotundelete',
+ 'permdenied-undelete' => 'apierror-permissiondenied-generic',
+ 'createonly-exists' => 'apierror-articleexists',
+ 'nocreate-missing' => 'apierror-missingtitle',
+ 'cantchangecontentmodel' => 'apierror-cantchangecontentmodel',
+ 'nosuchrcid' => 'apierror-nosuchrcid',
+ 'nosuchlogid' => 'apierror-nosuchlogid',
+ 'protect-invalidaction' => 'apierror-protect-invalidaction',
+ 'protect-invalidlevel' => 'apierror-protect-invalidlevel',
+ 'toofewexpiries' => 'apierror-toofewexpiries',
+ 'cantimport' => 'apierror-cantimport',
+ 'cantimport-upload' => 'apierror-cantimport-upload',
+ 'importnofile' => 'importnofile',
+ 'importuploaderrorsize' => 'importuploaderrorsize',
+ 'importuploaderrorpartial' => 'importuploaderrorpartial',
+ 'importuploaderrortemp' => 'importuploaderrortemp',
+ 'importcantopen' => 'importcantopen',
+ 'import-noarticle' => 'import-noarticle',
+ 'importbadinterwiki' => 'importbadinterwiki',
+ 'import-unknownerror' => 'apierror-import-unknownerror',
+ 'cantoverwrite-sharedfile' => 'apierror-cantoverwrite-sharedfile',
+ 'sharedfile-exists' => 'apierror-fileexists-sharedrepo-perm',
+ 'mustbeposted' => 'apierror-mustbeposted',
+ 'show' => 'apierror-show',
+ 'specialpage-cantexecute' => 'apierror-specialpage-cantexecute',
+ 'invalidoldimage' => 'apierror-invalidoldimage',
+ 'nodeleteablefile' => 'apierror-nodeleteablefile',
+ 'fileexists-forbidden' => 'fileexists-forbidden',
+ 'fileexists-shared-forbidden' => 'fileexists-shared-forbidden',
+ 'filerevert-badversion' => 'filerevert-badversion',
+ 'noimageredirect-anon' => 'apierror-noimageredirect-anon',
+ 'noimageredirect-logged' => 'apierror-noimageredirect',
+ 'spamdetected' => 'apierror-spamdetected',
+ 'contenttoobig' => 'apierror-contenttoobig',
+ 'noedit-anon' => 'apierror-noedit-anon',
+ 'noedit' => 'apierror-noedit',
+ 'wasdeleted' => 'apierror-pagedeleted',
+ 'blankpage' => 'apierror-emptypage',
+ 'editconflict' => 'editconflict',
+ 'hashcheckfailed' => 'apierror-badmd5',
+ 'missingtext' => 'apierror-notext',
+ 'emptynewsection' => 'apierror-emptynewsection',
+ 'revwrongpage' => 'apierror-revwrongpage',
+ 'undo-failure' => 'undo-failure',
+ 'content-not-allowed-here' => 'content-not-allowed-here',
+ 'edit-hook-aborted' => 'edit-hook-aborted',
+ 'edit-gone-missing' => 'edit-gone-missing',
+ 'edit-conflict' => 'edit-conflict',
+ 'edit-already-exists' => 'edit-already-exists',
+ 'invalid-file-key' => 'apierror-invalid-file-key',
+ 'nouploadmodule' => 'apierror-nouploadmodule',
+ 'uploaddisabled' => 'uploaddisabled',
+ 'copyuploaddisabled' => 'copyuploaddisabled',
+ 'copyuploadbaddomain' => 'apierror-copyuploadbaddomain',
+ 'copyuploadbadurl' => 'apierror-copyuploadbadurl',
+ 'filename-tooshort' => 'filename-tooshort',
+ 'filename-toolong' => 'filename-toolong',
+ 'illegal-filename' => 'illegal-filename',
+ 'filetype-missing' => 'filetype-missing',
+ 'mustbeloggedin' => 'apierror-mustbeloggedin',
+ ];
+
+ /**
+ * @deprecated do not use
+ * @param array|string|MessageSpecifier $error Element of a getUserPermissionsErrors()-style array
+ * @return ApiMessage
+ */
+ private function parseMsgInternal( $error ) {
+ $msg = Message::newFromSpecifier( $error );
+ if ( !$msg instanceof IApiMessage ) {
+ $key = $msg->getKey();
+ if ( isset( self::$messageMap[$key] ) ) {
+ $params = $msg->getParams();
+ array_unshift( $params, self::$messageMap[$key] );
+ } else {
+ $params = [ 'apierror-unknownerror', wfEscapeWikiText( $key ) ];
+ }
+ $msg = ApiMessage::create( $params );
+ }
+ return $msg;
+ }
+
+ /**
+ * Return the error message related to a certain array
+ * @deprecated since 1.29
+ * @param array|string|MessageSpecifier $error Element of a getUserPermissionsErrors()-style array
+ * @return [ 'code' => code, 'info' => info ]
+ */
+ public function parseMsg( $error ) {
+ // Check whether someone passed the whole array, instead of one element as
+ // documented. This breaks if it's actually an array of fallback keys, but
+ // that's long-standing misbehavior introduced in r87627 to incorrectly
+ // fix T30797.
+ if ( is_array( $error ) ) {
+ $first = reset( $error );
+ if ( is_array( $first ) ) {
+ wfDebug( __METHOD__ . ' was passed an array of arrays. ' . wfGetAllCallers( 5 ) );
+ $error = $first;
+ }
+ }
+
+ $msg = $this->parseMsgInternal( $error );
+ return [
+ 'code' => $msg->getApiCode(),
+ 'info' => ApiErrorFormatter::stripMarkup(
+ $msg->inLanguage( 'en' )->useDatabase( false )->text()
+ ),
+ 'data' => $msg->getApiData()
+ ];
+ }
+
+ /**
+ * Output the error message related to a certain array
+ * @deprecated since 1.29, use ApiBase::dieWithError() instead
+ * @param array|string|MessageSpecifier $error Element of a getUserPermissionsErrors()-style array
+ * @throws ApiUsageException always
+ */
+ public function dieUsageMsg( $error ) {
+ $this->dieWithError( $this->parseMsgInternal( $error ) );
+ }
+
+ /**
+ * Will only set a warning instead of failing if the global $wgDebugAPI
+ * is set to true. Otherwise behaves exactly as dieUsageMsg().
+ * @deprecated since 1.29, use ApiBase::dieWithErrorOrDebug() instead
+ * @param array|string|MessageSpecifier $error Element of a getUserPermissionsErrors()-style array
+ * @throws ApiUsageException
+ * @since 1.21
+ */
+ public function dieUsageMsgOrDebug( $error ) {
+ $this->dieWithErrorOrDebug( $this->parseMsgInternal( $error ) );
+ }
+
/**@}*/
}
diff --git a/includes/api/ApiBlock.php b/includes/api/ApiBlock.php
index e4c9d0a80b17..a4ea3857bb71 100644
--- a/includes/api/ApiBlock.php
+++ b/includes/api/ApiBlock.php
@@ -41,22 +41,18 @@ class ApiBlock extends ApiBase {
public function execute() {
global $wgContLang;
+ $this->checkUserRightsAny( 'block' );
+
$user = $this->getUser();
$params = $this->extractRequestParams();
- if ( !$user->isAllowed( 'block' ) ) {
- $this->dieUsageMsg( 'cantblock' );
- }
-
# bug 15810: blocked admins should have limited access here
if ( $user->isBlocked() ) {
$status = SpecialBlock::checkUnblockSelf( $params['user'], $user );
if ( $status !== true ) {
- $msg = $this->parseMsg( $status );
- $this->dieUsage(
- $msg['info'],
- $msg['code'],
- 0,
+ $this->dieWithError(
+ $status,
+ null,
[ 'blockinfo' => ApiQueryUserInfo::getBlockInfo( $user->getBlock() ) ]
);
}
@@ -68,14 +64,14 @@ class ApiBlock extends ApiBase {
if ( $target instanceof User &&
( $target->isAnon() /* doesn't exist */ || !User::isUsableName( $target->getName() ) )
) {
- $this->dieUsageMsg( [ 'nosuchuser', $params['user'] ] );
+ $this->dieWithError( [ 'nosuchusershort', $params['user'] ], 'nosuchuser' );
}
if ( $params['hidename'] && !$user->isAllowed( 'hideuser' ) ) {
- $this->dieUsageMsg( 'canthide' );
+ $this->dieWithError( 'apierror-canthide' );
}
if ( $params['noemail'] && !SpecialBlock::canBlockEmail( $user ) ) {
- $this->dieUsageMsg( 'cantblock-email' );
+ $this->dieWithError( 'apierror-cantblock-email' );
}
$data = [
@@ -100,8 +96,7 @@ class ApiBlock extends ApiBase {
$retval = SpecialBlock::processForm( $data, $this->getContext() );
if ( $retval !== true ) {
- // We don't care about multiple errors, just report one of them
- $this->dieUsageMsg( $retval );
+ $this->dieStatus( $this->errorArrayToStatus( $retval ) );
}
list( $target, /*...*/ ) = SpecialBlock::getTargetAndType( $params['user'] );
diff --git a/includes/api/ApiCSPReport.php b/includes/api/ApiCSPReport.php
index 5a0edfcd82dd..4139019ccf0a 100644
--- a/includes/api/ApiCSPReport.php
+++ b/includes/api/ApiCSPReport.php
@@ -137,8 +137,11 @@ class ApiCSPReport extends ApiBase {
}
$status = FormatJson::parse( $postBody, FormatJson::FORCE_ASSOC );
if ( !$status->isGood() ) {
- list( $code, ) = $this->getErrorFromStatus( $status );
- $this->error( $code, __METHOD__ );
+ $msg = $status->getErrors()[0]['message'];
+ if ( $msg instanceof Message ) {
+ $msg = $msg->getKey();
+ }
+ $this->error( $msg, __METHOD__ );
}
$report = $status->getValue();
@@ -176,7 +179,7 @@ class ApiCSPReport extends ApiBase {
*
* @param $code String error code
* @param $method String method that made error
- * @throws UsageException Always
+ * @throws ApiUsageException Always
*/
private function error( $code, $method ) {
$this->log->info( 'Error reading CSP report: ' . $code, [
@@ -184,7 +187,9 @@ class ApiCSPReport extends ApiBase {
'user-agent' => $this->getRequest()->getHeader( 'user-agent' )
] );
// 500 so it shows up in browser's developer console.
- $this->dieUsage( "Error processing CSP report: $code", 'cspreport-' . $code, 500 );
+ $this->dieWithError(
+ [ 'apierror-csp-report', wfEscapeWikiText( $code ) ], 'cspreport-' . $code, [], 500
+ );
}
public function getAllowedParams() {
diff --git a/includes/api/ApiChangeAuthenticationData.php b/includes/api/ApiChangeAuthenticationData.php
index aea28195f0a9..c25920e72859 100644
--- a/includes/api/ApiChangeAuthenticationData.php
+++ b/includes/api/ApiChangeAuthenticationData.php
@@ -35,7 +35,7 @@ class ApiChangeAuthenticationData extends ApiBase {
public function execute() {
if ( !$this->getUser()->isLoggedIn() ) {
- $this->dieUsage( 'Must be logged in to change authentication data', 'notloggedin' );
+ $this->dieWithError( 'apierror-mustbeloggedin-changeauthenticationdata', 'notloggedin' );
}
$helper = new ApiAuthManagerHelper( $this );
@@ -50,7 +50,7 @@ class ApiChangeAuthenticationData extends ApiBase {
$this->getConfig()->get( 'ChangeCredentialsBlacklist' )
);
if ( count( $reqs ) !== 1 ) {
- $this->dieUsage( 'Failed to create change request', 'badrequest' );
+ $this->dieWithError( 'apierror-changeauth-norequest', 'badrequest' );
}
$req = reset( $reqs );
diff --git a/includes/api/ApiCheckToken.php b/includes/api/ApiCheckToken.php
index dd88b5fe3a5e..3cc7a8a058df 100644
--- a/includes/api/ApiCheckToken.php
+++ b/includes/api/ApiCheckToken.php
@@ -43,9 +43,7 @@ class ApiCheckToken extends ApiBase {
);
if ( substr( $token, -strlen( urldecode( Token::SUFFIX ) ) ) === urldecode( Token::SUFFIX ) ) {
- $this->setWarning(
- "Check that symbols such as \"+\" in the token are properly percent-encoded in the URL."
- );
+ $this->addWarning( 'apiwarn-checktoken-percentencoding' );
}
if ( $tokenObj->match( $token, $maxage ) ) {
diff --git a/includes/api/ApiClientLogin.php b/includes/api/ApiClientLogin.php
index cbb1524cc7da..3f5bc0c0c899 100644
--- a/includes/api/ApiClientLogin.php
+++ b/includes/api/ApiClientLogin.php
@@ -57,8 +57,8 @@ class ApiClientLogin extends ApiBase {
$bits = wfParseUrl( $params['returnurl'] );
if ( !$bits || $bits['scheme'] === '' ) {
$encParamName = $this->encodeParamName( 'returnurl' );
- $this->dieUsage(
- "Invalid value '{$params['returnurl']}' for url parameter $encParamName",
+ $this->dieWithError(
+ [ 'apierror-badurl', $encParamName, wfEscapeWikiText( $params['returnurl'] ) ],
"badurl_{$encParamName}"
);
}
diff --git a/includes/api/ApiComparePages.php b/includes/api/ApiComparePages.php
index 7eb0bf3e8191..d6867eb52dff 100644
--- a/includes/api/ApiComparePages.php
+++ b/includes/api/ApiComparePages.php
@@ -34,8 +34,7 @@ class ApiComparePages extends ApiBase {
$revision = Revision::newFromId( $rev1 );
if ( !$revision ) {
- $this->dieUsage( 'The diff cannot be retrieved, ' .
- 'one revision does not exist or you do not have permission to view it.', 'baddiff' );
+ $this->dieWithError( 'apierror-baddiff' );
}
$contentHandler = $revision->getContentHandler();
@@ -65,11 +64,7 @@ class ApiComparePages extends ApiBase {
$difftext = $de->getDiffBody();
if ( $difftext === false ) {
- $this->dieUsage(
- 'The diff cannot be retrieved. Maybe one or both revisions do ' .
- 'not exist or you do not have permission to view them.',
- 'baddiff'
- );
+ $this->dieWithError( 'apierror-baddiff' );
}
ApiResult::setContentValue( $vals, 'body', $difftext );
@@ -89,22 +84,19 @@ class ApiComparePages extends ApiBase {
} elseif ( $titleText ) {
$title = Title::newFromText( $titleText );
if ( !$title || $title->isExternal() ) {
- $this->dieUsageMsg( [ 'invalidtitle', $titleText ] );
+ $this->dieWithError( [ 'apierror-invalidtitle', wfEscapeWikiText( $titleText ) ] );
}
return $title->getLatestRevID();
} elseif ( $titleId ) {
$title = Title::newFromID( $titleId );
if ( !$title ) {
- $this->dieUsageMsg( [ 'nosuchpageid', $titleId ] );
+ $this->dieWithError( [ 'apierror-nosuchpageid', $titleId ] );
}
return $title->getLatestRevID();
}
- $this->dieUsage(
- 'A title, a page ID, or a revision number is needed for both the from and the to parameters',
- 'inputneeded'
- );
+ $this->dieWithError( 'apierror-compare-inputneeded', 'inputneeded' );
}
public function getAllowedParams() {
diff --git a/includes/api/ApiContinuationManager.php b/includes/api/ApiContinuationManager.php
index 19e2453944b1..7da8ed9a5b19 100644
--- a/includes/api/ApiContinuationManager.php
+++ b/includes/api/ApiContinuationManager.php
@@ -40,7 +40,7 @@ class ApiContinuationManager {
* @param ApiBase $module Module starting the continuation
* @param ApiBase[] $allModules Contains ApiBase instances that will be executed
* @param array $generatedModules Names of modules that depend on the generator
- * @throws UsageException
+ * @throws ApiUsageException
*/
public function __construct(
ApiBase $module, array $allModules = [], array $generatedModules = []
@@ -57,10 +57,7 @@ class ApiContinuationManager {
if ( $continue !== '' ) {
$continue = explode( '||', $continue );
if ( count( $continue ) !== 2 ) {
- throw new UsageException(
- 'Invalid continue param. You should pass the original value returned by the previous query',
- 'badcontinue'
- );
+ throw ApiUsageException::newWithMessage( $module->getMain(), 'apierror-badcontinue' );
}
$this->generatorDone = ( $continue[0] === '-' );
$skip = explode( '|', $continue[1] );
diff --git a/includes/api/ApiDelete.php b/includes/api/ApiDelete.php
index 993c23e58244..50c24aeca8b5 100644
--- a/includes/api/ApiDelete.php
+++ b/includes/api/ApiDelete.php
@@ -45,7 +45,7 @@ class ApiDelete extends ApiBase {
$pageObj = $this->getTitleOrPageId( $params, 'fromdbmaster' );
if ( !$pageObj->exists() ) {
- $this->dieUsageMsg( 'notanarticle' );
+ $this->dieWithError( 'apierror-missingtitle' );
}
$titleObj = $pageObj->getTitle();
@@ -53,10 +53,7 @@ class ApiDelete extends ApiBase {
$user = $this->getUser();
// Check that the user is allowed to carry out the deletion
- $errors = $titleObj->getUserPermissionsErrors( 'delete', $user );
- if ( count( $errors ) ) {
- $this->dieUsageMsg( $errors[0] );
- }
+ $this->checkTitleUserPermissions( $titleObj, 'delete' );
// If change tagging was requested, check that the user is allowed to tag,
// and the tags are valid
@@ -80,9 +77,6 @@ class ApiDelete extends ApiBase {
$status = self::delete( $pageObj, $user, $reason, $params['tags'] );
}
- if ( is_array( $status ) ) {
- $this->dieUsageMsg( $status[0] );
- }
if ( !$status->isGood() ) {
$this->dieStatus( $status );
}
@@ -112,7 +106,7 @@ class ApiDelete extends ApiBase {
* @param User $user User doing the action
* @param string|null $reason Reason for the deletion. Autogenerated if null
* @param array $tags Tags to tag the deletion with
- * @return Status|array
+ * @return Status
*/
protected static function delete( Page $page, User $user, &$reason = null, $tags = [] ) {
$title = $page->getTitle();
@@ -124,7 +118,7 @@ class ApiDelete extends ApiBase {
$hasHistory = false;
$reason = $page->getAutoDeleteReason( $hasHistory );
if ( $reason === false ) {
- return [ [ 'cannotdelete', $title->getPrefixedText() ] ];
+ return Status::newFatal( 'cannotdelete', $title->getPrefixedText() );
}
}
@@ -141,7 +135,7 @@ class ApiDelete extends ApiBase {
* @param string $reason Reason for the deletion. Autogenerated if null.
* @param bool $suppress Whether to mark all deleted versions as restricted
* @param array $tags Tags to tag the deletion with
- * @return Status|array
+ * @return Status
*/
protected static function deleteFile( Page $page, User $user, $oldimage,
&$reason = null, $suppress = false, $tags = []
@@ -155,11 +149,11 @@ class ApiDelete extends ApiBase {
if ( $oldimage ) {
if ( !FileDeleteForm::isValidOldSpec( $oldimage ) ) {
- return [ [ 'invalidoldimage' ] ];
+ return Status::newFatal( 'invalidoldimage' );
}
$oldfile = RepoGroup::singleton()->getLocalRepo()->newFromArchiveName( $title, $oldimage );
if ( !$oldfile->exists() || !$oldfile->isLocal() || $oldfile->getRedirected() ) {
- return [ [ 'nodeleteablefile' ] ];
+ return Status::newFatal( 'nodeleteablefile' );
}
}
diff --git a/includes/api/ApiDisabled.php b/includes/api/ApiDisabled.php
index fc9752205ee1..41bf9b69c7b8 100644
--- a/includes/api/ApiDisabled.php
+++ b/includes/api/ApiDisabled.php
@@ -37,7 +37,7 @@
class ApiDisabled extends ApiBase {
public function execute() {
- $this->dieUsage( "The \"{$this->getModuleName()}\" module has been disabled.", 'moduledisabled' );
+ $this->dieWithError( [ 'apierror-moduledisabled', $this->getModuleName() ] );
}
public function isReadMode() {
diff --git a/includes/api/ApiEditPage.php b/includes/api/ApiEditPage.php
index d6de83430193..6b568701fd1d 100644
--- a/includes/api/ApiEditPage.php
+++ b/includes/api/ApiEditPage.php
@@ -40,12 +40,7 @@ class ApiEditPage extends ApiBase {
$user = $this->getUser();
$params = $this->extractRequestParams();
- if ( is_null( $params['text'] ) && is_null( $params['appendtext'] ) &&
- is_null( $params['prependtext'] ) &&
- $params['undo'] == 0
- ) {
- $this->dieUsageMsg( 'missingtext' );
- }
+ $this->requireAtLeastOneParameter( $params, 'text', 'appendtext', 'prependtext', 'undo' );
$pageObj = $this->getTitleOrPageId( $params );
$titleObj = $pageObj->getTitle();
@@ -55,9 +50,7 @@ class ApiEditPage extends ApiBase {
if ( $params['prependtext'] === null && $params['appendtext'] === null
&& $params['section'] !== 'new'
) {
- $this->dieUsage( 'You have attempted to edit using the "redirect"-following'
- . ' mode, which must be used in conjuction with section=new, prependtext'
- . ', or appendtext.', 'redirect-appendonly' );
+ $this->dieWithError( 'apierror-redirect-appendonly' );
}
if ( $titleObj->isRedirect() ) {
$oldTitle = $titleObj;
@@ -105,10 +98,7 @@ class ApiEditPage extends ApiBase {
if ( $params['undo'] > 0 ) {
// allow undo via api
} elseif ( $contentHandler->supportsDirectApiEditing() === false ) {
- $this->dieUsage(
- "Direct editing via API is not supported for content model $model used by $name",
- 'no-direct-editing'
- );
+ $this->dieWithError( [ 'apierror-no-direct-editing', $model, $name ] );
}
if ( !isset( $params['contentformat'] ) || $params['contentformat'] == '' ) {
@@ -118,49 +108,21 @@ class ApiEditPage extends ApiBase {
}
if ( !$contentHandler->isSupportedFormat( $contentFormat ) ) {
-
- $this->dieUsage( "The requested format $contentFormat is not supported for content model " .
- " $model used by $name", 'badformat' );
+ $this->dieWithError( [ 'apierror-badformat', $contentFormat, $model, $name ] );
}
if ( $params['createonly'] && $titleObj->exists() ) {
- $this->dieUsageMsg( 'createonly-exists' );
+ $this->dieWithError( 'apierror-articleexists' );
}
if ( $params['nocreate'] && !$titleObj->exists() ) {
- $this->dieUsageMsg( 'nocreate-missing' );
+ $this->dieWithError( 'apierror-missingtitle' );
}
// Now let's check whether we're even allowed to do this
- $errors = $titleObj->getUserPermissionsErrors( 'edit', $user );
- if ( !$titleObj->exists() ) {
- $errors = array_merge( $errors, $titleObj->getUserPermissionsErrors( 'create', $user ) );
- }
- if ( count( $errors ) ) {
- if ( is_array( $errors[0] ) ) {
- switch ( $errors[0][0] ) {
- case 'blockedtext':
- $this->dieUsage(
- 'You have been blocked from editing',
- 'blocked',
- 0,
- [ 'blockinfo' => ApiQueryUserInfo::getBlockInfo( $user->getBlock() ) ]
- );
- break;
- case 'autoblockedtext':
- $this->dieUsage(
- 'Your IP address has been blocked automatically, because it was used by a blocked user',
- 'autoblocked',
- 0,
- [ 'blockinfo' => ApiQueryUserInfo::getBlockInfo( $user->getBlock() ) ]
- );
- break;
- default:
- $this->dieUsageMsg( $errors[0] );
- }
- } else {
- $this->dieUsageMsg( $errors[0] );
- }
- }
+ $this->checkTitleUserPermissions(
+ $titleObj,
+ $titleObj->exists() ? 'edit' : [ 'edit', 'create' ]
+ );
$toMD5 = $params['text'];
if ( !is_null( $params['appendtext'] ) || !is_null( $params['prependtext'] ) ) {
@@ -178,8 +140,11 @@ class ApiEditPage extends ApiBase {
try {
$content = ContentHandler::makeContent( $text, $this->getTitle() );
} catch ( MWContentSerializationException $ex ) {
- $this->dieUsage( $ex->getMessage(), 'parseerror' );
-
+ // @todo: Internationalize MWContentSerializationException
+ $this->dieWithError(
+ [ 'apierror-contentserializationexception', wfEscapeWikiText( $ex->getMessage() ) ],
+ 'parseerror'
+ );
return;
}
} else {
@@ -191,17 +156,14 @@ class ApiEditPage extends ApiBase {
// @todo Add support for appending/prepending to the Content interface
if ( !( $content instanceof TextContent ) ) {
- $mode = $contentHandler->getModelID();
- $this->dieUsage( "Can't append to pages using content model $mode", 'appendnotsupported' );
+ $modelName = $contentHandler->getModelID();
+ $this->dieWithError( [ 'apierror-appendnotsupported', $modelName ] );
}
if ( !is_null( $params['section'] ) ) {
if ( !$contentHandler->supportsSections() ) {
$modelName = $contentHandler->getModelID();
- $this->dieUsage(
- "Sections are not supported for this content model: $modelName.",
- 'sectionsnotsupported'
- );
+ $this->dieWithError( [ 'apierror-sectionsnotsupported', $modelName ] );
}
if ( $params['section'] == 'new' ) {
@@ -213,7 +175,7 @@ class ApiEditPage extends ApiBase {
$content = $content->getSection( $section );
if ( !$content ) {
- $this->dieUsage( "There is no section {$section}.", 'nosuchsection' );
+ $this->dieWithError( [ 'apierror-nosuchsection', wfEscapeWikiText( $section ) ] );
}
}
}
@@ -238,22 +200,22 @@ class ApiEditPage extends ApiBase {
}
$undoRev = Revision::newFromId( $params['undo'] );
if ( is_null( $undoRev ) || $undoRev->isDeleted( Revision::DELETED_TEXT ) ) {
- $this->dieUsageMsg( [ 'nosuchrevid', $params['undo'] ] );
+ $this->dieWithError( [ 'apierror-nosuchrevid', $params['undo'] ] );
}
if ( $params['undoafter'] == 0 ) {
$undoafterRev = $undoRev->getPrevious();
}
if ( is_null( $undoafterRev ) || $undoafterRev->isDeleted( Revision::DELETED_TEXT ) ) {
- $this->dieUsageMsg( [ 'nosuchrevid', $params['undoafter'] ] );
+ $this->dieWithError( [ 'apierror-nosuchrevid', $params['undoafter'] ] );
}
if ( $undoRev->getPage() != $pageObj->getId() ) {
- $this->dieUsageMsg( [ 'revwrongpage', $undoRev->getId(),
+ $this->dieWithError( [ 'apierror-revwrongpage', $undoRev->getId(),
$titleObj->getPrefixedText() ] );
}
if ( $undoafterRev->getPage() != $pageObj->getId() ) {
- $this->dieUsageMsg( [ 'revwrongpage', $undoafterRev->getId(),
+ $this->dieWithError( [ 'apierror-revwrongpage', $undoafterRev->getId(),
$titleObj->getPrefixedText() ] );
}
@@ -264,7 +226,7 @@ class ApiEditPage extends ApiBase {
);
if ( !$newContent ) {
- $this->dieUsageMsg( 'undo-failure' );
+ $this->dieWithError( 'undo-failure', 'undofailure' );
}
if ( empty( $params['contentmodel'] )
&& empty( $params['contentformat'] )
@@ -293,7 +255,7 @@ class ApiEditPage extends ApiBase {
// See if the MD5 hash checks out
if ( !is_null( $params['md5'] ) && md5( $toMD5 ) !== $params['md5'] ) {
- $this->dieUsageMsg( 'hashcheckfailed' );
+ $this->dieWithError( 'apierror-badmd5' );
}
// EditPage wants to parse its stuff from a WebRequest
@@ -347,14 +309,13 @@ class ApiEditPage extends ApiBase {
if ( !is_null( $params['section'] ) ) {
$section = $params['section'];
if ( !preg_match( '/^((T-)?\d+|new)$/', $section ) ) {
- $this->dieUsage( "The section parameter must be a valid section id or 'new'",
- 'invalidsection' );
+ $this->dieWithError( 'apierror-invalidsection' );
}
$content = $pageObj->getContent();
if ( $section !== '0' && $section != 'new'
&& ( !$content || !$content->getSection( $section ) )
) {
- $this->dieUsage( "There is no section {$section}.", 'nosuchsection' );
+ $this->dieWithError( [ 'apierror-nosuchsection', $section ] );
}
$requestArray['wpSection'] = $params['section'];
} else {
@@ -423,7 +384,7 @@ class ApiEditPage extends ApiBase {
return;
}
- $this->dieUsageMsg( 'hookaborted' );
+ $this->dieWithError( 'hookaborted' );
}
// Do the actual save
@@ -445,67 +406,22 @@ class ApiEditPage extends ApiBase {
$r['result'] = 'Failure';
$apiResult->addValue( null, $this->getModuleName(), $r );
return;
- } else {
- $this->dieUsageMsg( 'hookaborted' );
}
-
- case EditPage::AS_PARSE_ERROR:
- $this->dieUsage( $status->getMessage(), 'parseerror' );
-
- case EditPage::AS_IMAGE_REDIRECT_ANON:
- $this->dieUsageMsg( 'noimageredirect-anon' );
-
- case EditPage::AS_IMAGE_REDIRECT_LOGGED:
- $this->dieUsageMsg( 'noimageredirect-logged' );
-
- case EditPage::AS_SPAM_ERROR:
- $this->dieUsageMsg( [ 'spamdetected', $result['spam'] ] );
+ if ( !$status->getErrors() ) {
+ $status->fatal( 'hookaborted' );
+ }
+ $this->dieStatus( $status );
case EditPage::AS_BLOCKED_PAGE_FOR_USER:
- $this->dieUsage(
- 'You have been blocked from editing',
+ $this->dieWithError(
+ 'apierror-blocked',
'blocked',
- 0,
[ 'blockinfo' => ApiQueryUserInfo::getBlockInfo( $user->getBlock() ) ]
);
- case EditPage::AS_MAX_ARTICLE_SIZE_EXCEEDED:
- case EditPage::AS_CONTENT_TOO_BIG:
- $this->dieUsageMsg( [ 'contenttoobig', $this->getConfig()->get( 'MaxArticleSize' ) ] );
-
- case EditPage::AS_READ_ONLY_PAGE_ANON:
- $this->dieUsageMsg( 'noedit-anon' );
-
- case EditPage::AS_READ_ONLY_PAGE_LOGGED:
- $this->dieUsageMsg( 'noedit' );
-
case EditPage::AS_READ_ONLY_PAGE:
$this->dieReadOnly();
- case EditPage::AS_RATE_LIMITED:
- $this->dieUsageMsg( 'actionthrottledtext' );
-
- case EditPage::AS_ARTICLE_WAS_DELETED:
- $this->dieUsageMsg( 'wasdeleted' );
-
- case EditPage::AS_NO_CREATE_PERMISSION:
- $this->dieUsageMsg( 'nocreate-loggedin' );
-
- case EditPage::AS_NO_CHANGE_CONTENT_MODEL:
- $this->dieUsageMsg( 'cantchangecontentmodel' );
-
- case EditPage::AS_BLANK_ARTICLE:
- $this->dieUsageMsg( 'blankpage' );
-
- case EditPage::AS_CONFLICT_DETECTED:
- $this->dieUsageMsg( 'editconflict' );
-
- case EditPage::AS_TEXTBOX_EMPTY:
- $this->dieUsageMsg( 'emptynewsection' );
-
- case EditPage::AS_CHANGE_TAG_ERROR:
- $this->dieStatus( $status );
-
case EditPage::AS_SUCCESS_NEW_ARTICLE:
$r['new'] = true;
// fall-through
@@ -526,15 +442,39 @@ class ApiEditPage extends ApiBase {
}
break;
- case EditPage::AS_SUMMARY_NEEDED:
- // Shouldn't happen since we set wpIgnoreBlankSummary, but just in case
- $this->dieUsageMsg( 'summaryrequired' );
-
- case EditPage::AS_END:
default:
- // $status came from WikiPage::doEditContent()
- $errors = $status->getErrorsArray();
- $this->dieUsageMsg( $errors[0] ); // TODO: Add new errors to message map
+ // EditPage sometimes only sets the status code without setting
+ // any actual error messages. Supply defaults for those cases.
+ $maxArticleSize = $this->getConfig()->get( 'MaxArticleSize' );
+ $defaultMessages = [
+ // Currently needed
+ EditPage::AS_IMAGE_REDIRECT_ANON => [ 'apierror-noimageredirect-anon' ],
+ EditPage::AS_IMAGE_REDIRECT_LOGGED => [ 'apierror-noimageredirect-logged' ],
+ EditPage::AS_CONTENT_TOO_BIG => [ 'apierror-contenttoobig', $maxArticleSize ],
+ EditPage::AS_MAX_ARTICLE_SIZE_EXCEEDED => [ 'apierror-contenttoobig', $maxArticleSize ],
+ EditPage::AS_READ_ONLY_PAGE_ANON => [ 'apierror-noedit-anon' ],
+ EditPage::AS_NO_CHANGE_CONTENT_MODEL => [ 'apierror-cantchangecontentmodel' ],
+ EditPage::AS_ARTICLE_WAS_DELETED => [ 'apierror-pagedeleted' ],
+ EditPage::AS_CONFLICT_DETECTED => [ 'editconflict' ],
+
+ // Currently shouldn't be needed
+ EditPage::AS_SPAM_ERROR => [ 'apierror-spamdetected', wfEscapeWikiText( $result['spam'] ) ],
+ EditPage::AS_READ_ONLY_PAGE_LOGGED => [ 'apierror-noedit' ],
+ EditPage::AS_RATE_LIMITED => [ 'apierror-ratelimited' ],
+ EditPage::AS_NO_CREATE_PERMISSION => [ 'nocreate-loggedin' ],
+ EditPage::AS_BLANK_ARTICLE => [ 'apierror-emptypage' ],
+ EditPage::AS_TEXTBOX_EMPTY => [ 'apierror-emptynewsection' ],
+ EditPage::AS_SUMMARY_NEEDED => [ 'apierror-summaryrequired' ],
+ ];
+ if ( !$status->getErrors() ) {
+ if ( isset( $defaultMessages[$status->value] ) ) {
+ call_user_func_array( [ $status, 'fatal' ], $defaultMessages[$status->value] );
+ } else {
+ wfWarn( __METHOD__ . ": Unknown EditPage code {$status->value} with no message" );
+ $status->fatal( 'apierror-unknownerror-editpage', $status->value );
+ }
+ }
+ $this->dieStatus( $status );
break;
}
$apiResult->addValue( null, $this->getModuleName(), $r );
diff --git a/includes/api/ApiEmailUser.php b/includes/api/ApiEmailUser.php
index 192378e8e871..8aff6f8afd46 100644
--- a/includes/api/ApiEmailUser.php
+++ b/includes/api/ApiEmailUser.php
@@ -36,7 +36,16 @@ class ApiEmailUser extends ApiBase {
// Validate target
$targetUser = SpecialEmailUser::getTarget( $params['target'] );
if ( !( $targetUser instanceof User ) ) {
- $this->dieUsageMsg( [ $targetUser ] );
+ switch ( $targetUser ) {
+ case 'notarget':
+ $this->dieWithError( 'apierror-notarget' );
+ case 'noemail':
+ $this->dieWithError( [ 'noemail', $params['target'] ] );
+ case 'nowikiemail':
+ $this->dieWithError( 'nowikiemailtext', 'nowikiemail' );
+ default:
+ $this->dieWithError( [ 'apierror-unknownerror', $targetUser ] );
+ }
}
// Check permissions and errors
@@ -46,7 +55,7 @@ class ApiEmailUser extends ApiBase {
$this->getConfig()
);
if ( $error ) {
- $this->dieUsageMsg( [ $error ] );
+ $this->dieWithError( $error );
}
$data = [
@@ -56,25 +65,16 @@ class ApiEmailUser extends ApiBase {
'CCMe' => $params['ccme'],
];
$retval = SpecialEmailUser::submit( $data, $this->getContext() );
-
- if ( $retval instanceof Status ) {
- // SpecialEmailUser sometimes returns a status
- // sometimes it doesn't.
- if ( $retval->isGood() ) {
- $retval = true;
- } else {
- $retval = $retval->getErrorsArray();
- }
+ if ( !$retval instanceof Status ) {
+ // This is probably the reason
+ $retval = Status::newFatal( 'hookaborted' );
}
- if ( $retval === true ) {
- $result = [ 'result' => 'Success' ];
- } else {
- $result = [
- 'result' => 'Failure',
- 'message' => $retval
- ];
- }
+ $result = array_filter( [
+ 'result' => $retval->isGood() ? 'Success' : $retval->isOk() ? 'Warnings' : 'Failure',
+ 'warnings' => $this->getErrorFormatter()->arrayFromStatus( $retval, 'warning' ),
+ 'errors' => $this->getErrorFormatter()->arrayFromStatus( $retval, 'error' ),
+ ] );
$this->getResult()->addValue( null, $this->getModuleName(), $result );
}
diff --git a/includes/api/ApiErrorFormatter.php b/includes/api/ApiErrorFormatter.php
index 6d9184f7818f..4fb19b88d5e9 100644
--- a/includes/api/ApiErrorFormatter.php
+++ b/includes/api/ApiErrorFormatter.php
@@ -43,7 +43,9 @@ class ApiErrorFormatter {
* @param ApiResult $result Into which data will be added
* @param Language $lang Used for i18n
* @param string $format
- * - text: Error message as wikitext
+ * - plaintext: Error message as something vaguely like plaintext
+ * (it's basically wikitext with HTML tags stripped and entities decoded)
+ * - wikitext: Error message as wikitext
* - html: Error message as HTML
* - raw: Raw message key and parameters, no human-readable text
* - none: Code and data only, no human-readable text
@@ -57,6 +59,15 @@ class ApiErrorFormatter {
}
/**
+ * Fetch the Language for this formatter
+ * @since 1.29
+ * @return Language
+ */
+ public function getLanguage() {
+ return $this->lang;
+ }
+
+ /**
* Fetch a dummy title to set on Messages
* @return Title
*/
@@ -69,53 +80,49 @@ class ApiErrorFormatter {
/**
* Add a warning to the result
- * @param string $moduleName
- * @param MessageSpecifier|array|string $msg i18n message for the warning
- * @param string $code Machine-readable code for the warning. Defaults as
- * for IApiMessage::getApiCode().
- * @param array $data Machine-readable data for the warning, if any.
- * Uses IApiMessage::getApiData() if $msg implements that interface.
+ * @param string|null $modulePath
+ * @param Message|array|string $msg Warning message. See ApiMessage::create().
+ * @param string|null $code See ApiMessage::create().
+ * @param array|null $data See ApiMessage::create().
*/
- public function addWarning( $moduleName, $msg, $code = null, $data = null ) {
+ public function addWarning( $modulePath, $msg, $code = null, $data = null ) {
$msg = ApiMessage::create( $msg, $code, $data )
->inLanguage( $this->lang )
->title( $this->getDummyTitle() )
->useDatabase( $this->useDB );
- $this->addWarningOrError( 'warning', $moduleName, $msg );
+ $this->addWarningOrError( 'warning', $modulePath, $msg );
}
/**
* Add an error to the result
- * @param string $moduleName
- * @param MessageSpecifier|array|string $msg i18n message for the error
- * @param string $code Machine-readable code for the warning. Defaults as
- * for IApiMessage::getApiCode().
- * @param array $data Machine-readable data for the warning, if any.
- * Uses IApiMessage::getApiData() if $msg implements that interface.
+ * @param string|null $modulePath
+ * @param Message|array|string $msg Warning message. See ApiMessage::create().
+ * @param string|null $code See ApiMessage::create().
+ * @param array|null $data See ApiMessage::create().
*/
- public function addError( $moduleName, $msg, $code = null, $data = null ) {
+ public function addError( $modulePath, $msg, $code = null, $data = null ) {
$msg = ApiMessage::create( $msg, $code, $data )
->inLanguage( $this->lang )
->title( $this->getDummyTitle() )
->useDatabase( $this->useDB );
- $this->addWarningOrError( 'error', $moduleName, $msg );
+ $this->addWarningOrError( 'error', $modulePath, $msg );
}
/**
- * Add warnings and errors from a Status object to the result
- * @param string $moduleName
- * @param Status $status
+ * Add warnings and errors from a StatusValue object to the result
+ * @param string|null $modulePath
+ * @param StatusValue $status
* @param string[] $types 'warning' and/or 'error'
*/
public function addMessagesFromStatus(
- $moduleName, Status $status, $types = [ 'warning', 'error' ]
+ $modulePath, StatusValue $status, $types = [ 'warning', 'error' ]
) {
- if ( $status->isGood() || !$status->errors ) {
+ if ( $status->isGood() || !$status->getErrors() ) {
return;
}
$types = (array)$types;
- foreach ( $status->errors as $error ) {
+ foreach ( $status->getErrors() as $error ) {
if ( !in_array( $error['type'], $types, true ) ) {
continue;
}
@@ -127,40 +134,37 @@ class ApiErrorFormatter {
$tag = 'warning';
}
- if ( is_array( $error ) && isset( $error['message'] ) ) {
- // Normal case
- if ( $error['message'] instanceof Message ) {
- $msg = ApiMessage::create( $error['message'], null, [] );
- } else {
- $args = isset( $error['params'] ) ? $error['params'] : [];
- array_unshift( $args, $error['message'] );
- $error += [ 'params' => [] ];
- $msg = ApiMessage::create( $args, null, [] );
- }
- } elseif ( is_array( $error ) ) {
- // Weird case handled by Message::getErrorMessage
- $msg = ApiMessage::create( $error, null, [] );
- } else {
- // Another weird case handled by Message::getErrorMessage
- $msg = ApiMessage::create( $error, null, [] );
- }
-
- $msg->inLanguage( $this->lang )
+ $msg = ApiMessage::create( $error )
+ ->inLanguage( $this->lang )
->title( $this->getDummyTitle() )
->useDatabase( $this->useDB );
- $this->addWarningOrError( $tag, $moduleName, $msg );
+ $this->addWarningOrError( $tag, $modulePath, $msg );
}
}
/**
- * Format messages from a Status as an array
- * @param Status $status
+ * Format a message as an array
+ * @param Message|array|string $msg Message. See ApiMessage::create().
+ * @param string|null $format
+ * @return array
+ */
+ public function formatMessage( $msg, $format = null ) {
+ $msg = ApiMessage::create( $msg )
+ ->inLanguage( $this->lang )
+ ->title( $this->getDummyTitle() )
+ ->useDatabase( $this->useDB );
+ return $this->formatMessageInternal( $msg, $format ?: $this->format );
+ }
+
+ /**
+ * Format messages from a StatusValue as an array
+ * @param StatusValue $status
* @param string $type 'warning' or 'error'
* @param string|null $format
* @return array
*/
- public function arrayFromStatus( Status $status, $type = 'error', $format = null ) {
- if ( $status->isGood() || !$status->errors ) {
+ public function arrayFromStatus( StatusValue $status, $type = 'error', $format = null ) {
+ if ( $status->isGood() || !$status->getErrors() ) {
return [];
}
@@ -168,24 +172,69 @@ class ApiErrorFormatter {
$formatter = new ApiErrorFormatter(
$result, $this->lang, $format ?: $this->format, $this->useDB
);
- $formatter->addMessagesFromStatus( 'dummy', $status, [ $type ] );
+ $formatter->addMessagesFromStatus( null, $status, [ $type ] );
switch ( $type ) {
case 'error':
- return (array)$result->getResultData( [ 'errors', 'dummy' ] );
+ return (array)$result->getResultData( [ 'errors' ] );
case 'warning':
- return (array)$result->getResultData( [ 'warnings', 'dummy' ] );
+ return (array)$result->getResultData( [ 'warnings' ] );
}
}
/**
- * Actually add the warning or error to the result
- * @param string $tag 'warning' or 'error'
- * @param string $moduleName
+ * Turn wikitext into something resembling plaintext
+ * @since 1.29
+ * @param string $text
+ * @return string
+ */
+ public static function stripMarkup( $text ) {
+ // Turn semantic quoting tags to quotes
+ $ret = preg_replace( '!</?(var|kbd|samp|code)>!', '"', $text );
+
+ // Strip tags and decode.
+ $ret = html_entity_decode( strip_tags( $ret ), ENT_QUOTES | ENT_HTML5 );
+
+ return $ret;
+ }
+
+ /**
+ * Format a Message object for raw format
+ * @param MessageSpecifier $msg
+ * @return array
+ */
+ private function formatRawMessage( MessageSpecifier $msg ) {
+ $ret = [
+ 'key' => $msg->getKey(),
+ 'params' => $msg->getParams(),
+ ];
+ ApiResult::setIndexedTagName( $ret['params'], 'param' );
+
+ // Transform Messages as parameters in the style of Message::fooParam().
+ foreach ( $ret['params'] as $i => $param ) {
+ if ( $param instanceof MessageSpecifier ) {
+ $ret['params'][$i] = [ 'message' => $this->formatRawMessage( $param ) ];
+ }
+ }
+ return $ret;
+ }
+
+ /**
+ * Format a message as an array
+ * @since 1.29
* @param ApiMessage|ApiRawMessage $msg
+ * @param string|null $format
+ * @return array
*/
- protected function addWarningOrError( $tag, $moduleName, $msg ) {
+ protected function formatMessageInternal( $msg, $format ) {
$value = [ 'code' => $msg->getApiCode() ];
- switch ( $this->format ) {
+ switch ( $format ) {
+ case 'plaintext':
+ $value += [
+ 'text' => self::stripMarkup( $msg->text() ),
+ ApiResult::META_CONTENT => 'text',
+ ];
+ break;
+
case 'wikitext':
$value += [
'text' => $msg->text(),
@@ -201,19 +250,34 @@ class ApiErrorFormatter {
break;
case 'raw':
- $value += [
- 'key' => $msg->getKey(),
- 'params' => $msg->getParams(),
- ];
- ApiResult::setIndexedTagName( $value['params'], 'param' );
+ $value += $this->formatRawMessage( $msg );
break;
case 'none':
break;
}
- $value += $msg->getApiData();
+ $data = $msg->getApiData();
+ if ( $data ) {
+ $value['data'] = $msg->getApiData() + [
+ ApiResult::META_TYPE => 'assoc',
+ ];
+ }
+ return $value;
+ }
- $path = [ $tag . 's', $moduleName ];
+ /**
+ * Actually add the warning or error to the result
+ * @param string $tag 'warning' or 'error'
+ * @param string|null $modulePath
+ * @param ApiMessage|ApiRawMessage $msg
+ */
+ protected function addWarningOrError( $tag, $modulePath, $msg ) {
+ $value = $this->formatMessageInternal( $msg, $this->format );
+ if ( $modulePath !== null ) {
+ $value += [ 'module' => $modulePath ];
+ }
+
+ $path = [ $tag . 's' ];
$existing = $this->result->getResultData( $path );
if ( $existing === null || !in_array( $value, $existing ) ) {
$flags = ApiResult::NO_SIZE_CHECK;
@@ -243,19 +307,19 @@ class ApiErrorFormatter_BackCompat extends ApiErrorFormatter {
parent::__construct( $result, Language::factory( 'en' ), 'none', false );
}
- public function arrayFromStatus( Status $status, $type = 'error', $format = null ) {
- if ( $status->isGood() || !$status->errors ) {
+ public function arrayFromStatus( StatusValue $status, $type = 'error', $format = null ) {
+ if ( $status->isGood() || !$status->getErrors() ) {
return [];
}
$result = [];
foreach ( $status->getErrorsByType( $type ) as $error ) {
- if ( $error['message'] instanceof Message ) {
- $error = [
- 'message' => $error['message']->getKey(),
- 'params' => $error['message']->getParams(),
- ] + $error;
- }
+ $msg = ApiMessage::create( $error );
+ $error = [
+ 'message' => $msg->getKey(),
+ 'params' => $msg->getParams(),
+ 'code' => $msg->getApiCode(),
+ ] + $error;
ApiResult::setIndexedTagName( $error['params'], 'param' );
$result[] = $error;
}
@@ -264,24 +328,32 @@ class ApiErrorFormatter_BackCompat extends ApiErrorFormatter {
return $result;
}
- protected function addWarningOrError( $tag, $moduleName, $msg ) {
- $value = $msg->plain();
+ protected function formatMessageInternal( $msg, $format ) {
+ return [
+ 'code' => $msg->getApiCode(),
+ 'info' => $msg->text(),
+ ] + $msg->getApiData();
+ }
+
+ protected function addWarningOrError( $tag, $modulePath, $msg ) {
+ $value = self::stripMarkup( $msg->text() );
if ( $tag === 'error' ) {
// In BC mode, only one error
- $code = $msg->getApiCode();
- if ( isset( ApiBase::$messageMap[$code] ) ) {
- // Backwards compatibility
- $code = ApiBase::$messageMap[$code]['code'];
- }
-
$value = [
- 'code' => $code,
+ 'code' => $msg->getApiCode(),
'info' => $value,
] + $msg->getApiData();
$this->result->addValue( null, 'error', $value,
ApiResult::OVERRIDE | ApiResult::ADD_ON_TOP | ApiResult::NO_SIZE_CHECK );
} else {
+ if ( $modulePath === null ) {
+ $moduleName = 'unknown';
+ } else {
+ $i = strrpos( $modulePath, '+' );
+ $moduleName = $i === false ? $modulePath : substr( $modulePath, $i + 1 );
+ }
+
// Don't add duplicate warnings
$tag .= 's';
$path = [ $tag, $moduleName ];
diff --git a/includes/api/ApiExpandTemplates.php b/includes/api/ApiExpandTemplates.php
index 10fb1824be73..6f7cf652c3f4 100644
--- a/includes/api/ApiExpandTemplates.php
+++ b/includes/api/ApiExpandTemplates.php
@@ -42,11 +42,9 @@ class ApiExpandTemplates extends ApiBase {
$this->requireMaxOneParameter( $params, 'prop', 'generatexml' );
if ( $params['prop'] === null ) {
- $this->logFeatureUsage( 'action=expandtemplates&!prop' );
- $this->setWarning( 'Because no values have been specified for the prop parameter, a ' .
- 'legacy format has been used for the output. This format is deprecated, and in ' .
- 'the future, a default value will be set for the prop parameter, causing the new' .
- 'format to always be used.' );
+ $this->addDeprecation(
+ 'apiwarn-deprecation-expandtemplates-prop', 'action=expandtemplates&!prop'
+ );
$prop = [];
} else {
$prop = array_flip( $params['prop'] );
@@ -57,13 +55,13 @@ class ApiExpandTemplates extends ApiBase {
if ( $revid !== null ) {
$rev = Revision::newFromId( $revid );
if ( !$rev ) {
- $this->dieUsage( "There is no revision ID $revid", 'missingrev' );
+ $this->dieWithError( [ 'apierror-nosuchrevid', $revid ] );
}
$title_obj = $rev->getTitle();
} else {
$title_obj = Title::newFromText( $params['title'] );
if ( !$title_obj || $title_obj->isExternal() ) {
- $this->dieUsageMsg( [ 'invalidtitle', $params['title'] ] );
+ $this->dieWithError( [ 'apierror-invalidtitle', wfEscapeWikiText( $params['title'] ) ] );
}
}
@@ -161,9 +159,7 @@ class ApiExpandTemplates extends ApiBase {
}
if ( isset( $prop['modules'] ) &&
!isset( $prop['jsconfigvars'] ) && !isset( $prop['encodedjsconfigvars'] ) ) {
- $this->setWarning( "Property 'modules' was set but not 'jsconfigvars' " .
- "or 'encodedjsconfigvars'. Configuration variables are necessary " .
- 'for proper module usage.' );
+ $this->addWarning( 'apiwarn-moduleswithoutvars' );
}
}
}
diff --git a/includes/api/ApiFeedContributions.php b/includes/api/ApiFeedContributions.php
index c7dc303ada45..97720c6e9f84 100644
--- a/includes/api/ApiFeedContributions.php
+++ b/includes/api/ApiFeedContributions.php
@@ -43,16 +43,16 @@ class ApiFeedContributions extends ApiBase {
$config = $this->getConfig();
if ( !$config->get( 'Feed' ) ) {
- $this->dieUsage( 'Syndication feeds are not available', 'feed-unavailable' );
+ $this->dieWithError( 'feed-unavailable' );
}
$feedClasses = $config->get( 'FeedClasses' );
if ( !isset( $feedClasses[$params['feedformat']] ) ) {
- $this->dieUsage( 'Invalid subscription feed type', 'feed-invalid' );
+ $this->dieWithError( 'feed-invalid' );
}
if ( $params['showsizediff'] && $this->getConfig()->get( 'MiserMode' ) ) {
- $this->dieUsage( 'Size difference is disabled in Miser Mode', 'sizediffdisabled' );
+ $this->dieWithError( 'apierror-sizediffdisabled' );
}
$msg = wfMessage( 'Contributions' )->inContentLanguage()->text();
diff --git a/includes/api/ApiFeedRecentChanges.php b/includes/api/ApiFeedRecentChanges.php
index 813ac013a02f..e0e50edd9c68 100644
--- a/includes/api/ApiFeedRecentChanges.php
+++ b/includes/api/ApiFeedRecentChanges.php
@@ -47,12 +47,12 @@ class ApiFeedRecentChanges extends ApiBase {
$this->params = $this->extractRequestParams();
if ( !$config->get( 'Feed' ) ) {
- $this->dieUsage( 'Syndication feeds are not available', 'feed-unavailable' );
+ $this->dieWithError( 'feed-unavailable' );
}
$feedClasses = $config->get( 'FeedClasses' );
if ( !isset( $feedClasses[$this->params['feedformat']] ) ) {
- $this->dieUsage( 'Invalid subscription feed type', 'feed-invalid' );
+ $this->dieWithError( 'feed-invalid' );
}
$this->getMain()->setCacheMode( 'public' );
@@ -98,7 +98,7 @@ class ApiFeedRecentChanges extends ApiBase {
if ( $specialClass === 'SpecialRecentchangeslinked' ) {
$title = Title::newFromText( $this->params['target'] );
if ( !$title ) {
- $this->dieUsageMsg( [ 'invalidtitle', $this->params['target'] ] );
+ $this->dieWithError( [ 'apierror-invalidtitle', wfEscapeWikiText( $this->params['target'] ) ] );
}
$feed = new ChangesFeed( $feedFormat, false );
diff --git a/includes/api/ApiFeedWatchlist.php b/includes/api/ApiFeedWatchlist.php
index af5b1afc28e5..b9bb761b3c3d 100644
--- a/includes/api/ApiFeedWatchlist.php
+++ b/includes/api/ApiFeedWatchlist.php
@@ -56,11 +56,11 @@ class ApiFeedWatchlist extends ApiBase {
$params = $this->extractRequestParams();
if ( !$config->get( 'Feed' ) ) {
- $this->dieUsage( 'Syndication feeds are not available', 'feed-unavailable' );
+ $this->dieWithError( 'feed-unavailable' );
}
if ( !isset( $feedClasses[$params['feedformat']] ) ) {
- $this->dieUsage( 'Invalid subscription feed type', 'feed-invalid' );
+ $this->dieWithError( 'feed-invalid' );
}
// limit to the number of hours going from now back
@@ -151,15 +151,26 @@ class ApiFeedWatchlist extends ApiBase {
$msg = wfMessage( 'watchlist' )->inContentLanguage()->escaped();
$feed = new $feedClasses[$feedFormat] ( $feedTitle, $msg, $feedUrl );
- if ( $e instanceof UsageException ) {
- $errorCode = $e->getCodeString();
+ if ( $e instanceof ApiUsageException ) {
+ foreach ( $e->getStatusValue()->getErrors() as $error ) {
+ $msg = ApiMessage::create( $error )
+ ->inLanguage( $this->getLanguage() );
+ $errorTitle = $this->msg( 'api-feed-error-title', $msg->getApiCode() );
+ $errorText = $msg->text();
+ $feedItems[] = new FeedItem( $errorTitle, $errorText, '', '', '' );
+ }
} else {
- // Something is seriously wrong
- $errorCode = 'internal_api_error';
+ if ( $e instanceof UsageException ) {
+ $errorCode = $e->getCodeString();
+ } else {
+ // Something is seriously wrong
+ $errorCode = 'internal_api_error';
+ }
+ $errorTitle = $this->msg( 'api-feed-error-title', $msg->getApiCode() );
+ $errorText = $e->getMessage();
+ $feedItems[] = new FeedItem( $errorTitle, $errorText, '', '', '' );
}
- $errorText = $e->getMessage();
- $feedItems[] = new FeedItem( "Error ($errorCode)", $errorText, '', '', '' );
ApiFormatFeedWrapper::setResult( $this->getResult(), $feed, $feedItems );
}
}
diff --git a/includes/api/ApiFileRevert.php b/includes/api/ApiFileRevert.php
index 97464d61db73..736898edcfa7 100644
--- a/includes/api/ApiFileRevert.php
+++ b/includes/api/ApiFileRevert.php
@@ -45,7 +45,7 @@ class ApiFileRevert extends ApiBase {
$this->validateParameters();
// Check whether we're allowed to revert this file
- $this->checkPermissions( $this->getUser() );
+ $this->checkTitleUserPermissions( $this->file->getTitle(), [ 'edit', 'upload' ] );
$sourceUrl = $this->file->getArchiveVirtualUrl( $this->archiveName );
$status = $this->file->upload(
@@ -71,23 +71,6 @@ class ApiFileRevert extends ApiBase {
}
/**
- * Checks that the user has permissions to perform this revert.
- * Dies with usage message on inadequate permissions.
- * @param User $user The user to check.
- */
- protected function checkPermissions( $user ) {
- $title = $this->file->getTitle();
- $permissionErrors = array_merge(
- $title->getUserPermissionsErrors( 'edit', $user ),
- $title->getUserPermissionsErrors( 'upload', $user )
- );
-
- if ( $permissionErrors ) {
- $this->dieUsageMsg( $permissionErrors[0] );
- }
- }
-
- /**
* Validate the user parameters and set $this->archiveName and $this->file.
* Throws an error if validation fails
*/
@@ -95,21 +78,23 @@ class ApiFileRevert extends ApiBase {
// Validate the input title
$title = Title::makeTitleSafe( NS_FILE, $this->params['filename'] );
if ( is_null( $title ) ) {
- $this->dieUsageMsg( [ 'invalidtitle', $this->params['filename'] ] );
+ $this->dieWithError(
+ [ 'apierror-invalidtitle', wfEscapeWikiText( $this->params['filename'] ) ]
+ );
}
$localRepo = RepoGroup::singleton()->getLocalRepo();
// Check if the file really exists
$this->file = $localRepo->newFile( $title );
if ( !$this->file->exists() ) {
- $this->dieUsageMsg( 'notanarticle' );
+ $this->dieWithError( 'apierror-missingtitle' );
}
// Check if the archivename is valid for this file
$this->archiveName = $this->params['archivename'];
$oldFile = $localRepo->newFromArchiveName( $title, $this->archiveName );
if ( !$oldFile->exists() ) {
- $this->dieUsageMsg( 'filerevert-badversion' );
+ $this->dieWithError( 'filerevert-badversion' );
}
}
diff --git a/includes/api/ApiFormatJson.php b/includes/api/ApiFormatJson.php
index 2e917e1a4fc5..8ebfe48cf88a 100644
--- a/includes/api/ApiFormatJson.php
+++ b/includes/api/ApiFormatJson.php
@@ -84,8 +84,8 @@ class ApiFormatJson extends ApiFormatBase {
break;
default:
- $this->dieUsage( __METHOD__ .
- ': Unknown value for \'formatversion\'', 'unknownformatversion' );
+ // Should have been caught during parameter validation
+ $this->dieDebug( __METHOD__, 'Unknown value for \'formatversion\'' );
}
}
$data = $this->getResult()->getResultData( null, $transform );
diff --git a/includes/api/ApiFormatPhp.php b/includes/api/ApiFormatPhp.php
index fc25f4772390..a744f57becf1 100644
--- a/includes/api/ApiFormatPhp.php
+++ b/includes/api/ApiFormatPhp.php
@@ -55,7 +55,8 @@ class ApiFormatPhp extends ApiFormatBase {
break;
default:
- $this->dieUsage( __METHOD__ . ': Unknown value for \'formatversion\'', 'unknownformatversion' );
+ // Should have been caught during parameter validation
+ $this->dieDebug( __METHOD__, 'Unknown value for \'formatversion\'' );
}
$text = serialize( $this->getResult()->getResultData( null, $transforms ) );
@@ -67,11 +68,7 @@ class ApiFormatPhp extends ApiFormatBase {
in_array( 'wfOutputHandler', ob_list_handlers(), true ) &&
preg_match( '/\<\s*cross-domain-policy(?=\s|\>)/i', $text )
) {
- $this->dieUsage(
- 'This response cannot be represented using format=php. ' .
- 'See https://phabricator.wikimedia.org/T68776',
- 'internalerror'
- );
+ $this->dieWithError( 'apierror-formatphp', 'internalerror' );
}
$this->printText( $text );
diff --git a/includes/api/ApiFormatRaw.php b/includes/api/ApiFormatRaw.php
index 9da040ca0b20..228b47ea651f 100644
--- a/includes/api/ApiFormatRaw.php
+++ b/includes/api/ApiFormatRaw.php
@@ -49,7 +49,7 @@ class ApiFormatRaw extends ApiFormatBase {
public function getMimeType() {
$data = $this->getResult()->getResultData();
- if ( isset( $data['error'] ) ) {
+ if ( isset( $data['error'] ) || isset( $data['errors'] ) ) {
return $this->errorFallback->getMimeType();
}
@@ -62,7 +62,7 @@ class ApiFormatRaw extends ApiFormatBase {
public function initPrinter( $unused = false ) {
$data = $this->getResult()->getResultData();
- if ( isset( $data['error'] ) ) {
+ if ( isset( $data['error'] ) || isset( $data['errors'] ) ) {
$this->errorFallback->initPrinter( $unused );
if ( $this->mFailWithHTTPError ) {
$this->getMain()->getRequest()->response()->statusHeader( 400 );
@@ -74,7 +74,7 @@ class ApiFormatRaw extends ApiFormatBase {
public function closePrinter() {
$data = $this->getResult()->getResultData();
- if ( isset( $data['error'] ) ) {
+ if ( isset( $data['error'] ) || isset( $data['errors'] ) ) {
$this->errorFallback->closePrinter();
} else {
parent::closePrinter();
@@ -83,7 +83,7 @@ class ApiFormatRaw extends ApiFormatBase {
public function execute() {
$data = $this->getResult()->getResultData();
- if ( isset( $data['error'] ) ) {
+ if ( isset( $data['error'] ) || isset( $data['errors'] ) ) {
$this->errorFallback->execute();
return;
}
diff --git a/includes/api/ApiFormatXml.php b/includes/api/ApiFormatXml.php
index a45dbebfb51f..e4dfda0f572c 100644
--- a/includes/api/ApiFormatXml.php
+++ b/includes/api/ApiFormatXml.php
@@ -269,17 +269,17 @@ class ApiFormatXml extends ApiFormatBase {
protected function addXslt() {
$nt = Title::newFromText( $this->mXslt );
if ( is_null( $nt ) || !$nt->exists() ) {
- $this->setWarning( 'Invalid or non-existent stylesheet specified' );
+ $this->addWarning( 'apiwarn-invalidxmlstylesheet' );
return;
}
if ( $nt->getNamespace() != NS_MEDIAWIKI ) {
- $this->setWarning( 'Stylesheet should be in the MediaWiki namespace.' );
+ $this->addWarning( 'apiwarn-invalidxmlstylesheetns' );
return;
}
if ( substr( $nt->getText(), -4 ) !== '.xsl' ) {
- $this->setWarning( 'Stylesheet should have .xsl extension.' );
+ $this->addWarning( 'apiwarn-invalidxmlstylesheetext' );
return;
}
diff --git a/includes/api/ApiImageRotate.php b/includes/api/ApiImageRotate.php
index 37cb80a9fc29..72fb16d19ba4 100644
--- a/includes/api/ApiImageRotate.php
+++ b/includes/api/ApiImageRotate.php
@@ -56,23 +56,29 @@ class ApiImageRotate extends ApiBase {
$file = wfFindFile( $title, [ 'latest' => true ] );
if ( !$file ) {
$r['result'] = 'Failure';
- $r['errormessage'] = 'File does not exist';
+ $r['errors'] = $this->getErrorFormatter()->arrayFromStatus(
+ Status::newFatal( 'apierror-filedoesnotexist' )
+ );
$result[] = $r;
continue;
}
$handler = $file->getHandler();
if ( !$handler || !$handler->canRotate() ) {
$r['result'] = 'Failure';
- $r['errormessage'] = 'File type cannot be rotated';
+ $r['errors'] = $this->getErrorFormatter()->arrayFromStatus(
+ Status::newFatal( 'apierror-filetypecannotberotated' )
+ );
$result[] = $r;
continue;
}
// Check whether we're allowed to rotate this file
- $permError = $this->checkPermissions( $this->getUser(), $file->getTitle() );
- if ( $permError !== null ) {
+ $permError = $this->checkTitleUserPermissions( $file->getTitle(), [ 'edit', 'upload' ] );
+ if ( $permError ) {
$r['result'] = 'Failure';
- $r['errormessage'] = $permError;
+ $r['errors'] = $this->getErrorFormatter()->arrayFromStatus(
+ $this->errorArrayToStatus( $permError )
+ );
$result[] = $r;
continue;
}
@@ -80,7 +86,9 @@ class ApiImageRotate extends ApiBase {
$srcPath = $file->getLocalRefPath();
if ( $srcPath === false ) {
$r['result'] = 'Failure';
- $r['errormessage'] = 'Cannot get local file path';
+ $r['errors'] = $this->getErrorFormatter()->arrayFromStatus(
+ Status::newFatal( 'apierror-filenopath' )
+ );
$result[] = $r;
continue;
}
@@ -102,11 +110,13 @@ class ApiImageRotate extends ApiBase {
$r['result'] = 'Success';
} else {
$r['result'] = 'Failure';
- $r['errormessage'] = $this->getErrorFormatter()->arrayFromStatus( $status );
+ $r['errors'] = $this->getErrorFormatter()->arrayFromStatus( $status );
}
} else {
$r['result'] = 'Failure';
- $r['errormessage'] = $err->toText();
+ $r['errors'] = $this->getErrorFormatter()->arrayFromStatus(
+ Status::newFatal( ApiMessage::create( $err->getMsg() ) )
+ );
}
$result[] = $r;
}
@@ -130,28 +140,6 @@ class ApiImageRotate extends ApiBase {
return $this->mPageSet;
}
- /**
- * Checks that the user has permissions to perform rotations.
- * @param User $user The user to check
- * @param Title $title
- * @return string|null Permission error message, or null if there is no error
- */
- protected function checkPermissions( $user, $title ) {
- $permissionErrors = array_merge(
- $title->getUserPermissionsErrors( 'edit', $user ),
- $title->getUserPermissionsErrors( 'upload', $user )
- );
-
- if ( $permissionErrors ) {
- // Just return the first error
- $msg = $this->parseMsg( $permissionErrors[0] );
-
- return $msg['info'];
- }
-
- return null;
- }
-
public function mustBePosted() {
return true;
}
diff --git a/includes/api/ApiImport.php b/includes/api/ApiImport.php
index 10106ff02b4e..3f48f38a0ef1 100644
--- a/includes/api/ApiImport.php
+++ b/includes/api/ApiImport.php
@@ -42,10 +42,10 @@ class ApiImport extends ApiBase {
$isUpload = false;
if ( isset( $params['interwikisource'] ) ) {
if ( !$user->isAllowed( 'import' ) ) {
- $this->dieUsageMsg( 'cantimport' );
+ $this->dieWithError( 'apierror-cantimport' );
}
if ( !isset( $params['interwikipage'] ) ) {
- $this->dieUsageMsg( [ 'missingparam', 'interwikipage' ] );
+ $this->dieWithError( [ 'apierror-missingparam', 'interwikipage' ] );
}
$source = ImportStreamSource::newFromInterwiki(
$params['interwikisource'],
@@ -56,7 +56,7 @@ class ApiImport extends ApiBase {
} else {
$isUpload = true;
if ( !$user->isAllowed( 'importupload' ) ) {
- $this->dieUsageMsg( 'cantimport-upload' );
+ $this->dieWithError( 'apierror-cantimport-upload' );
}
$source = ImportStreamSource::newFromUpload( 'xml' );
}
@@ -83,7 +83,7 @@ class ApiImport extends ApiBase {
try {
$importer->doImport();
} catch ( Exception $e ) {
- $this->dieUsageMsg( [ 'import-unknownerror', $e->getMessage() ] );
+ $this->dieWithError( [ 'apierror-import-unknownerror', wfEscapeWikiText( $e->getMessage() ) ] );
}
$resultData = $reporter->getData();
diff --git a/includes/api/ApiLinkAccount.php b/includes/api/ApiLinkAccount.php
index 1017607ce6a2..9a21e7620cf5 100644
--- a/includes/api/ApiLinkAccount.php
+++ b/includes/api/ApiLinkAccount.php
@@ -49,7 +49,7 @@ class ApiLinkAccount extends ApiBase {
public function execute() {
if ( !$this->getUser()->isLoggedIn() ) {
- $this->dieUsage( 'Must be logged in to link accounts', 'notloggedin' );
+ $this->dieWithError( 'apierror-mustbeloggedin-linkaccounts', 'notloggedin' );
}
$params = $this->extractRequestParams();
@@ -60,8 +60,8 @@ class ApiLinkAccount extends ApiBase {
$bits = wfParseUrl( $params['returnurl'] );
if ( !$bits || $bits['scheme'] === '' ) {
$encParamName = $this->encodeParamName( 'returnurl' );
- $this->dieUsage(
- "Invalid value '{$params['returnurl']}' for url parameter $encParamName",
+ $this->dieWithError(
+ [ 'apierror-badurl', $encParamName, wfEscapeWikiText( $params['returnurl'] ) ],
"badurl_{$encParamName}"
);
}
diff --git a/includes/api/ApiLogin.php b/includes/api/ApiLogin.php
index 6ac261dd3a00..723dc33c78a3 100644
--- a/includes/api/ApiLogin.php
+++ b/includes/api/ApiLogin.php
@@ -72,10 +72,11 @@ class ApiLogin extends ApiBase {
try {
$this->requirePostedParameters( [ 'password', 'token' ] );
- } catch ( UsageException $ex ) {
+ } catch ( ApiUsageException $ex ) {
// Make this a warning for now, upgrade to an error in 1.29.
- $this->setWarning( $ex->getMessage() );
- $this->logFeatureUsage( 'login-params-in-query-string' );
+ foreach ( $ex->getStatusValue()->getErrors() as $error ) {
+ $this->addDeprecation( $error, 'login-params-in-query-string' );
+ }
}
$params = $this->extractRequestParams();
@@ -146,15 +147,10 @@ class ApiLogin extends ApiBase {
switch ( $res->status ) {
case AuthenticationResponse::PASS:
if ( $this->getConfig()->get( 'EnableBotPasswords' ) ) {
- $warn = 'Main-account login via action=login is deprecated and may stop working ' .
- 'without warning.';
- $warn .= ' To continue login with action=login, see [[Special:BotPasswords]].';
- $warn .= ' To safely continue using main-account login, see action=clientlogin.';
+ $this->addDeprecation( 'apiwarn-deprecation-login-botpw', 'main-account-login' );
} else {
- $warn = 'Login via action=login is deprecated and may stop working without warning.';
- $warn .= ' To safely log in, see action=clientlogin.';
+ $this->addDeprecation( 'apiwarn-deprecation-login-nobotpw', 'main-account-login' );
}
- $this->setWarning( $warn );
$authRes = 'Success';
$loginType = 'AuthManager';
break;
@@ -194,16 +190,16 @@ class ApiLogin extends ApiBase {
case 'NeedToken':
$result['token'] = $token->toString();
- $this->setWarning( 'Fetching a token via action=login is deprecated. ' .
- 'Use action=query&meta=tokens&type=login instead.' );
- $this->logFeatureUsage( 'action=login&!lgtoken' );
+ $this->addDeprecation( 'apiwarn-deprecation-login-token', 'action=login&!lgtoken' );
break;
case 'WrongToken':
break;
case 'Failed':
- $result['reason'] = $message->useDatabase( 'false' )->inLanguage( 'en' )->text();
+ $result['reason'] = ApiErrorFormatter::stripMarkup(
+ $message->useDatabase( false )->inLanguage( 'en' )->text()
+ );
break;
case 'Aborted':
diff --git a/includes/api/ApiLogout.php b/includes/api/ApiLogout.php
index 6a26e2e3502b..d5c28f1d6a05 100644
--- a/includes/api/ApiLogout.php
+++ b/includes/api/ApiLogout.php
@@ -45,9 +45,11 @@ class ApiLogout extends ApiBase {
// Make sure it's possible to log out
if ( !$session->canSetUser() ) {
- $this->dieUsage(
- 'Cannot log out when using ' .
- $session->getProvider()->describe( Language::factory( 'en' ) ),
+ $this->dieWithError(
+ [
+ 'cannotlogoutnow-text',
+ $session->getProvider()->describe( $this->getErrorFormatter()->getLanguage() )
+ ],
'cannotlogout'
);
}
diff --git a/includes/api/ApiMain.php b/includes/api/ApiMain.php
index 38299b471163..fe6ed417cafa 100644
--- a/includes/api/ApiMain.php
+++ b/includes/api/ApiMain.php
@@ -47,6 +47,11 @@ class ApiMain extends ApiBase {
const API_DEFAULT_FORMAT = 'jsonfm';
/**
+ * When no uselang parameter is given, this language will be used
+ */
+ const API_DEFAULT_USELANG = 'user';
+
+ /**
* List of available modules: action name => module class
*/
private static $Modules = [
@@ -140,7 +145,7 @@ class ApiMain extends ApiBase {
*/
private $mPrinter;
- private $mModuleMgr, $mResult, $mErrorFormatter;
+ private $mModuleMgr, $mResult, $mErrorFormatter = null;
/** @var ApiContinuationManager|null */
private $mContinuationManager;
private $mAction;
@@ -229,7 +234,11 @@ class ApiMain extends ApiBase {
}
}
- $uselang = $this->getParameter( 'uselang' );
+ $this->mResult = new ApiResult( $this->getConfig()->get( 'APIMaxResultSize' ) );
+
+ // Setup uselang. This doesn't use $this->getParameter()
+ // because we're not ready to handle errors yet.
+ $uselang = $request->getVal( 'uselang', self::API_DEFAULT_USELANG );
if ( $uselang === 'user' ) {
// Assume the parent context is going to return the user language
// for uselang=user (see T85635).
@@ -247,6 +256,29 @@ class ApiMain extends ApiBase {
}
}
+ // Set up the error formatter. This doesn't use $this->getParameter()
+ // because we're not ready to handle errors yet.
+ $errorFormat = $request->getVal( 'errorformat', 'bc' );
+ $errorLangCode = $request->getVal( 'errorlang', 'uselang' );
+ $errorsUseDB = $request->getCheck( 'errorsuselocal' );
+ if ( in_array( $errorFormat, [ 'plaintext', 'wikitext', 'html', 'raw', 'none' ], true ) ) {
+ if ( $errorLangCode === 'uselang' ) {
+ $errorLang = $this->getLanguage();
+ } elseif ( $errorLangCode === 'content' ) {
+ global $wgContLang;
+ $errorLang = $wgContLang;
+ } else {
+ $errorLangCode = RequestContext::sanitizeLangCode( $errorLangCode );
+ $errorLang = Language::factory( $errorLangCode );
+ }
+ $this->mErrorFormatter = new ApiErrorFormatter(
+ $this->mResult, $errorLang, $errorFormat, $errorsUseDB
+ );
+ } else {
+ $this->mErrorFormatter = new ApiErrorFormatter_BackCompat( $this->mResult );
+ }
+ $this->mResult->setErrorFormatter( $this->getErrorFormatter() );
+
$this->mModuleMgr = new ApiModuleManager( $this );
$this->mModuleMgr->addModules( self::$Modules, 'action' );
$this->mModuleMgr->addModules( $config->get( 'APIModules' ), 'action' );
@@ -255,9 +287,6 @@ class ApiMain extends ApiBase {
Hooks::run( 'ApiMain::moduleManager', [ $this->mModuleMgr ] );
- $this->mResult = new ApiResult( $this->getConfig()->get( 'APIMaxResultSize' ) );
- $this->mErrorFormatter = new ApiErrorFormatter_BackCompat( $this->mResult );
- $this->mResult->setErrorFormatter( $this->mErrorFormatter );
$this->mContinuationManager = null;
$this->mEnableWrite = $enableWrite;
@@ -464,7 +493,9 @@ class ApiMain extends ApiBase {
public function createPrinterByName( $format ) {
$printer = $this->mModuleMgr->getModule( $format, 'format' );
if ( $printer === null ) {
- $this->dieUsage( "Unrecognized format: {$format}", 'unknown_format' );
+ $this->dieWithError(
+ [ 'apierror-unknownformat', wfEscapeWikiText( $format ) ], 'unknown_format'
+ );
}
return $printer;
@@ -542,7 +573,7 @@ class ApiMain extends ApiBase {
*/
protected function handleException( Exception $e ) {
// Bug 63145: Rollback any open database transactions
- if ( !( $e instanceof UsageException ) ) {
+ if ( !( $e instanceof ApiUsageException || $e instanceof UsageException ) ) {
// UsageExceptions are intentional, so don't rollback if that's the case
try {
MWExceptionHandler::rollbackMasterChangesAndLog( $e );
@@ -557,7 +588,7 @@ class ApiMain extends ApiBase {
Hooks::run( 'ApiMain::onException', [ $this, $e ] );
// Log it
- if ( !( $e instanceof UsageException ) ) {
+ if ( !( $e instanceof ApiUsageException || $e instanceof UsageException ) ) {
MWExceptionHandler::logException( $e );
}
@@ -565,13 +596,13 @@ class ApiMain extends ApiBase {
// If this fails, an unhandled exception should be thrown so that global error
// handler will process and log it.
- $errCode = $this->substituteResultWithError( $e );
+ $errCodes = $this->substituteResultWithError( $e );
// Error results should not be cached
$this->setCacheMode( 'private' );
$response = $this->getRequest()->response();
- $headerStr = 'MediaWiki-API-Error: ' . $errCode;
+ $headerStr = 'MediaWiki-API-Error: ' . join( ', ', $errCodes );
$response->header( $headerStr );
// Reset and print just the error message
@@ -580,14 +611,31 @@ class ApiMain extends ApiBase {
// Printer may not be initialized if the extractRequestParams() fails for the main module
$this->createErrorPrinter();
+ $failed = false;
try {
$this->printResult( $e->getCode() );
+ } catch ( ApiUsageException $ex ) {
+ // The error printer itself is failing. Try suppressing its request
+ // parameters and redo.
+ $failed = true;
+ $this->addWarning( 'apiwarn-errorprinterfailed' );
+ foreach ( $ex->getStatusValue()->getErrors() as $error ) {
+ try {
+ $this->mPrinter->addWarning( $error );
+ } catch ( Exception $ex2 ) {
+ // WTF?
+ $this->addWarning( $error );
+ }
+ }
} catch ( UsageException $ex ) {
// The error printer itself is failing. Try suppressing its request
// parameters and redo.
- $this->setWarning(
- 'Error printer failed (will retry without params): ' . $ex->getMessage()
+ $failed = true;
+ $this->addWarning(
+ [ 'apiwarn-errorprinterfailed-ex', $ex->getMessage() ], 'errorprinterfailed'
);
+ }
+ if ( $failed ) {
$this->mPrinter = null;
$this->createErrorPrinter();
$this->mPrinter->forceDefaultParams();
@@ -958,99 +1006,145 @@ class ApiMain extends ApiBase {
/**
* Create an error message for the given exception.
*
- * If the exception is a UsageException then
- * UsageException::getMessageArray() will be called to create the message.
+ * If an ApiUsageException, errors/warnings will be extracted from the
+ * embedded StatusValue.
+ *
+ * If a base UsageException, the getMessageArray() method will be used to
+ * extract the code and English message for a single error (no warnings).
+ *
+ * Any other exception will be returned with a generic code and wrapper
+ * text around the exception's (presumably English) message as a single
+ * error (no warnings).
*
* @param Exception $e
- * @return array ['code' => 'some string', 'info' => 'some other string']
+ * @param string $type 'error' or 'warning'
+ * @return ApiMessage[]
* @since 1.27
*/
- protected function errorMessageFromException( $e ) {
- if ( $e instanceof UsageException ) {
+ protected function errorMessagesFromException( $e, $type = 'error' ) {
+ $messages = [];
+ if ( $e instanceof ApiUsageException ) {
+ foreach ( $e->getStatusValue()->getErrorsByType( $type ) as $error ) {
+ $messages[] = ApiMessage::create( $error );
+ }
+ } elseif ( $type !== 'error' ) {
+ // None of the rest have any messages for non-error types
+ } elseif ( $e instanceof UsageException ) {
// User entered incorrect parameters - generate error response
- $errMessage = $e->getMessageArray();
+ $data = $e->getMessageArray();
+ $code = $data['code'];
+ $info = $data['info'];
+ unset( $data['code'], $data['info'] );
+ $messages[] = new ApiRawMessage( [ '$1', $info ], $code, $data );
} else {
- $config = $this->getConfig();
// Something is seriously wrong
+ $config = $this->getConfig();
+ $code = 'internal_api_error_' . get_class( $e );
if ( ( $e instanceof DBQueryError ) && !$config->get( 'ShowSQLErrors' ) ) {
- $info = 'Database query error';
+ $params = [ 'apierror-databaseerror', WebRequest::getRequestId() ];
} else {
- $info = "Exception Caught: {$e->getMessage()}";
+ $params = [
+ 'apierror-exceptioncaught',
+ WebRequest::getRequestId(),
+ wfEscapeWikiText( $e->getMessage() )
+ ];
}
-
- $errMessage = [
- 'code' => 'internal_api_error_' . get_class( $e ),
- 'info' => '[' . WebRequest::getRequestId() . '] ' . $info,
- ];
+ $messages[] = ApiMessage::create( $params, $code );
}
- return $errMessage;
+ return $messages;
}
/**
* Replace the result data with the information about an exception.
- * Returns the error code
* @param Exception $e
- * @return string
+ * @return string[] Error codes
*/
protected function substituteResultWithError( $e ) {
$result = $this->getResult();
+ $formatter = $this->getErrorFormatter();
$config = $this->getConfig();
+ $errorCodes = [];
- $errMessage = $this->errorMessageFromException( $e );
- if ( $e instanceof UsageException ) {
- // User entered incorrect parameters - generate error response
+ // Remember existing warnings and errors across the reset
+ $errors = $result->getResultData( [ 'errors' ] );
+ $warnings = $result->getResultData( [ 'warnings' ] );
+ $result->reset();
+ if ( $warnings !== null ) {
+ $result->addValue( null, 'warnings', $warnings, ApiResult::NO_SIZE_CHECK );
+ }
+ if ( $errors !== null ) {
+ $result->addValue( null, 'errors', $errors, ApiResult::NO_SIZE_CHECK );
+
+ // Collect the copied error codes for the return value
+ foreach ( $errors as $error ) {
+ if ( isset( $error['code'] ) ) {
+ $errorCodes[$error['code']] = true;
+ }
+ }
+ }
+
+ // Add errors from the exception
+ $modulePath = $e instanceof ApiUsageException ? $e->getModulePath() : null;
+ foreach ( $this->errorMessagesFromException( $e, 'error' ) as $msg ) {
+ $errorCodes[$msg->getApiCode()] = true;
+ $formatter->addError( $modulePath, $msg );
+ }
+ foreach ( $this->errorMessagesFromException( $e, 'warning' ) as $msg ) {
+ $formatter->addWarning( $modulePath, $msg );
+ }
+
+ // Add additional data. Path depends on whether we're in BC mode or not.
+ // Data depends on the type of exception.
+ if ( $formatter instanceof ApiErrorFormatter_BackCompat ) {
+ $path = [ 'error' ];
+ } else {
+ $path = null;
+ }
+ if ( $e instanceof ApiUsageException || $e instanceof UsageException ) {
$link = wfExpandUrl( wfScript( 'api' ) );
- ApiResult::setContentValue( $errMessage, 'docref', "See $link for API usage" );
+ $result->addContentValue(
+ $path,
+ 'docref',
+ $this->msg( 'api-usage-docref', $link )->inLanguage( $formatter->getLanguage() )->text()
+ );
} else {
- // Something is seriously wrong
if ( $config->get( 'ShowExceptionDetails' ) ) {
- ApiResult::setContentValue(
- $errMessage,
+ $result->addContentValue(
+ $path,
'trace',
- MWExceptionHandler::getRedactedTraceAsString( $e )
+ $this->msg( 'api-exception-trace',
+ get_class( $e ),
+ $e->getFile(),
+ $e->getLine(),
+ MWExceptionHandler::getRedactedTraceAsString( $e )
+ )->inLanguage( $formatter->getLanguage() )->text()
);
}
}
- // Remember all the warnings to re-add them later
- $warnings = $result->getResultData( [ 'warnings' ] );
+ // Add the id and such
+ $this->addRequestedFields( [ 'servedby' ] );
- $result->reset();
- // Re-add the id
- $requestid = $this->getParameter( 'requestid' );
- if ( !is_null( $requestid ) ) {
- $result->addValue( null, 'requestid', $requestid, ApiResult::NO_SIZE_CHECK );
- }
- if ( $config->get( 'ShowHostnames' ) ) {
- // servedby is especially useful when debugging errors
- $result->addValue( null, 'servedby', wfHostname(), ApiResult::NO_SIZE_CHECK );
- }
- if ( $warnings !== null ) {
- $result->addValue( null, 'warnings', $warnings, ApiResult::NO_SIZE_CHECK );
- }
-
- $result->addValue( null, 'error', $errMessage, ApiResult::NO_SIZE_CHECK );
-
- return $errMessage['code'];
+ return array_keys( $errorCodes );
}
/**
- * Set up for the execution.
- * @return array
+ * Add requested fields to the result
+ * @param string[] $force Which fields to force even if not requested. Accepted values are:
+ * - servedby
*/
- protected function setupExecuteAction() {
- // First add the id to the top element
+ protected function addRequestedFields( $force = [] ) {
$result = $this->getResult();
+
$requestid = $this->getParameter( 'requestid' );
- if ( !is_null( $requestid ) ) {
- $result->addValue( null, 'requestid', $requestid );
+ if ( $requestid !== null ) {
+ $result->addValue( null, 'requestid', $requestid, ApiResult::NO_SIZE_CHECK );
}
- if ( $this->getConfig()->get( 'ShowHostnames' ) ) {
- $servedby = $this->getParameter( 'servedby' );
- if ( $servedby ) {
- $result->addValue( null, 'servedby', wfHostname() );
- }
+ if ( $this->getConfig()->get( 'ShowHostnames' ) && (
+ in_array( 'servedby', $force, true ) || $this->getParameter( 'servedby' )
+ ) ) {
+ $result->addValue( null, 'servedby', wfHostname(), ApiResult::NO_SIZE_CHECK );
}
if ( $this->getParameter( 'curtimestamp' ) ) {
@@ -1058,13 +1152,23 @@ class ApiMain extends ApiBase {
ApiResult::NO_SIZE_CHECK );
}
- $params = $this->extractRequestParams();
+ if ( $this->getParameter( 'responselanginfo' ) ) {
+ $result->addValue( null, 'uselang', $this->getLanguage()->getCode(),
+ ApiResult::NO_SIZE_CHECK );
+ $result->addValue( null, 'errorlang', $this->getErrorFormatter()->getLanguage()->getCode(),
+ ApiResult::NO_SIZE_CHECK );
+ }
+ }
- $this->mAction = $params['action'];
+ /**
+ * Set up for the execution.
+ * @return array
+ */
+ protected function setupExecuteAction() {
+ $this->addRequestedFields();
- if ( !is_string( $this->mAction ) ) {
- $this->dieUsage( 'The API requires a valid action parameter', 'unknown_action' );
- }
+ $params = $this->extractRequestParams();
+ $this->mAction = $params['action'];
return $params;
}
@@ -1073,13 +1177,15 @@ class ApiMain extends ApiBase {
* Set up the module for response
* @return ApiBase The module that will handle this action
* @throws MWException
- * @throws UsageException
+ * @throws ApiUsageException
*/
protected function setupModule() {
// Instantiate the module requested by the user
$module = $this->mModuleMgr->getModule( $this->mAction, 'action' );
if ( $module === null ) {
- $this->dieUsage( 'The API requires a valid action parameter', 'unknown_action' );
+ $this->dieWithError(
+ [ 'apierror-unknownaction', wfEscapeWikiText( $this->mAction ) ], 'unknown_action'
+ );
}
$moduleParams = $module->extractRequestParams();
@@ -1098,13 +1204,13 @@ class ApiMain extends ApiBase {
}
if ( !isset( $moduleParams['token'] ) ) {
- $this->dieUsageMsg( [ 'missingparam', 'token' ] );
+ $module->dieWithError( [ 'apierror-missingparam', 'token' ] );
}
$module->requirePostedParameters( [ 'token' ] );
if ( !$module->validateToken( $moduleParams['token'], $moduleParams ) ) {
- $this->dieUsageMsg( 'sessionfailure' );
+ $module->dieWithError( 'apierror-badtoken' );
}
}
@@ -1128,10 +1234,10 @@ class ApiMain extends ApiBase {
$response->header( 'X-Database-Lag: ' . intval( $lag ) );
if ( $this->getConfig()->get( 'ShowHostnames' ) ) {
- $this->dieUsage( "Waiting for $host: $lag seconds lagged", 'maxlag' );
+ $this->dieWithError( [ 'apierror-maxlag', $lag, $host ] );
}
- $this->dieUsage( "Waiting for a database server: $lag seconds lagged", 'maxlag' );
+ $this->dieWithError( [ 'apierror-maxlag-generic', $lag ], 'maxlag' );
}
}
@@ -1262,19 +1368,16 @@ class ApiMain extends ApiBase {
if ( $module->isReadMode() && !User::isEveryoneAllowed( 'read' ) &&
!$user->isAllowed( 'read' )
) {
- $this->dieUsageMsg( 'readrequired' );
+ $this->dieWithError( 'apierror-readapidenied' );
}
if ( $module->isWriteMode() ) {
if ( !$this->mEnableWrite ) {
- $this->dieUsageMsg( 'writedisabled' );
+ $this->dieWithError( 'apierror-noapiwrite' );
} elseif ( !$user->isAllowed( 'writeapi' ) ) {
- $this->dieUsageMsg( 'writerequired' );
+ $this->dieWithError( 'apierror-writeapidenied' );
} elseif ( $this->getRequest()->getHeader( 'Promise-Non-Write-API-Action' ) ) {
- $this->dieUsage(
- 'Promise-Non-Write-API-Action HTTP header cannot be sent to write API modules',
- 'promised-nonwrite-api'
- );
+ $this->dieWithError( 'apierror-promised-nonwrite-api' );
}
$this->checkReadOnly( $module );
@@ -1283,7 +1386,7 @@ class ApiMain extends ApiBase {
// Allow extensions to stop execution for arbitrary reasons.
$message = false;
if ( !Hooks::run( 'ApiCheckCanExecute', [ $module, $user, &$message ] ) ) {
- $this->dieUsageMsg( $message );
+ $this->dieWithError( $message );
}
}
@@ -1329,12 +1432,9 @@ class ApiMain extends ApiBase {
"Api request failed as read only because the following DBs are lagged: $laggedServers"
);
- $parsed = $this->parseMsg( [ 'readonlytext' ] );
- $this->dieUsage(
- $parsed['info'],
- $parsed['code'],
- /* http error */
- 0,
+ $this->dieWithError(
+ 'readonly_lag',
+ 'readonly',
[ 'readonlyreason' => "Waiting for $numLagged lagged database(s)" ]
);
}
@@ -1350,12 +1450,12 @@ class ApiMain extends ApiBase {
switch ( $params['assert'] ) {
case 'user':
if ( $user->isAnon() ) {
- $this->dieUsage( 'Assertion that the user is logged in failed', 'assertuserfailed' );
+ $this->dieWithError( 'apierror-assertuserfailed' );
}
break;
case 'bot':
if ( !$user->isAllowed( 'bot' ) ) {
- $this->dieUsage( 'Assertion that the user has the bot right failed', 'assertbotfailed' );
+ $this->dieWithError( 'apierror-assertbotfailed' );
}
break;
}
@@ -1363,9 +1463,8 @@ class ApiMain extends ApiBase {
if ( isset( $params['assertuser'] ) ) {
$assertUser = User::newFromName( $params['assertuser'], false );
if ( !$assertUser || !$this->getUser()->equals( $assertUser ) ) {
- $this->dieUsage(
- 'Assertion that the user is "' . $params['assertuser'] . '" failed',
- 'assertnameduserfailed'
+ $this->dieWithError(
+ [ 'apierror-assertnameduserfailed', wfEscapeWikiText( $params['assertuser'] ) ]
);
}
}
@@ -1381,7 +1480,7 @@ class ApiMain extends ApiBase {
if ( !$request->wasPosted() && $module->mustBePosted() ) {
// Module requires POST. GET request might still be allowed
// if $wgDebugApi is true, otherwise fail.
- $this->dieUsageMsgOrDebug( [ 'mustbeposted', $this->mAction ] );
+ $this->dieWithErrorOrDebug( [ 'apierror-mustbeposted', $this->mAction ] );
}
// See if custom printer is used
@@ -1396,8 +1495,7 @@ class ApiMain extends ApiBase {
( $this->getUser()->isLoggedIn() &&
$this->getUser()->requiresHTTPS() )
) ) {
- $this->logFeatureUsage( 'https-expected' );
- $this->setWarning( 'HTTP used when HTTPS was expected' );
+ $this->addDeprecation( 'apiwarn-deprecation-httpsexpected', 'https-expected' );
}
}
@@ -1481,7 +1579,9 @@ class ApiMain extends ApiBase {
];
if ( $e ) {
- $logCtx['errorCodes'][] = $this->errorMessageFromException( $e )['code'];
+ foreach ( $this->errorMessagesFromException( $e ) as $msg ) {
+ $logCtx['errorCodes'][] = $msg->getApiCode();
+ }
}
// Construct space separated message for 'api' log channel
@@ -1560,9 +1660,7 @@ class ApiMain extends ApiBase {
if ( $this->getRequest()->getArray( $name ) !== null ) {
// See bug 10262 for why we don't just implode( '|', ... ) the
// array.
- $this->setWarning(
- "Parameter '$name' uses unsupported PHP array syntax"
- );
+ $this->addWarning( [ 'apiwarn-unsupportedarray', $name ] );
}
$ret = $default;
}
@@ -1602,8 +1700,7 @@ class ApiMain extends ApiBase {
if ( !$this->mInternalMode ) {
// Printer has not yet executed; don't warn that its parameters are unused
- $printerParams = array_map(
- [ $this->mPrinter, 'encodeParamName' ],
+ $printerParams = $this->mPrinter->encodeParamName(
array_keys( $this->mPrinter->getFinalParams() ?: [] )
);
$unusedParams = array_diff( $allParams, $paramsUsed, $printerParams );
@@ -1612,8 +1709,11 @@ class ApiMain extends ApiBase {
}
if ( count( $unusedParams ) ) {
- $s = count( $unusedParams ) > 1 ? 's' : '';
- $this->setWarning( "Unrecognized parameter$s: '" . implode( $unusedParams, "', '" ) . "'" );
+ $this->addWarning( [
+ 'apierror-unrecognizedparams',
+ Message::listParam( array_map( 'wfEscapeWikiText', $unusedParams ), 'comma' ),
+ count( $unusedParams )
+ ] );
}
}
@@ -1624,7 +1724,7 @@ class ApiMain extends ApiBase {
*/
protected function printResult( $httpCode = 0 ) {
if ( $this->getConfig()->get( 'DebugAPI' ) !== false ) {
- $this->setWarning( 'SECURITY WARNING: $wgDebugAPI is enabled' );
+ $this->addWarning( 'apiwarn-wgDebugAPI' );
}
$printer = $this->mPrinter;
@@ -1678,9 +1778,20 @@ class ApiMain extends ApiBase {
'requestid' => null,
'servedby' => false,
'curtimestamp' => false,
+ 'responselanginfo' => false,
'origin' => null,
'uselang' => [
- ApiBase::PARAM_DFLT => 'user',
+ ApiBase::PARAM_DFLT => self::API_DEFAULT_USELANG,
+ ],
+ 'errorformat' => [
+ ApiBase::PARAM_TYPE => [ 'plaintext', 'wikitext', 'html', 'raw', 'none', 'bc' ],
+ ApiBase::PARAM_DFLT => 'bc',
+ ],
+ 'errorlang' => [
+ ApiBase::PARAM_DFLT => 'uselang',
+ ],
+ 'errorsuselocal' => [
+ ApiBase::PARAM_DFLT => false,
],
];
}
@@ -1732,7 +1843,7 @@ class ApiMain extends ApiBase {
$help['permissions'] .= Html::rawElement( 'dd', null,
$this->msg( 'api-help-permissions-granted-to' )
->numParams( count( $groups ) )
- ->params( $this->getLanguage()->commaList( $groups ) )
+ ->params( Message::listParam( $groups ) )
->parse()
);
}
@@ -1832,70 +1943,6 @@ class ApiMain extends ApiBase {
}
/**
- * This exception will be thrown when dieUsage is called to stop module execution.
- *
- * @ingroup API
- */
-class UsageException extends MWException {
-
- private $mCodestr;
-
- /**
- * @var null|array
- */
- private $mExtraData;
-
- /**
- * @param string $message
- * @param string $codestr
- * @param int $code
- * @param array|null $extradata
- */
- public function __construct( $message, $codestr, $code = 0, $extradata = null ) {
- parent::__construct( $message, $code );
- $this->mCodestr = $codestr;
- $this->mExtraData = $extradata;
-
- // This should never happen, so throw an exception about it that will
- // hopefully get logged with a backtrace (T138585)
- if ( !is_string( $codestr ) || $codestr === '' ) {
- throw new InvalidArgumentException( 'Invalid $codestr, was ' .
- ( $codestr === '' ? 'empty string' : gettype( $codestr ) )
- );
- }
- }
-
- /**
- * @return string
- */
- public function getCodeString() {
- return $this->mCodestr;
- }
-
- /**
- * @return array
- */
- public function getMessageArray() {
- $result = [
- 'code' => $this->mCodestr,
- 'info' => $this->getMessage()
- ];
- if ( is_array( $this->mExtraData ) ) {
- $result = array_merge( $result, $this->mExtraData );
- }
-
- return $result;
- }
-
- /**
- * @return string
- */
- public function __toString() {
- return "{$this->getCodeString()}: {$this->getMessage()}";
- }
-}
-
-/**
* For really cool vim folding this needs to be at the end:
* vim: foldmarker=@{,@} foldmethod=marker
*/
diff --git a/includes/api/ApiManageTags.php b/includes/api/ApiManageTags.php
index 617db227df7a..3299f73b5b0e 100644
--- a/includes/api/ApiManageTags.php
+++ b/includes/api/ApiManageTags.php
@@ -32,11 +32,9 @@ class ApiManageTags extends ApiBase {
if ( $params['operation'] !== 'delete'
&& !$this->getUser()->isAllowed( 'managechangetags' )
) {
- $this->dieUsage( "You don't have permission to manage change tags",
- 'permissiondenied' );
+ $this->dieWithError( 'tags-manage-no-permission', 'permissiondenied' );
} elseif ( !$this->getUser()->isAllowed( 'deletechangetags' ) ) {
- $this->dieUsage( "You don't have permission to delete change tags",
- 'permissiondenied' );
+ $this->dieWithError( 'tags-delete-no-permission', 'permissiondenied' );
}
$result = $this->getResult();
diff --git a/includes/api/ApiMergeHistory.php b/includes/api/ApiMergeHistory.php
index 276f1c0ebeee..357698e13c40 100644
--- a/includes/api/ApiMergeHistory.php
+++ b/includes/api/ApiMergeHistory.php
@@ -42,24 +42,24 @@ class ApiMergeHistory extends ApiBase {
if ( isset( $params['from'] ) ) {
$fromTitle = Title::newFromText( $params['from'] );
if ( !$fromTitle || $fromTitle->isExternal() ) {
- $this->dieUsageMsg( [ 'invalidtitle', $params['from'] ] );
+ $this->dieWithError( [ 'apierror-invalidtitle', wfEscapeWikiText( $params['from'] ) ] );
}
} elseif ( isset( $params['fromid'] ) ) {
$fromTitle = Title::newFromID( $params['fromid'] );
if ( !$fromTitle ) {
- $this->dieUsageMsg( [ 'nosuchpageid', $params['fromid'] ] );
+ $this->dieWithError( [ 'apierror-nosuchpageid', $params['fromid'] ] );
}
}
if ( isset( $params['to'] ) ) {
$toTitle = Title::newFromText( $params['to'] );
if ( !$toTitle || $toTitle->isExternal() ) {
- $this->dieUsageMsg( [ 'invalidtitle', $params['to'] ] );
+ $this->dieWithError( [ 'apierror-invalidtitle', wfEscapeWikiText( $params['to'] ) ] );
}
} elseif ( isset( $params['toid'] ) ) {
$toTitle = Title::newFromID( $params['toid'] );
if ( !$toTitle ) {
- $this->dieUsageMsg( [ 'nosuchpageid', $params['toid'] ] );
+ $this->dieWithError( [ 'apierror-nosuchpageid', $params['toid'] ] );
}
}
diff --git a/includes/api/ApiMessage.php b/includes/api/ApiMessage.php
index ae6677864157..9d69a771d423 100644
--- a/includes/api/ApiMessage.php
+++ b/includes/api/ApiMessage.php
@@ -36,9 +36,10 @@ interface IApiMessage extends MessageSpecifier {
/**
* Returns a machine-readable code for use by the API
*
- * The message key is often sufficient, but sometimes there are multiple
- * messages used for what is really the same underlying condition (e.g.
- * badaccess-groups and badaccess-group0)
+ * If no code was specifically set, the message key is used as the code
+ * after removing "apiwarn-" or "apierror-" prefixes and applying
+ * backwards-compatibility mappings.
+ *
* @return string
*/
public function getApiCode();
@@ -51,7 +52,7 @@ interface IApiMessage extends MessageSpecifier {
/**
* Sets the machine-readable code for use by the API
- * @param string|null $code If null, the message key should be returned by self::getApiCode()
+ * @param string|null $code If null, uses the default (see self::getApiCode())
* @param array|null $data If non-null, passed to self::setApiData()
*/
public function setApiCode( $code, array $data = null );
@@ -69,14 +70,95 @@ interface IApiMessage extends MessageSpecifier {
* @ingroup API
*/
trait ApiMessageTrait {
+
+ /**
+ * Compatibility code mappings for various MW messages.
+ * @todo Ideally anything relying on this should be changed to use ApiMessage.
+ */
+ protected static $messageMap = [
+ 'actionthrottledtext' => 'ratelimited',
+ 'autoblockedtext' => 'autoblocked',
+ 'badaccess-group0' => 'permissiondenied',
+ 'badaccess-groups' => 'permissiondenied',
+ 'badipaddress' => 'invalidip',
+ 'blankpage' => 'emptypage',
+ 'blockedtext' => 'blocked',
+ 'cannotdelete' => 'cantdelete',
+ 'cannotundelete' => 'cantundelete',
+ 'cantmove-titleprotected' => 'protectedtitle',
+ 'cantrollback' => 'onlyauthor',
+ 'confirmedittext' => 'confirmemail',
+ 'content-not-allowed-here' => 'contentnotallowedhere',
+ 'deleteprotected' => 'cantedit',
+ 'delete-toobig' => 'bigdelete',
+ 'edit-conflict' => 'editconflict',
+ 'imagenocrossnamespace' => 'nonfilenamespace',
+ 'imagetypemismatch' => 'filetypemismatch',
+ 'importbadinterwiki' => 'badinterwiki',
+ 'importcantopen' => 'cantopenfile',
+ 'import-noarticle' => 'badinterwiki',
+ 'importnofile' => 'nofile',
+ 'importuploaderrorpartial' => 'partialupload',
+ 'importuploaderrorsize' => 'filetoobig',
+ 'importuploaderrortemp' => 'notempdir',
+ 'ipb_already_blocked' => 'alreadyblocked',
+ 'ipb_blocked_as_range' => 'blockedasrange',
+ 'ipb_cant_unblock' => 'cantunblock',
+ 'ipb_expiry_invalid' => 'invalidexpiry',
+ 'ip_range_invalid' => 'invalidrange',
+ 'mailnologin' => 'cantsend',
+ 'markedaspatrollederror-noautopatrol' => 'noautopatrol',
+ 'movenologintext' => 'cantmove-anon',
+ 'movenotallowed' => 'cantmove',
+ 'movenotallowedfile' => 'cantmovefile',
+ 'namespaceprotected' => 'protectednamespace',
+ 'nocreate-loggedin' => 'cantcreate',
+ 'nocreatetext' => 'cantcreate-anon',
+ 'noname' => 'invaliduser',
+ 'nosuchusershort' => 'nosuchuser',
+ 'notanarticle' => 'missingtitle',
+ 'nouserspecified' => 'invaliduser',
+ 'ns-specialprotected' => 'unsupportednamespace',
+ 'protect-cantedit' => 'cantedit',
+ 'protectedinterface' => 'protectednamespace-interface',
+ 'protectedpagetext' => 'protectedpage',
+ 'range_block_disabled' => 'rangedisabled',
+ 'rcpatroldisabled' => 'patroldisabled',
+ 'readonlytext' => 'readonly',
+ 'sessionfailure' => 'badtoken',
+ 'titleprotected' => 'protectedtitle',
+ 'undo-failure' => 'undofailure',
+ 'userrights-nodatabase' => 'nosuchdatabase',
+ 'userrights-no-interwiki' => 'nointerwikiuserrights',
+ ];
+
protected $apiCode = null;
protected $apiData = [];
public function getApiCode() {
- return $this->apiCode === null ? $this->getKey() : $this->apiCode;
+ if ( $this->apiCode === null ) {
+ $key = $this->getKey();
+ if ( isset( self::$messageMap[$key] ) ) {
+ $this->apiCode = self::$messageMap[$key];
+ } elseif ( $key === 'apierror-missingparam' ) {
+ /// @todo: Kill this case along with ApiBase::$messageMap
+ $this->apiCode = 'no' . $this->getParams()[0];
+ } elseif ( substr( $key, 0, 8 ) === 'apiwarn-' ) {
+ $this->apiCode = substr( $key, 8 );
+ } elseif ( substr( $key, 0, 9 ) === 'apierror-' ) {
+ $this->apiCode = substr( $key, 9 );
+ } else {
+ $this->apiCode = $key;
+ }
+ }
+ return $this->apiCode;
}
public function setApiCode( $code, array $data = null ) {
+ if ( $code !== null && !( is_string( $code ) && $code !== '' ) ) {
+ throw new InvalidArgumentException( "Invalid code \"$code\"" );
+ }
+
$this->apiCode = $code;
if ( $data !== null ) {
$this->setApiData( $data );
@@ -124,9 +206,25 @@ class ApiMessage extends Message implements IApiMessage {
* @param Message|RawMessage|array|string $msg
* @param string|null $code
* @param array|null $data
- * @return ApiMessage
+ * @return IApiMessage
*/
public static function create( $msg, $code = null, array $data = null ) {
+ if ( is_array( $msg ) ) {
+ // From StatusValue
+ if ( isset( $msg['message'] ) ) {
+ if ( isset( $msg['params'] ) ) {
+ $msg = array_merge( [ $msg['message'] ], $msg['params'] );
+ } else {
+ $msg = [ $msg['message'] ];
+ }
+ }
+
+ // Weirdness that comes in sometimes, including the above
+ if ( $msg[0] instanceof MessageSpecifier ) {
+ $msg = $msg[0];
+ }
+ }
+
if ( $msg instanceof IApiMessage ) {
return $msg;
} elseif ( $msg instanceof RawMessage ) {
@@ -143,7 +241,6 @@ class ApiMessage extends Message implements IApiMessage {
* - string: passed to Message::__construct
* @param string|null $code
* @param array|null $data
- * @return ApiMessage
*/
public function __construct( $msg, $code = null, array $data = null ) {
if ( $msg instanceof Message ) {
@@ -158,8 +255,7 @@ class ApiMessage extends Message implements IApiMessage {
} else {
parent::__construct( $msg );
}
- $this->apiCode = $code;
- $this->apiData = (array)$data;
+ $this->setApiCode( $code, $data );
}
}
@@ -192,7 +288,6 @@ class ApiRawMessage extends RawMessage implements IApiMessage {
} else {
parent::__construct( $msg );
}
- $this->apiCode = $code;
- $this->apiData = (array)$data;
+ $this->setApiCode( $code, $data );
}
}
diff --git a/includes/api/ApiMove.php b/includes/api/ApiMove.php
index 29e67b07cd57..7c8aa90ce9ce 100644
--- a/includes/api/ApiMove.php
+++ b/includes/api/ApiMove.php
@@ -41,23 +41,23 @@ class ApiMove extends ApiBase {
if ( isset( $params['from'] ) ) {
$fromTitle = Title::newFromText( $params['from'] );
if ( !$fromTitle || $fromTitle->isExternal() ) {
- $this->dieUsageMsg( [ 'invalidtitle', $params['from'] ] );
+ $this->dieWithError( [ 'apierror-invalidtitle', wfEscapeWikiText( $params['from'] ) ] );
}
} elseif ( isset( $params['fromid'] ) ) {
$fromTitle = Title::newFromID( $params['fromid'] );
if ( !$fromTitle ) {
- $this->dieUsageMsg( [ 'nosuchpageid', $params['fromid'] ] );
+ $this->dieWithError( [ 'apierror-nosuchpageid', $params['fromid'] ] );
}
}
if ( !$fromTitle->exists() ) {
- $this->dieUsageMsg( 'notanarticle' );
+ $this->dieWithError( 'apierror-missingtitle' );
}
$fromTalk = $fromTitle->getTalkPage();
$toTitle = Title::newFromText( $params['to'] );
if ( !$toTitle || $toTitle->isExternal() ) {
- $this->dieUsageMsg( [ 'invalidtitle', $params['to'] ] );
+ $this->dieWithError( [ 'apierror-invalidtitle', wfEscapeWikiText( $params['to'] ) ] );
}
$toTalk = $toTitle->getTalkPage();
@@ -66,15 +66,15 @@ class ApiMove extends ApiBase {
&& wfFindFile( $toTitle )
) {
if ( !$params['ignorewarnings'] && $user->isAllowed( 'reupload-shared' ) ) {
- $this->dieUsageMsg( 'sharedfile-exists' );
+ $this->dieWithError( 'apierror-fileexists-sharedrepo-perm' );
} elseif ( !$user->isAllowed( 'reupload-shared' ) ) {
- $this->dieUsageMsg( 'cantoverwrite-sharedfile' );
+ $this->dieWithError( 'apierror-cantoverwrite-sharedfile' );
}
}
// Rate limit
if ( $user->pingLimiter( 'move' ) ) {
- $this->dieUsageMsg( 'actionthrottledtext' );
+ $this->dieWithError( 'apierror-ratelimited' );
}
// Move the page
@@ -108,10 +108,8 @@ class ApiMove extends ApiBase {
$r['talkto'] = $toTalk->getPrefixedText();
$r['talkmoveoverredirect'] = $toTalkExists;
} else {
- // We're not gonna dieUsage() on failure, since we already changed something
- $error = $this->getErrorFromStatus( $status );
- $r['talkmove-error-code'] = $error[0];
- $r['talkmove-error-info'] = $error[1];
+ // We're not going to dieWithError() on failure, since we already changed something
+ $r['talkmove-errors'] = $this->getErrorFormatter()->arrayFromStatus( $status );
}
}
@@ -184,7 +182,8 @@ class ApiMove extends ApiBase {
$retval = [];
$success = $fromTitle->moveSubpages( $toTitle, true, $reason, !$noredirect );
if ( isset( $success[0] ) ) {
- return [ 'error' => $this->parseMsg( $success ) ];
+ $status = $this->errorArrayToStatus( $success );
+ return [ 'errors' => $this->getErrorFormatter()->arrayFromStatus( $status ) ];
}
// At least some pages could be moved
@@ -192,7 +191,8 @@ class ApiMove extends ApiBase {
foreach ( $success as $oldTitle => $newTitle ) {
$r = [ 'from' => $oldTitle ];
if ( is_array( $newTitle ) ) {
- $r['error'] = $this->parseMsg( reset( $newTitle ) );
+ $status = $this->errorArrayToStatus( $newTitle );
+ $r['errors'] = $this->getErrorFormatter()->arrayFromStatus( $status );
} else {
// Success
$r['to'] = $newTitle;
diff --git a/includes/api/ApiOpenSearch.php b/includes/api/ApiOpenSearch.php
index ace776c92396..e6fe27ca2a34 100644
--- a/includes/api/ApiOpenSearch.php
+++ b/includes/api/ApiOpenSearch.php
@@ -391,14 +391,14 @@ class ApiOpenSearchFormatJson extends ApiFormatJson {
}
public function execute() {
- if ( !$this->getResult()->getResultData( 'error' ) ) {
- $result = $this->getResult();
-
+ $result = $this->getResult();
+ if ( !$result->getResultData( 'error' ) && !$result->getResultData( 'errors' ) ) {
// Ignore warnings or treat as errors, as requested
$warnings = $result->removeValue( 'warnings', null );
if ( $this->warningsAsError && $warnings ) {
- $this->dieUsage(
- 'Warnings cannot be represented in OpenSearch JSON format', 'warnings', 0,
+ $this->dieWithError(
+ 'apierror-opensearch-json-warnings',
+ 'warnings',
[ 'warnings' => $warnings ]
);
}
diff --git a/includes/api/ApiOptions.php b/includes/api/ApiOptions.php
index 8bfe447df59c..466d1865d68c 100644
--- a/includes/api/ApiOptions.php
+++ b/includes/api/ApiOptions.php
@@ -36,22 +36,26 @@ class ApiOptions extends ApiBase {
*/
public function execute() {
if ( $this->getUser()->isAnon() ) {
- $this->dieUsage( 'Anonymous users cannot change preferences', 'notloggedin' );
- } elseif ( !$this->getUser()->isAllowed( 'editmyoptions' ) ) {
- $this->dieUsage( "You don't have permission to edit your options", 'permissiondenied' );
+ $this->dieWithError(
+ [ 'apierror-mustbeloggedin', $this->msg( 'action-editmyoptions' ) ], 'notloggedin'
+ );
}
+ $this->checkUserRightsAny( 'editmyoptions' );
+
$params = $this->extractRequestParams();
$changed = false;
if ( isset( $params['optionvalue'] ) && !isset( $params['optionname'] ) ) {
- $this->dieUsageMsg( [ 'missingparam', 'optionname' ] );
+ $this->dieWithError( [ 'apierror-missingparam', 'optionname' ] );
}
// Load the user from the master to reduce CAS errors on double post (T95839)
$user = $this->getUser()->getInstanceForUpdate();
if ( !$user ) {
- $this->dieUsage( 'Anonymous users cannot change preferences', 'notloggedin' );
+ $this->dieWithError(
+ [ 'apierror-mustbeloggedin', $this->msg( 'action-editmyoptions' ) ], 'notloggedin'
+ );
}
if ( $params['reset'] ) {
@@ -71,7 +75,7 @@ class ApiOptions extends ApiBase {
$changes[$params['optionname']] = $newValue;
}
if ( !$changed && !count( $changes ) ) {
- $this->dieUsage( 'No changes were requested', 'nochanges' );
+ $this->dieWithError( 'apierror-nochanges' );
}
$prefs = Preferences::getPreferences( $user, $this->getContext() );
@@ -98,26 +102,26 @@ class ApiOptions extends ApiBase {
case 'userjs':
// Allow non-default preferences prefixed with 'userjs-', to be set by user scripts
if ( strlen( $key ) > 255 ) {
- $validation = 'key too long (no more than 255 bytes allowed)';
+ $validation = $this->msg( 'apiwarn-validationfailed-keytoolong', Message::numParam( 255 ) );
} elseif ( preg_match( '/[^a-zA-Z0-9_-]/', $key ) !== 0 ) {
- $validation = 'invalid key (only a-z, A-Z, 0-9, _, - allowed)';
+ $validation = $this->msg( 'apiwarn-validationfailed-badchars' );
} else {
$validation = true;
}
break;
case 'special':
- $validation = 'cannot be set by this module';
+ $validation = $this->msg( 'apiwarn-validationfailed-cannotset' );
break;
case 'unused':
default:
- $validation = 'not a valid preference';
+ $validation = $this->msg( 'apiwarn-validationfailed-badpref' );
break;
}
if ( $validation === true ) {
$user->setOption( $key, $value );
$changed = true;
} else {
- $this->setWarning( "Validation error for '$key': $validation" );
+ $this->addWarning( [ 'apiwarn-validationfailed', wfEscapeWikitext( $key ), $validation ] );
}
}
diff --git a/includes/api/ApiPageSet.php b/includes/api/ApiPageSet.php
index 853a8056be7a..4cf896f1dcf4 100644
--- a/includes/api/ApiPageSet.php
+++ b/includes/api/ApiPageSet.php
@@ -155,10 +155,10 @@ class ApiPageSet extends ApiBase {
}
$generator = $dbSource->getModuleManager()->getModule( $generatorName, null, true );
if ( $generator === null ) {
- $this->dieUsage( 'Unknown generator=' . $generatorName, 'badgenerator' );
+ $this->dieWithError( [ 'apierror-badgenerator-unknown', $generatorName ], 'badgenerator' );
}
if ( !$generator instanceof ApiQueryGeneratorBase ) {
- $this->dieUsage( "Module $generatorName cannot be used as a generator", 'badgenerator' );
+ $this->dieWithError( [ 'apierror-badgenerator-notgenerator', $generatorName ], 'badgenerator' );
}
// Create a temporary pageset to store generator's output,
// add any additional fields generator may need, and execute pageset to populate titles/pageids
@@ -194,13 +194,27 @@ class ApiPageSet extends ApiBase {
}
if ( isset( $this->mParams['pageids'] ) ) {
if ( isset( $dataSource ) ) {
- $this->dieUsage( "Cannot use 'pageids' at the same time as '$dataSource'", 'multisource' );
+ $this->dieWithError(
+ [
+ 'apierror-invalidparammix-cannotusewith',
+ $this->encodeParamName( 'pageids' ),
+ $this->encodeParamName( $dataSource )
+ ],
+ 'multisource'
+ );
}
$dataSource = 'pageids';
}
if ( isset( $this->mParams['revids'] ) ) {
if ( isset( $dataSource ) ) {
- $this->dieUsage( "Cannot use 'revids' at the same time as '$dataSource'", 'multisource' );
+ $this->dieWithError(
+ [
+ 'apierror-invalidparammix-cannotusewith',
+ $this->encodeParamName( 'revids' ),
+ $this->encodeParamName( $dataSource )
+ ],
+ 'multisource'
+ );
}
$dataSource = 'revids';
}
@@ -216,9 +230,7 @@ class ApiPageSet extends ApiBase {
break;
case 'revids':
if ( $this->mResolveRedirects ) {
- $this->setWarning( 'Redirect resolution cannot be used ' .
- 'together with the revids= parameter. Any redirects ' .
- 'the revids= point to have not been resolved.' );
+ $this->addWarning( 'apiwarn-redirectsandrevids' );
}
$this->mResolveRedirects = false;
$this->initFromRevIDs( $this->mParams['revids'] );
diff --git a/includes/api/ApiParamInfo.php b/includes/api/ApiParamInfo.php
index ffc3fc2e3217..a9b3dde94722 100644
--- a/includes/api/ApiParamInfo.php
+++ b/includes/api/ApiParamInfo.php
@@ -66,14 +66,17 @@ class ApiParamInfo extends ApiBase {
if ( $submodules ) {
try {
$module = $this->getModuleFromPath( $path );
- } catch ( UsageException $ex ) {
- $this->setWarning( $ex->getMessage() );
+ } catch ( ApiUsageException $ex ) {
+ foreach ( $ex->getStatusValue()->getErrors() as $error ) {
+ $this->addWarning( $error );
+ }
+ continue;
}
$submodules = $this->listAllSubmodules( $module, $recursive );
if ( $submodules ) {
$modules = array_merge( $modules, $submodules );
} else {
- $this->setWarning( "Module $path has no submodules" );
+ $this->addWarning( [ 'apierror-badmodule-nosubmodules', $path ], 'badmodule' );
}
} else {
$modules[] = $path;
@@ -108,8 +111,10 @@ class ApiParamInfo extends ApiBase {
foreach ( $modules as $m ) {
try {
$module = $this->getModuleFromPath( $m );
- } catch ( UsageException $ex ) {
- $this->setWarning( $ex->getMessage() );
+ } catch ( ApiUsageException $ex ) {
+ foreach ( $ex->getStatusValue()->getErrors() as $error ) {
+ $this->addWarning( $error );
+ }
continue;
}
$key = 'modules';
diff --git a/includes/api/ApiParse.php b/includes/api/ApiParse.php
index 0cad5dee9ce0..2263b8f83c34 100644
--- a/includes/api/ApiParse.php
+++ b/includes/api/ApiParse.php
@@ -36,18 +36,18 @@ class ApiParse extends ApiBase {
/** @var Content $pstContent */
private $pstContent = null;
- private function checkReadPermissions( Title $title ) {
- if ( !$title->userCan( 'read', $this->getUser() ) ) {
- $this->dieUsage( "You don't have permission to view this page", 'permissiondenied' );
- }
- }
-
public function execute() {
// The data is hot but user-dependent, like page views, so we set vary cookies
$this->getMain()->setCacheMode( 'anon-public-user-private' );
// Get parameters
$params = $this->extractRequestParams();
+
+ // No easy way to say that text & title are allowed together while the
+ // rest aren't, so just do it in two calls.
+ $this->requireMaxOneParameter( $params, 'page', 'pageid', 'oldid', 'text' );
+ $this->requireMaxOneParameter( $params, 'page', 'pageid', 'oldid', 'title' );
+
$text = $params['text'];
$title = $params['title'];
if ( $title === null ) {
@@ -65,21 +65,12 @@ class ApiParse extends ApiBase {
$model = $params['contentmodel'];
$format = $params['contentformat'];
- if ( !is_null( $page ) && ( !is_null( $text ) || $titleProvided ) ) {
- $this->dieUsage(
- 'The page parameter cannot be used together with the text and title parameters',
- 'params'
- );
- }
-
$prop = array_flip( $params['prop'] );
if ( isset( $params['section'] ) ) {
$this->section = $params['section'];
if ( !preg_match( '/^((T-)?\d+|new)$/', $this->section ) ) {
- $this->dieUsage(
- 'The section parameter must be a valid section id or "new"', 'invalidsection'
- );
+ $this->dieWithError( 'apierror-invalidsection' );
}
} else {
$this->section = false;
@@ -97,21 +88,20 @@ class ApiParse extends ApiBase {
if ( !is_null( $oldid ) || !is_null( $pageid ) || !is_null( $page ) ) {
if ( $this->section === 'new' ) {
- $this->dieUsage(
- 'section=new cannot be combined with oldid, pageid or page parameters. ' .
- 'Please use text', 'params'
- );
+ $this->dieWithError( 'apierror-invalidparammix-parse-new-section', 'invalidparammix' );
}
if ( !is_null( $oldid ) ) {
// Don't use the parser cache
$rev = Revision::newFromId( $oldid );
if ( !$rev ) {
- $this->dieUsage( "There is no revision ID $oldid", 'missingrev' );
+ $this->dieWithError( [ 'apierror-nosuchrevid', $oldid ] );
}
- $this->checkReadPermissions( $rev->getTitle() );
+ $this->checkTitleUserPermissions( $rev->getTitle(), 'read' );
if ( !$rev->userCan( Revision::DELETED_TEXT, $this->getUser() ) ) {
- $this->dieUsage( "You don't have permission to view deleted revisions", 'permissiondenied' );
+ $this->dieWithError(
+ [ 'apierror-permissiondenied', $this->msg( 'action-deletedtext' ) ]
+ );
}
$titleObj = $rev->getTitle();
@@ -131,7 +121,9 @@ class ApiParse extends ApiBase {
$this->content = $rev->getContent( Revision::FOR_THIS_USER, $this->getUser() );
if ( $this->section !== false ) {
- $this->content = $this->getSectionContent( $this->content, 'r' . $rev->getId() );
+ $this->content = $this->getSectionContent(
+ $this->content, $this->msg( 'revid', $rev->getId() )
+ );
}
// Should we save old revision parses to the parser cache?
@@ -167,10 +159,10 @@ class ApiParse extends ApiBase {
$pageObj = $this->getTitleOrPageId( $pageParams, 'fromdb' );
$titleObj = $pageObj->getTitle();
if ( !$titleObj || !$titleObj->exists() ) {
- $this->dieUsage( "The page you specified doesn't exist", 'missingtitle' );
+ $this->dieWithError( 'apierror-missingtitle' );
}
- $this->checkReadPermissions( $titleObj );
+ $this->checkTitleUserPermissions( $titleObj, 'read' );
$wgTitle = $titleObj;
if ( isset( $prop['revid'] ) ) {
@@ -201,7 +193,7 @@ class ApiParse extends ApiBase {
} else { // Not $oldid, $pageid, $page. Hence based on $text
$titleObj = Title::newFromText( $title );
if ( !$titleObj || $titleObj->isExternal() ) {
- $this->dieUsageMsg( [ 'invalidtitle', $title ] );
+ $this->dieWithError( [ 'apierror-invalidtitle', wfEscapeWikiText( $title ) ] );
}
$wgTitle = $titleObj;
if ( $titleObj->canExist() ) {
@@ -217,10 +209,7 @@ class ApiParse extends ApiBase {
if ( !$textProvided ) {
if ( $titleProvided && ( $prop || $params['generatexml'] ) ) {
- $this->setWarning(
- "'title' used without 'text', and parsed page properties were requested " .
- "(did you mean to use 'page' instead of 'title'?)"
- );
+ $this->addWarning( 'apiwarn-parse-titlewithouttext' );
}
// Prevent warning from ContentHandler::makeContent()
$text = '';
@@ -230,13 +219,17 @@ class ApiParse extends ApiBase {
// API title, but default to wikitext to keep BC.
if ( $textProvided && !$titleProvided && is_null( $model ) ) {
$model = CONTENT_MODEL_WIKITEXT;
- $this->setWarning( "No 'title' or 'contentmodel' was given, assuming $model." );
+ $this->addWarning( [ 'apiwarn-parse-nocontentmodel', $model ] );
}
try {
$this->content = ContentHandler::makeContent( $text, $titleObj, $model, $format );
} catch ( MWContentSerializationException $ex ) {
- $this->dieUsage( $ex->getMessage(), 'parseerror' );
+ // @todo: Internationalize MWContentSerializationException
+ $this->dieWithError(
+ [ 'apierror-contentserializationexception', wfEscapeWikiText( $ex->getMessage() ) ],
+ 'parseerror'
+ );
}
if ( $this->section !== false ) {
@@ -357,10 +350,7 @@ class ApiParse extends ApiBase {
if ( isset( $prop['headitems'] ) ) {
$result_array['headitems'] = $this->formatHeadItems( $p_result->getHeadItems() );
- $this->logFeatureUsage( 'action=parse&prop=headitems' );
- $this->setWarning( 'headitems is deprecated since MediaWiki 1.28. '
- . 'Use prop=headhtml when creating new HTML documents, or '
- . 'prop=modules|jsconfigvars when updating a document client-side.' );
+ $this->addDeprecation( 'apiwarn-deprecation-parse-headitems', 'action=parse&prop=headitems' );
}
if ( isset( $prop['headhtml'] ) ) {
@@ -397,9 +387,7 @@ class ApiParse extends ApiBase {
if ( isset( $prop['modules'] ) &&
!isset( $prop['jsconfigvars'] ) && !isset( $prop['encodedjsconfigvars'] ) ) {
- $this->setWarning( 'Property "modules" was set but not "jsconfigvars" ' .
- 'or "encodedjsconfigvars". Configuration variables are necessary ' .
- 'for proper module usage.' );
+ $this->addWarning( 'apiwarn-moduleswithoutvars' );
}
if ( isset( $prop['indicators'] ) ) {
@@ -435,7 +423,7 @@ class ApiParse extends ApiBase {
if ( isset( $prop['parsetree'] ) || $params['generatexml'] ) {
if ( $this->content->getModel() != CONTENT_MODEL_WIKITEXT ) {
- $this->dieUsage( 'parsetree is only supported for wikitext content', 'notwikitext' );
+ $this->dieWithError( 'apierror-parsetree-notwikitext', 'notwikitext' );
}
$wgParser->startExternalParse( $titleObj, $popts, Parser::OT_PREPROCESS );
@@ -516,7 +504,7 @@ class ApiParse extends ApiBase {
// getParserOutput will save to Parser cache if able
$pout = $page->getParserOutput( $popts );
if ( !$pout ) {
- $this->dieUsage( "There is no revision ID {$page->getLatest()}", 'missingrev' );
+ $this->dieWithError( [ 'apierror-nosuchrevid', $page->getLatest() ] );
}
if ( $getWikitext ) {
$this->content = $page->getContent( Revision::RAW );
@@ -538,7 +526,9 @@ class ApiParse extends ApiBase {
if ( $this->section !== false && $content !== null ) {
$content = $this->getSectionContent(
$content,
- !is_null( $pageId ) ? 'page id ' . $pageId : $page->getTitle()->getPrefixedText()
+ !is_null( $pageId )
+ ? $this->msg( 'pageid', $pageId )
+ : $page->getTitle()->getPrefixedText()
);
}
return $content;
@@ -548,17 +538,17 @@ class ApiParse extends ApiBase {
* Extract the requested section from the given Content
*
* @param Content $content
- * @param string $what Identifies the content in error messages, e.g. page title.
+ * @param string|Message $what Identifies the content in error messages, e.g. page title.
* @return Content|bool
*/
private function getSectionContent( Content $content, $what ) {
// Not cached (save or load)
$section = $content->getSection( $this->section );
if ( $section === false ) {
- $this->dieUsage( "There is no section {$this->section} in $what", 'nosuchsection' );
+ $this->dieWithError( [ 'apierror-nosuchsection-what', $this->section, $what ], 'nosuchsection' );
}
if ( $section === null ) {
- $this->dieUsage( "Sections are not supported by $what", 'nosuchsection' );
+ $this->dieWithError( [ 'apierror-sectionsnotsupported-what', $what ], 'nosuchsection' );
$section = false;
}
diff --git a/includes/api/ApiPatrol.php b/includes/api/ApiPatrol.php
index 62528825ded0..c33542f1c7aa 100644
--- a/includes/api/ApiPatrol.php
+++ b/includes/api/ApiPatrol.php
@@ -40,19 +40,16 @@ class ApiPatrol extends ApiBase {
if ( isset( $params['rcid'] ) ) {
$rc = RecentChange::newFromId( $params['rcid'] );
if ( !$rc ) {
- $this->dieUsageMsg( [ 'nosuchrcid', $params['rcid'] ] );
+ $this->dieWithError( [ 'apierror-nosuchrcid', $params['rcid'] ] );
}
} else {
$rev = Revision::newFromId( $params['revid'] );
if ( !$rev ) {
- $this->dieUsageMsg( [ 'nosuchrevid', $params['revid'] ] );
+ $this->dieWithError( [ 'apierror-nosuchrevid', $params['revid'] ] );
}
$rc = $rev->getRecentChange();
if ( !$rc ) {
- $this->dieUsage(
- 'The revision ' . $params['revid'] . " can't be patrolled as it's too old",
- 'notpatrollable'
- );
+ $this->dieWithError( [ 'apierror-notpatrollable', $params['revid'] ] );
}
}
@@ -70,7 +67,7 @@ class ApiPatrol extends ApiBase {
$retval = $rc->doMarkPatrolled( $user, false, $tags );
if ( $retval ) {
- $this->dieUsageMsg( reset( $retval ) );
+ $this->dieStatus( $this->errorArrayToStatus( $retval, $user ) );
}
$result = [ 'rcid' => intval( $rc->getAttribute( 'rc_id' ) ) ];
diff --git a/includes/api/ApiProtect.php b/includes/api/ApiProtect.php
index d28906069fc0..746dc9a16bb7 100644
--- a/includes/api/ApiProtect.php
+++ b/includes/api/ApiProtect.php
@@ -36,11 +36,7 @@ class ApiProtect extends ApiBase {
$pageObj = $this->getTitleOrPageId( $params, 'fromdbmaster' );
$titleObj = $pageObj->getTitle();
- $errors = $titleObj->getUserPermissionsErrors( 'protect', $this->getUser() );
- if ( $errors ) {
- // We don't care about multiple errors, just report one of them
- $this->dieUsageMsg( reset( $errors ) );
- }
+ $this->checkTitleUserPermissions( $titleObj, 'protect' );
$user = $this->getUser();
$tags = $params['tags'];
@@ -58,8 +54,8 @@ class ApiProtect extends ApiBase {
if ( count( $expiry ) == 1 ) {
$expiry = array_fill( 0, count( $params['protections'] ), $expiry[0] );
} else {
- $this->dieUsageMsg( [
- 'toofewexpiries',
+ $this->dieWithError( [
+ 'apierror-toofewexpiries',
count( $expiry ),
count( $params['protections'] )
] );
@@ -76,17 +72,17 @@ class ApiProtect extends ApiBase {
$protections[$p[0]] = ( $p[1] == 'all' ? '' : $p[1] );
if ( $titleObj->exists() && $p[0] == 'create' ) {
- $this->dieUsageMsg( 'create-titleexists' );
+ $this->dieWithError( 'apierror-create-titleexists' );
}
if ( !$titleObj->exists() && $p[0] != 'create' ) {
- $this->dieUsageMsg( 'missingtitle-createonly' );
+ $this->dieWithError( 'apierror-missingtitle-createonly' );
}
if ( !in_array( $p[0], $restrictionTypes ) && $p[0] != 'create' ) {
- $this->dieUsageMsg( [ 'protect-invalidaction', $p[0] ] );
+ $this->dieWithError( [ 'apierror-protect-invalidaction', wfEscapeWikiText( $p[0] ) ] );
}
if ( !in_array( $p[1], $this->getConfig()->get( 'RestrictionLevels' ) ) && $p[1] != 'all' ) {
- $this->dieUsageMsg( [ 'protect-invalidlevel', $p[1] ] );
+ $this->dieWithError( [ 'apierror-protect-invalidlevel', wfEscapeWikiText( $p[1] ) ] );
}
if ( wfIsInfinity( $expiry[$i] ) ) {
@@ -94,12 +90,12 @@ class ApiProtect extends ApiBase {
} else {
$exp = strtotime( $expiry[$i] );
if ( $exp < 0 || !$exp ) {
- $this->dieUsageMsg( [ 'invalidexpiry', $expiry[$i] ] );
+ $this->dieWithError( [ 'apierror-invalidexpiry', wfEscapeWikiText( $expiry[$i] ) ] );
}
$exp = wfTimestamp( TS_MW, $exp );
if ( $exp < wfTimestampNow() ) {
- $this->dieUsageMsg( [ 'pastexpiry', $expiry[$i] ] );
+ $this->dieWithError( [ 'apierror-pastexpiry', wfEscapeWikiText( $expiry[$i] ) ] );
}
$expiryarray[$p[0]] = $exp;
}
diff --git a/includes/api/ApiPurge.php b/includes/api/ApiPurge.php
index 8bbd88dfec46..324d030fdbf4 100644
--- a/includes/api/ApiPurge.php
+++ b/includes/api/ApiPurge.php
@@ -39,8 +39,7 @@ class ApiPurge extends ApiBase {
public function execute() {
$main = $this->getMain();
if ( !$main->isInternalMode() && !$main->getRequest()->wasPosted() ) {
- $this->logFeatureUsage( 'purge-via-GET' );
- $this->setWarning( 'Use of action=purge via GET is deprecated. Use POST instead.' );
+ $this->addDeprecation( 'apiwarn-deprecation-purge-get', 'purge-via-GET' );
}
$params = $this->extractRequestParams();
@@ -69,8 +68,7 @@ class ApiPurge extends ApiBase {
$page->doPurge( $flags );
$r['purged'] = true;
} else {
- $error = $this->parseMsg( [ 'actionthrottledtext' ] );
- $this->setWarning( $error['info'] );
+ $this->addWarning( 'apierror-ratelimited' );
}
if ( $forceLinkUpdate || $forceRecursiveLinkUpdate ) {
@@ -114,8 +112,7 @@ class ApiPurge extends ApiBase {
}
}
} else {
- $error = $this->parseMsg( [ 'actionthrottledtext' ] );
- $this->setWarning( $error['info'] );
+ $this->addWarning( 'apierror-ratelimited' );
$forceLinkUpdate = false;
}
}
diff --git a/includes/api/ApiQuery.php b/includes/api/ApiQuery.php
index 16bd725e3cb0..8196cfa2bbfd 100644
--- a/includes/api/ApiQuery.php
+++ b/includes/api/ApiQuery.php
@@ -310,7 +310,7 @@ class ApiQuery extends ApiBase {
ApiBase::dieDebug( __METHOD__, 'Error instantiating module' );
}
if ( !$wasPosted && $instance->mustBePosted() ) {
- $this->dieUsageMsgOrDebug( [ 'mustbeposted', $moduleName ] );
+ $this->dieWithErrorOrDebug( [ 'apierror-mustbeposted', $moduleName ] );
}
// Ignore duplicates. TODO 2.0: die()?
if ( !array_key_exists( $moduleName, $modules ) ) {
@@ -415,11 +415,7 @@ class ApiQuery extends ApiBase {
}
if ( !$fit ) {
- $this->dieUsage(
- 'The value of $wgAPIMaxResultSize on this wiki is ' .
- 'too small to hold basic result information',
- 'badconfig'
- );
+ $this->dieWithError( 'apierror-badconfig-resulttoosmall', 'badconfig' );
}
if ( $this->mParams['export'] ) {
diff --git a/includes/api/ApiQueryAllDeletedRevisions.php b/includes/api/ApiQueryAllDeletedRevisions.php
index 3073a951b022..b09b97702db5 100644
--- a/includes/api/ApiQueryAllDeletedRevisions.php
+++ b/includes/api/ApiQueryAllDeletedRevisions.php
@@ -41,15 +41,10 @@ class ApiQueryAllDeletedRevisions extends ApiQueryRevisionsBase {
* @return void
*/
protected function run( ApiPageSet $resultPageSet = null ) {
- $user = $this->getUser();
// Before doing anything at all, let's check permissions
- if ( !$user->isAllowed( 'deletedhistory' ) ) {
- $this->dieUsage(
- 'You don\'t have permission to view deleted revision information',
- 'permissiondenied'
- );
- }
+ $this->checkUserRightsAny( 'deletedhistory' );
+ $user = $this->getUser();
$db = $this->getDB();
$params = $this->extractRequestParams( false );
@@ -75,16 +70,20 @@ class ApiQueryAllDeletedRevisions extends ApiQueryRevisionsBase {
foreach ( [ 'from', 'to', 'prefix', 'excludeuser' ] as $param ) {
if ( !is_null( $params[$param] ) ) {
$p = $this->getModulePrefix();
- $this->dieUsage( "The '{$p}{$param}' parameter cannot be used with '{$p}user'",
- 'badparams' );
+ $this->dieWithError(
+ [ 'apierror-invalidparammix-cannotusewith', $p.$param, "{$p}user" ],
+ 'invalidparammix'
+ );
}
}
} else {
foreach ( [ 'start', 'end' ] as $param ) {
if ( !is_null( $params[$param] ) ) {
$p = $this->getModulePrefix();
- $this->dieUsage( "The '{$p}{$param}' parameter may only be used with '{$p}user'",
- 'badparams' );
+ $this->dieWithError(
+ [ 'apierror-invalidparammix-mustusewith', $p.$param, "{$p}user" ],
+ 'invalidparammix'
+ );
}
}
}
@@ -100,7 +99,7 @@ class ApiQueryAllDeletedRevisions extends ApiQueryRevisionsBase {
$optimizeGenerateTitles = true;
} else {
$p = $this->getModulePrefix();
- $this->setWarning( "For better performance when generating titles, set {$p}dir=newer" );
+ $this->addWarning( [ 'apiwarn-alldeletedrevisions-performance', $p ], 'performance' );
}
}
@@ -148,12 +147,7 @@ class ApiQueryAllDeletedRevisions extends ApiQueryRevisionsBase {
$this->addFields( [ 'ar_text', 'ar_flags', 'old_text', 'old_flags' ] );
// This also means stricter restrictions
- if ( !$user->isAllowedAny( 'undelete', 'deletedtext' ) ) {
- $this->dieUsage(
- 'You don\'t have permission to view deleted revision content',
- 'permissiondenied'
- );
- }
+ $this->checkUserRightsAny( [ 'deletedtext', 'undelete' ] );
}
$miser_ns = null;
diff --git a/includes/api/ApiQueryAllImages.php b/includes/api/ApiQueryAllImages.php
index 8734f380bb6a..e3e5ed6c9f48 100644
--- a/includes/api/ApiQueryAllImages.php
+++ b/includes/api/ApiQueryAllImages.php
@@ -64,11 +64,7 @@ class ApiQueryAllImages extends ApiQueryGeneratorBase {
*/
public function executeGenerator( $resultPageSet ) {
if ( $resultPageSet->isResolvingRedirects() ) {
- $this->dieUsage(
- 'Use "gaifilterredir=nonredirects" option instead of "redirects" ' .
- 'when using allimages as a generator',
- 'params'
- );
+ $this->dieWithError( 'apierror-allimages-redirect', 'invalidparammix' );
}
$this->run( $resultPageSet );
@@ -81,10 +77,7 @@ class ApiQueryAllImages extends ApiQueryGeneratorBase {
private function run( $resultPageSet = null ) {
$repo = $this->mRepo;
if ( !$repo instanceof LocalRepo ) {
- $this->dieUsage(
- 'Local file repository does not support querying all images',
- 'unsupportedrepo'
- );
+ $this->dieWithError( 'apierror-unsupportedrepo' );
}
$prefix = $this->getModulePrefix();
@@ -109,16 +102,24 @@ class ApiQueryAllImages extends ApiQueryGeneratorBase {
$disallowed = [ 'start', 'end', 'user' ];
foreach ( $disallowed as $pname ) {
if ( isset( $params[$pname] ) ) {
- $this->dieUsage(
- "Parameter '{$prefix}{$pname}' can only be used with {$prefix}sort=timestamp",
- 'badparams'
+ $this->dieWithError(
+ [
+ 'apierror-invalidparammix-mustusewith',
+ "{$prefix}{$pname}",
+ "{$prefix}sort=timestamp"
+ ],
+ 'invalidparammix'
);
}
}
if ( $params['filterbots'] != 'all' ) {
- $this->dieUsage(
- "Parameter '{$prefix}filterbots' can only be used with {$prefix}sort=timestamp",
- 'badparams'
+ $this->dieWithError(
+ [
+ 'apierror-invalidparammix-mustusewith',
+ "{$prefix}filterbots",
+ "{$prefix}sort=timestamp"
+ ],
+ 'invalidparammix'
);
}
@@ -146,18 +147,21 @@ class ApiQueryAllImages extends ApiQueryGeneratorBase {
$disallowed = [ 'from', 'to', 'prefix' ];
foreach ( $disallowed as $pname ) {
if ( isset( $params[$pname] ) ) {
- $this->dieUsage(
- "Parameter '{$prefix}{$pname}' can only be used with {$prefix}sort=name",
- 'badparams'
+ $this->dieWithError(
+ [
+ 'apierror-invalidparammix-mustusewith',
+ "{$prefix}{$pname}",
+ "{$prefix}sort=name"
+ ],
+ 'invalidparammix'
);
}
}
if ( !is_null( $params['user'] ) && $params['filterbots'] != 'all' ) {
// Since filterbots checks if each user has the bot right, it
// doesn't make sense to use it with user
- $this->dieUsage(
- "Parameters '{$prefix}user' and '{$prefix}filterbots' cannot be used together",
- 'badparams'
+ $this->dieWithError(
+ [ 'apierror-invalidparammix-cannotusewith', "{$prefix}user", "{$prefix}filterbots" ]
);
}
@@ -214,13 +218,13 @@ class ApiQueryAllImages extends ApiQueryGeneratorBase {
if ( isset( $params['sha1'] ) ) {
$sha1 = strtolower( $params['sha1'] );
if ( !$this->validateSha1Hash( $sha1 ) ) {
- $this->dieUsage( 'The SHA1 hash provided is not valid', 'invalidsha1hash' );
+ $this->dieWithError( 'apierror-invalidsha1hash' );
}
$sha1 = Wikimedia\base_convert( $sha1, 16, 36, 31 );
} elseif ( isset( $params['sha1base36'] ) ) {
$sha1 = strtolower( $params['sha1base36'] );
if ( !$this->validateSha1Base36Hash( $sha1 ) ) {
- $this->dieUsage( 'The SHA1Base36 hash provided is not valid', 'invalidsha1base36hash' );
+ $this->dieWithError( 'apierror-invalidsha1base36hash' );
}
}
if ( $sha1 ) {
@@ -229,7 +233,7 @@ class ApiQueryAllImages extends ApiQueryGeneratorBase {
if ( !is_null( $params['mime'] ) ) {
if ( $this->getConfig()->get( 'MiserMode' ) ) {
- $this->dieUsage( 'MIME search disabled in Miser Mode', 'mimesearchdisabled' );
+ $this->dieWithError( 'apierror-mimesearchdisabled' );
}
$mimeConds = [];
diff --git a/includes/api/ApiQueryAllLinks.php b/includes/api/ApiQueryAllLinks.php
index ac906056a3c5..c3636c6bcf8e 100644
--- a/includes/api/ApiQueryAllLinks.php
+++ b/includes/api/ApiQueryAllLinks.php
@@ -116,9 +116,13 @@ class ApiQueryAllLinks extends ApiQueryGeneratorBase {
$matches = array_intersect_key( $prop, $this->props + [ 'ids' => 1 ] );
if ( $matches ) {
$p = $this->getModulePrefix();
- $this->dieUsage(
- "Cannot use {$p}prop=" . implode( '|', array_keys( $matches ) ) . " with {$p}unique",
- 'params'
+ $this->dieWithError(
+ [
+ 'apierror-invalidparammix-cannotusewith',
+ "{$p}prop=" . implode( '|', array_keys( $matches ) ),
+ "{$p}unique"
+ ],
+ 'invalidparammix'
);
}
$this->addOption( 'DISTINCT' );
diff --git a/includes/api/ApiQueryAllMessages.php b/includes/api/ApiQueryAllMessages.php
index e0ba4ea1c19c..244effc5238a 100644
--- a/includes/api/ApiQueryAllMessages.php
+++ b/includes/api/ApiQueryAllMessages.php
@@ -41,7 +41,9 @@ class ApiQueryAllMessages extends ApiQueryBase {
if ( is_null( $params['lang'] ) ) {
$langObj = $this->getLanguage();
} elseif ( !Language::isValidCode( $params['lang'] ) ) {
- $this->dieUsage( 'Invalid language code for parameter lang', 'invalidlang' );
+ $this->dieWithError(
+ [ 'apierror-invalidlang', $this->encodeParamName( 'lang' ) ], 'invalidlang'
+ );
} else {
$langObj = Language::factory( $params['lang'] );
}
@@ -50,7 +52,7 @@ class ApiQueryAllMessages extends ApiQueryBase {
if ( !is_null( $params['title'] ) ) {
$title = Title::newFromText( $params['title'] );
if ( !$title || $title->isExternal() ) {
- $this->dieUsageMsg( [ 'invalidtitle', $params['title'] ] );
+ $this->dieWithError( [ 'apierror-invalidtitle', wfEscapeWikiText( $params['title'] ) ] );
}
} else {
$title = Title::newFromText( 'API' );
diff --git a/includes/api/ApiQueryAllPages.php b/includes/api/ApiQueryAllPages.php
index 6a0f124fafd1..7460bd537759 100644
--- a/includes/api/ApiQueryAllPages.php
+++ b/includes/api/ApiQueryAllPages.php
@@ -50,11 +50,7 @@ class ApiQueryAllPages extends ApiQueryGeneratorBase {
*/
public function executeGenerator( $resultPageSet ) {
if ( $resultPageSet->isResolvingRedirects() ) {
- $this->dieUsage(
- 'Use "gapfilterredir=nonredirects" option instead of "redirects" ' .
- 'when using allpages as a generator',
- 'params'
- );
+ $this->dieWithError( 'apierror-allpages-generator-redirects', 'params' );
}
$this->run( $resultPageSet );
@@ -157,7 +153,9 @@ class ApiQueryAllPages extends ApiQueryGeneratorBase {
$this->addOption( 'DISTINCT' );
} elseif ( isset( $params['prlevel'] ) ) {
- $this->dieUsage( 'prlevel may not be used without prtype', 'params' );
+ $this->dieWithError(
+ [ 'apierror-invalidparammix-mustusewith', 'prlevel', 'prtype' ], 'invalidparammix'
+ );
}
if ( $params['filterlanglinks'] == 'withoutlanglinks' ) {
diff --git a/includes/api/ApiQueryAllUsers.php b/includes/api/ApiQueryAllUsers.php
index b7ed9dda007f..2e2ac320fd58 100644
--- a/includes/api/ApiQueryAllUsers.php
+++ b/includes/api/ApiQueryAllUsers.php
@@ -110,9 +110,7 @@ class ApiQueryAllUsers extends ApiQueryBase {
}
}
- if ( !is_null( $params['group'] ) && !is_null( $params['excludegroup'] ) ) {
- $this->dieUsage( 'group and excludegroup cannot be used together', 'group-excludegroup' );
- }
+ $this->requireMaxOneParameter( $params, 'group', 'excludegroup' );
if ( !is_null( $params['group'] ) && count( $params['group'] ) ) {
// Filter only users that belong to a given group. This might
diff --git a/includes/api/ApiQueryBacklinks.php b/includes/api/ApiQueryBacklinks.php
index fb502e40e75f..4c323206ef70 100644
--- a/includes/api/ApiQueryBacklinks.php
+++ b/includes/api/ApiQueryBacklinks.php
@@ -348,8 +348,8 @@ class ApiQueryBacklinks extends ApiQueryGeneratorBase {
// only image titles are allowed for the root in imageinfo mode
if ( !$this->hasNS && $this->rootTitle->getNamespace() !== NS_FILE ) {
- $this->dieUsage(
- "The title for {$this->getModuleName()} query must be a file",
+ $this->dieWithError(
+ [ 'apierror-imageusage-badtitle', $this->getModuleName() ],
'bad_image_title'
);
}
diff --git a/includes/api/ApiQueryBacklinksprop.php b/includes/api/ApiQueryBacklinksprop.php
index 8e89c32e50d0..ef7b9af9869f 100644
--- a/includes/api/ApiQueryBacklinksprop.php
+++ b/includes/api/ApiQueryBacklinksprop.php
@@ -238,7 +238,7 @@ class ApiQueryBacklinksprop extends ApiQueryGeneratorBase {
if ( isset( $show['fragment'] ) && isset( $show['!fragment'] ) ||
isset( $show['redirect'] ) && isset( $show['!redirect'] )
) {
- $this->dieUsageMsg( 'show' );
+ $this->dieWithError( 'apierror-show' );
}
$this->addWhereIf( "rd_fragment != $emptyString", isset( $show['fragment'] ) );
$this->addWhereIf(
diff --git a/includes/api/ApiQueryBase.php b/includes/api/ApiQueryBase.php
index bba53755c111..af2aed550476 100644
--- a/includes/api/ApiQueryBase.php
+++ b/includes/api/ApiQueryBase.php
@@ -421,7 +421,7 @@ abstract class ApiQueryBase extends ApiBase {
$likeQuery = LinkFilter::makeLikeArray( $query, $protocol );
if ( !$likeQuery ) {
- $this->dieUsage( 'Invalid query', 'bad_query' );
+ $this->dieWithError( 'apierror-badquery' );
}
$likeQuery = LinkFilter::keepOneWildcard( $likeQuery );
@@ -547,7 +547,7 @@ abstract class ApiQueryBase extends ApiBase {
$t = Title::makeTitleSafe( $namespace, $titlePart . 'x' );
if ( !$t || $t->hasFragment() ) {
// Invalid title (e.g. bad chars) or contained a '#'.
- $this->dieUsageMsg( [ 'invalidtitle', $titlePart ] );
+ $this->dieWithError( [ 'apierror-invalidtitle', wfEscapeWikiText( $titlePart ) ] );
}
if ( $namespace != $t->getNamespace() || $t->isExternal() ) {
// This can happen in two cases. First, if you call titlePartToKey with a title part
@@ -555,7 +555,7 @@ abstract class ApiQueryBase extends ApiBase {
// difficult to handle such a case. Such cases cannot exist and are therefore treated
// as invalid user input. The second case is when somebody specifies a title interwiki
// prefix.
- $this->dieUsageMsg( [ 'invalidtitle', $titlePart ] );
+ $this->dieWithError( [ 'apierror-invalidtitle', wfEscapeWikiText( $titlePart ) ] );
}
return substr( $t->getDBkey(), 0, -1 );
@@ -573,7 +573,7 @@ abstract class ApiQueryBase extends ApiBase {
$t = Title::newFromText( $titlePart . 'x', $defaultNamespace );
if ( !$t || $t->hasFragment() || $t->isExternal() ) {
// Invalid title (e.g. bad chars) or contained a '#'.
- $this->dieUsageMsg( [ 'invalidtitle', $titlePart ] );
+ $this->dieWithError( [ 'apierror-invalidtitle', wfEscapeWikiText( $titlePart ) ] );
}
return [ $t->getNamespace(), substr( $t->getDBkey(), 0, -1 ) ];
diff --git a/includes/api/ApiQueryBlocks.php b/includes/api/ApiQueryBlocks.php
index 5d7c664aac28..ef79efd35875 100644
--- a/includes/api/ApiQueryBlocks.php
+++ b/includes/api/ApiQueryBlocks.php
@@ -114,16 +114,13 @@ class ApiQueryBlocks extends ApiQueryBase {
$cidrLimit = $blockCIDRLimit['IPv6'];
$prefixLen = 3; // IP::toHex output is prefixed with "v6-"
} else {
- $this->dieUsage( 'IP parameter is not valid', 'param_ip' );
+ $this->dieWithError( 'apierror-badip', 'param_ip' );
}
# Check range validity, if it's a CIDR
list( $ip, $range ) = IP::parseCIDR( $params['ip'] );
if ( $ip !== false && $range !== false && $range < $cidrLimit ) {
- $this->dieUsage(
- "$type CIDR ranges broader than /$cidrLimit are not accepted",
- 'cidrtoobroad'
- );
+ $this->dieWithError( [ 'apierror-cidrtoobroad', $type, $cidrLimit ] );
}
# Let IP::parseRange handle calculating $upper, instead of duplicating the logic here.
@@ -154,7 +151,7 @@ class ApiQueryBlocks extends ApiQueryBase {
|| ( isset( $show['range'] ) && isset( $show['!range'] ) )
|| ( isset( $show['temp'] ) && isset( $show['!temp'] ) )
) {
- $this->dieUsageMsg( 'show' );
+ $this->dieWithError( 'apierror-show' );
}
$this->addWhereIf( 'ipb_user = 0', isset( $show['!account'] ) );
@@ -237,13 +234,19 @@ class ApiQueryBlocks extends ApiQueryBase {
protected function prepareUsername( $user ) {
if ( !$user ) {
- $this->dieUsage( 'User parameter may not be empty', 'param_user' );
+ $encParamName = $this->encodeParamName( 'users' );
+ $this->dieWithError( [ 'apierror-baduser', $encParamName, wfEscapeWikiText( $user ) ],
+ "baduser_{$encParamName}"
+ );
}
$name = User::isIP( $user )
? $user
: User::getCanonicalName( $user, 'valid' );
if ( $name === false ) {
- $this->dieUsage( "User name {$user} is not valid", 'param_user' );
+ $encParamName = $this->encodeParamName( 'users' );
+ $this->dieWithError( [ 'apierror-baduser', $encParamName, wfEscapeWikiText( $user ) ],
+ "baduser_{$encParamName}"
+ );
}
return $name;
}
diff --git a/includes/api/ApiQueryCategories.php b/includes/api/ApiQueryCategories.php
index 63d0f6da13fd..f2498cae20cf 100644
--- a/includes/api/ApiQueryCategories.php
+++ b/includes/api/ApiQueryCategories.php
@@ -74,7 +74,7 @@ class ApiQueryCategories extends ApiQueryGeneratorBase {
foreach ( $params['categories'] as $cat ) {
$title = Title::newFromText( $cat );
if ( !$title || $title->getNamespace() != NS_CATEGORY ) {
- $this->setWarning( "\"$cat\" is not a category" );
+ $this->addWarning( [ 'apiwarn-invalidcategory', wfEscapeWikiText( $cat ) ] );
} else {
$cats[] = $title->getDBkey();
}
@@ -96,7 +96,7 @@ class ApiQueryCategories extends ApiQueryGeneratorBase {
}
if ( isset( $show['hidden'] ) && isset( $show['!hidden'] ) ) {
- $this->dieUsageMsg( 'show' );
+ $this->dieWithError( 'apierror-show' );
}
if ( isset( $show['hidden'] ) || isset( $show['!hidden'] ) || isset( $prop['hidden'] ) ) {
$this->addOption( 'STRAIGHT_JOIN' );
diff --git a/includes/api/ApiQueryCategoryMembers.php b/includes/api/ApiQueryCategoryMembers.php
index 4865ad56f916..02961aa299d7 100644
--- a/includes/api/ApiQueryCategoryMembers.php
+++ b/includes/api/ApiQueryCategoryMembers.php
@@ -65,7 +65,7 @@ class ApiQueryCategoryMembers extends ApiQueryGeneratorBase {
$categoryTitle = $this->getTitleOrPageId( $params )->getTitle();
if ( $categoryTitle->getNamespace() != NS_CATEGORY ) {
- $this->dieUsage( 'The category name you entered is not valid', 'invalidcategory' );
+ $this->dieWithError( 'apierror-invalidcategory' );
}
$prop = array_flip( $params['prop'] );
@@ -153,7 +153,8 @@ class ApiQueryCategoryMembers extends ApiQueryGeneratorBase {
$startsortkey = Collation::singleton()->getSortKey( $params['startsortkeyprefix'] );
} elseif ( $params['starthexsortkey'] !== null ) {
if ( !$this->validateHexSortkey( $params['starthexsortkey'] ) ) {
- $this->dieUsage( 'The starthexsortkey provided is not valid', 'bad_starthexsortkey' );
+ $encParamName = $this->encodeParamName( 'starthexsortkey' );
+ $this->dieWithError( [ 'apierror-badparameter', $encParamName ], "badvalue_$encParamName" );
}
$startsortkey = hex2bin( $params['starthexsortkey'] );
} else {
@@ -163,7 +164,8 @@ class ApiQueryCategoryMembers extends ApiQueryGeneratorBase {
$endsortkey = Collation::singleton()->getSortKey( $params['endsortkeyprefix'] );
} elseif ( $params['endhexsortkey'] !== null ) {
if ( !$this->validateHexSortkey( $params['endhexsortkey'] ) ) {
- $this->dieUsage( 'The endhexsortkey provided is not valid', 'bad_endhexsortkey' );
+ $encParamName = $this->encodeParamName( 'endhexsortkey' );
+ $this->dieWithError( [ 'apierror-badparameter', $encParamName ], "badvalue_$encParamName" );
}
$endsortkey = hex2bin( $params['endhexsortkey'] );
} else {
diff --git a/includes/api/ApiQueryDeletedRevisions.php b/includes/api/ApiQueryDeletedRevisions.php
index cfd0653d9e5d..d0b82144690b 100644
--- a/includes/api/ApiQueryDeletedRevisions.php
+++ b/includes/api/ApiQueryDeletedRevisions.php
@@ -39,12 +39,7 @@ class ApiQueryDeletedRevisions extends ApiQueryRevisionsBase {
protected function run( ApiPageSet $resultPageSet = null ) {
$user = $this->getUser();
// Before doing anything at all, let's check permissions
- if ( !$user->isAllowed( 'deletedhistory' ) ) {
- $this->dieUsage(
- 'You don\'t have permission to view deleted revision information',
- 'permissiondenied'
- );
- }
+ $this->checkUserRightsAny( 'deletedhistory' );
$pageSet = $this->getPageSet();
$pageMap = $pageSet->getGoodAndMissingTitlesByNamespace();
@@ -63,9 +58,7 @@ class ApiQueryDeletedRevisions extends ApiQueryRevisionsBase {
$db = $this->getDB();
- if ( !is_null( $params['user'] ) && !is_null( $params['excludeuser'] ) ) {
- $this->dieUsage( 'user and excludeuser cannot be used together', 'badparams' );
- }
+ $this->requireMaxOneParameter( $params, 'user', 'excludeuser' );
$this->addTables( 'archive' );
if ( $resultPageSet === null ) {
@@ -106,12 +99,7 @@ class ApiQueryDeletedRevisions extends ApiQueryRevisionsBase {
$this->addFields( [ 'ar_text', 'ar_flags', 'old_text', 'old_flags' ] );
// This also means stricter restrictions
- if ( !$user->isAllowedAny( 'undelete', 'deletedtext' ) ) {
- $this->dieUsage(
- 'You don\'t have permission to view deleted revision content',
- 'permissiondenied'
- );
- }
+ $this->checkUserRightsAny( [ 'deletedtext', 'undelete' ] );
}
$dir = $params['dir'];
diff --git a/includes/api/ApiQueryDeletedrevs.php b/includes/api/ApiQueryDeletedrevs.php
index d58efa1d5a08..6a259cd00a80 100644
--- a/includes/api/ApiQueryDeletedrevs.php
+++ b/includes/api/ApiQueryDeletedrevs.php
@@ -37,21 +37,12 @@ class ApiQueryDeletedrevs extends ApiQueryBase {
}
public function execute() {
- $user = $this->getUser();
// Before doing anything at all, let's check permissions
- if ( !$user->isAllowed( 'deletedhistory' ) ) {
- $this->dieUsage(
- 'You don\'t have permission to view deleted revision information',
- 'permissiondenied'
- );
- }
+ $this->checkUserRightsAny( 'deletedhistory' );
- $this->setWarning(
- 'list=deletedrevs has been deprecated. Please use prop=deletedrevisions or ' .
- 'list=alldeletedrevisions instead.'
- );
- $this->logFeatureUsage( 'action=query&list=deletedrevs' );
+ $this->addDeprecation( 'apiwarn-deprecation-deletedrevs', 'action=query&list=deletedrevs' );
+ $user = $this->getUser();
$db = $this->getDB();
$params = $this->extractRequestParams( false );
$prop = array_flip( $params['prop'] );
@@ -70,9 +61,6 @@ class ApiQueryDeletedrevs extends ApiQueryBase {
if ( isset( $prop['token'] ) ) {
$p = $this->getModulePrefix();
- $this->setWarning(
- "{$p}prop=token has been deprecated. Please use action=query&meta=tokens instead."
- );
}
// If we're in a mode that breaks the same-origin policy, no tokens can
@@ -105,19 +93,19 @@ class ApiQueryDeletedrevs extends ApiQueryBase {
// Ignore namespace and unique due to inability to know whether they were purposely set
foreach ( [ 'from', 'to', 'prefix', /*'namespace', 'unique'*/ ] as $p ) {
if ( !is_null( $params[$p] ) ) {
- $this->dieUsage( "The '{$p}' parameter cannot be used in modes 1 or 2", 'badparams' );
+ $this->dieWithError( [ 'apierror-deletedrevs-param-not-1-2', $p ], 'badparams' );
}
}
} else {
foreach ( [ 'start', 'end' ] as $p ) {
if ( !is_null( $params[$p] ) ) {
- $this->dieUsage( "The {$p} parameter cannot be used in mode 3", 'badparams' );
+ $this->dieWithError( [ 'apierror-deletedrevs-param-not-3', $p ], 'badparams' );
}
}
}
if ( !is_null( $params['user'] ) && !is_null( $params['excludeuser'] ) ) {
- $this->dieUsage( 'user and excludeuser cannot be used together', 'badparams' );
+ $this->dieWithError( 'user and excludeuser cannot be used together', 'badparams' );
}
$this->addTables( 'archive' );
@@ -162,12 +150,7 @@ class ApiQueryDeletedrevs extends ApiQueryBase {
$this->addFields( [ 'ar_text', 'ar_flags', 'ar_text_id', 'old_text', 'old_flags' ] );
// This also means stricter restrictions
- if ( !$user->isAllowedAny( 'undelete', 'deletedtext' ) ) {
- $this->dieUsage(
- 'You don\'t have permission to view deleted revision content',
- 'permissiondenied'
- );
- }
+ $this->checkUserRightsAny( [ 'deletedtext', 'undelete' ] );
}
// Check limits
$userMax = $fld_content ? ApiBase::LIMIT_SML1 : ApiBase::LIMIT_BIG1;
diff --git a/includes/api/ApiQueryDisabled.php b/includes/api/ApiQueryDisabled.php
index e1c97e149b8b..9476066dfc0a 100644
--- a/includes/api/ApiQueryDisabled.php
+++ b/includes/api/ApiQueryDisabled.php
@@ -37,7 +37,7 @@
class ApiQueryDisabled extends ApiQueryBase {
public function execute() {
- $this->setWarning( "The \"{$this->getModuleName()}\" module has been disabled." );
+ $this->addWarning( [ 'apierror-moduledisabled', $this->getModuleName() ] );
}
public function getAllowedParams() {
diff --git a/includes/api/ApiQueryFilearchive.php b/includes/api/ApiQueryFilearchive.php
index 03be491e7cc1..116dbb3d34db 100644
--- a/includes/api/ApiQueryFilearchive.php
+++ b/includes/api/ApiQueryFilearchive.php
@@ -38,15 +38,10 @@ class ApiQueryFilearchive extends ApiQueryBase {
}
public function execute() {
- $user = $this->getUser();
// Before doing anything at all, let's check permissions
- if ( !$user->isAllowed( 'deletedhistory' ) ) {
- $this->dieUsage(
- 'You don\'t have permission to view deleted file information',
- 'permissiondenied'
- );
- }
+ $this->checkUserRightsAny( 'deletedhistory' );
+ $user = $this->getUser();
$db = $this->getDB();
$params = $this->extractRequestParams();
@@ -112,13 +107,13 @@ class ApiQueryFilearchive extends ApiQueryBase {
if ( $sha1Set ) {
$sha1 = strtolower( $params['sha1'] );
if ( !$this->validateSha1Hash( $sha1 ) ) {
- $this->dieUsage( 'The SHA1 hash provided is not valid', 'invalidsha1hash' );
+ $this->dieWithError( 'apierror-invalidsha1hash' );
}
$sha1 = Wikimedia\base_convert( $sha1, 16, 36, 31 );
} elseif ( $sha1base36Set ) {
$sha1 = strtolower( $params['sha1base36'] );
if ( !$this->validateSha1Base36Hash( $sha1 ) ) {
- $this->dieUsage( 'The SHA1Base36 hash provided is not valid', 'invalidsha1base36hash' );
+ $this->dieWithError( 'apierror-invalidsha1base36hash' );
}
}
if ( $sha1 ) {
diff --git a/includes/api/ApiQueryIWBacklinks.php b/includes/api/ApiQueryIWBacklinks.php
index 75681077dec7..6e2fb67b8d99 100644
--- a/includes/api/ApiQueryIWBacklinks.php
+++ b/includes/api/ApiQueryIWBacklinks.php
@@ -51,7 +51,14 @@ class ApiQueryIWBacklinks extends ApiQueryGeneratorBase {
$params = $this->extractRequestParams();
if ( isset( $params['title'] ) && !isset( $params['prefix'] ) ) {
- $this->dieUsageMsg( [ 'missingparam', 'prefix' ] );
+ $this->dieWithError(
+ [
+ 'apierror-invalidparammix-mustusewith',
+ $this->encodeParamName( 'title' ),
+ $this->encodeParamName( 'prefix' ),
+ ],
+ 'invalidparammix'
+ );
}
if ( !is_null( $params['continue'] ) ) {
diff --git a/includes/api/ApiQueryIWLinks.php b/includes/api/ApiQueryIWLinks.php
index 6d9c2ca86115..cfd990b21301 100644
--- a/includes/api/ApiQueryIWLinks.php
+++ b/includes/api/ApiQueryIWLinks.php
@@ -45,7 +45,14 @@ class ApiQueryIWLinks extends ApiQueryBase {
$prop = array_flip( (array)$params['prop'] );
if ( isset( $params['title'] ) && !isset( $params['prefix'] ) ) {
- $this->dieUsageMsg( [ 'missingparam', 'prefix' ] );
+ $this->dieWithError(
+ [
+ 'apierror-invalidparammix-mustusewith',
+ $this->encodeParamName( 'title' ),
+ $this->encodeParamName( 'prefix' ),
+ ],
+ 'invalidparammix'
+ );
}
// Handle deprecated param
diff --git a/includes/api/ApiQueryImageInfo.php b/includes/api/ApiQueryImageInfo.php
index d1fcfa3f07c9..0bbfad3a836b 100644
--- a/includes/api/ApiQueryImageInfo.php
+++ b/includes/api/ApiQueryImageInfo.php
@@ -280,8 +280,7 @@ class ApiQueryImageInfo extends ApiQueryBase {
$h = $image->getHandler();
if ( !$h ) {
- $this->setWarning( 'Could not create thumbnail because ' .
- $image->getName() . ' does not have an associated image handler' );
+ $this->addWarning( [ 'apiwarn-nothumb-noimagehandler', wfEscapeWikiText( $image->getName() ) ] );
return $thumbParams;
}
@@ -292,23 +291,24 @@ class ApiQueryImageInfo extends ApiQueryBase {
// we could still render the image using width and height parameters,
// and this type of thing could happen between different versions of
// handlers.
- $this->setWarning( "Could not parse {$p}urlparam for " . $image->getName()
- . '. Using only width and height' );
+ $this->addWarning( [ 'apiwarn-badurlparam', $p, wfEscapeWikiText( $image->getName() ) ] );
$this->checkParameterNormalise( $image, $thumbParams );
return $thumbParams;
}
if ( isset( $paramList['width'] ) && isset( $thumbParams['width'] ) ) {
if ( intval( $paramList['width'] ) != intval( $thumbParams['width'] ) ) {
- $this->setWarning( "Ignoring width value set in {$p}urlparam ({$paramList['width']}) "
- . "in favor of width value derived from {$p}urlwidth/{$p}urlheight "
- . "({$thumbParams['width']})" );
+ $this->addWarning(
+ [ 'apiwarn-urlparamwidth', $p, $paramList['width'], $thumbParams['width'] ]
+ );
}
}
foreach ( $paramList as $name => $value ) {
if ( !$h->validateParam( $name, $value ) ) {
- $this->dieUsage( "Invalid value for {$p}urlparam ($name=$value)", 'urlparam' );
+ $this->dieWithError(
+ [ 'apierror-invalidurlparam', $p, wfEscapeWikiText( $name ), wfEscapeWikiText( $value ) ]
+ );
}
}
@@ -337,8 +337,7 @@ class ApiQueryImageInfo extends ApiQueryBase {
// in the actual normalised version, only if we can actually normalise them,
// so we use the functions scope to throw away the normalisations.
if ( !$h->normaliseParams( $image, $finalParams ) ) {
- $this->dieUsage( 'Could not normalise image parameters for ' .
- $image->getName(), 'urlparamnormal' );
+ $this->dieWithError( [ 'apierror-urlparamnormal', wfEscapeWikiText( $image->getName() ) ] );
}
}
diff --git a/includes/api/ApiQueryImages.php b/includes/api/ApiQueryImages.php
index e04d8c888f1f..ae6f5bf564aa 100644
--- a/includes/api/ApiQueryImages.php
+++ b/includes/api/ApiQueryImages.php
@@ -90,7 +90,7 @@ class ApiQueryImages extends ApiQueryGeneratorBase {
foreach ( $params['images'] as $img ) {
$title = Title::newFromText( $img );
if ( !$title || $title->getNamespace() != NS_FILE ) {
- $this->setWarning( "\"$img\" is not a file" );
+ $this->addWarning( [ 'apiwarn-notfile', wfEscapeWikiText( $img ) ] );
} else {
$images[] = $title->getDBkey();
}
diff --git a/includes/api/ApiQueryInfo.php b/includes/api/ApiQueryInfo.php
index d28702053625..fd6503801f77 100644
--- a/includes/api/ApiQueryInfo.php
+++ b/includes/api/ApiQueryInfo.php
@@ -427,7 +427,7 @@ class ApiQueryInfo extends ApiQueryBase {
foreach ( $this->params['token'] as $t ) {
$val = call_user_func( $tokenFunctions[$t], $pageid, $title );
if ( $val === false ) {
- $this->setWarning( "Action '$t' is not allowed for the current user" );
+ $this->addWarning( [ 'apiwarn-tokennotallowed', $t ] );
} else {
$pageInfo[$t . 'token'] = $val;
}
diff --git a/includes/api/ApiQueryLangBacklinks.php b/includes/api/ApiQueryLangBacklinks.php
index a6153de96102..8d5b5f3ea6a2 100644
--- a/includes/api/ApiQueryLangBacklinks.php
+++ b/includes/api/ApiQueryLangBacklinks.php
@@ -51,7 +51,14 @@ class ApiQueryLangBacklinks extends ApiQueryGeneratorBase {
$params = $this->extractRequestParams();
if ( isset( $params['title'] ) && !isset( $params['lang'] ) ) {
- $this->dieUsageMsg( [ 'missingparam', 'lang' ] );
+ $this->dieWithError(
+ [
+ 'apierror-invalidparammix-mustusewith',
+ $this->encodeParamName( 'title' ),
+ $this->encodeParamName( 'lang' )
+ ],
+ 'nolang'
+ );
}
if ( !is_null( $params['continue'] ) ) {
diff --git a/includes/api/ApiQueryLangLinks.php b/includes/api/ApiQueryLangLinks.php
index 67f2c9eceaa6..55e3c85265a5 100644
--- a/includes/api/ApiQueryLangLinks.php
+++ b/includes/api/ApiQueryLangLinks.php
@@ -44,7 +44,14 @@ class ApiQueryLangLinks extends ApiQueryBase {
$prop = array_flip( (array)$params['prop'] );
if ( isset( $params['title'] ) && !isset( $params['lang'] ) ) {
- $this->dieUsageMsg( [ 'missingparam', 'lang' ] );
+ $this->dieWithError(
+ [
+ 'apierror-invalidparammix-mustusewith',
+ $this->encodeParamName( 'title' ),
+ $this->encodeParamName( 'lang' ),
+ ],
+ 'invalidparammix'
+ );
}
// Handle deprecated param
diff --git a/includes/api/ApiQueryLinks.php b/includes/api/ApiQueryLinks.php
index 6e5239f7b932..e9ae132df723 100644
--- a/includes/api/ApiQueryLinks.php
+++ b/includes/api/ApiQueryLinks.php
@@ -94,7 +94,7 @@ class ApiQueryLinks extends ApiQueryGeneratorBase {
foreach ( $params[$this->titlesParam] as $t ) {
$title = Title::newFromText( $t );
if ( !$title ) {
- $this->setWarning( "\"$t\" is not a valid title" );
+ $this->addWarning( [ 'apiwarn-invalidtitle', wfEscapeWikiText( $t ) ] );
} else {
$lb->addObj( $title );
}
diff --git a/includes/api/ApiQueryLogEvents.php b/includes/api/ApiQueryLogEvents.php
index 122594d1753f..2dcd0b4f8838 100644
--- a/includes/api/ApiQueryLogEvents.php
+++ b/includes/api/ApiQueryLogEvents.php
@@ -121,10 +121,10 @@ class ApiQueryLogEvents extends ApiQueryBase {
}
if ( !$valid ) {
- $valueName = $this->encodeParamName( 'action' );
- $this->dieUsage(
- "Unrecognized value for parameter '$valueName': {$logAction}",
- "unknown_$valueName"
+ $encParamName = $this->encodeParamName( 'action' );
+ $this->dieWithError(
+ [ 'apierror-unrecognizedvalue', $encParamName, wfEscapeWikiText( $logAction ) ],
+ "unknown_$encParamName"
);
}
@@ -173,7 +173,7 @@ class ApiQueryLogEvents extends ApiQueryBase {
if ( !is_null( $title ) ) {
$titleObj = Title::newFromText( $title );
if ( is_null( $titleObj ) ) {
- $this->dieUsage( "Bad title value '$title'", 'param_title' );
+ $this->dieWithError( [ 'apierror-invalidtitle', wfEscapeWikiText( $title ) ] );
}
$this->addWhereFld( 'log_namespace', $titleObj->getNamespace() );
$this->addWhereFld( 'log_title', $titleObj->getDBkey() );
@@ -187,12 +187,12 @@ class ApiQueryLogEvents extends ApiQueryBase {
if ( !is_null( $prefix ) ) {
if ( $this->getConfig()->get( 'MiserMode' ) ) {
- $this->dieUsage( 'Prefix search disabled in Miser Mode', 'prefixsearchdisabled' );
+ $this->dieWithError( 'apierror-prefixsearchdisabled' );
}
$title = Title::newFromText( $prefix );
if ( is_null( $title ) ) {
- $this->dieUsage( "Bad title value '$prefix'", 'param_prefix' );
+ $this->dieWithError( [ 'apierror-invalidtitle', wfEscapeWikiText( $prefix ) ] );
}
$this->addWhereFld( 'log_namespace', $title->getNamespace() );
$this->addWhere( 'log_title ' . $db->buildLike( $title->getDBkey(), $db->anyString() ) );
diff --git a/includes/api/ApiQueryMyStashedFiles.php b/includes/api/ApiQueryMyStashedFiles.php
index 0c70a8a4ef8e..1324f2ff498a 100644
--- a/includes/api/ApiQueryMyStashedFiles.php
+++ b/includes/api/ApiQueryMyStashedFiles.php
@@ -36,7 +36,7 @@ class ApiQueryMyStashedFiles extends ApiQueryBase {
$user = $this->getUser();
if ( $user->isAnon() ) {
- $this->dieUsage( 'The upload stash is only available to logged-in users.', 'stashnotloggedin' );
+ $this->dieWithError( 'apierror-mustbeloggedin-uploadstash', 'stashnotloggedin' );
}
// Note: If user is logged in but cannot upload, they can still see
diff --git a/includes/api/ApiQueryQueryPage.php b/includes/api/ApiQueryQueryPage.php
index 9ba757c0784c..908cdee667b9 100644
--- a/includes/api/ApiQueryQueryPage.php
+++ b/includes/api/ApiQueryQueryPage.php
@@ -62,7 +62,7 @@ class ApiQueryQueryPage extends ApiQueryGeneratorBase {
/** @var $qp QueryPage */
$qp = new $this->qpMap[$params['page']]();
if ( !$qp->userCanExecute( $this->getUser() ) ) {
- $this->dieUsageMsg( 'specialpage-cantexecute' );
+ $this->dieWithError( 'apierror-specialpage-cantexecute' );
}
$r = [ 'name' => $params['page'] ];
diff --git a/includes/api/ApiQueryRecentChanges.php b/includes/api/ApiQueryRecentChanges.php
index 8b11dc2a47d2..8d149274fd5d 100644
--- a/includes/api/ApiQueryRecentChanges.php
+++ b/includes/api/ApiQueryRecentChanges.php
@@ -195,7 +195,7 @@ class ApiQueryRecentChanges extends ApiQueryGeneratorBase {
|| ( isset( $show['patrolled'] ) && isset( $show['unpatrolled'] ) )
|| ( isset( $show['!patrolled'] ) && isset( $show['unpatrolled'] ) )
) {
- $this->dieUsageMsg( 'show' );
+ $this->dieWithError( 'apierror-show' );
}
// Check permissions
@@ -204,10 +204,7 @@ class ApiQueryRecentChanges extends ApiQueryGeneratorBase {
|| isset( $show['unpatrolled'] )
) {
if ( !$user->useRCPatrol() && !$user->useNPPatrol() ) {
- $this->dieUsage(
- 'You need patrol or patrolmarks permission to request the patrolled flag',
- 'permissiondenied'
- );
+ $this->dieWithError( 'apierror-permissiondenied-patrolflag', 'permissiondenied' );
}
}
@@ -239,9 +236,7 @@ class ApiQueryRecentChanges extends ApiQueryGeneratorBase {
);
}
- if ( !is_null( $params['user'] ) && !is_null( $params['excludeuser'] ) ) {
- $this->dieUsage( 'user and excludeuser cannot be used together', 'user-excludeuser' );
- }
+ $this->requireMaxOneParameter( $params, 'user', 'excludeuser' );
if ( !is_null( $params['user'] ) ) {
$this->addWhereFld( 'rc_user_text', $params['user'] );
@@ -274,10 +269,7 @@ class ApiQueryRecentChanges extends ApiQueryGeneratorBase {
$this->initProperties( $prop );
if ( $this->fld_patrolled && !$user->useRCPatrol() && !$user->useNPPatrol() ) {
- $this->dieUsage(
- 'You need patrol or patrolmarks permission to request the patrolled flag',
- 'permissiondenied'
- );
+ $this->dieWithError( 'apierror-permissiondenied-patrolflag', 'permissiondenied' );
}
/* Add fields to our query if they are specified as a needed parameter. */
@@ -571,7 +563,7 @@ class ApiQueryRecentChanges extends ApiQueryGeneratorBase {
$val = call_user_func( $tokenFunctions[$t], $row->rc_cur_id,
$title, RecentChange::newFromRow( $row ) );
if ( $val === false ) {
- $this->setWarning( "Action '$t' is not allowed for the current user" );
+ $this->addWarning( [ 'apiwarn-tokennotallowed', $t ] );
} else {
$vals[$t . 'token'] = $val;
}
diff --git a/includes/api/ApiQueryRevisions.php b/includes/api/ApiQueryRevisions.php
index 3259927a23b6..48f604664fbb 100644
--- a/includes/api/ApiQueryRevisions.php
+++ b/includes/api/ApiQueryRevisions.php
@@ -110,19 +110,14 @@ class ApiQueryRevisions extends ApiQueryRevisionsBase {
}
if ( $revCount > 0 && $enumRevMode ) {
- $this->dieUsage(
- 'The revids= parameter may not be used with the list options ' .
- '(limit, startid, endid, dirNewer, start, end).',
- 'revids'
+ $this->dieWithError(
+ [ 'apierror-revisions-nolist', $this->getModulePrefix() ], 'invalidparammix'
);
}
if ( $pageCount > 1 && $enumRevMode ) {
- $this->dieUsage(
- 'titles, pageids or a generator was used to supply multiple pages, ' .
- 'but the limit, startid, endid, dirNewer, user, excludeuser, start ' .
- 'and end parameters may only be used on a single page.',
- 'multpages'
+ $this->dieWithError(
+ [ 'apierror-revisions-singlepage', $this->getModulePrefix() ], 'invalidparammix'
);
}
@@ -170,14 +165,19 @@ class ApiQueryRevisions extends ApiQueryRevisionsBase {
if ( $this->fetchContent ) {
// For each page we will request, the user must have read rights for that page
$user = $this->getUser();
+ $status = Status::newGood();
/** @var $title Title */
foreach ( $pageSet->getGoodTitles() as $title ) {
if ( !$title->userCan( 'read', $user ) ) {
- $this->dieUsage(
- 'The current user is not allowed to read ' . $title->getPrefixedText(),
- 'accessdenied' );
+ $status->fatal( ApiMessage::create(
+ [ 'apierror-cannotviewtitle', wfEscapeWikiText( $title->getPrefixedText() ) ],
+ 'accessdenied'
+ ) );
}
}
+ if ( !$status->isGood() ) {
+ $this->dieStatus( $status );
+ }
$this->addTables( 'text' );
$this->addJoinConds(
@@ -201,17 +201,9 @@ class ApiQueryRevisions extends ApiQueryRevisionsBase {
// page_timestamp or usertext_timestamp if we have an IP rvuser
// This is mostly to prevent parameter errors (and optimize SQL?)
- if ( $params['startid'] !== null && $params['start'] !== null ) {
- $this->dieUsage( 'start and startid cannot be used together', 'badparams' );
- }
-
- if ( $params['endid'] !== null && $params['end'] !== null ) {
- $this->dieUsage( 'end and endid cannot be used together', 'badparams' );
- }
-
- if ( $params['user'] !== null && $params['excludeuser'] !== null ) {
- $this->dieUsage( 'user and excludeuser cannot be used together', 'badparams' );
- }
+ $this->requireMaxOneParameter( $params, 'startid', 'start' );
+ $this->requireMaxOneParameter( $params, 'endid', 'end' );
+ $this->requireMaxOneParameter( $params, 'user', 'excludeuser' );
if ( $params['continue'] !== null ) {
$cont = explode( '|', $params['continue'] );
@@ -344,7 +336,7 @@ class ApiQueryRevisions extends ApiQueryRevisionsBase {
foreach ( $this->token as $t ) {
$val = call_user_func( $tokenFunctions[$t], $title->getArticleID(), $title, $revision );
if ( $val === false ) {
- $this->setWarning( "Action '$t' is not allowed for the current user" );
+ $this->addWarning( [ 'apiwarn-tokennotallowed', $t ] );
} else {
$rev[$t . 'token'] = $val;
}
diff --git a/includes/api/ApiQueryRevisionsBase.php b/includes/api/ApiQueryRevisionsBase.php
index 266d6999bac0..696ec878670c 100644
--- a/includes/api/ApiQueryRevisionsBase.php
+++ b/includes/api/ApiQueryRevisionsBase.php
@@ -70,10 +70,7 @@ abstract class ApiQueryRevisionsBase extends ApiQueryGeneratorBase {
&& $params['diffto'] != 'prev' && $params['diffto'] != 'next'
) {
$p = $this->getModulePrefix();
- $this->dieUsage(
- "{$p}diffto must be set to a non-negative number, \"prev\", \"next\" or \"cur\"",
- 'diffto'
- );
+ $this->dieWithError( [ 'apierror-baddiffto', $p ], 'diffto' );
}
// Check whether the revision exists and is readable,
// DifferenceEngine returns a rather ambiguous empty
@@ -81,10 +78,10 @@ abstract class ApiQueryRevisionsBase extends ApiQueryGeneratorBase {
if ( $params['diffto'] != 0 ) {
$difftoRev = Revision::newFromId( $params['diffto'] );
if ( !$difftoRev ) {
- $this->dieUsageMsg( [ 'nosuchrevid', $params['diffto'] ] );
+ $this->dieWithError( [ 'apierror-nosuchrevid', $params['diffto'] ] );
}
if ( !$difftoRev->userCan( Revision::DELETED_TEXT, $this->getUser() ) ) {
- $this->setWarning( "Couldn't diff to r{$difftoRev->getId()}: content is hidden" );
+ $this->addWarning( [ 'apiwarn-difftohidden', $difftoRev->getId() ] );
$params['diffto'] = null;
}
}
@@ -262,8 +259,12 @@ abstract class ApiQueryRevisionsBase extends ApiQueryGeneratorBase {
if ( $content && $this->section !== false ) {
$content = $content->getSection( $this->section, false );
if ( !$content ) {
- $this->dieUsage(
- "There is no section {$this->section} in r" . $revision->getId(),
+ $this->dieWithError(
+ [
+ 'apierror-nosuchsection-what',
+ wfEscapeWikiText( $this->section ),
+ $this->msg( 'revid', $revision->getId() )
+ ],
'nosuchsection'
);
}
@@ -294,9 +295,14 @@ abstract class ApiQueryRevisionsBase extends ApiQueryGeneratorBase {
$vals['parsetree'] = $xml;
} else {
$vals['badcontentformatforparsetree'] = true;
- $this->setWarning( 'Conversion to XML is supported for wikitext only, ' .
- $title->getPrefixedDBkey() .
- ' uses content model ' . $content->getModel() );
+ $this->addWarning(
+ [
+ 'apierror-parsetree-notwikitext-title',
+ wfEscapeWikiText( $title->getPrefixedText() ),
+ $content->getModel()
+ ],
+ 'parsetree-notwikitext'
+ );
}
}
}
@@ -315,9 +321,11 @@ abstract class ApiQueryRevisionsBase extends ApiQueryGeneratorBase {
ParserOptions::newFromContext( $this->getContext() )
);
} else {
- $this->setWarning( 'Template expansion is supported for wikitext only, ' .
- $title->getPrefixedDBkey() .
- ' uses content model ' . $content->getModel() );
+ $this->addWarning( [
+ 'apierror-templateexpansion-notwikitext',
+ wfEscapeWikiText( $title->getPrefixedText() ),
+ $content->getModel()
+ ] );
$vals['badcontentformat'] = true;
$text = false;
}
@@ -336,9 +344,8 @@ abstract class ApiQueryRevisionsBase extends ApiQueryGeneratorBase {
$model = $content->getModel();
if ( !$content->isSupportedFormat( $format ) ) {
- $name = $title->getPrefixedDBkey();
- $this->setWarning( "The requested format {$this->contentFormat} is not " .
- "supported for content model $model used by $name" );
+ $name = wfEscapeWikiText( $title->getPrefixedText() );
+ $this->addWarning( [ 'apierror-badformat', $this->contentFormat, $model, $name ] );
$vals['badcontentformat'] = true;
$text = false;
} else {
@@ -370,9 +377,8 @@ abstract class ApiQueryRevisionsBase extends ApiQueryGeneratorBase {
if ( $this->contentFormat
&& !ContentHandler::getForModelID( $model )->isSupportedFormat( $this->contentFormat )
) {
- $name = $title->getPrefixedDBkey();
- $this->setWarning( "The requested format {$this->contentFormat} is not " .
- "supported for content model $model used by $name" );
+ $name = wfEscapeWikiText( $title->getPrefixedText() );
+ $this->addWarning( [ 'apierror-badformat', $this->contentFormat, $model, $name ] );
$vals['diff']['badcontentformat'] = true;
$engine = null;
} else {
diff --git a/includes/api/ApiQuerySearch.php b/includes/api/ApiQuerySearch.php
index 9962d5ec2031..64bc43f07b69 100644
--- a/includes/api/ApiQuerySearch.php
+++ b/includes/api/ApiQuerySearch.php
@@ -64,12 +64,14 @@ class ApiQuerySearch extends ApiQueryGeneratorBase {
// Deprecated parameters
if ( isset( $prop['hasrelated'] ) ) {
- $this->logFeatureUsage( 'action=search&srprop=hasrelated' );
- $this->setWarning( 'srprop=hasrelated has been deprecated' );
+ $this->addDeprecation(
+ [ 'apiwarn-deprecation-parameter', 'srprop=hasrelated' ], 'action=search&srprop=hasrelated'
+ );
}
if ( isset( $prop['score'] ) ) {
- $this->logFeatureUsage( 'action=search&srprop=score' );
- $this->setWarning( 'srprop=score has been deprecated' );
+ $this->addDeprecation(
+ [ 'apiwarn-deprecation-parameter', 'srprop=score' ], 'action=search&srprop=score'
+ );
}
// Create search engine instance and set options
@@ -122,10 +124,10 @@ class ApiQuerySearch extends ApiQueryGeneratorBase {
$status
);
} else {
- $this->dieUsage( $status->getWikiText( false, false, 'en' ), 'search-error' );
+ $this->dieStatus( $status );
}
} elseif ( is_null( $matches ) ) {
- $this->dieUsage( "{$what} search is disabled", "search-{$what}-disabled" );
+ $this->dieWithError( [ 'apierror-searchdisabled', $what ], "search-{$what}-disabled" );
}
if ( $resultPageSet === null ) {
diff --git a/includes/api/ApiQuerySiteinfo.php b/includes/api/ApiQuerySiteinfo.php
index 19e0c939e912..6fc6aa370c00 100644
--- a/includes/api/ApiQuerySiteinfo.php
+++ b/includes/api/ApiQuerySiteinfo.php
@@ -447,10 +447,7 @@ class ApiQuerySiteinfo extends ApiQueryBase {
$showHostnames = $this->getConfig()->get( 'ShowHostnames' );
if ( $includeAll ) {
if ( !$showHostnames ) {
- $this->dieUsage(
- 'Cannot view all servers info unless $wgShowHostnames is true',
- 'includeAllDenied'
- );
+ $this->dieWithError( 'apierror-siteinfo-includealldenied', 'includeAllDenied' );
}
$lags = $lb->getLagTimes();
diff --git a/includes/api/ApiQueryStashImageInfo.php b/includes/api/ApiQueryStashImageInfo.php
index b039a1ec45d9..981cb0948359 100644
--- a/includes/api/ApiQueryStashImageInfo.php
+++ b/includes/api/ApiQueryStashImageInfo.php
@@ -33,7 +33,7 @@ class ApiQueryStashImageInfo extends ApiQueryImageInfo {
public function execute() {
if ( !$this->getUser()->isLoggedIn() ) {
- $this->dieUsage( 'You must be logged-in to have an upload stash', 'notloggedin' );
+ $this->dieWithError( 'apierror-mustbeloggedin-uploadstash', 'notloggedin' );
}
$params = $this->extractRequestParams();
@@ -45,9 +45,7 @@ class ApiQueryStashImageInfo extends ApiQueryImageInfo {
$result = $this->getResult();
- if ( !$params['filekey'] && !$params['sessionkey'] ) {
- $this->dieUsage( 'One of filekey or sessionkey must be supplied', 'nofilekey' );
- }
+ $this->requireAtLeastOneParameter( $params, 'filekey', 'sessionkey' );
// Alias sessionkey to filekey, but give an existing filekey precedence.
if ( !$params['filekey'] && $params['sessionkey'] ) {
@@ -65,10 +63,11 @@ class ApiQueryStashImageInfo extends ApiQueryImageInfo {
$result->addIndexedTagName( [ 'query', $this->getModuleName() ], $modulePrefix );
}
// @todo Update exception handling here to understand current getFile exceptions
+ // @todo Internationalize the exceptions
} catch ( UploadStashFileNotFoundException $e ) {
- $this->dieUsage( 'File not found: ' . $e->getMessage(), 'invalidsessiondata' );
+ $this->dieWithError( [ 'apierror-stashedfilenotfound', wfEscapeWikiText( $e->getMessage() ) ] );
} catch ( UploadStashBadPathException $e ) {
- $this->dieUsage( 'Bad path: ' . $e->getMessage(), 'invalidsessiondata' );
+ $this->dieWithError( [ 'apierror-stashpathinvalid', wfEscapeWikiText( $e->getMessage() ) ] );
}
}
diff --git a/includes/api/ApiQueryTokens.php b/includes/api/ApiQueryTokens.php
index de5a377417d3..5b700dbc9c0d 100644
--- a/includes/api/ApiQueryTokens.php
+++ b/includes/api/ApiQueryTokens.php
@@ -40,7 +40,7 @@ class ApiQueryTokens extends ApiQueryBase {
];
if ( $this->lacksSameOriginSecurity() ) {
- $this->setWarning( 'Tokens may not be obtained when the same-origin policy is not applied' );
+ $this->addWarning( [ 'apiwarn-tokens-origin' ] );
return;
}
diff --git a/includes/api/ApiQueryUserContributions.php b/includes/api/ApiQueryUserContributions.php
index b85bec4899c5..b6d871b81797 100644
--- a/includes/api/ApiQueryUserContributions.php
+++ b/includes/api/ApiQueryUserContributions.php
@@ -78,11 +78,17 @@ class ApiQueryContributions extends ApiQueryBase {
$this->params['user'] = [ $this->params['user'] ];
}
if ( !count( $this->params['user'] ) ) {
- $this->dieUsage( 'User parameter may not be empty.', 'param_user' );
+ $encParamName = $this->encodeParamName( 'user' );
+ $this->dieWithError(
+ [ 'apierror-paramempty', $encParamName ], "paramempty_$encParamName"
+ );
}
foreach ( $this->params['user'] as $u ) {
if ( is_null( $u ) || $u === '' ) {
- $this->dieUsage( 'User parameter may not be empty', 'param_user' );
+ $encParamName = $this->encodeParamName( 'user' );
+ $this->dieWithError(
+ [ 'apierror-paramempty', $encParamName ], "paramempty_$encParamName"
+ );
}
if ( User::isIP( $u ) ) {
@@ -91,7 +97,10 @@ class ApiQueryContributions extends ApiQueryBase {
} else {
$name = User::getCanonicalName( $u, 'valid' );
if ( $name === false ) {
- $this->dieUsage( "User name {$u} is not valid", 'param_user' );
+ $encParamName = $this->encodeParamName( 'user' );
+ $this->dieWithError(
+ [ 'apierror-baduser', $encParamName, wfEscapeWikiText( $u ) ], "baduser_$encParamName"
+ );
}
$this->usernames[] = $name;
}
@@ -254,7 +263,7 @@ class ApiQueryContributions extends ApiQueryBase {
|| ( isset( $show['top'] ) && isset( $show['!top'] ) )
|| ( isset( $show['new'] ) && isset( $show['!new'] ) )
) {
- $this->dieUsageMsg( 'show' );
+ $this->dieWithError( 'apierror-show' );
}
$this->addWhereIf( 'rev_minor_edit = 0', isset( $show['!minor'] ) );
@@ -285,10 +294,7 @@ class ApiQueryContributions extends ApiQueryBase {
$this->fld_patrolled
) {
if ( !$user->useRCPatrol() && !$user->useNPPatrol() ) {
- $this->dieUsage(
- 'You need the patrol right to request the patrolled flag',
- 'permissiondenied'
- );
+ $this->dieWithError( 'apierror-permissiondenied-patrolflag', 'permissiondenied' );
}
// Use a redundant join condition on both
diff --git a/includes/api/ApiQueryUserInfo.php b/includes/api/ApiQueryUserInfo.php
index d3cd0c48c41d..3b604786ab55 100644
--- a/includes/api/ApiQueryUserInfo.php
+++ b/includes/api/ApiQueryUserInfo.php
@@ -170,8 +170,13 @@ class ApiQueryUserInfo extends ApiQueryBase {
if ( isset( $this->prop['preferencestoken'] ) ) {
$p = $this->getModulePrefix();
- $this->setWarning(
- "{$p}prop=preferencestoken has been deprecated. Please use action=query&meta=tokens instead."
+ $this->addDeprecation(
+ [
+ 'apiwarn-deprecation-withreplacement',
+ "{$p}prop=preferencestoken",
+ 'action=query&meta=tokens',
+ ],
+ "meta=userinfo&{$p}prop=preferencestoken"
);
}
if ( isset( $this->prop['preferencestoken'] ) &&
diff --git a/includes/api/ApiQueryUsers.php b/includes/api/ApiQueryUsers.php
index 9b45b9192392..65d3797a7471 100644
--- a/includes/api/ApiQueryUsers.php
+++ b/includes/api/ApiQueryUsers.php
@@ -226,7 +226,7 @@ class ApiQueryUsers extends ApiQueryBase {
foreach ( $params['token'] as $t ) {
$val = call_user_func( $tokenFunctions[$t], $user );
if ( $val === false ) {
- $this->setWarning( "Action '$t' is not allowed for the current user" );
+ $this->addWarning( [ 'apiwarn-tokennotallowed', $t ] );
} else {
$data[$name][$t . 'token'] = $val;
}
@@ -253,7 +253,7 @@ class ApiQueryUsers extends ApiQueryBase {
foreach ( $params['token'] as $t ) {
$val = call_user_func( $tokenFunctions[$t], $iwUser );
if ( $val === false ) {
- $this->setWarning( "Action '$t' is not allowed for the current user" );
+ $this->addWarning( [ 'apiwarn-tokennotallowed', $t ] );
} else {
$data[$u][$t . 'token'] = $val;
}
diff --git a/includes/api/ApiQueryWatchlist.php b/includes/api/ApiQueryWatchlist.php
index 42ea55dd7055..6b5ceb703fc1 100644
--- a/includes/api/ApiQueryWatchlist.php
+++ b/includes/api/ApiQueryWatchlist.php
@@ -82,7 +82,7 @@ class ApiQueryWatchlist extends ApiQueryGeneratorBase {
if ( $this->fld_patrol ) {
if ( !$user->useRCPatrol() && !$user->useNPPatrol() ) {
- $this->dieUsage( 'patrol property is not available', 'patrol' );
+ $this->dieWithError( 'apierror-permissiondenied-patrolflag', 'patrol' );
}
}
}
@@ -134,7 +134,7 @@ class ApiQueryWatchlist extends ApiQueryGeneratorBase {
/* Check for conflicting parameters. */
if ( $this->showParamsConflicting( $show ) ) {
- $this->dieUsageMsg( 'show' );
+ $this->dieWithError( 'apierror-show' );
}
// Check permissions.
@@ -142,10 +142,7 @@ class ApiQueryWatchlist extends ApiQueryGeneratorBase {
|| isset( $show[WatchedItemQueryService::FILTER_NOT_PATROLLED] )
) {
if ( !$user->useRCPatrol() && !$user->useNPPatrol() ) {
- $this->dieUsage(
- 'You need the patrol right to request the patrolled flag',
- 'permissiondenied'
- );
+ $this->dieWithError( 'apierror-permissiondenied-patrolflag', 'permissiondenied' );
}
}
@@ -160,9 +157,7 @@ class ApiQueryWatchlist extends ApiQueryGeneratorBase {
}
}
- if ( !is_null( $params['user'] ) && !is_null( $params['excludeuser'] ) ) {
- $this->dieUsage( 'user and excludeuser cannot be used together', 'user-excludeuser' );
- }
+ $this->requireMaxOneParameter( $params, 'user', 'excludeuser' );
if ( !is_null( $params['user'] ) ) {
$options['onlyByUser'] = $params['user'];
}
diff --git a/includes/api/ApiQueryWatchlistRaw.php b/includes/api/ApiQueryWatchlistRaw.php
index 806861e8009c..a1078a5d4824 100644
--- a/includes/api/ApiQueryWatchlistRaw.php
+++ b/includes/api/ApiQueryWatchlistRaw.php
@@ -60,7 +60,7 @@ class ApiQueryWatchlistRaw extends ApiQueryGeneratorBase {
if ( isset( $show[WatchedItemQueryService::FILTER_CHANGED] )
&& isset( $show[WatchedItemQueryService::FILTER_NOT_CHANGED] )
) {
- $this->dieUsageMsg( 'show' );
+ $this->dieWithError( 'apierror-show' );
}
$options = [];
diff --git a/includes/api/ApiRemoveAuthenticationData.php b/includes/api/ApiRemoveAuthenticationData.php
index d72c8a407ef6..359d045fdd32 100644
--- a/includes/api/ApiRemoveAuthenticationData.php
+++ b/includes/api/ApiRemoveAuthenticationData.php
@@ -45,7 +45,7 @@ class ApiRemoveAuthenticationData extends ApiBase {
public function execute() {
if ( !$this->getUser()->isLoggedIn() ) {
- $this->dieUsage( 'Must be logged in to remove authentication data', 'notloggedin' );
+ $this->dieWithError( 'apierror-mustbeloggedin-removeauth', 'notloggedin' );
}
$params = $this->extractRequestParams();
@@ -67,7 +67,7 @@ class ApiRemoveAuthenticationData extends ApiBase {
}
);
if ( count( $reqs ) !== 1 ) {
- $this->dieUsage( 'Failed to create change request', 'badrequest' );
+ $this->dieWithError( 'apierror-changeauth-norequest', 'badrequest' );
}
$req = reset( $reqs );
diff --git a/includes/api/ApiResetPassword.php b/includes/api/ApiResetPassword.php
index 2d7f5dff2335..b5fa8ed859e0 100644
--- a/includes/api/ApiResetPassword.php
+++ b/includes/api/ApiResetPassword.php
@@ -52,7 +52,7 @@ class ApiResetPassword extends ApiBase {
public function execute() {
if ( !$this->hasAnyRoutes() ) {
- $this->dieUsage( 'No password reset routes are available.', 'moduledisabled' );
+ $this->dieWithError( 'apihelp-resetpassword-description-noroutes', 'moduledisabled' );
}
$params = $this->extractRequestParams() + [
diff --git a/includes/api/ApiResult.php b/includes/api/ApiResult.php
index 6e27fc892044..61a4394e74d3 100644
--- a/includes/api/ApiResult.php
+++ b/includes/api/ApiResult.php
@@ -413,11 +413,9 @@ class ApiResult implements ApiSerializable {
$newsize = $this->size + self::size( $value );
if ( $this->maxSize !== false && $newsize > $this->maxSize ) {
- /// @todo Add i18n message when replacing calls to ->setWarning()
- $msg = new ApiRawMessage( 'This result was truncated because it would otherwise ' .
- 'be larger than the limit of $1 bytes', 'truncatedresult' );
- $msg->numParams( $this->maxSize );
- $this->errorFormatter->addWarning( 'result', $msg );
+ $this->errorFormatter->addWarning(
+ 'result', [ 'apiwarn-truncatedresult', Message::numParam( $this->maxSize ) ]
+ );
return false;
}
$this->size = $newsize;
diff --git a/includes/api/ApiRevisionDelete.php b/includes/api/ApiRevisionDelete.php
index ed9fba27c01e..0251bdbddd13 100644
--- a/includes/api/ApiRevisionDelete.php
+++ b/includes/api/ApiRevisionDelete.php
@@ -36,24 +36,22 @@ class ApiRevisionDelete extends ApiBase {
$params = $this->extractRequestParams();
$user = $this->getUser();
- if ( !$user->isAllowed( RevisionDeleter::getRestriction( $params['type'] ) ) ) {
- $this->dieUsageMsg( 'badaccess-group0' );
- }
+ $this->checkUserRightsAny( RevisionDeleter::getRestriction( $params['type'] ) );
if ( $user->isBlocked() ) {
$this->dieBlocked( $user->getBlock() );
}
if ( !$params['ids'] ) {
- $this->dieUsage( "At least one value is required for 'ids'", 'badparams' );
+ $this->dieWithError( [ 'apierror-paramempty', 'ids' ], 'paramempty_ids' );
}
$hide = $params['hide'] ?: [];
$show = $params['show'] ?: [];
if ( array_intersect( $hide, $show ) ) {
- $this->dieUsage( "Mutually exclusive values for 'hide' and 'show'", 'badparams' );
+ $this->dieWithError( 'apierror-revdel-mutuallyexclusive', 'badparams' );
} elseif ( !$hide && !$show ) {
- $this->dieUsage( "At least one value is required for 'hide' or 'show'", 'badparams' );
+ $this->dieWithError( 'apierror-revdel-paramneeded', 'badparams' );
}
$bits = [
'content' => RevisionDeleter::getRevdelConstant( $params['type'] ),
@@ -72,9 +70,7 @@ class ApiRevisionDelete extends ApiBase {
}
if ( $params['suppress'] === 'yes' ) {
- if ( !$user->isAllowed( 'suppressrevision' ) ) {
- $this->dieUsageMsg( 'badaccess-group0' );
- }
+ $this->checkUserRightsAny( 'suppressrevision' );
$bitfield[Revision::DELETED_RESTRICTED] = 1;
} elseif ( $params['suppress'] === 'no' ) {
$bitfield[Revision::DELETED_RESTRICTED] = 0;
@@ -88,7 +84,7 @@ class ApiRevisionDelete extends ApiBase {
}
$targetObj = RevisionDeleter::suggestTarget( $params['type'], $targetObj, $params['ids'] );
if ( $targetObj === null ) {
- $this->dieUsage( 'A target title is required for this RevDel type', 'needtarget' );
+ $this->dieWithError( [ 'apierror-revdel-needtarget' ], 'needtarget' );
}
$list = RevisionDeleter::createList(
diff --git a/includes/api/ApiRollback.php b/includes/api/ApiRollback.php
index b9911da1385b..c8020872311b 100644
--- a/includes/api/ApiRollback.php
+++ b/includes/api/ApiRollback.php
@@ -69,24 +69,8 @@ class ApiRollback extends ApiBase {
$params['tags']
);
- // We don't care about multiple errors, just report one of them
if ( $retval ) {
- if ( isset( $retval[0][0] ) &&
- ( $retval[0][0] == 'alreadyrolled' || $retval[0][0] == 'cantrollback' )
- ) {
- $error = $retval[0];
- $userMessage = $this->msg( $error[0], array_slice( $error, 1 ) );
- // dieUsageMsg() doesn't support $extraData
- $errorCode = $error[0];
- $errorInfo = isset( ApiBase::$messageMap[$errorCode] ) ?
- ApiBase::$messageMap[$errorCode]['info'] :
- $errorCode;
- $this->dieUsage( $errorInfo, $errorCode, 0, [
- 'messageHtml' => $userMessage->parseAsBlock()
- ] );
- }
-
- $this->dieUsageMsg( reset( $retval ) );
+ $this->dieStatus( $this->errorArrayToStatus( $retval, $user ) );
}
$watch = 'preferences';
@@ -181,7 +165,7 @@ class ApiRollback extends ApiBase {
? $params['user']
: User::getCanonicalName( $params['user'] );
if ( !$this->mUser ) {
- $this->dieUsageMsg( [ 'invaliduser', $params['user'] ] );
+ $this->dieWithError( [ 'apierror-invaliduser', wfEscapeWikiText( $params['user'] ) ] );
}
return $this->mUser;
@@ -202,17 +186,17 @@ class ApiRollback extends ApiBase {
if ( isset( $params['title'] ) ) {
$this->mTitleObj = Title::newFromText( $params['title'] );
if ( !$this->mTitleObj || $this->mTitleObj->isExternal() ) {
- $this->dieUsageMsg( [ 'invalidtitle', $params['title'] ] );
+ $this->dieWithError( [ 'apierror-invalidtitle', wfEscapeWikiText( $params['title'] ) ] );
}
} elseif ( isset( $params['pageid'] ) ) {
$this->mTitleObj = Title::newFromID( $params['pageid'] );
if ( !$this->mTitleObj ) {
- $this->dieUsageMsg( [ 'nosuchpageid', $params['pageid'] ] );
+ $this->dieWithError( [ 'apierror-nosuchpageid', $params['pageid'] ] );
}
}
if ( !$this->mTitleObj->exists() ) {
- $this->dieUsageMsg( 'notanarticle' );
+ $this->dieWithError( 'apierror-missingtitle' );
}
return $this->mTitleObj;
diff --git a/includes/api/ApiSetNotificationTimestamp.php b/includes/api/ApiSetNotificationTimestamp.php
index 3412f38ed3c2..5769ff6d3917 100644
--- a/includes/api/ApiSetNotificationTimestamp.php
+++ b/includes/api/ApiSetNotificationTimestamp.php
@@ -38,11 +38,9 @@ class ApiSetNotificationTimestamp extends ApiBase {
$user = $this->getUser();
if ( $user->isAnon() ) {
- $this->dieUsage( 'Anonymous users cannot use watchlist change notifications', 'notloggedin' );
- }
- if ( !$user->isAllowed( 'editmywatchlist' ) ) {
- $this->dieUsage( 'You don\'t have permission to edit your watchlist', 'permissiondenied' );
+ $this->dieWithError( 'watchlistanontext', 'notloggedin' );
}
+ $this->checkUserRightsAny( 'editmywatchlist' );
$params = $this->extractRequestParams();
$this->requireMaxOneParameter( $params, 'timestamp', 'torevid', 'newerthanrevid' );
@@ -52,8 +50,12 @@ class ApiSetNotificationTimestamp extends ApiBase {
$pageSet = $this->getPageSet();
if ( $params['entirewatchlist'] && $pageSet->getDataSource() !== null ) {
- $this->dieUsage(
- "Cannot use 'entirewatchlist' at the same time as '{$pageSet->getDataSource()}'",
+ $this->dieWithError(
+ [
+ 'apierror-invalidparammix-cannotusewith',
+ $this->encodeParamName( 'entirewatchlist' ),
+ $pageSet->encodeParamName( $pageSet->getDataSource() )
+ ],
'multisource'
);
}
@@ -71,7 +73,7 @@ class ApiSetNotificationTimestamp extends ApiBase {
if ( isset( $params['torevid'] ) ) {
if ( $params['entirewatchlist'] || $pageSet->getGoodTitleCount() > 1 ) {
- $this->dieUsage( 'torevid may only be used with a single page', 'multpages' );
+ $this->dieWithError( [ 'apierror-multpages', $this->encodeParamName( 'torevid' ) ] );
}
$title = reset( $pageSet->getGoodTitles() );
if ( $title ) {
@@ -85,7 +87,7 @@ class ApiSetNotificationTimestamp extends ApiBase {
}
} elseif ( isset( $params['newerthanrevid'] ) ) {
if ( $params['entirewatchlist'] || $pageSet->getGoodTitleCount() > 1 ) {
- $this->dieUsage( 'newerthanrevid may only be used with a single page', 'multpages' );
+ $this->dieWithError( [ 'apierror-multpages', $this->encodeParamName( 'newerthanrevid' ) ] );
}
$title = reset( $pageSet->getGoodTitles() );
if ( $title ) {
diff --git a/includes/api/ApiStashEdit.php b/includes/api/ApiStashEdit.php
index 92cbe9053a7b..e29fda536f6f 100644
--- a/includes/api/ApiStashEdit.php
+++ b/includes/api/ApiStashEdit.php
@@ -51,7 +51,7 @@ class ApiStashEdit extends ApiBase {
$params = $this->extractRequestParams();
if ( $user->isBot() ) { // sanity
- $this->dieUsage( 'This interface is not supported for bots', 'botsnotsupported' );
+ $this->dieWithError( 'apierror-botsnotsupported' );
}
$cache = ObjectCache::getLocalClusterInstance();
@@ -61,9 +61,14 @@ class ApiStashEdit extends ApiBase {
if ( !ContentHandler::getForModelID( $params['contentmodel'] )
->isSupportedFormat( $params['contentformat'] )
) {
- $this->dieUsage( 'Unsupported content model/format', 'badmodelformat' );
+ $this->dieWithError(
+ [ 'apierror-badformat-generic', $params['contentformat'], $params['contentmodel'] ],
+ 'badmodelformat'
+ );
}
+ $this->requireAtLeastOneParameter( $params, 'stashedtexthash', 'text' );
+
$text = null;
$textHash = null;
if ( strlen( $params['stashedtexthash'] ) ) {
@@ -72,15 +77,18 @@ class ApiStashEdit extends ApiBase {
$textKey = $cache->makeKey( 'stashedit', 'text', $textHash );
$text = $cache->get( $textKey );
if ( !is_string( $text ) ) {
- $this->dieUsage( 'No stashed text found with the given hash', 'missingtext' );
+ $this->dieWithError( 'apierror-stashedit-missingtext', 'missingtext' );
}
} elseif ( $params['text'] !== null ) {
// Trim and fix newlines so the key SHA1's match (see WebRequest::getText())
$text = rtrim( str_replace( "\r\n", "\n", $params['text'] ) );
$textHash = sha1( $text );
} else {
- $this->dieUsage(
- 'The text or stashedtexthash parameter must be given', 'missingtextparam' );
+ $this->dieWithError( [
+ 'apierror-missingparam-at-least-one-of',
+ Message::listParam( [ '<var>stashedtexthash</var>', '<var>text</var>' ] ),
+ 2,
+ ], 'missingparam' );
}
$textContent = ContentHandler::makeContent(
@@ -91,11 +99,11 @@ class ApiStashEdit extends ApiBase {
// Page exists: get the merged content with the proposed change
$baseRev = Revision::newFromPageId( $page->getId(), $params['baserevid'] );
if ( !$baseRev ) {
- $this->dieUsage( "No revision ID {$params['baserevid']}", 'missingrev' );
+ $this->dieWithError( [ 'apierror-nosuchrevid', $params['baserevid'] ] );
}
$currentRev = $page->getRevision();
if ( !$currentRev ) {
- $this->dieUsage( "No current revision of page ID {$page->getId()}", 'missingrev' );
+ $this->dieWithError( [ 'apierror-missingrev-pageid', $page->getId() ], 'missingrev' );
}
// Merge in the new version of the section to get the proposed version
$editContent = $page->replaceSectionAtRev(
@@ -105,7 +113,7 @@ class ApiStashEdit extends ApiBase {
$baseRev->getId()
);
if ( !$editContent ) {
- $this->dieUsage( 'Could not merge updated section.', 'replacefailed' );
+ $this->dieWithError( 'apierror-sectionreplacefailed', 'replacefailed' );
}
if ( $currentRev->getId() == $baseRev->getId() ) {
// Base revision was still the latest; nothing to merge
@@ -115,7 +123,7 @@ class ApiStashEdit extends ApiBase {
$baseContent = $baseRev->getContent();
$currentContent = $currentRev->getContent();
if ( !$baseContent || !$currentContent ) {
- $this->dieUsage( "Missing content for page ID {$page->getId()}", 'missingrev' );
+ $this->dieWithError( [ 'apierror-missingcontent-pageid', $page->getId() ], 'missingrev' );
}
$handler = ContentHandler::getForModelID( $baseContent->getModel() );
$content = $handler->merge3( $baseContent, $editContent, $currentContent );
diff --git a/includes/api/ApiTag.php b/includes/api/ApiTag.php
index f88c2dbc62f2..f6c058454784 100644
--- a/includes/api/ApiTag.php
+++ b/includes/api/ApiTag.php
@@ -30,10 +30,7 @@ class ApiTag extends ApiBase {
$user = $this->getUser();
// make sure the user is allowed
- if ( !$user->isAllowed( 'changetags' ) ) {
- $this->dieUsage( "You don't have permission to add or remove change tags from individual edits",
- 'permissiondenied' );
- }
+ $this->checkUserRightsAny( 'changetags' );
if ( $user->isBlocked() ) {
$this->dieBlocked( $user->getBlock() );
@@ -88,7 +85,8 @@ class ApiTag extends ApiBase {
if ( !$valid ) {
$idResult['status'] = 'error';
- $idResult += $this->parseMsg( [ "nosuch$type", $id ] );
+ // Messages: apierror-nosuchrcid apierror-nosuchrevid apierror-nosuchlogid
+ $idResult += $this->getErrorFormatter()->formatMessage( [ "apierror-nosuch$type", $id ] );
return $idResult;
}
diff --git a/includes/api/ApiTokens.php b/includes/api/ApiTokens.php
index 4940394fe824..fc2951a9db8b 100644
--- a/includes/api/ApiTokens.php
+++ b/includes/api/ApiTokens.php
@@ -31,10 +31,10 @@
class ApiTokens extends ApiBase {
public function execute() {
- $this->setWarning(
- 'action=tokens has been deprecated. Please use action=query&meta=tokens instead.'
+ $this->addDeprecation(
+ [ 'apiwarn-deprecation-withreplacement', 'action=tokens', 'action=query&meta=tokens' ],
+ 'action=tokens'
);
- $this->logFeatureUsage( 'action=tokens' );
$params = $this->extractRequestParams();
$res = [
@@ -46,7 +46,7 @@ class ApiTokens extends ApiBase {
$val = call_user_func( $types[$type], null, null );
if ( $val === false ) {
- $this->setWarning( "Action '$type' is not allowed for the current user" );
+ $this->addWarning( [ 'apiwarn-tokennotallowed', $type ] );
} else {
$res[$type . 'token'] = $val;
}
diff --git a/includes/api/ApiUnblock.php b/includes/api/ApiUnblock.php
index ace41a4e364c..523a888d1277 100644
--- a/includes/api/ApiUnblock.php
+++ b/includes/api/ApiUnblock.php
@@ -39,25 +39,18 @@ class ApiUnblock extends ApiBase {
$user = $this->getUser();
$params = $this->extractRequestParams();
- if ( is_null( $params['id'] ) && is_null( $params['user'] ) ) {
- $this->dieUsageMsg( 'unblock-notarget' );
- }
- if ( !is_null( $params['id'] ) && !is_null( $params['user'] ) ) {
- $this->dieUsageMsg( 'unblock-idanduser' );
- }
+ $this->requireOnlyOneParameter( $params, 'id', 'user' );
if ( !$user->isAllowed( 'block' ) ) {
- $this->dieUsageMsg( 'cantunblock' );
+ $this->dieWithError( 'apierror-permissiondenied-unblock', 'permissiondenied' );
}
# bug 15810: blocked admins should have limited access here
if ( $user->isBlocked() ) {
$status = SpecialBlock::checkUnblockSelf( $params['user'], $user );
if ( $status !== true ) {
- $msg = $this->parseMsg( $status );
- $this->dieUsage(
- $msg['info'],
- $msg['code'],
- 0,
+ $this->dieWithError(
+ $status,
+ null,
[ 'blockinfo' => ApiQueryUserInfo::getBlockInfo( $user->getBlock() ) ]
);
}
@@ -79,7 +72,7 @@ class ApiUnblock extends ApiBase {
$block = Block::newFromTarget( $data['Target'] );
$retval = SpecialUnblock::processUnblock( $data, $this->getContext() );
if ( $retval !== true ) {
- $this->dieUsageMsg( $retval[0] );
+ $this->dieStatus( $this->errorArrayToStatus( $retval ) );
}
$res['id'] = $block->getId();
diff --git a/includes/api/ApiUndelete.php b/includes/api/ApiUndelete.php
index e24f2ced59c1..7fda1ea01a40 100644
--- a/includes/api/ApiUndelete.php
+++ b/includes/api/ApiUndelete.php
@@ -33,18 +33,16 @@ class ApiUndelete extends ApiBase {
$this->useTransactionalTimeLimit();
$params = $this->extractRequestParams();
- $user = $this->getUser();
- if ( !$user->isAllowed( 'undelete' ) ) {
- $this->dieUsageMsg( 'permdenied-undelete' );
- }
+ $this->checkUserRightsAny( 'undelete' );
+ $user = $this->getUser();
if ( $user->isBlocked() ) {
$this->dieBlocked( $user->getBlock() );
}
$titleObj = Title::newFromText( $params['title'] );
if ( !$titleObj || $titleObj->isExternal() ) {
- $this->dieUsageMsg( [ 'invalidtitle', $params['title'] ] );
+ $this->dieWithError( [ 'apierror-invalidtitle', wfEscapeWikiText( $params['title'] ) ] );
}
// Check if user can add tags
@@ -76,7 +74,7 @@ class ApiUndelete extends ApiBase {
$params['tags']
);
if ( !is_array( $retval ) ) {
- $this->dieUsageMsg( 'cannotundelete' );
+ $this->dieWithError( 'apierror-cantundelete' );
}
if ( $retval[1] ) {
diff --git a/includes/api/ApiUpload.php b/includes/api/ApiUpload.php
index 7b44f409932c..6bdd68f93783 100644
--- a/includes/api/ApiUpload.php
+++ b/includes/api/ApiUpload.php
@@ -36,7 +36,7 @@ class ApiUpload extends ApiBase {
public function execute() {
// Check whether upload is enabled
if ( !UploadBase::isEnabled() ) {
- $this->dieUsageMsg( 'uploaddisabled' );
+ $this->dieWithError( 'uploaddisabled' );
}
$user = $this->getUser();
@@ -61,11 +61,10 @@ class ApiUpload extends ApiBase {
if ( !$this->selectUploadModule() ) {
return; // not a true upload, but a status request or similar
} elseif ( !isset( $this->mUpload ) ) {
- $this->dieUsage( 'No upload module set', 'nomodule' );
+ $this->dieDebug( __METHOD__, 'No upload module set' );
}
} catch ( UploadStashException $e ) { // XXX: don't spam exception log
- list( $msg, $code ) = $this->handleStashException( get_class( $e ), $e->getMessage() );
- $this->dieUsage( $msg, $code );
+ $this->dieStatus( $this->handleStashException( $e ) );
}
// First check permission to upload
@@ -75,19 +74,17 @@ class ApiUpload extends ApiBase {
/** @var $status Status */
$status = $this->mUpload->fetchFile();
if ( !$status->isGood() ) {
- $errors = $status->getErrorsArray();
- $error = array_shift( $errors[0] );
- $this->dieUsage( 'Error fetching file from remote source', $error, 0, $errors[0] );
+ $this->dieStatus( $status );
}
// Check if the uploaded file is sane
if ( $this->mParams['chunk'] ) {
$maxSize = UploadBase::getMaxUploadSize();
if ( $this->mParams['filesize'] > $maxSize ) {
- $this->dieUsage( 'The file you submitted was too large', 'file-too-large' );
+ $this->dieWithError( 'file-too-large' );
}
if ( !$this->mUpload->getTitle() ) {
- $this->dieUsage( 'Invalid file title supplied', 'internal-error' );
+ $this->dieWithError( 'illegal-filename' );
}
} elseif ( $this->mParams['async'] && $this->mParams['filekey'] ) {
// defer verification to background process
@@ -102,7 +99,7 @@ class ApiUpload extends ApiBase {
if ( !$this->mParams['stash'] ) {
$permErrors = $this->mUpload->verifyTitlePermissions( $user );
if ( $permErrors !== true ) {
- $this->dieRecoverableError( $permErrors[0], 'filename' );
+ $this->dieRecoverableError( $permErrors, 'filename' );
}
}
@@ -110,8 +107,7 @@ class ApiUpload extends ApiBase {
try {
$result = $this->getContextResult();
} catch ( UploadStashException $e ) { // XXX: don't spam exception log
- list( $msg, $code ) = $this->handleStashException( get_class( $e ), $e->getMessage() );
- $this->dieUsage( $msg, $code );
+ $this->dieStatus( $this->handleStashException( $e ) );
}
$this->getResult()->addValue( null, $this->getModuleName(), $result );
@@ -146,7 +142,7 @@ class ApiUpload extends ApiBase {
// Check throttle after we've handled warnings
if ( UploadBase::isThrottled( $this->getUser() )
) {
- $this->dieUsageMsg( 'actionthrottledtext' );
+ $this->dieWithError( 'apierror-ratelimited' );
}
// This is the most common case -- a normal upload with no warnings
@@ -208,16 +204,12 @@ class ApiUpload extends ApiBase {
// Sanity check sizing
if ( $totalSoFar > $this->mParams['filesize'] ) {
- $this->dieUsage(
- 'Offset plus current chunk is greater than claimed file size', 'invalid-chunk'
- );
+ $this->dieWithError( 'apierror-invalid-chunk' );
}
// Enforce minimum chunk size
if ( $totalSoFar != $this->mParams['filesize'] && $chunkSize < $minChunkSize ) {
- $this->dieUsage(
- "Minimum chunk size is $minChunkSize bytes for non-final chunks", 'chunk-too-small'
- );
+ $this->dieWithError( [ 'apierror-chunk-too-small', Message::numParam( $minChunkSize ) ] );
}
if ( $this->mParams['offset'] == 0 ) {
@@ -229,11 +221,9 @@ class ApiUpload extends ApiBase {
$progress = UploadBase::getSessionStatus( $this->getUser(), $filekey );
if ( !$progress ) {
// Probably can't get here, but check anyway just in case
- $this->dieUsage( 'No chunked upload session with this key', 'stashfailed' );
+ $this->dieWithError( 'apierror-stashfailed-nosession', 'stashfailed' );
} elseif ( $progress['result'] !== 'Continue' || $progress['stage'] !== 'uploading' ) {
- $this->dieUsage(
- 'Chunked upload is already completed, check status for details', 'stashfailed'
- );
+ $this->dieWithError( 'apierror-stashfailed-complete', 'stashfailed' );
}
$status = $this->mUpload->addChunk(
@@ -352,16 +342,13 @@ class ApiUpload extends ApiBase {
list( $exceptionType, $message ) = $status->getMessage()->getParams();
$debugMessage = 'Stashing temporary file failed: ' . $exceptionType . ' ' . $message;
wfDebug( __METHOD__ . ' ' . $debugMessage . "\n" );
- list( $msg, $code ) = $this->handleStashException( $exceptionType, $message );
- $status = Status::newFatal( new ApiRawMessage( $msg, $code ) );
}
// Bad status
if ( $failureMode !== 'optional' ) {
$this->dieStatus( $status );
} else {
- list( $code, $msg ) = $this->getErrorFromStatus( $status );
- $data['stashfailed'] = $msg;
+ $data['stasherrors'] = $this->getErrorFormatter()->arrayFromStatus( $status );
return null;
}
}
@@ -370,25 +357,25 @@ class ApiUpload extends ApiBase {
* Throw an error that the user can recover from by providing a better
* value for $parameter
*
- * @param array|string|MessageSpecifier $error Error suitable for passing to dieUsageMsg()
- * @param string $parameter Parameter that needs revising
- * @param array $data Optional extra data to pass to the user
- * @param string $code Error code to use if the error is unknown
- * @throws UsageException
+ * @param array $errors Array of Message objects, message keys, key+param
+ * arrays, or StatusValue::getErrors()-style arrays
+ * @param string|null $parameter Parameter that needs revising
+ * @throws ApiUsageException
*/
- private function dieRecoverableError( $error, $parameter, $data = [], $code = 'unknownerror' ) {
+ private function dieRecoverableError( $errors, $parameter = null ) {
$this->performStash( 'optional', $data );
- $data['invalidparameter'] = $parameter;
- $parsed = $this->parseMsg( $error );
- if ( isset( $parsed['data'] ) ) {
- $data = array_merge( $data, $parsed['data'] );
- }
- if ( $parsed['code'] === 'unknownerror' ) {
- $parsed['code'] = $code;
+ if ( $parameter ) {
+ $data['invalidparameter'] = $parameter;
}
- $this->dieUsage( $parsed['info'], $parsed['code'], 0, $data );
+ $sv = StatusValue::newGood();
+ foreach ( $errors as $error ) {
+ $msg = ApiMessage::create( $error );
+ $msg->setApiData( $msg->getApiData() + $data );
+ $sv->fatal( $msg );
+ }
+ $this->dieStatus( $sv );
}
/**
@@ -398,20 +385,18 @@ class ApiUpload extends ApiBase {
* @param Status $status
* @param string $overrideCode Error code to use if there isn't one from IApiMessage
* @param array|null $moreExtraData
- * @throws UsageException
+ * @throws ApiUsageException
*/
public function dieStatusWithCode( $status, $overrideCode, $moreExtraData = null ) {
- $extraData = null;
- list( $code, $msg ) = $this->getErrorFromStatus( $status, $extraData );
- $errors = $status->getErrorsByType( 'error' ) ?: $status->getErrorsByType( 'warning' );
- if ( !( $errors[0]['message'] instanceof IApiMessage ) ) {
- $code = $overrideCode;
- }
- if ( $moreExtraData ) {
- $extraData = $extraData ?: [];
- $extraData += $moreExtraData;
+ $sv = StatusValue::newGood();
+ foreach ( $status->getErrors() as $error ) {
+ $msg = ApiMessage::create( $error, $overrideCode );
+ if ( $moreExtraData ) {
+ $msg->setApiData( $msg->getApiData() + $moreExtraData );
+ }
+ $sv->fatal( $msg );
}
- $this->dieUsage( $msg, $code, 0, $extraData );
+ $this->dieStatus( $sv );
}
/**
@@ -434,7 +419,7 @@ class ApiUpload extends ApiBase {
if ( $this->mParams['filekey'] && $this->mParams['checkstatus'] ) {
$progress = UploadBase::getSessionStatus( $this->getUser(), $this->mParams['filekey'] );
if ( !$progress ) {
- $this->dieUsage( 'No result in status data', 'missingresult' );
+ $this->dieWithError( 'api-upload-missingresult', 'missingresult' );
} elseif ( !$progress['status']->isGood() ) {
$this->dieStatusWithCode( $progress['status'], 'stashfailed' );
}
@@ -466,7 +451,7 @@ class ApiUpload extends ApiBase {
// The following modules all require the filename parameter to be set
if ( is_null( $this->mParams['filename'] ) ) {
- $this->dieUsageMsg( [ 'missingparam', 'filename' ] );
+ $this->dieWithError( [ 'apierror-missingparam', 'filename' ] );
}
if ( $this->mParams['chunk'] ) {
@@ -474,7 +459,7 @@ class ApiUpload extends ApiBase {
$this->mUpload = new UploadFromChunks( $this->getUser() );
if ( isset( $this->mParams['filekey'] ) ) {
if ( $this->mParams['offset'] === 0 ) {
- $this->dieUsage( 'Cannot supply a filekey when offset is 0', 'badparams' );
+ $this->dieWithError( 'apierror-upload-filekeynotallowed', 'filekeynotallowed' );
}
// handle new chunk
@@ -485,7 +470,7 @@ class ApiUpload extends ApiBase {
);
} else {
if ( $this->mParams['offset'] !== 0 ) {
- $this->dieUsage( 'Must supply a filekey when offset is non-zero', 'badparams' );
+ $this->dieWithError( 'apierror-upload-filekeyneeded', 'filekeyneeded' );
}
// handle first chunk
@@ -497,7 +482,7 @@ class ApiUpload extends ApiBase {
} elseif ( isset( $this->mParams['filekey'] ) ) {
// Upload stashed in a previous request
if ( !UploadFromStash::isValidKey( $this->mParams['filekey'] ) ) {
- $this->dieUsageMsg( 'invalid-file-key' );
+ $this->dieWithError( 'apierror-invalid-file-key' );
}
$this->mUpload = new UploadFromStash( $this->getUser() );
@@ -515,15 +500,15 @@ class ApiUpload extends ApiBase {
} elseif ( isset( $this->mParams['url'] ) ) {
// Make sure upload by URL is enabled:
if ( !UploadFromUrl::isEnabled() ) {
- $this->dieUsageMsg( 'copyuploaddisabled' );
+ $this->dieWithError( 'copyuploaddisabled' );
}
if ( !UploadFromUrl::isAllowedHost( $this->mParams['url'] ) ) {
- $this->dieUsageMsg( 'copyuploadbaddomain' );
+ $this->dieWithError( 'apierror-copyuploadbaddomain' );
}
if ( !UploadFromUrl::isAllowedUrl( $this->mParams['url'] ) ) {
- $this->dieUsageMsg( 'copyuploadbadurl' );
+ $this->dieWithError( 'apierror-copyuploadbadurl' );
}
$this->mUpload = new UploadFromUrl;
@@ -545,10 +530,10 @@ class ApiUpload extends ApiBase {
if ( $permission !== true ) {
if ( !$user->isLoggedIn() ) {
- $this->dieUsageMsg( [ 'mustbeloggedin', 'upload' ] );
+ $this->dieWithError( [ 'apierror-mustbeloggedin', $this->msg( 'action-upload' ) ] );
}
- $this->dieUsageMsg( 'badaccess-groups' );
+ $this->dieStatus( User::newFatalPermissionDeniedStatus( $permission ) );
}
// Check blocks
@@ -583,28 +568,31 @@ class ApiUpload extends ApiBase {
switch ( $verification['status'] ) {
// Recoverable errors
case UploadBase::MIN_LENGTH_PARTNAME:
- $this->dieRecoverableError( 'filename-tooshort', 'filename' );
+ $this->dieRecoverableError( [ 'filename-tooshort' ], 'filename' );
break;
case UploadBase::ILLEGAL_FILENAME:
- $this->dieRecoverableError( 'illegal-filename', 'filename',
- [ 'filename' => $verification['filtered'] ] );
+ $this->dieRecoverableError(
+ [ ApiMessage::create(
+ 'illegal-filename', null, [ 'filename' => $verification['filtered'] ]
+ ) ], 'filename'
+ );
break;
case UploadBase::FILENAME_TOO_LONG:
- $this->dieRecoverableError( 'filename-toolong', 'filename' );
+ $this->dieRecoverableError( [ 'filename-toolong' ], 'filename' );
break;
case UploadBase::FILETYPE_MISSING:
- $this->dieRecoverableError( 'filetype-missing', 'filename' );
+ $this->dieRecoverableError( [ 'filetype-missing' ], 'filename' );
break;
case UploadBase::WINDOWS_NONASCII_FILENAME:
- $this->dieRecoverableError( 'windows-nonascii-filename', 'filename' );
+ $this->dieRecoverableError( [ 'windows-nonascii-filename' ], 'filename' );
break;
// Unrecoverable errors
case UploadBase::EMPTY_FILE:
- $this->dieUsage( 'The file you submitted was empty', 'empty-file' );
+ $this->dieWithError( 'empty-file' );
break;
case UploadBase::FILE_TOO_LARGE:
- $this->dieUsage( 'The file you submitted was too large', 'file-too-large' );
+ $this->dieWithError( 'file-too-large' );
break;
case UploadBase::FILETYPE_BADTYPE:
@@ -612,57 +600,47 @@ class ApiUpload extends ApiBase {
'filetype' => $verification['finalExt'],
'allowed' => array_values( array_unique( $this->getConfig()->get( 'FileExtensions' ) ) )
];
+ $extensions = array_unique( $this->getConfig()->get( 'FileExtensions' ) );
+ $msg = [
+ 'filetype-banned-type',
+ null, // filled in below
+ Message::listParam( $extensions, 'comma' ),
+ count( $extensions ),
+ null, // filled in below
+ ];
ApiResult::setIndexedTagName( $extradata['allowed'], 'ext' );
- $msg = 'Filetype not permitted: ';
if ( isset( $verification['blacklistedExt'] ) ) {
- $msg .= implode( ', ', $verification['blacklistedExt'] );
+ $msg[1] = Message::listParam( $verification['blacklistedExt'], 'comma' );
+ $msg[4] = count( $verification['blacklistedExt'] );
$extradata['blacklisted'] = array_values( $verification['blacklistedExt'] );
ApiResult::setIndexedTagName( $extradata['blacklisted'], 'ext' );
} else {
- $msg .= $verification['finalExt'];
+ $msg[1] = $verification['finalExt'];
+ $msg[4] = 1;
}
- $this->dieUsage( $msg, 'filetype-banned', 0, $extradata );
+
+ $this->dieWithError( $msg, 'filetype-banned', $extradata );
break;
+
case UploadBase::VERIFICATION_ERROR:
- $parsed = $this->parseMsg( $verification['details'] );
- $info = "This file did not pass file verification: {$parsed['info']}";
- if ( $verification['details'][0] instanceof IApiMessage ) {
- $code = $parsed['code'];
- } else {
- // For backwards-compatibility, all of the errors from UploadBase::verifyFile() are
- // reported as 'verification-error', and the real error code is reported in 'details'.
- $code = 'verification-error';
- }
- if ( $verification['details'][0] instanceof IApiMessage ) {
- $msg = $verification['details'][0];
+ $msg = ApiMessage::create( $verification['details'], 'verification-error' );
+ if ( $verification['details'][0] instanceof MessageSpecifier ) {
$details = array_merge( [ $msg->getKey() ], $msg->getParams() );
} else {
$details = $verification['details'];
}
ApiResult::setIndexedTagName( $details, 'detail' );
- $data = [ 'details' => $details ];
- if ( isset( $parsed['data'] ) ) {
- $data = array_merge( $data, $parsed['data'] );
- }
-
- $this->dieUsage( $info, $code, 0, $data );
+ $msg->setApiData( $msg->getApiData() + [ 'details' => $details ] );
+ $this->dieWithError( $msg );
break;
+
case UploadBase::HOOK_ABORTED:
- if ( is_array( $verification['error'] ) ) {
- $params = $verification['error'];
- } elseif ( $verification['error'] !== '' ) {
- $params = [ $verification['error'] ];
- } else {
- $params = [ 'hookaborted' ];
- }
- $key = array_shift( $params );
- $msg = $this->msg( $key, $params )->inLanguage( 'en' )->useDatabase( false )->text();
- $this->dieUsage( $msg, 'hookaborted', 0, [ 'details' => $verification['error'] ] );
+ $this->dieWithError( $params, 'hookaborted', [ 'details' => $verification['error'] ] );
break;
default:
- $this->dieUsage( 'An unknown error occurred', 'unknown-error',
- 0, [ 'details' => [ 'code' => $verification['status'] ] ] );
+ $this->dieWithError( 'apierror-unknownerror-nocode', 'unknown-error',
+ [ 'details' => [ 'code' => $verification['status'] ] ] );
break;
}
}
@@ -735,41 +713,31 @@ class ApiUpload extends ApiBase {
/**
* Handles a stash exception, giving a useful error to the user.
- * @param string $exceptionType Class name of the exception we encountered.
- * @param string $message Message of the exception we encountered.
- * @return array Array of message and code, suitable for passing to dieUsage()
+ * @todo Internationalize the exceptions
+ * @param Exception $e
+ * @return StatusValue
*/
- protected function handleStashException( $exceptionType, $message ) {
- switch ( $exceptionType ) {
+ protected function handleStashException( $e ) {
+ $err = wfEscapeWikiText( $e->getMessage() );
+ switch ( get_class( $exception ) ) {
case 'UploadStashFileNotFoundException':
- return [
- 'Could not find the file in the stash: ' . $message,
- 'stashedfilenotfound'
- ];
+ return StatusValue::newFatal( 'apierror-stashedfilenotfound', $err );
case 'UploadStashBadPathException':
- return [
- 'File key of improper format or otherwise invalid: ' . $message,
- 'stashpathinvalid'
- ];
+ return StatusValue::newFatal( 'apierror-stashpathinvalid', $err );
case 'UploadStashFileException':
- return [
- 'Could not store upload in the stash: ' . $message,
- 'stashfilestorage'
- ];
+ return StatusValue::newFatal( 'apierror-stashfilestorage', $err );
case 'UploadStashZeroLengthFileException':
- return [
- 'File is of zero length, and could not be stored in the stash: ' .
- $message,
- 'stashzerolength'
- ];
+ return StatusValue::newFatal( 'apierror-stashzerolength', $err );
case 'UploadStashNotLoggedInException':
- return [ 'Not logged in: ' . $message, 'stashnotloggedin' ];
+ return StatusValue::newFatal( ApiMessage::create(
+ [ 'apierror-mustbeloggedin', $this->msg( 'action-upload' ) ], 'stashnotloggedin'
+ ) );
case 'UploadStashWrongOwnerException':
- return [ 'Wrong owner: ' . $message, 'stashwrongowner' ];
+ return StatusValue::newFatal( 'apierror-stashwrongowner', $err );
case 'UploadStashNoSuchKeyException':
- return [ 'No such filekey: ' . $message, 'stashnosuchfilekey' ];
+ return StatusValue::newFatal( 'apierror-stashnosuchfilekey', $err );
default:
- return [ $exceptionType . ': ' . $message, 'stasherror' ];
+ return StatusValue::newFatal( 'uploadstash-exception', get_class( $e ), $err );
}
}
@@ -821,7 +789,7 @@ class ApiUpload extends ApiBase {
if ( $this->mParams['async'] ) {
$progress = UploadBase::getSessionStatus( $this->getUser(), $this->mParams['filekey'] );
if ( $progress && $progress['result'] === 'Poll' ) {
- $this->dieUsage( 'Upload from stash already in progress.', 'publishfailed' );
+ $this->dieWithError( 'apierror-upload-inprogress', 'publishfailed' );
}
UploadBase::setSessionStatus(
$this->getUser(),
@@ -848,14 +816,7 @@ class ApiUpload extends ApiBase {
$this->mParams['text'], $watch, $this->getUser(), $this->mParams['tags'] );
if ( !$status->isGood() ) {
- // Is there really no better way to do this?
- $errors = $status->getErrorsByType( 'error' );
- $msg = array_merge( [ $errors[0]['message'] ], $errors[0]['params'] );
- $data = $status->getErrorsArray();
- ApiResult::setIndexedTagName( $data, 'error' );
- // For backwards-compatibility, we use the 'internal-error' fallback key and merge $data
- // into the root of the response (rather than something sane like [ 'details' => $data ]).
- $this->dieRecoverableError( $msg, null, $data, 'internal-error' );
+ $this->dieRecoverableError( $status->getErrors() );
}
$result['result'] = 'Success';
}
diff --git a/includes/api/ApiUsageException.php b/includes/api/ApiUsageException.php
new file mode 100644
index 000000000000..7e21ab5ba446
--- /dev/null
+++ b/includes/api/ApiUsageException.php
@@ -0,0 +1,217 @@
+<?php
+/**
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ * http://www.gnu.org/copyleft/gpl.html
+ *
+ * @file
+ * @defgroup API API
+ */
+
+/**
+ * This exception will be thrown when dieUsage is called to stop module execution.
+ *
+ * @ingroup API
+ * @deprecated since 1.29, use ApiUsageException instead
+ */
+class UsageException extends MWException {
+
+ private $mCodestr;
+
+ /**
+ * @var null|array
+ */
+ private $mExtraData;
+
+ /**
+ * @param string $message
+ * @param string $codestr
+ * @param int $code
+ * @param array|null $extradata
+ */
+ public function __construct( $message, $codestr, $code = 0, $extradata = null ) {
+ parent::__construct( $message, $code );
+ $this->mCodestr = $codestr;
+ $this->mExtraData = $extradata;
+
+ // This should never happen, so throw an exception about it that will
+ // hopefully get logged with a backtrace (T138585)
+ if ( !is_string( $codestr ) || $codestr === '' ) {
+ throw new InvalidArgumentException( 'Invalid $codestr, was ' .
+ ( $codestr === '' ? 'empty string' : gettype( $codestr ) )
+ );
+ }
+ }
+
+ /**
+ * @return string
+ */
+ public function getCodeString() {
+ return $this->mCodestr;
+ }
+
+ /**
+ * @return array
+ */
+ public function getMessageArray() {
+ $result = [
+ 'code' => $this->mCodestr,
+ 'info' => $this->getMessage()
+ ];
+ if ( is_array( $this->mExtraData ) ) {
+ $result = array_merge( $result, $this->mExtraData );
+ }
+
+ return $result;
+ }
+
+ /**
+ * @return string
+ */
+ public function __toString() {
+ return "{$this->getCodeString()}: {$this->getMessage()}";
+ }
+}
+
+/**
+ * Exception used to abort API execution with an error
+ *
+ * If possible, use ApiBase::dieWithError() instead of throwing this directly.
+ *
+ * @ingroup API
+ * @note This currently extends UsageException for backwards compatibility, so
+ * all the existing code that catches UsageException won't break when stuff
+ * starts throwing ApiUsageException. Eventually UsageException will go away
+ * and this will (probably) extend MWException directly.
+ */
+class ApiUsageException extends UsageException {
+
+ protected $modulePath;
+ protected $status;
+
+ /**
+ * @param ApiBase|null $module API module responsible for the error, if known
+ * @param StatusValue $status Status holding errors
+ * @param int $httpCode HTTP error code to use
+ */
+ public function __construct(
+ ApiBase $module = null, StatusValue $status, $httpCode = 0
+ ) {
+ if ( $status->isOK() ) {
+ throw new InvalidArgumentException( __METHOD__ . ' requires a fatal Status' );
+ }
+
+ $this->modulePath = $module ? $module->getModulePath() : null;
+ $this->status = $status;
+
+ // Bug T46111: Messages in the log files should be in English and not
+ // customized by the local wiki.
+ $enMsg = clone $this->getApiMessage();
+ $enMsg->inLanguage( 'en' )->useDatabase( false );
+ parent::__construct(
+ ApiErrorFormatter::stripMarkup( $enMsg->text() ),
+ $enMsg->getApiCode(),
+ $httpCode,
+ $enMsg->getApiData()
+ );
+ }
+
+ /**
+ * @param ApiBase|null $module API module responsible for the error, if known
+ * @param string|array|Message $msg See ApiMessage::create()
+ * @param string|null $code See ApiMessage::create()
+ * @param array|null $data See ApiMessage::create()
+ * @param int $httpCode HTTP error code to use
+ * @return static
+ */
+ public static function newWithMessage(
+ ApiBase $module = null, $msg, $code = null, $data = null, $httpCode = 0
+ ) {
+ return new static(
+ $module,
+ StatusValue::newFatal( ApiMessage::create( $msg, $code, $data ) ),
+ $httpCode
+ );
+ }
+
+ /**
+ * @returns ApiMessage
+ */
+ private function getApiMessage() {
+ $errors = $this->status->getErrorsByType( 'error' );
+ if ( !$errors ) {
+ $errors = $this->status->getErrors();
+ }
+ if ( !$errors ) {
+ $msg = new ApiMessage( 'apierror-unknownerror-nocode', 'unknownerror' );
+ } else {
+ $msg = ApiMessage::create( $errors[0] );
+ }
+ return $msg;
+ }
+
+ /**
+ * Fetch the responsible module name
+ * @return string|null
+ */
+ public function getModulePath() {
+ return $this->modulePath;
+ }
+
+ /**
+ * Fetch the error status
+ * @return StatusValue
+ */
+ public function getStatusValue() {
+ return $this->status;
+ }
+
+ /**
+ * @deprecated Do not use. This only exists here because UsageException is in
+ * the inheritance chain for backwards compatibility.
+ * @inheritdoc
+ */
+ public function getCodeString() {
+ return $this->getApiMessage()->getApiCode();
+ }
+
+ /**
+ * @deprecated Do not use. This only exists here because UsageException is in
+ * the inheritance chain for backwards compatibility.
+ * @inheritdoc
+ */
+ public function getMessageArray() {
+ $enMsg = clone $this->getApiMessage();
+ $enMsg->inLanguage( 'en' )->useDatabase( false );
+
+ return [
+ 'code' => $enMsg->getApiCode(),
+ 'info' => ApiErrorFormatter::stripMarkup( $enMsg->text() ),
+ ] + $enMsg->getApiData();
+ }
+
+ /**
+ * @return string
+ */
+ public function __toString() {
+ $enMsg = clone $this->getApiMessage();
+ $enMsg->inLanguage( 'en' )->useDatabase( false );
+ $text = ApiErrorFormatter::stripMarkup( $enMsg->text() );
+
+ return get_class( $this ) . ": {$enMsg->getApiCode()}: {$text} "
+ . "in {$this->getFile()}:{$this->getLine()}\n"
+ . "Stack trace:\n{$this->getTraceAsString()}";
+ }
+
+}
diff --git a/includes/api/ApiWatch.php b/includes/api/ApiWatch.php
index 3a7a082148dc..d257e9005fe3 100644
--- a/includes/api/ApiWatch.php
+++ b/includes/api/ApiWatch.php
@@ -35,12 +35,10 @@ class ApiWatch extends ApiBase {
public function execute() {
$user = $this->getUser();
if ( !$user->isLoggedIn() ) {
- $this->dieUsage( 'You must be logged-in to have a watchlist', 'notloggedin' );
+ $this->dieWithError( 'watchlistanontext', 'notloggedin' );
}
- if ( !$user->isAllowed( 'editmywatchlist' ) ) {
- $this->dieUsage( 'You don\'t have permission to edit your watchlist', 'permissiondenied' );
- }
+ $this->checkUserRightsAny( 'editmywatchlist' );
$params = $this->extractRequestParams();
@@ -78,16 +76,19 @@ class ApiWatch extends ApiBase {
} ) );
if ( $extraParams ) {
- $p = $this->getModulePrefix();
- $this->dieUsage(
- "The parameter {$p}title can not be used with " . implode( ', ', $extraParams ),
+ $this->dieWithError(
+ [
+ 'apierror-invalidparammix-cannotusewith',
+ $this->encodeParamName( 'title' ),
+ $pageSet->encodeParamName( $extraParams[0] )
+ ],
'invalidparammix'
);
}
$title = Title::newFromText( $params['title'] );
if ( !$title || !$title->isWatchable() ) {
- $this->dieUsageMsg( [ 'invalidtitle', $params['title'] ] );
+ $this->dieWithError( [ 'invalidtitle', $params['title'] ] );
}
$res = $this->watchTitle( $title, $user, $params, true );
}
@@ -128,7 +129,11 @@ class ApiWatch extends ApiBase {
if ( $compatibilityMode ) {
$this->dieStatus( $status );
}
- $res['error'] = $this->getErrorFromStatus( $status );
+ $res['errors'] = $this->getErrorFormatter()->arrayFromStatus( $status, 'error' );
+ $res['warnings'] = $this->getErrorFormatter()->arrayFromStatus( $status, 'warning' );
+ if ( !$res['warnings'] ) {
+ unset( $res['warnings'] );
+ }
}
return $res;
diff --git a/includes/api/i18n/en.json b/includes/api/i18n/en.json
index 28cd746adc09..442bdf4cf273 100644
--- a/includes/api/i18n/en.json
+++ b/includes/api/i18n/en.json
@@ -17,8 +17,12 @@
"apihelp-main-param-requestid": "Any value given here will be included in the response. May be used to distinguish requests.",
"apihelp-main-param-servedby": "Include the hostname that served the request in the results.",
"apihelp-main-param-curtimestamp": "Include the current timestamp in the result.",
+ "apihelp-main-param-responselanginfo": "Include the languages used for <var>uselang</var> and <var>errorlang</var> in the result.",
"apihelp-main-param-origin": "When accessing the API using a cross-domain AJAX request (CORS), set this to the originating domain. This must be included in any pre-flight request, and therefore must be part of the request URI (not the POST body).\n\nFor authenticated requests, this must match one of the origins in the <code>Origin</code> header exactly, so it has to be set to something like <kbd>https://en.wikipedia.org</kbd> or <kbd>https://meta.wikimedia.org</kbd>. If this parameter does not match the <code>Origin</code> header, a 403 response will be returned. If this parameter matches the <code>Origin</code> header and the origin is whitelisted, the <code>Access-Control-Allow-Origin</code> and <code>Access-Control-Allow-Credentials</code> headers will be set.\n\nFor non-authenticated requests, specify the value <kbd>*</kbd>. This will cause the <code>Access-Control-Allow-Origin</code> header to be set, but <code>Access-Control-Allow-Credentials</code> will be <code>false</code> and all user-specific data will be restricted.",
"apihelp-main-param-uselang": "Language to use for message translations. <kbd>[[Special:ApiHelp/query+siteinfo|action=query&meta=siteinfo]]</kbd> with <kbd>siprop=languages</kbd> returns a list of language codes, or specify <kbd>user</kbd> to use the current user's language preference, or specify <kbd>content</kbd> to use this wiki's content language.",
+ "apihelp-main-param-errorformat": "Format to use for warning and error text output.\n; plaintext: Wikitext with HTML tags removed and entities replaced.\n; wikitext: Unparsed wikitext.\n; html: HTML.\n; raw: Message key and parameters.\n; none: No text output, only the error codes.\n; bc: Format used prior to MediaWiki 1.29. <var>errorlang</var> and <var>errorsusedb</var> are ignored.",
+ "apihelp-main-param-errorlang": "Language to use for warnings and errors. <kbd>[[Special:ApiHelp/query+siteinfo|action=query&meta=siteinfo]]</kbd> with <kbd>siprop=languages</kbd> returns a list of language codes, or specify <kbd>content</kbd> to use this wiki's content language, or specify <kbd>uselang</kbd> to use the same value as the <var>uselang</var> parameter.",
+ "apihelp-main-param-errorsuselocal": "If given, error texts will use locally-customized messages from the {{ns:MediaWiki}} namespace.",
"apihelp-block-description": "Block a user.",
"apihelp-block-param-user": "Username, IP address, or IP address range to block.",
@@ -485,7 +489,7 @@
"apihelp-query+allmessages-param-prop": "Which properties to get.",
"apihelp-query+allmessages-param-enableparser": "Set to enable parser, will preprocess the wikitext of message (substitute magic words, handle templates, etc.).",
"apihelp-query+allmessages-param-nocontent": "If set, do not include the content of the messages in the output.",
- "apihelp-query+allmessages-param-includelocal": "Also include local messages, i.e. messages that don't exist in the software but do exist as a MediaWiki: page.\nThis lists all MediaWiki: pages, so it will also list those that aren't really messages such as [[MediaWiki:Common.js|Common.js]].",
+ "apihelp-query+allmessages-param-includelocal": "Also include local messages, i.e. messages that don't exist in the software but do exist as in the {{ns:MediaWiki}} namespace.\nThis lists all {{ns:MediaWiki}}-namespace pages, so it will also list those that aren't really messages such as [[MediaWiki:Common.js|Common.js]].",
"apihelp-query+allmessages-param-args": "Arguments to be substituted into message.",
"apihelp-query+allmessages-param-filter": "Return only messages with names that contain this string.",
"apihelp-query+allmessages-param-customised": "Return only messages in this customisation state.",
@@ -1443,7 +1447,7 @@
"apihelp-phpfm-description": "Output data in serialized PHP format (pretty-print in HTML).",
"apihelp-rawfm-description": "Output data, including debugging elements, in JSON format (pretty-print in HTML).",
"apihelp-xml-description": "Output data in XML format.",
- "apihelp-xml-param-xslt": "If specified, adds the named page as an XSL stylesheet. The value must be a title in the {{ns:mediawiki}} namespace ending in <code>.xsl</code>.",
+ "apihelp-xml-param-xslt": "If specified, adds the named page as an XSL stylesheet. The value must be a title in the {{ns:MediaWiki}} namespace ending in <code>.xsl</code>.",
"apihelp-xml-param-includexmlnamespace": "If specified, adds an XML namespace.",
"apihelp-xmlfm-description": "Output data in XML format (pretty-print in HTML).",
@@ -1526,6 +1530,238 @@
"api-help-authmanagerhelper-continue": "This request is a continuation after an earlier <samp>UI</samp> or <samp>REDIRECT</samp> response. Either this or <var>$1returnurl</var> is required.",
"api-help-authmanagerhelper-additional-params": "This module accepts additional parameters depending on the available authentication requests. Use <kbd>[[Special:ApiHelp/query+authmanagerinfo|action=query&meta=authmanagerinfo]]</kbd> with <kbd>amirequestsfor=$1</kbd> (or a previous response from this module, if applicable) to determine the requests available and the fields that they use.",
+ "apierror-allimages-redirect": "Use <kbd>gaifilterredir=nonredirects</kbd> instead of <var>redirects</var> when using <kbd>allimages</kbd> as a generator.",
+ "apierror-allpages-generator-redirects": "Use <kbd>gapfilterredir=nonredirects</kbd> instead of <var>redirects</var> when using <kbd>allpages</kbd> as a generator.",
+ "apierror-appendnotsupported": "Can't append to pages using content model $1.",
+ "apierror-articleexists": "The article you tried to create has been created already.",
+ "apierror-assertbotfailed": "Assertion that the user has the <code>bot</code> right failed.",
+ "apierror-assertnameduserfailed": "Assertion that the user is \"$1\" failed.",
+ "apierror-assertuserfailed": "Assertion that the user is logged in failed.",
+ "apierror-autoblocked": "Your IP address has been blocked automatically, because it was used by a blocked user.",
+ "apierror-badconfig-resulttoosmall": "The value of <code>$wgAPIMaxResultSize</code> on this wiki is too small to hold basic result information.",
+ "apierror-badcontinue": "Invalid continue param. You should pass the original value returned by the previous query.",
+ "apierror-baddiff": "The diff cannot be retrieved, one or both revisions do not exist or you do not have permission to view them.",
+ "apierror-baddiffto": "<var>$1diffto</var> must be set to a non-negative number, <kbd>prev</kbd>, <kbd>next</kbd> or <kbd>cur</kbd>.",
+ "apierror-badformat-generic": "The requested format $1 is not supported for content model $2.",
+ "apierror-badformat": "The requested format $1 is not supported for content model $2 used by $3.",
+ "apierror-badgenerator-notgenerator": "Module <kbd>$1</kbd> cannot be used as a generator.",
+ "apierror-badgenerator-unknown": "Unknown <kbd>generator=$1</kbd>.",
+ "apierror-badip": "IP parameter is not valid.",
+ "apierror-badmd5": "The supplied MD5 hash was incorrect.",
+ "apierror-badmodule-badsubmodule": "The module <kbd>$1</kbd> does not have a submodule \"$2\".",
+ "apierror-badmodule-nosubmodules": "The module <kbd>$1</kbd> has no submodules.",
+ "apierror-badparameter": "Invalid value for parameter <var>$1</var>.",
+ "apierror-badquery": "Invalid query.",
+ "apierror-badtimestamp": "Invalid value \"$2\" for timestamp parameter <var>$1</var>.",
+ "apierror-badtoken": "Invalid CSRF token.",
+ "apierror-badupload": "File upload parameter <var>$1</var> is not a file upload; be sure to use <code>multipart/form-data</code> for your POST and include a filename in the <code>Content-Disposition</code> header.",
+ "apierror-badurl": "Invalid value \"$2\" for URL parameter <var>$1</var>.",
+ "apierror-baduser": "Invalid value \"$2\" for user parameter <var>$1</var>.",
+ "apierror-badvalue-notmultivalue": "U+001F multi-value separation may only be used for multi-valued parameters.",
+ "apierror-bad-watchlist-token": "Incorrect watchlist token provided. Please set a correct token in [[Special:Preferences]].",
+ "apierror-blockedfrommail": "You have been blocked from sending email.",
+ "apierror-blocked": "You have been blocked from editing.",
+ "apierror-botsnotsupported": "This interface is not supported for bots.",
+ "apierror-cannotreauthenticate": "This action is not available as your identity cannot be verified.",
+ "apierror-cannotviewtitle": "You are not allowed to view $1.",
+ "apierror-cantblock-email": "You don't have permission to block users from sending email through the wiki.",
+ "apierror-cantblock": "You don't have permission to block users.",
+ "apierror-cantchangecontentmodel": "You don't have permission to change the content model of a page.",
+ "apierror-canthide": "You don't have permission to hide user names from the block log.",
+ "apierror-cantimport-upload": "You don't have permission to import uploaded pages.",
+ "apierror-cantimport": "You don't have permission to import pages.",
+ "apierror-cantoverwrite-sharedfile": "The target file exists on a shared repository and you do not have permission to override it.",
+ "apierror-cantsend": "You are not logged in, you do not have a confirmed email address, or you are not allowed to send email to other users, so you cannot send email.",
+ "apierror-cantundelete": "Couldn't undelete: the requested revisions may not exist, or may have been undeleted already.",
+ "apierror-changeauth-norequest": "Failed to create change request.",
+ "apierror-chunk-too-small": "Minimum chunk size is $1 {{PLURAL:$1|byte|bytes}} for non-final chunks.",
+ "apierror-cidrtoobroad": "$1 CIDR ranges broader than /$2 are not accepted.",
+ "apierror-compare-inputneeded": "A title, a page ID, or a revision number is needed for both the <var>from</var> and the <var>to</var> parameters.",
+ "apierror-contentserializationexception": "Content serialization failed: $1",
+ "apierror-contenttoobig": "The content you supplied exceeds the article size limit of $1 {{PLURAL:$1|kilobyte|kilobytes}}.",
+ "apierror-copyuploadbaddomain": "Uploads by URL are not allowed from this domain.",
+ "apierror-copyuploadbadurl": "Upload not allowed from this URL.",
+ "apierror-create-titleexists": "Existing titles can't be protected with <kbd>create</kbd>.",
+ "apierror-csp-report": "Error processing CSP report: $1.",
+ "apierror-databaseerror": "[$1] Database query error.",
+ "apierror-deletedrevs-param-not-1-2": "The <var>$1</var> parameter cannot be used in modes 1 or 2.",
+ "apierror-deletedrevs-param-not-3": "The <var>$1</var> parameter cannot be used in mode 3.",
+ "apierror-emptynewsection": "Creating empty new sections is not possible.",
+ "apierror-emptypage": "Creating new, empty pages is not allowed.",
+ "apierror-exceptioncaught": "[$1] Exception caught: $2",
+ "apierror-filedoesnotexist": "File does not exist.",
+ "apierror-fileexists-sharedrepo-perm": "The target file exists on a shared repository. Use the <var>ignorewarnings</var> parameter to override it.",
+ "apierror-filenopath": "Cannot get local file path.",
+ "apierror-filetypecannotberotated": "File type cannot be rotated.",
+ "apierror-formatphp": "This response cannot be represented using <kbd>format=php</kbd>. See https://phabricator.wikimedia.org/T68776.",
+ "apierror-imageusage-badtitle": "The title for <kbd>$1</kbd> must be a file.",
+ "apierror-import-unknownerror": "Unknown error on import: $1.",
+ "apierror-integeroutofrange-abovebotmax": "<var>$1</var> may not be over $2 (set to $3) for bots or sysops.",
+ "apierror-integeroutofrange-abovemax": "<var>$1</var> may not be over $2 (set to $3) for users.",
+ "apierror-integeroutofrange-belowminimum": "<var>$1</var> may not be less than $2 (set to $3).",
+ "apierror-invalidcategory": "The category name you entered is not valid.",
+ "apierror-invalid-chunk": "Offset plus current chunk is greater than claimed file size.",
+ "apierror-invalidexpiry": "Invalid expiry time \"$1\".",
+ "apierror-invalid-file-key": "Not a valid file key.",
+ "apierror-invalidlang": "Invalid language code for parameter <var>$1</var>.",
+ "apierror-invalidoldimage": "The oldimage parameter has invalid format.",
+ "apierror-invalidparammix-cannotusewith": "The <kbd>$1</kbd> parameter cannot be used with <kbd>$2</kbd>.",
+ "apierror-invalidparammix-mustusewith": "The <kbd>$1</kbd> parameter may only be used with <kbd>$2</kbd>.",
+ "apierror-invalidparammix-parse-new-section": "<kbd>section=new</kbd> cannot be combined with the <var>oldid</var>, <var>pageid</var> or <var>page</var> parameters. Please use <var>title</var> and <var>text</var>.",
+ "apierror-invalidparammix": "The {{PLURAL:$2|parameters}} $1 can not be used together.",
+ "apierror-invalidsection": "The section parameter must be a valid section ID or <kbd>new</kbd>.",
+ "apierror-invalidsha1base36hash": "The SHA1Base36 hash provided is not valid.",
+ "apierror-invalidsha1hash": "The SHA1 hash provided is not valid.",
+ "apierror-invalidtitle": "Bad title \"$1\".",
+ "apierror-invalidurlparam": "Invalid value for <var>$1urlparam</var> (<kbd>$2=$3</kbd>).",
+ "apierror-invaliduser": "Invalid username \"$1\".",
+ "apierror-maxlag-generic": "Waiting for a database server: $1 {{PLURAL:$1|second|seconds}} lagged.",
+ "apierror-maxlag": "Waiting for $2: $1 {{PLURAL:$1|second|seconds}} lagged.",
+ "apierror-mimesearchdisabled": "MIME search is disabled in Miser Mode.",
+ "apierror-missingcontent-pageid": "Missing content for page ID $1.",
+ "apierror-missingparam-at-least-one-of": "{{PLURAL:$2|The parameter|At least one of the parameters}} $1 is required.",
+ "apierror-missingparam-one-of": "{{PLURAL:$2|The parameter|One of the parameters}} $1 is required.",
+ "apierror-missingparam": "The <var>$1</var> parameter must be set.",
+ "apierror-missingrev-pageid": "No current revision of page ID $1.",
+ "apierror-missingtitle-createonly": "Missing titles can only be protected with <kbd>create</kbd>.",
+ "apierror-missingtitle": "The page you specified doesn't exist.",
+ "apierror-missingtitle-byname": "The page $1 doesn't exist.",
+ "apierror-moduledisabled": "The <kbd>$1</kbd> module has been disabled.",
+ "apierror-multival-only-one-of": "{{PLURAL:$3|Only|Only one of}} $2 is allowed for parameter <var>$1</var>.",
+ "apierror-multival-only-one": "Only one value is allowed for parameter <var>$1</var>.",
+ "apierror-multpages": "<var>$1</var> may only be used with a single page.",
+ "apierror-mustbeloggedin-changeauth": "You must be logged in to change authentication data.",
+ "apierror-mustbeloggedin-generic": "You must be logged in.",
+ "apierror-mustbeloggedin-linkaccounts": "You must be logged in to link accounts.",
+ "apierror-mustbeloggedin-removeauth": "You must be logged in to remove authentication data.",
+ "apierror-mustbeloggedin-uploadstash": "The upload stash is only available to logged-in users.",
+ "apierror-mustbeloggedin": "You must be logged in to $1.",
+ "apierror-mustbeposted": "The <kbd>$1</kbd> module requires a POST request.",
+ "apierror-mustpostparams": "The following {{PLURAL:$2|parameter was|parameters were}} found in the query string, but must be in the POST body: $1.",
+ "apierror-noapiwrite": "Editing of this wiki through the API is disabled. Make sure the <code>$wgEnableWriteAPI=true;</code> statement is included in the wiki's <code>LocalSettings.php</code> file.",
+ "apierror-nochanges": "No changes were requested.",
+ "apierror-nodeleteablefile": "No such old version of the file.",
+ "apierror-no-direct-editing": "Direct editing via API is not supported for content model $1 used by $2.",
+ "apierror-noedit-anon": "Anonymous users can't edit pages.",
+ "apierror-noedit": "You don't have permission to edit pages.",
+ "apierror-noimageredirect-anon": "Anonymous users can't create image redirects.",
+ "apierror-noimageredirect": "You don't have permission to create image redirects.",
+ "apierror-nosuchlogid": "There is no log entry with ID $1.",
+ "apierror-nosuchpageid": "There is no page with ID $1.",
+ "apierror-nosuchrcid": "There is no recent change with ID $1.",
+ "apierror-nosuchrevid": "There is no revision with ID $1.",
+ "apierror-nosuchsection": "There is no section $1.",
+ "apierror-nosuchsection-what": "There is no section $1 in $2.",
+ "apierror-notarget": "You have not specified a valid target for this action.",
+ "apierror-notpatrollable": "The revision r$1 can't be patrolled as it's too old.",
+ "apierror-nouploadmodule": "No upload module set.",
+ "apierror-opensearch-json-warnings": "Warnings cannot be represented in OpenSearch JSON format.",
+ "apierror-pagecannotexist": "Namespace doesn't allow actual pages.",
+ "apierror-pagedeleted": "The page has been deleted since you fetched its timestamp.",
+ "apierror-paramempty": "The parameter <var>$1</var> may not be empty.",
+ "apierror-parsetree-notwikitext": "<kbd>prop=parsetree</kbd> is only supported for wikitext content.",
+ "apierror-parsetree-notwikitext-title": "<kbd>prop=parsetree</kbd> is only supported for wikitext content. $1 uses content model $2.",
+ "apierror-pastexpiry": "Expiry time \"$1\" is in the past.",
+ "apierror-permissiondenied": "You don't have permission to $1.",
+ "apierror-permissiondenied-generic": "Permission denied.",
+ "apierror-permissiondenied-patrolflag": "You need the <code>patrol</code> or <code>patrolmarks</code> right to request the patrolled flag.",
+ "apierror-permissiondenied-unblock": "You don't have permission to unblock users.",
+ "apierror-prefixsearchdisabled": "Prefix search is disabled in Miser Mode.",
+ "apierror-promised-nonwrite-api": "The <code>Promise-Non-Write-API-Action</code> HTTP header cannot be sent to write-mode API modules.",
+ "apierror-protect-invalidaction": "Invalid protection type \"$1\".",
+ "apierror-protect-invalidlevel": "Invalid protection level \"$1\".",
+ "apierror-ratelimited": "You've exceeded your rate limit. Please wait some time and try again.",
+ "apierror-readapidenied": "You need read permission to use this module.",
+ "apierror-readonly": "The wiki is currently in read-only mode.",
+ "apierror-reauthenticate": "You have not authenticated recently in this session, please reauthenticate.",
+ "apierror-redirect-appendonly": "You have attempted to edit using the redirect-following mode, which must be used in conjuction with <kbd>section=new</kbd>, <var>prependtext</var>, or <var>appendtext</var>.",
+ "apierror-revdel-mutuallyexclusive": "The same field cannot be used in both <var>hide</var> and <var>show</var>.",
+ "apierror-revdel-needtarget": "A target title is required for this RevDel type.",
+ "apierror-revdel-paramneeded": "At least one value is required for <var>hide</var> and/or <var>show</var>.",
+ "apierror-revisions-norevids": "The <var>revids</var> parameter may not be used with the list options (<var>$1limit</var>, <var>$1startid</var>, <var>$1endid</var>, <kbd>$1dir=newer</kbd>, <var>$1user</var>, <var>$1excludeuser</var>, <var>$1start</var>, and <var>$1end</var>).",
+ "apierror-revisions-singlepage": "<var>titles</var>, <var>pageids</var> or a generator was used to supply multiple pages, but the <var>$1limit</var>, <var>$1startid</var>, <var>$1endid</var>, <kbd>$1dir=newer</kbd>, <var>$1user</var>, <var>$1excludeuser</var>, <var>$1start</var>, and <var>$1end</var> parameters may only be used on a single page.",
+ "apierror-revwrongpage": "r$1 is not a revision of $2.",
+ "apierror-searchdisabled": "<var>$1</var> search is disabled.",
+ "apierror-sectionreplacefailed": "Could not merge updated section.",
+ "apierror-sectionsnotsupported": "Sections are not supported for content model $1.",
+ "apierror-sectionsnotsupported-what": "Sections are not supported by $1.",
+ "apierror-show": "Incorrect parameter - mutually exclusive values may not be supplied.",
+ "apierror-siteinfo-includealldenied": "Cannot view all servers' info unless <var>$wgShowHostNames</var> is true.",
+ "apierror-sizediffdisabled": "Size difference is disabled in Miser Mode.",
+ "apierror-spamdetected": "Your edit was refused because it contained a spam fragment: <code>$1</code>.",
+ "apierror-specialpage-cantexecute": "You don't have permission to view the results of this special page.",
+ "apierror-stashedfilenotfound": "Could not find the file in the stash: $1.",
+ "apierror-stashedit-missingtext": "No stashed text found with the given hash.",
+ "apierror-stashfailed-complete": "Chunked upload is already completed, check status for details.",
+ "apierror-stashfailed-nosession": "No chunked upload session with this key.",
+ "apierror-stashfilestorage": "Could not store upload in the stash: $1",
+ "apierror-stashnosuchfilekey": "No such filekey: $1.",
+ "apierror-stashpathinvalid": "File key of improper format or otherwise invalid: $1.",
+ "apierror-stashwrongowner": "Wrong owner: $1",
+ "apierror-stashzerolength": "File is of zero length, and could not be stored in the stash: $1.",
+ "apierror-templateexpansion-notwikitext": "Template expansion is only supported for wikitext content. $1 uses content model $2.",
+ "apierror-toofewexpiries": "$1 expiry {{PLURAL:$1|timestamp was|timestamps were}} provided where $2 {{PLURAL:$2|was|were}} needed.",
+ "apierror-unknownaction": "The action specified, <kbd>$1</kbd>, is not recognized.",
+ "apierror-unknownerror-editpage": "Unknown EditPage error: $1.",
+ "apierror-unknownerror-nocode": "Unknown error.",
+ "apierror-unknownerror": "Unknown error: \"$1\".",
+ "apierror-unknownformat": "Unrecognized format \"$1\".",
+ "apierror-unrecognizedparams": "Unrecognized {{PLURAL:$2|parameter|parameters}}: $1.",
+ "apierror-unrecognizedvalue": "Unrecognized value for parameter <var>$1</var>: $2.",
+ "apierror-unsupportedrepo": "Local file repository does not support querying all images.",
+ "apierror-upload-filekeyneeded": "Must supply a <var>filekey</var> when <var>offset</var> is non-zero.",
+ "apierror-upload-filekeynotallowed": "Cannot supply a <var>filekey</var> when <var>offset</var> is 0.",
+ "apierror-upload-inprogress": "Upload from stash already in progress.",
+ "apierror-upload-missingresult": "No result in status data.",
+ "apierror-urlparamnormal": "Could not normalize image parameters for $1.",
+ "apierror-writeapidenied": "You're not allowed to edit this wiki through the API.",
+
+ "apiwarn-alldeletedrevisions-performance": "For better performance when generating titles, set <kbd>$1dir=newer</kbd>.",
+ "apiwarn-badurlparam": "Could not parse <var>$1urlparam</var> for $2. Using only width and height.",
+ "apiwarn-badutf8": "The value passed for <var>$1</var> contains invalid or non-normalized data. Textual data should be valid, NFC-normalized Unicode without C0 control characters other than HT (\\t), LF (\\n), and CR (\\r).",
+ "apiwarn-checktoken-percentencoding": "Check that symbols such as \"+\" in the token are properly percent-encoded in the URL.",
+ "apiwarn-deprecation-deletedrevs": "<kbd>list=deletedrevs</kbd> has been deprecated. Please use <kbd>prop=deletedrevisions</kbd> or <kbd>list=alldeletedrevisions</kbd> instead.",
+ "apiwarn-deprecation-expandtemplates-prop": "Because no values have been specified for the <var>prop</var> parameter, a legacy format has been used for the output. This format is deprecated, and in the future, a default value will be set for the <var>prop</var> parameter, causing the new format to always be used.",
+ "apiwarn-deprecation-httpsexpected": "HTTP used when HTTPS was expected.",
+ "apiwarn-deprecation-login-botpw": "Main-account login via <kbd>action=login</kbd> is deprecated and may stop working without warning. To continue login with <kbd>action=login</kbd>, see [[Special:BotPasswords]]. To safely continue using main-account login, see <kbd>action=clientlogin</kbd>.",
+ "apiwarn-deprecation-login-nobotpw": "Main-account login via <kbd>action=login</kbd> is deprecated and may stop working without warning. To safely log in, see <kbd>action=clientlogin</kbd>.",
+ "apiwarn-deprecation-login-token": "Fetching a token via <kbd>action=login</kbd> is deprecated. Use <kbd>action=query&meta=tokens&type=login</kbd> instead.",
+ "apiwarn-deprecation-parameter": "The parameter <var>$1</var> has been deprecated.",
+ "apiwarn-deprecation-parse-headitems": "<kbd>prop=headitems</kbd> is deprecated since MediaWiki 1.28. Use <kbd>prop=headhtml</kbd> when creating new HTML documents, or <kbd>prop=modules|jsconfigvars</kbd> when updating a document client-side.",
+ "apiwarn-deprecation-purge-get": "Use of <kbd>action=purge</kbd> via GET is deprecated. Use POST instead.",
+ "apiwarn-deprecation-withreplacement": "<kbd>$1</kbd> has been deprecated. Please use <kbd>$2</kbd> instead.",
+ "apiwarn-difftohidden": "Couldn't diff to r$1: content is hidden.",
+ "apiwarn-errorprinterfailed": "Error printer failed. Will retry without params.",
+ "apiwarn-errorprinterfailed-ex": "Error printer failed (will retry without params): $1",
+ "apiwarn-invalidcategory": "\"$1\" is not a category.",
+ "apiwarn-invalidtitle": "\"$1\" is not a valid title.",
+ "apiwarn-invalidxmlstylesheetext": "Stylesheet should have <code>.xsl</code> extension.",
+ "apiwarn-invalidxmlstylesheet": "Invalid or non-existent stylesheet specified.",
+ "apiwarn-invalidxmlstylesheetns": "Stylesheet should be in the {{ns:MediaWiki}} namespace.",
+ "apiwarn-moduleswithoutvars": "Property <kbd>modules</kbd> was set but not <kbd>jsconfigvars</kbd> or <kbd>encodedjsconfigvars</kbd>. Configuration variables are necessary for proper module usage.",
+ "apiwarn-notfile": "\"$1\" is not a file.",
+ "apiwarn-nothumb-noimagehandler": "Could not create thumbnail because $1 does not have an associated image handler.",
+ "apiwarn-parse-nocontentmodel": "No <var>title</var> or <var>contentmodel</var> was given, assuming $1.",
+ "apiwarn-parse-titlewithouttext": "<var>title</var> used without <var>text</var>, and parsed page properties were requested. Did you mean to use <var>page</var> instead of <var>title</var>?",
+ "apiwarn-redirectsandrevids": "Redirect resolution cannot be used together with the <var>revids</var> parameter. Any redirects the <var>revids</var> point to have not been resolved.",
+ "apiwarn-tokennotallowed": "Action \"$1\" is not allowed for the current user.",
+ "apiwarn-tokens-origin": "Tokens may not be obtained when the same-origin policy is not applied.",
+ "apiwarn-toomanyvalues": "Too many values supplied for parameter <var>$1</var>: the limit is $2.",
+ "apiwarn-truncatedresult": "This result was truncated because it would otherwise be larger than the limit of $1 bytes.",
+ "apiwarn-unclearnowtimestamp": "Passing \"$2\" for timestamp parameter <var>$1</var> has been deprecated. If for some reason you need to explicitly specify the current time without calculating it client-side, use <kbd>now<kbd>.",
+ "apiwarn-unrecognizedvalues": "Unrecognized {{PLURAL:$3|value|values}} for parameter <var>$1</var>: $2.",
+ "apiwarn-unsupportedarray": "Parameter <var>$1</var> uses unsupported PHP array syntax.",
+ "apiwarn-urlparamwidth": "Ignoring width value set in <var>$1urlparam</var> ($2) in favor of width value derived from <var>$1urlwidth</var>/<var>$1urlheight</var> ($3).",
+ "apiwarn-validationfailed-badchars": "invalid characters in key (only <code>a-z</code>, <code>A-Z</code>, <code>0-9</code>, <code>_</code>, and <code>-</code> are allowed).",
+ "apiwarn-validationfailed-badpref": "not a valid preference.",
+ "apiwarn-validationfailed-cannotset": "cannot be set by this module.",
+ "apiwarn-validationfailed-keytoolong": "key too long (no more than $1 bytes allowed).",
+ "apiwarn-validationfailed": "Validation error for <kbd>$1</kbd>: $2",
+ "apiwarn-wgDebugAPI": "<strong>Security Warning</strong>: <var>$wgDebugAPI</var> is enabled.",
+
+ "api-feed-error-title": "Error ($1)",
+ "api-usage-docref": "See $1 for API usage.",
+ "api-exception-trace": "$1 at $2($3)\n$4",
"api-credits-header": "Credits",
"api-credits": "API developers:\n* Yuri Astrakhan (creator, lead developer Sep 2006–Sep 2007)\n* Roan Kattouw (lead developer Sep 2007–2009)\n* Victor Vasiliev\n* Bryan Tong Minh\n* Sam Reed\n* Brad Jorsch (lead developer 2013–present)\n\nPlease send your comments, suggestions and questions to mediawiki-api@lists.wikimedia.org\nor file a bug report at https://phabricator.wikimedia.org/."
}
diff --git a/includes/api/i18n/qqq.json b/includes/api/i18n/qqq.json
index fd6a4dd609d0..c5d9bc041e8a 100644
--- a/includes/api/i18n/qqq.json
+++ b/includes/api/i18n/qqq.json
@@ -29,6 +29,10 @@
"apihelp-main-param-curtimestamp": "{{doc-apihelp-param|main|curtimestamp}}",
"apihelp-main-param-origin": "{{doc-apihelp-param|main|origin}}",
"apihelp-main-param-uselang": "{{doc-apihelp-param|main|uselang}}",
+ "apihelp-main-param-errorformat": "{{doc-apihelp-param|main|errorformat}}",
+ "apihelp-main-param-errorlang": "{{doc-apihelp-param|main|errorlang}}",
+ "apihelp-main-param-errorsuselocal": "{{doc-apihelp-param|main|errorsuselocal}}",
+ "apihelp-main-param-responselanginfo": "{{doc-apihelp-param|main|responselanginfo}}",
"apihelp-block-description": "{{doc-apihelp-description|block}}",
"apihelp-block-param-user": "{{doc-apihelp-param|block|user}}",
"apihelp-block-param-expiry": "{{doc-apihelp-param|block|expiry}}\n{{doc-important|Do not translate \"5 months\", \"2 weeks\", \"infinite\", \"indefinite\" or \"never\"!}}",
@@ -1420,6 +1424,236 @@
"api-help-authmanagerhelper-returnurl": "{{doc-apihelp-param|description=the \"returnurl\" parameter for AuthManager-using API modules|noseealso=1}}",
"api-help-authmanagerhelper-continue": "{{doc-apihelp-param|description=the \"continue\" parameter for AuthManager-using API modules|noseealso=1}}",
"api-help-authmanagerhelper-additional-params": "Message to display for AuthManager modules that take additional parameters to populate AuthenticationRequests. Parameters:\n* $1 - AuthManager action used by this module\n* $2 - Module parameter prefix, e.g. \"login\"\n* $3 - Module name, e.g. \"clientlogin\"\n* $4 - Module path, e.g. \"clientlogin\"",
+ "apierror-allimages-redirect": "{{doc-apierror}}",
+ "apierror-allpages-generator-redirects": "{{doc-apierror}}",
+ "apierror-appendnotsupported": "{{doc-apierror}}\n\nParameters:\n* $1 - Content model",
+ "apierror-articleexists": "{{doc-apierror}}",
+ "apierror-assertbotfailed": "{{doc-apierror}}",
+ "apierror-assertnameduserfailed": "{{doc-apierror}}\n\nParameters:\n* $1 - User name passed in.",
+ "apierror-assertuserfailed": "{{doc-apierror}}",
+ "apierror-autoblocked": "{{doc-apierror}}",
+ "apierror-bad-watchlist-token": "{{doc-apierror}}",
+ "apierror-badconfig-resulttoosmall": "{{doc-apierror}}",
+ "apierror-badcontinue": "{{doc-apierror}}",
+ "apierror-baddiff": "{{doc-apierror}}",
+ "apierror-baddiffto": "{{doc-apierror}}\n\nParameters:\n* $1 - Module parameter prefix, e.g. \"bl\".",
+ "apierror-badformat": "{{doc-apierror}}\n\nParameters:\n* $1 - Content format.\n* $2 - Content model.\n* $3 - Title using the model.",
+ "apierror-badformat-generic": "{{doc-apierror}}\n\nParameters:\n* $1 - Content format.\n* $2 - Content model.",
+ "apierror-badgenerator-notgenerator": "{{doc-apierror}}\n\nParameters:\n* $1 - Generator module name.",
+ "apierror-badgenerator-unknown": "{{doc-apierror}}\n\nParameters:\n* $1 - Generator module name.",
+ "apierror-badip": "{{doc-apierror}}",
+ "apierror-badmd5": "{{doc-apierror}}",
+ "apierror-badmodule-badsubmodule": "{{doc-apierror}}\n\nParameters:\n* $1 - Module path.\n* $2 - Submodule name.",
+ "apierror-badmodule-nosubmodules": "{{doc-apierror}}\n\nParameters:\n* $1 - Module path.",
+ "apierror-badparameter": "{{doc-apierror}}\n\nParameters:\n* $1 - Parameter name.",
+ "apierror-badquery": "{{doc-apierror}}",
+ "apierror-badtimestamp": "{{doc-apierror}}\n\nParameters:\n* $1 - Parameter name.\n* $2 - Value of the parameter.",
+ "apierror-badtoken": "{{doc-apierror}}",
+ "apierror-badupload": "{{doc-apierror}}\n\nParameters:\n* $1 - Parameter name.",
+ "apierror-badurl": "{{doc-apierror}}\n\nParameters:\n* $1 - Parameter name.\n* $2 - Value of the parameter.",
+ "apierror-baduser": "{{doc-apierror}}\n\nParameters:\n* $1 - Parameter name.\n* $2 - Value of the parameter.",
+ "apierror-badvalue-notmultivalue": "{{doc-apierror}}",
+ "apierror-blocked": "{{doc-apierror}}",
+ "apierror-blockedfrommail": "{{doc-apierror}}",
+ "apierror-botsnotsupported": "{{doc-apierror}}",
+ "apierror-cannotreauthenticate": "{{doc-apierror}}",
+ "apierror-cannotviewtitle": "{{doc-apierror}}\n\nParameters:\n* $1 - Title.",
+ "apierror-cantblock": "{{doc-apierror}}",
+ "apierror-cantblock-email": "{{doc-apierror}}",
+ "apierror-cantchangecontentmodel": "{{doc-apierror}}",
+ "apierror-canthide": "{{doc-apierror}}",
+ "apierror-cantimport": "{{doc-apierror}}",
+ "apierror-cantimport-upload": "{{doc-apierror}}",
+ "apierror-cantoverwrite-sharedfile": "{{doc-apierror}}",
+ "apierror-cantsend": "{{doc-apierror}}",
+ "apierror-cantundelete": "{{doc-apierror}}",
+ "apierror-changeauth-norequest": "{{doc-apierror}}",
+ "apierror-chunk-too-small": "{{doc-apierror}}\n\nParameters:\n* $1 - Minimum size in bytes.",
+ "apierror-cidrtoobroad": "{{doc-apierror}}\n\nParameters:\n* $1 - \"IPv4\" or \"IPv6\"\n* $2 - Minimum CIDR mask length.",
+ "apierror-compare-inputneeded": "{{doc-apierror}}",
+ "apierror-contentserializationexception": "{{doc-apierror}}\n\nParameters:\n* $1 - Exception text, may end with punctuation. Currently this is probably English, hopefully we'll fix that in the future.",
+ "apierror-contenttoobig": "{{doc-apierror}}\n\nParameters:\n* $1 - Maximum article size in kilobytes.",
+ "apierror-copyuploadbaddomain": "{{doc-apierror}}",
+ "apierror-copyuploadbadurl": "{{doc-apierror}}",
+ "apierror-create-titleexists": "{{doc-apierror}}",
+ "apierror-csp-report": "{{doc-apierror}}\n\nParameters:\n* $1 - Error code, e.g. \"toobig\".",
+ "apierror-databaseerror": "{{doc-apierror}}\n\nParameters:\n* $1 - Exception log ID code. This is meaningless to the end user, but can be used by people with access to the logs to easily find the logged error.",
+ "apierror-deletedrevs-param-not-1-2": "{{doc-apierror}}\n\nParameters:\n* $1 - Parameter name.\n\nSee also:\n* {{msg-mw|apihelp-query+deletedrevs-description}}",
+ "apierror-deletedrevs-param-not-3": "{{doc-apierror}}\n\nParameters:\n* $1 - Parameter name.\n\nSee also:\n* {{msg-mw|apihelp-query+deletedrevs-description}}",
+ "apierror-emptynewsection": "{{doc-apierror}}",
+ "apierror-emptypage": "{{doc-apierror}}",
+ "apierror-exceptioncaught": "{{doc-apierror}}\n\nParameters:\n* $1 - Exception log ID code. This is meaningless to the end user, but can be used by people with access to the logs to easily find the logged error.\n* $2 - Exception message, which may end with punctuation. Probably in English.",
+ "apierror-filedoesnotexist": "{{doc-apierror}}",
+ "apierror-fileexists-sharedrepo-perm": "{{doc-apierror}}",
+ "apierror-filenopath": "{{doc-apierror}}",
+ "apierror-filetypecannotberotated": "{{doc-apierror}}",
+ "apierror-formatphp": "{{doc-apierror}}",
+ "apierror-imageusage-badtitle": "{{doc-apierror}}\n\nParameters:\n* $1 - Module name.",
+ "apierror-import-unknownerror": "{{doc-apierror}}\n\nParameters:\n* $1 - Error message returned by the import, probably in English.",
+ "apierror-integeroutofrange-abovebotmax": "{{doc-apierror}}\n\nParameters:\n* $1 - Parameter name\n* $2 - Maximum allowed value\n* $3 - Supplied value",
+ "apierror-integeroutofrange-abovemax": "{{doc-apierror}}\n\nParameters:\n* $1 - Parameter name\n* $2 - Maximum allowed value\n* $3 - Supplied value",
+ "apierror-integeroutofrange-belowminimum": "{{doc-apierror}}\n\nParameters:\n* $1 - Parameter name\n* $2 - Minimum allowed value\n* $3 - Supplied value",
+ "apierror-invalid-chunk": "{{doc-apierror}}",
+ "apierror-invalid-file-key": "{{doc-apierror}}",
+ "apierror-invalidcategory": "{{doc-apierror}}",
+ "apierror-invalidexpiry": "{{doc-apierror}}\n\nParameters:\n* $1 - Value provided.",
+ "apierror-invalidlang": "{{doc-apierror}}\n\nParameters:\n* $1 - Parameter name.",
+ "apierror-invalidoldimage": "{{doc-apierror}}",
+ "apierror-invalidparammix": "{{doc-apierror}}\n\nParameters:\n* $1 - List of parameter names or \"parameter=value\" text.\n* $2 - Number of parameters.",
+ "apierror-invalidparammix-cannotusewith": "{{doc-apierror}}\n\nParameters:\n* $1 - Parameter name or \"parameter=value\" text.\n* $2 - Parameter name or \"parameter=value\" text.",
+ "apierror-invalidparammix-mustusewith": "{{doc-apierror}}\n\nParameters:\n* $1 - Parameter name or \"parameter=value\" text.\n* $2 - Parameter name or \"parameter=value\" text.",
+ "apierror-invalidparammix-parse-new-section": "{{doc-apierror}}",
+ "apierror-invalidsection": "{{doc-apierror}}",
+ "apierror-invalidsha1base36hash": "{{doc-apierror}}",
+ "apierror-invalidsha1hash": "{{doc-apierror}}",
+ "apierror-invalidtitle": "{{doc-apierror}}\n\nParameters:\n* $1 - Title that is invalid",
+ "apierror-invalidurlparam": "{{doc-apierror}}\n\nParameters:\n* $1 - Module parameter prefix, e.g. \"bl\".\n* $2 - Key\n* $3 - Value.",
+ "apierror-invaliduser": "{{doc-apierror}}\n\nParameters:\n* $1 - User name that is invalid.",
+ "apierror-maxlag": "{{doc-apierror}}\n\nParameters:\n* $1 - Database lag in seconds.\n* $2 - Database server that is lagged.",
+ "apierror-maxlag-generic": "{{doc-apierror}}\n\nParameters:\n* $1 - Database is lag in seconds.",
+ "apierror-mimesearchdisabled": "{{doc-apierror}}",
+ "apierror-missingcontent-pageid": "{{doc-apierror}}\n\nParameters:\n* $1 - Page ID number.",
+ "apierror-missingparam": "{{doc-apierror}}\n\nParameters:\n* $1 - Parameter name.",
+ "apierror-missingparam-at-least-one-of": "{{doc-apierror}}\n\nParameters:\n* $1 - List of parameter names.\n* $2 - Number of parameters.",
+ "apierror-missingparam-one-of": "{{doc-apierror}}\n\nParameters:\n* $1 - List of parameter names.\n* $2 - Number of parameters.",
+ "apierror-missingrev-pageid": "{{doc-apierror}}\n\nParameters:\n* $1 - Page ID number.",
+ "apierror-missingtitle": "{{doc-apierror}}",
+ "apierror-missingtitle-byname": "{{doc-apierror}}",
+ "apierror-missingtitle-createonly": "{{doc-apierror}}",
+ "apierror-moduledisabled": "{{doc-apierror}}\n\nParameters:\n* $1 - Name of the module.",
+ "apierror-multival-only-one": "{{doc-apierror}}\n\nParameters:\n* $1 - Parameter name.",
+ "apierror-multival-only-one-of": "{{doc-apierror}}\n\nParameters:\n* $1 - Parameter name.\n* $2 - Possible values for the parameter.\n* $3 - Number of values.",
+ "apierror-multpages": "{{doc-apierror}}\n\nParameters:\n* $1 - Parameter name",
+ "apierror-mustbeloggedin": "{{doc-apierror}}\n\nParameters:\n* $1 - One of the action-* messages (for example {{msg-mw|action-edit}}) or other such messages tagged with {{tl|doc-action}} in their documentation\n\nPlease report at [[Support]] if you are unable to properly translate this message. Also see [[phab:T16246]] (now closed) for background.\n\nSee also:\n* {{msg-mw|apierror-permissiondenied}}\n* {{msg-mw|permissionserrorstext-withaction}}",
+ "apierror-mustbeloggedin-changeauth": "{{doc-apierror}}",
+ "apierror-mustbeloggedin-generic": "{{doc-apierror}}",
+ "apierror-mustbeloggedin-linkaccounts": "{{doc-apierror}}",
+ "apierror-mustbeloggedin-removeauth": "{{doc-apierror}}",
+ "apierror-mustbeloggedin-uploadstash": "{{doc-apierror}}",
+ "apierror-mustbeposted": "{{doc-apierror}}\n\nParameters:\n* $1 - Module name.",
+ "apierror-mustpostparams": "{{doc-apierror}}\n\nParameters:\n* $1 - Parameter names.\n* $2 - Number of parameters.",
+ "apierror-no-direct-editing": "{{doc-apierror}}\n\nParameters:\n* $1 - Content model.\n* $2 - Title using the model.",
+ "apierror-noapiwrite": "{{doc-apierror}}",
+ "apierror-nochanges": "{{doc-apierror}}",
+ "apierror-nodeleteablefile": "{{doc-apierror}}",
+ "apierror-noedit": "{{doc-apierror}}",
+ "apierror-noedit-anon": "{{doc-apierror}}",
+ "apierror-noimageredirect": "{{doc-apierror}}",
+ "apierror-noimageredirect-anon": "{{doc-apierror}}",
+ "apierror-nosuchlogid": "{{doc-apierror}}\n\nParameters:\n* $1 - Log ID number.",
+ "apierror-nosuchpageid": "{{doc-apierror}}\n\nParameters:\n* $1 - Page ID number.",
+ "apierror-nosuchrcid": "{{doc-apierror}}\n\nParameters:\n* $1 - RecentChanges ID number.",
+ "apierror-nosuchrevid": "{{doc-apierror}}\n\nParameters:\n* $1 - Revision ID number.",
+ "apierror-nosuchsection": "{{doc-apierror}}\n\nParameters:\n* $1 - Section identifier. Probably a number or \"T-\" followed by a number.",
+ "apierror-nosuchsection-what": "{{doc-apierror}}\n\nParameters:\n* $1 - Section identifier. Probably a number or \"T-\" followed by a number.\n* $2 - Page title, revision ID formatted with {{msg-mw|revid}}, or page ID formatted with {{msg-mw|pageid}}.",
+ "apierror-notarget": "{{doc-apierror}}",
+ "apierror-notpatrollable": "{{doc-apierror}}\n\nParameters:\n* $1 - Revision ID number.",
+ "apierror-nouploadmodule": "{{doc-apierror}}",
+ "apierror-opensearch-json-warnings": "{{doc-apierror}}",
+ "apierror-pagecannotexist": "{{doc-apierror}}",
+ "apierror-pagedeleted": "{{doc-apierror}}",
+ "apierror-paramempty": "{{doc-apierror}}\n\nParameters:\n* $1 - Parameter name.",
+ "apierror-parsetree-notwikitext": "{{doc-apierror}}",
+ "apierror-parsetree-notwikitext-title": "{{doc-apierror}}\n\nParameters:\n* $1 - Page title.\n* $2 - Content model.",
+ "apierror-pastexpiry": "{{doc-apierror}}\n\nParameters:\n* $1 - Supplied expiry time.",
+ "apierror-permissiondenied-generic": "{{doc-apierror}}",
+ "apierror-permissiondenied-patrolflag": "{{doc-apierror}}\n\nSee also:\n* {{msg-mw|apierror-permissiondenied}}",
+ "apierror-permissiondenied-unblock": "{{doc-apierror}}\n\nSee also:\n* {{msg-mw|apierror-permissiondenied}}",
+ "apierror-permissiondenied": "{{doc-apierror}}\n\nParameters:\n* $1 - One of the action-* messages (for example {{msg-mw|action-edit}}) or other such messages tagged with {{tl|doc-action}} in their documentation\n\nPlease report at [[Support]] if you are unable to properly translate this message. Also see [[phab:T16246]] (now closed) for background.\n\nSee also:\n* {{msg-mw|permissionserrorstext-withaction}}",
+ "apierror-prefixsearchdisabled": "{{doc-apierror}}",
+ "apierror-promised-nonwrite-api": "{{doc-apierror}}",
+ "apierror-protect-invalidaction": "{{doc-apierror}}\n\nParameters:\n* $1 - Supplied protection type.",
+ "apierror-protect-invalidlevel": "{{doc-apierror}}\n\nParameters:\n* $1 - Supplied protection level.",
+ "apierror-ratelimited": "{{doc-apierror}}",
+ "apierror-readapidenied": "{{doc-apierror}}",
+ "apierror-readonly": "{{doc-apierror}}",
+ "apierror-reauthenticate": "{{doc-apierror}}",
+ "apierror-redirect-appendonly": "{{doc-apierror}}",
+ "apierror-revdel-mutuallyexclusive": "{{doc-apierror}}",
+ "apierror-revdel-needtarget": "{{doc-apierror}}",
+ "apierror-revdel-paramneeded": "{{doc-apierror}}",
+ "apierror-revisions-norevids": "{{doc-apierror}}\n\nParameters:\n* $1 - Module parameter prefix, e.g. \"bl\".",
+ "apierror-revisions-singlepage": "{{doc-apierror}}\n\nParameters:\n* $1 - Module parameter prefix, e.g. \"bl\".",
+ "apierror-revwrongpage": "{{doc-apierror}}\n\nParameters:\n* $1 - Revision ID number.\n* $2 - Page title.",
+ "apierror-searchdisabled": "{{doc-apierror}}\n\nParameters:\n* $1 - Search parameter that is disabled.",
+ "apierror-sectionreplacefailed": "{{doc-apierror}}",
+ "apierror-sectionsnotsupported": "{{doc-apierror}}\n\nParameters:\n* $1 - Content model that doesn't support sections.",
+ "apierror-sectionsnotsupported-what": "{{doc-apierror}}\n\nParameters:\n* $1 - Page title, revision ID formatted with {{msg-mw|revid}}, or page ID formatted with {{msg-mw|pageid}}.",
+ "apierror-show": "{{doc-apierror}}",
+ "apierror-siteinfo-includealldenied": "{{doc-apierror}}",
+ "apierror-sizediffdisabled": "{{doc-apierror}}",
+ "apierror-spamdetected": "{{doc-apierror}}\n\nParameters:\n* $1 - Matching \"spam filter\".\n\nSee also:\n* {{msg-mw|spamprotectionmatch}}",
+ "apierror-specialpage-cantexecute": "{{doc-apierror}}",
+ "apierror-stashedfilenotfound": "{{doc-apierror}}\n\nParameters:\n* $1 - Exception text. Currently this is probably English, hopefully we'll fix that in the future.",
+ "apierror-stashedit-missingtext": "{{doc-apierror}}",
+ "apierror-stashfailed-complete": "{{doc-apierror}}",
+ "apierror-stashfailed-nosession": "{{doc-apierror}}",
+ "apierror-stashfilestorage": "{{doc-apierror}}\n\nParameters:\n* $1 - Exception text, which may already end with punctuation. Currently this is probably English, hopefully we'll fix that in the future.",
+ "apierror-stashnosuchfilekey": "{{doc-apierror}}\n\nParameters:\n* $1 - Exception text. Currently this is probably English, hopefully we'll fix that in the future.",
+ "apierror-stashpathinvalid": "{{doc-apierror}}\n\nParameters:\n* $1 - Exception text. Currently this is probably English, hopefully we'll fix that in the future.",
+ "apierror-stashwrongowner": "{{doc-apierror}}\n\nParameters:\n* $1 - Exception text, which should already end with punctuation. Currently this is probably English, hopefully we'll fix that in the future.",
+ "apierror-stashzerolength": "{{doc-apierror}}\n\nParameters:\n* $1 - Exception text. Currently this is probably English, hopefully we'll fix that in the future.",
+ "apierror-templateexpansion-notwikitext": "{{doc-apierror}}\n\nParameters:\n* $1 - Page title.\n* $2 - Content model.",
+ "apierror-toofewexpiries": "{{doc-apierror}}\n\nParameters:\n* $1 - Number provided.\n* $2 - Number needed.",
+ "apierror-unknownaction": "{{doc-apierror}}\n\nParameters:\n* $1 - Action provided.",
+ "apierror-unknownerror": "{{doc-apierror}}\n\nParameters:\n* $1 - Error code (possibly a message key) not handled by ApiBase::parseMsg().",
+ "apierror-unknownerror-editpage": "{{doc-apierror}}\n\nParameters:\n* $1 - Error code (an integer).",
+ "apierror-unknownerror-nocode": "{{doc-apierror}}",
+ "apierror-unknownformat": "{{doc-apierror}}\n\nParameters:\n* $1 - Format provided.",
+ "apierror-unrecognizedparams": "{{doc-apierror}}\n\nParameters:\n* $1 - List of parameters.\n* $2 - Number of parameters.",
+ "apierror-unrecognizedvalue": "{{doc-apierror}}\n\nParameters:\n* $1 - Parameter name.\n* $2 - Parameter value.",
+ "apierror-unsupportedrepo": "{{doc-apierror}}",
+ "apierror-upload-filekeyneeded": "{{doc-apierror}}",
+ "apierror-upload-filekeynotallowed": "{{doc-apierror}}",
+ "apierror-upload-inprogress": "{{doc-apierror}}",
+ "apierror-upload-missingresult": "{{doc-apierror}}",
+ "apierror-urlparamnormal": "{{doc-apierror}}\n\nParameters:\n* $1 - Image title.",
+ "apierror-writeapidenied": "{{doc-apierror}}",
+ "apiwarn-alldeletedrevisions-performance": "{{doc-apierror}}\n\nParameters:\n* $1 - Module parameter prefix, e.g. \"bl\".",
+ "apiwarn-badurlparam": "{{doc-apierror}}\n\nParameters:\n* $1 - Module parameter prefix, e.g. \"bl\".\n* $2 - Image title.",
+ "apiwarn-badutf8": "{{doc-apierror}}\n\nParameters:\n* $1 - Parameter name.",
+ "apiwarn-checktoken-percentencoding": "{{doc-apierror}}",
+ "apiwarn-deprecation-deletedrevs": "{{doc-apierror}}",
+ "apiwarn-deprecation-expandtemplates-prop": "{{doc-apierror}}",
+ "apiwarn-deprecation-httpsexpected": "{{doc-apierror}}",
+ "apiwarn-deprecation-login-botpw": "{{doc-apierror}}",
+ "apiwarn-deprecation-login-nobotpw": "{{doc-apierror}}",
+ "apiwarn-deprecation-login-token": "{{doc-apierror}}",
+ "apiwarn-deprecation-parameter": "{{doc-apierror}}\n\nParameters:\n* $1 - Parameter name.",
+ "apiwarn-deprecation-parse-headitems": "{{doc-apierror}}",
+ "apiwarn-deprecation-purge-get": "{{doc-apierror}}",
+ "apiwarn-deprecation-withreplacement": "{{doc-apierror}}\n\nParameters:\n* $1 - Query string fragment that is deprecated, e.g. \"action=tokens\".\n* $2 - Query string fragment to use instead, e.g. \"action=tokens\".",
+ "apiwarn-difftohidden": "{{doc-apierror}}\n\nParameters:\n* $1 - Revision ID number.",
+ "apiwarn-errorprinterfailed": "{{doc-apierror}}",
+ "apiwarn-errorprinterfailed-ex": "{{doc-apierror}}\n\nParameters:\n* $1 - Exception message, which may already end in punctuation. Probably in English.",
+ "apiwarn-invalidcategory": "{{doc-apierror}}\n\nParameters:\n* $1 - Supplied category name.",
+ "apiwarn-invalidtitle": "{{doc-apierror}}\n\nParameters:\n* $1 - Supplied title.",
+ "apiwarn-invalidxmlstylesheet": "{{doc-apierror}}",
+ "apiwarn-invalidxmlstylesheetext": "{{doc-apierror}}",
+ "apiwarn-invalidxmlstylesheetns": "{{doc-apierror}}",
+ "apiwarn-moduleswithoutvars": "{{doc-apierror}}",
+ "apiwarn-notfile": "{{doc-apierror}}\n\nParameters:\n* $1 - Supplied file name.",
+ "apiwarn-nothumb-noimagehandler": "{{doc-apierror}}\n\nParameters:\n* $1 - File name.",
+ "apiwarn-parse-nocontentmodel": "{{doc-apierror}}\n\nParameters:\n* $1 - Content model being assumed.",
+ "apiwarn-parse-titlewithouttext": "{{doc-apierror}}",
+ "apiwarn-redirectsandrevids": "{{doc-apierror}}",
+ "apiwarn-tokennotallowed": "{{doc-apierror}}\n\nParameters:\n* $1 - Token type being requested, typically named after the action requiring the token.",
+ "apiwarn-tokens-origin": "{{doc-apierror}}",
+ "apiwarn-toomanyvalues": "{{doc-apierror}}\n\nParameters:\n* $1 - Parameter name.\n* $2 - Maximum number of values allowed.",
+ "apiwarn-truncatedresult": "{{doc-apierror}}\n\nParameters:\n* $1 - Size limit in bytes.",
+ "apiwarn-unclearnowtimestamp": "{{doc-apierror}}\n\nParameters:\n* $1 - Parameter name.\n* $2 - Supplied value.",
+ "apiwarn-unrecognizedvalues": "{{doc-apierror}}\n\nParameters:\n* $1 - Parameter name.\n* $2 - List of unknown values supplied.\n* $3 - Number of unknown values.",
+ "apiwarn-unsupportedarray": "{{doc-apierror}}\n\nParameters:\n* $1 - Parameter name.",
+ "apiwarn-urlparamwidth": "{{doc-apierror}}\n\nParameters:\n* $1 - Module parameter prefix, e.g. \"bl\".\n* $2 - Width being ignored.\n* $3 - Width being used.",
+ "apiwarn-validationfailed": "{{doc-apierror}}\n\nParameters:\n* $1 - User preference name.\n* $2 - Failure message, such as {{msg-mw|apiwarn-validationfailed-badpref}}. Probably already ends with punctuation",
+ "apiwarn-validationfailed-badchars": "{{doc-apierror}}\n\nUsed with {{msg-mw|apiwarn-validationfailed}}.",
+ "apiwarn-validationfailed-badpref": "{{doc-apierror}}\n\nUsed with {{msg-mw|apiwarn-validationfailed}}.",
+ "apiwarn-validationfailed-cannotset": "{{doc-apierror}}\n\nUsed with {{msg-mw|apiwarn-validationfailed}}.",
+ "apiwarn-validationfailed-keytoolong": "{{doc-apierror}}\n\nUsed with {{msg-mw|apiwarn-validationfailed}}.\n\nParameters:\n* $1 - Maximum allowed key length in bytes.",
+ "apiwarn-wgDebugAPI": "{{doc-apierror}}",
+ "api-feed-error-title": "Used as a feed item title when an error occurs in <kbd>action=feedwatchlist</kbd>.\n\nParameters:\n* $1 - API error code",
+ "api-usage-docref": "\n\nParameters:\n* $1 - URL of the API auto-generated documentation.",
+ "api-exception-trace": "\n\nParameters:\n* $1 - Exception class.\n* $2 - File from which the exception was thrown.\n* $3 - Line number from which the exception was thrown.\n* $4 - Exception backtrace.",
"api-credits-header": "Header for the API credits section in the API help output\n{{Identical|Credit}}",
"api-credits": "API credits text, displayed in the API help output"
}
diff --git a/includes/specials/SpecialApiHelp.php b/includes/specials/SpecialApiHelp.php
index 74b474ace46d..54480132e4ca 100644
--- a/includes/specials/SpecialApiHelp.php
+++ b/includes/specials/SpecialApiHelp.php
@@ -77,6 +77,11 @@ class SpecialApiHelp extends UnlistedSpecialPage {
$main = new ApiMain( $this->getContext(), false );
try {
$module = $main->getModuleFromPath( $moduleName );
+ } catch ( ApiUsageException $ex ) {
+ $this->getOutput()->addHTML( Html::rawElement( 'span', [ 'class' => 'error' ],
+ $this->msg( 'apihelp-no-such-module', $moduleName )->inContentLanguage()->parse()
+ ) );
+ return;
} catch ( UsageException $ex ) {
$this->getOutput()->addHTML( Html::rawElement( 'span', [ 'class' => 'error' ],
$this->msg( 'apihelp-no-such-module', $moduleName )->inContentLanguage()->parse()
diff --git a/includes/specials/SpecialEmailuser.php b/includes/specials/SpecialEmailuser.php
index a550e8853bae..c0f1c3dfe0e0 100644
--- a/includes/specials/SpecialEmailuser.php
+++ b/includes/specials/SpecialEmailuser.php
@@ -306,7 +306,7 @@ class SpecialEmailUser extends UnlistedSpecialPage {
* @since 1.20
* @param array $data
* @param HTMLForm $form
- * @return Status|string|bool
+ * @return Status|bool
*/
public static function uiSubmit( array $data, HTMLForm $form ) {
return self::submit( $data, $form->getContext() );
@@ -319,8 +319,7 @@ class SpecialEmailUser extends UnlistedSpecialPage {
*
* @param array $data
* @param IContextSource $context
- * @return Status|string|bool Status object, or potentially a String on error
- * or maybe even true on success if anything uses the EmailUser hook.
+ * @return Status|bool
*/
public static function submit( array $data, IContextSource $context ) {
$config = $context->getConfig();
@@ -328,7 +327,7 @@ class SpecialEmailUser extends UnlistedSpecialPage {
$target = self::getTarget( $data['Target'] );
if ( !$target instanceof User ) {
// Messages used here: notargettext, noemailtext, nowikiemailtext
- return $context->msg( $target . 'text' )->parseAsBlock();
+ return Status::newFatal( $target . 'text' );
}
$to = MailAddress::newFromUser( $target );
@@ -341,9 +340,33 @@ class SpecialEmailUser extends UnlistedSpecialPage {
$text .= $context->msg( 'emailuserfooter',
$from->name, $to->name )->inContentLanguage()->text();
- $error = '';
+ $error = false;
if ( !Hooks::run( 'EmailUser', [ &$to, &$from, &$subject, &$text, &$error ] ) ) {
- return $error;
+ if ( $error instanceof Status ) {
+ return $error;
+ } elseif ( $error === false || $error === '' || $error === [] ) {
+ // Possibly to tell HTMLForm to pretend there was no submission?
+ return false;
+ } elseif ( $error === true ) {
+ // Hook sent the mail itself and indicates success?
+ return Status::newGood();
+ } elseif ( is_array( $error ) ) {
+ $status = Status::newGood();
+ foreach ( $error as $e ) {
+ $status->fatal( $e );
+ }
+ return $status;
+ } elseif ( $error instanceof MessageSpecifier ) {
+ return Status::newFatal( $error );
+ } else {
+ // Ugh. Either a raw HTML string, or something that's supposed
+ // to be treated like one.
+ $type = is_object( $error ) ? get_class( $error ) : gettype( $error );
+ wfDeprecated( "EmailUser hook returning a $type as \$error", '1.29' );
+ return Status::newFatal( new ApiRawMessage(
+ [ '$1', Message::rawParam( (string)$error ) ], 'hookaborted'
+ ) );
+ }
}
if ( $config->get( 'UserEmailUseReplyTo' ) ) {
diff --git a/includes/specials/SpecialUnblock.php b/includes/specials/SpecialUnblock.php
index cff8bf463abe..a8b5e5e5ed76 100644
--- a/includes/specials/SpecialUnblock.php
+++ b/includes/specials/SpecialUnblock.php
@@ -175,7 +175,7 @@ class SpecialUnblock extends SpecialPage {
* @param array $data
* @param IContextSource $context
* @throws ErrorPageError
- * @return array|bool Array(message key, parameters) on failure, True on success
+ * @return array|bool Array( Array( message key, parameters ) ) on failure, True on success
*/
public static function processUnblock( array $data, IContextSource $context ) {
$performer = $context->getUser();
@@ -211,7 +211,7 @@ class SpecialUnblock extends SpecialPage {
# Delete block
if ( !$block->delete() ) {
- return [ 'ipb_cant_unblock', htmlspecialchars( $block->getTarget() ) ];
+ return [ [ 'ipb_cant_unblock', htmlspecialchars( $block->getTarget() ) ] ];
}
# Unset _deleted fields as needed
diff --git a/languages/i18n/en.json b/languages/i18n/en.json
index afd13f0016d3..dad1737df9ad 100644
--- a/languages/i18n/en.json
+++ b/languages/i18n/en.json
@@ -1303,11 +1303,13 @@
"action-upload_by_url": "upload this file from a URL",
"action-writeapi": "use the write API",
"action-delete": "delete this page",
- "action-deleterevision": "delete this revision",
- "action-deletedhistory": "view this page's deleted history",
+ "action-deleterevision": "delete revisions",
+ "action-deletelogentry": "delete log entries",
+ "action-deletedhistory": "view a page's deleted history",
+ "action-deletedtext": "view deleted revision text",
"action-browsearchive": "search deleted pages",
- "action-undelete": "undelete this page",
- "action-suppressrevision": "review and restore this hidden revision",
+ "action-undelete": "undelete pages",
+ "action-suppressrevision": "review and restore hidden revisions",
"action-suppressionlog": "view this private log",
"action-block": "block this user from editing",
"action-protect": "change protection levels for this page",
@@ -1322,6 +1324,7 @@
"action-userrights-interwiki": "edit user rights of users on other wikis",
"action-siteadmin": "lock or unlock the database",
"action-sendemail": "send emails",
+ "action-editmyoptions": "edit your preferences",
"action-editmywatchlist": "edit your watchlist",
"action-viewmywatchlist": "view your watchlist",
"action-viewmyprivateinfo": "view your private information",
@@ -4244,5 +4247,7 @@
"usercssispublic": "Please note: CSS subpages should not contain confidential data as they are viewable by other users.",
"restrictionsfield-badip": "Invalid IP address or range: $1",
"restrictionsfield-label": "Allowed IP ranges:",
- "restrictionsfield-help": "One IP address or CIDR range per line. To enable everything, use<br><code>0.0.0.0/0</code><br><code>::/0</code>"
+ "restrictionsfield-help": "One IP address or CIDR range per line. To enable everything, use<br><code>0.0.0.0/0</code><br><code>::/0</code>",
+ "revid": "r$1",
+ "pageid": "page ID $1"
}
diff --git a/languages/i18n/qqq.json b/languages/i18n/qqq.json
index 936fd8b8acfb..bc4bc4ced473 100644
--- a/languages/i18n/qqq.json
+++ b/languages/i18n/qqq.json
@@ -1489,6 +1489,8 @@
"action-delete": "{{Doc-action|delete}}",
"action-deleterevision": "{{Doc-action|deleterevision}}",
"action-deletedhistory": "{{Doc-action|deletedhistory}}",
+ "action-deletedtext": "{{Doc-action|deletedtext}}",
+ "action-deletelogentry": "{{Doc-action|deletelogentry}}",
"action-browsearchive": "{{Doc-action|browsearchive}}",
"action-undelete": "{{Doc-action|undelete}}",
"action-suppressrevision": "{{Doc-action|suppressrevision}}",
@@ -1508,6 +1510,7 @@
"action-sendemail": "{{doc-action|sendemail}}\n{{Identical|E-mail}}",
"action-editmywatchlist": "{{doc-action|editmywatchlist}}\n{{Identical|Edit your watchlist}}",
"action-viewmywatchlist": "{{doc-action|viewmywatchlist}}\n{{Identical|View your watchlist}}",
+ "action-editmyoptions": "{{Doc-action|editmyoptions}}",
"action-viewmyprivateinfo": "{{doc-action|viewmyprivateinfo}}",
"action-editmyprivateinfo": "{{doc-action|editmyprivateinfo}}",
"action-editcontentmodel": "{{doc-action|editcontentmodel}}",
@@ -4428,5 +4431,7 @@
"usercssispublic": "A reminder to users that CSS subpages are not preferences but normal pages, and thus can be viewed by other users and the general public. This message is shown to a user whenever they are editing a subpage in their own user-space that ends in .css. See also {{msg-mw|userjsispublic}}",
"restrictionsfield-badip": "An error message shown when one entered an invalid IP address or range in a restrictions field (such as Special:BotPassword). $1 is the IP address.",
"restrictionsfield-label": "Field label shown for restriction fields (e.g. on Special:BotPassword).",
- "restrictionsfield-help": "Placeholder text displayed in restriction fields (e.g. on Special:BotPassword)."
+ "restrictionsfield-help": "Placeholder text displayed in restriction fields (e.g. on Special:BotPassword).",
+ "revid": "Used to format a revision ID number in text. Parameters:\n* $1 - Revision ID number.",
+ "pageid": "Used to format a page ID number in text. Parameters:\n* $1 - Page ID number."
}
diff --git a/resources/src/mediawiki/page/rollback.js b/resources/src/mediawiki/page/rollback.js
index cb46b110dc40..d94b158538b7 100644
--- a/resources/src/mediawiki/page/rollback.js
+++ b/resources/src/mediawiki/page/rollback.js
@@ -30,6 +30,7 @@
$spinner = $.createSpinner( { size: 'small', type: 'inline' } );
$link.hide().after( $spinner );
+ // @todo: data.messageHtml is no more. Convert to using errorformat=html.
api = new mw.Api();
api.rollback( page, user )
.then( function ( data ) {
diff --git a/tests/phpunit/includes/WatchedItemQueryServiceUnitTest.php b/tests/phpunit/includes/WatchedItemQueryServiceUnitTest.php
index 93687df2d5bf..bdec0a50d90f 100644
--- a/tests/phpunit/includes/WatchedItemQueryServiceUnitTest.php
+++ b/tests/phpunit/includes/WatchedItemQueryServiceUnitTest.php
@@ -1233,7 +1233,7 @@ class WatchedItemQueryServiceUnitTest extends PHPUnit_Framework_TestCase {
->with( 'watchlisttoken' )
->willReturn( '0123456789abcdef' );
- $this->setExpectedException( UsageException::class, 'Incorrect watchlist token provided' );
+ $this->setExpectedException( ApiUsageException::class, 'Incorrect watchlist token provided' );
$queryService->getWatchedItemsWithRecentChangeInfo(
$user,
[ 'watchlistOwner' => $otherUser, 'watchlistOwnerToken' => $token ]
diff --git a/tests/phpunit/includes/api/ApiBaseTest.php b/tests/phpunit/includes/api/ApiBaseTest.php
index 8b75d562816f..96f3e44f0f21 100644
--- a/tests/phpunit/includes/api/ApiBaseTest.php
+++ b/tests/phpunit/includes/api/ApiBaseTest.php
@@ -20,7 +20,7 @@ class ApiBaseTest extends ApiTestCase {
}
/**
- * @expectedException UsageException
+ * @expectedException ApiUsageException
* @covers ApiBase::requireOnlyOneParameter
*/
public function testRequireOnlyOneParameterZero() {
@@ -32,7 +32,7 @@ class ApiBaseTest extends ApiTestCase {
}
/**
- * @expectedException UsageException
+ * @expectedException ApiUsageException
* @covers ApiBase::requireOnlyOneParameter
*/
public function testRequireOnlyOneParameterTrue() {
@@ -58,10 +58,10 @@ class ApiBaseTest extends ApiTestCase {
$context->setRequest( new FauxRequest( $input !== null ? [ 'foo' => $input ] : [] ) );
$wrapper->mMainModule = new ApiMain( $context );
- if ( $expected instanceof UsageException ) {
+ if ( $expected instanceof ApiUsageException ) {
try {
$wrapper->getParameterFromSettings( 'foo', $paramSettings, true );
- } catch ( UsageException $ex ) {
+ } catch ( ApiUsageException $ex ) {
$this->assertEquals( $expected, $ex );
}
} else {
@@ -73,9 +73,7 @@ class ApiBaseTest extends ApiTestCase {
public static function provideGetParameterFromSettings() {
$warnings = [
- 'The value passed for \'foo\' contains invalid or non-normalized data. Textual data should ' .
- 'be valid, NFC-normalized Unicode without C0 control characters other than ' .
- 'HT (\\t), LF (\\n), and CR (\\r).'
+ [ 'apiwarn-badutf8', 'foo' ],
];
$c0 = '';
@@ -96,7 +94,7 @@ class ApiBaseTest extends ApiTestCase {
'String param, required, empty' => [
'',
[ ApiBase::PARAM_DFLT => 'default', ApiBase::PARAM_REQUIRED => true ],
- new UsageException( 'The foo parameter must be set', 'nofoo' ),
+ ApiUsageException::newWithMessage( null, [ 'apierror-missingparam', 'foo' ] ),
[]
],
'Multi-valued parameter' => [
@@ -126,4 +124,48 @@ class ApiBaseTest extends ApiTestCase {
];
}
+ public function testErrorArrayToStatus() {
+ $mock = new MockApi();
+
+ // Sanity check empty array
+ $expect = Status::newGood();
+ $this->assertEquals( $expect, $mock->errorArrayToStatus( [] ) );
+
+ // No blocked $user, so no special block handling
+ $expect = Status::newGood();
+ $expect->fatal( 'blockedtext' );
+ $expect->fatal( 'autoblockedtext' );
+ $expect->fatal( 'mainpage' );
+ $expect->fatal( 'parentheses', 'foobar' );
+ $this->assertEquals( $expect, $mock->errorArrayToStatus( [
+ [ 'blockedtext' ],
+ [ 'autoblockedtext' ],
+ 'mainpage',
+ [ 'parentheses', 'foobar' ],
+ ] ) );
+
+ // Has a blocked $user, so special block handling
+ $user = $this->getMutableTestUser()->getUser();
+ $block = new \Block( [
+ 'address' => $user->getName(),
+ 'user' => $user->getID(),
+ 'reason' => __METHOD__,
+ 'expiry' => time() + 100500,
+ ] );
+ $block->insert();
+ $blockinfo = [ 'blockinfo' => ApiQueryUserInfo::getBlockInfo( $block ) ];
+
+ $expect = Status::newGood();
+ $expect->fatal( ApiMessage::create( 'apierror-blocked', 'blocked', $blockinfo ) );
+ $expect->fatal( ApiMessage::create( 'apierror-autoblocked', 'autoblocked', $blockinfo ) );
+ $expect->fatal( 'mainpage' );
+ $expect->fatal( 'parentheses', 'foobar' );
+ $this->assertEquals( $expect, $mock->errorArrayToStatus( [
+ [ 'blockedtext' ],
+ [ 'autoblockedtext' ],
+ 'mainpage',
+ [ 'parentheses', 'foobar' ],
+ ], $user ) );
+ }
+
}
diff --git a/tests/phpunit/includes/api/ApiBlockTest.php b/tests/phpunit/includes/api/ApiBlockTest.php
index d2dccf997d27..08fc1286cf1f 100644
--- a/tests/phpunit/includes/api/ApiBlockTest.php
+++ b/tests/phpunit/includes/api/ApiBlockTest.php
@@ -65,8 +65,8 @@ class ApiBlockTest extends ApiTestCase {
}
/**
- * @expectedException UsageException
- * @expectedExceptionMessage The token parameter must be set
+ * @expectedException ApiUsageException
+ * @expectedExceptionMessage The "token" parameter must be set
*/
public function testBlockingActionWithNoToken() {
$this->doApiRequest(
diff --git a/tests/phpunit/includes/api/ApiContinuationManagerTest.php b/tests/phpunit/includes/api/ApiContinuationManagerTest.php
index 3ad16d132224..bb4ea7589373 100644
--- a/tests/phpunit/includes/api/ApiContinuationManagerTest.php
+++ b/tests/phpunit/includes/api/ApiContinuationManagerTest.php
@@ -160,10 +160,8 @@ class ApiContinuationManagerTest extends MediaWikiTestCase {
try {
self::getManager( 'foo', $allModules, [ 'mock1', 'mock2' ] );
$this->fail( 'Expected exception not thrown' );
- } catch ( UsageException $ex ) {
- $this->assertSame(
- 'Invalid continue param. You should pass the original value returned by the previous query',
- $ex->getMessage(),
+ } catch ( ApiUsageException $ex ) {
+ $this->assertTrue( ApiTestCase::apiExceptionHasCode( $ex, 'badcontinue' ),
'Expected exception'
);
}
diff --git a/tests/phpunit/includes/api/ApiEditPageTest.php b/tests/phpunit/includes/api/ApiEditPageTest.php
index 02d0a0dc57af..0ffcbca762d6 100644
--- a/tests/phpunit/includes/api/ApiEditPageTest.php
+++ b/tests/phpunit/includes/api/ApiEditPageTest.php
@@ -195,9 +195,9 @@ class ApiEditPageTest extends ApiTestCase {
'section' => '9999',
'text' => 'text',
] );
- $this->fail( "Should have raised a UsageException" );
- } catch ( UsageException $e ) {
- $this->assertEquals( 'nosuchsection', $e->getCodeString() );
+ $this->fail( "Should have raised an ApiUsageException" );
+ } catch ( ApiUsageException $e ) {
+ $this->assertTrue( self::apiExceptionHasCode( $e, 'nosuchsection' ) );
}
}
@@ -333,8 +333,8 @@ class ApiEditPageTest extends ApiTestCase {
], null, self::$users['sysop']->getUser() );
$this->fail( 'redirect-appendonly error expected' );
- } catch ( UsageException $ex ) {
- $this->assertEquals( 'redirect-appendonly', $ex->getCodeString() );
+ } catch ( ApiUsageException $ex ) {
+ $this->assertTrue( self::apiExceptionHasCode( $ex, 'redirect-appendonly' ) );
}
}
@@ -369,8 +369,8 @@ class ApiEditPageTest extends ApiTestCase {
], null, self::$users['sysop']->getUser() );
$this->fail( 'edit conflict expected' );
- } catch ( UsageException $ex ) {
- $this->assertEquals( 'editconflict', $ex->getCodeString() );
+ } catch ( ApiUsageException $ex ) {
+ $this->assertTrue( self::apiExceptionHasCode( $ex, 'editconflict' ) );
}
}
@@ -474,7 +474,7 @@ class ApiEditPageTest extends ApiTestCase {
public function testCheckDirectApiEditingDisallowed_forNonTextContent() {
$this->setExpectedException(
- 'UsageException',
+ 'ApiUsageException',
'Direct editing via API is not supported for content model ' .
'testing used by Dummy:ApiEditPageTest_nonTextPageEdit'
);
diff --git a/tests/phpunit/includes/api/ApiErrorFormatterTest.php b/tests/phpunit/includes/api/ApiErrorFormatterTest.php
index d13b00be2e1a..1b7f6bff5737 100644
--- a/tests/phpunit/includes/api/ApiErrorFormatterTest.php
+++ b/tests/phpunit/includes/api/ApiErrorFormatterTest.php
@@ -7,6 +7,30 @@ class ApiErrorFormatterTest extends MediaWikiLangTestCase {
/**
* @covers ApiErrorFormatter
+ */
+ public function testErrorFormatterBasics() {
+ $result = new ApiResult( 8388608 );
+ $formatter = new ApiErrorFormatter( $result, Language::factory( 'de' ), 'wikitext', false );
+ $this->assertSame( 'de', $formatter->getLanguage()->getCode() );
+
+ $formatter->addMessagesFromStatus( null, Status::newGood() );
+ $this->assertSame(
+ [ ApiResult::META_TYPE => 'assoc' ],
+ $result->getResultData()
+ );
+
+ $this->assertSame( [], $formatter->arrayFromStatus( Status::newGood() ) );
+
+ $wrappedFormatter = TestingAccessWrapper::newFromObject( $formatter );
+ $this->assertSame(
+ 'Blah "kbd" <X> 😊',
+ $wrappedFormatter->stripMarkup( 'Blah <kbd>kbd</kbd> <b>&lt;X&gt;</b> &#x1f60a;' ),
+ 'stripMarkup'
+ );
+ }
+
+ /**
+ * @covers ApiErrorFormatter
* @dataProvider provideErrorFormatter
*/
public function testErrorFormatter( $format, $lang, $useDB,
@@ -22,7 +46,7 @@ class ApiErrorFormatterTest extends MediaWikiLangTestCase {
$formatter->addWarning( 'string', 'mainpage' );
$formatter->addError( 'err', 'mainpage' );
- $this->assertSame( $expect1, $result->getResultData(), 'Simple test' );
+ $this->assertEquals( $expect1, $result->getResultData(), 'Simple test' );
$result->reset();
$formatter->addWarning( 'foo', 'mainpage' );
@@ -35,6 +59,17 @@ class ApiErrorFormatterTest extends MediaWikiLangTestCase {
$formatter->addError( 'errWithData', $msg2 );
$this->assertSame( $expect2, $result->getResultData(), 'Complex test' );
+ $this->assertEquals(
+ $this->removeModuleTag( $expect2['warnings'][2] ),
+ $formatter->formatMessage( $msg1 ),
+ 'formatMessage test 1'
+ );
+ $this->assertEquals(
+ $this->removeModuleTag( $expect2['warnings'][3] ),
+ $formatter->formatMessage( $msg2 ),
+ 'formatMessage test 2'
+ );
+
$result->reset();
$status = Status::newGood();
$status->warning( 'mainpage' );
@@ -47,245 +82,256 @@ class ApiErrorFormatterTest extends MediaWikiLangTestCase {
$this->assertSame( $expect3, $result->getResultData(), 'Status test' );
$this->assertSame(
- $expect3['errors']['status'],
+ array_map( [ $this, 'removeModuleTag' ], $expect3['errors'] ),
$formatter->arrayFromStatus( $status, 'error' ),
'arrayFromStatus test for error'
);
$this->assertSame(
- $expect3['warnings']['status'],
+ array_map( [ $this, 'removeModuleTag' ], $expect3['warnings'] ),
$formatter->arrayFromStatus( $status, 'warning' ),
'arrayFromStatus test for warning'
);
}
+ private function removeModuleTag( $s ) {
+ if ( is_array( $s ) ) {
+ unset( $s['module'] );
+ }
+ return $s;
+ }
+
public static function provideErrorFormatter() {
- $mainpagePlain = wfMessage( 'mainpage' )->useDatabase( false )->plain();
- $parensPlain = wfMessage( 'parentheses', 'foobar' )->useDatabase( false )->plain();
- $mainpageText = wfMessage( 'mainpage' )->inLanguage( 'de' )->text();
- $parensText = wfMessage( 'parentheses', 'foobar' )->inLanguage( 'de' )->text();
+ $mainpageText = wfMessage( 'mainpage' )->inLanguage( 'de' )->useDatabase( false )->text();
+ $parensText = wfMessage( 'parentheses', 'foobar' )->inLanguage( 'de' )
+ ->useDatabase( false )->text();
+ $mainpageHTML = wfMessage( 'mainpage' )->inLanguage( 'en' )->parse();
+ $parensHTML = wfMessage( 'parentheses', 'foobar' )->inLanguage( 'en' )->parse();
$C = ApiResult::META_CONTENT;
$I = ApiResult::META_INDEXED_TAG_NAME;
+ $overriddenData = [ 'overriddenData' => true, ApiResult::META_TYPE => 'assoc' ];
return [
- [ 'wikitext', 'de', true,
+ $tmp = [ 'wikitext', 'de', false,
[
'errors' => [
- 'err' => [
- [ 'code' => 'mainpage', 'text' => $mainpageText, $C => 'text' ],
- $I => 'error',
- ],
+ [ 'code' => 'mainpage', 'text' => $mainpageText, 'module' => 'err', $C => 'text' ],
+ $I => 'error',
],
'warnings' => [
- 'string' => [
- [ 'code' => 'mainpage', 'text' => $mainpageText, $C => 'text' ],
- $I => 'warning',
- ],
+ [ 'code' => 'mainpage', 'text' => $mainpageText, 'module' => 'string', $C => 'text' ],
+ $I => 'warning',
],
],
[
'errors' => [
- 'errWithData' => [
- [ 'code' => 'overriddenCode', 'text' => $mainpageText,
- 'overriddenData' => true, $C => 'text' ],
- $I => 'error',
- ],
+ [ 'code' => 'overriddenCode', 'text' => $mainpageText,
+ 'data' => $overriddenData, 'module' => 'errWithData', $C => 'text' ],
+ $I => 'error',
],
'warnings' => [
- 'messageWithData' => [
- [ 'code' => 'overriddenCode', 'text' => $mainpageText,
- 'overriddenData' => true, $C => 'text' ],
- $I => 'warning',
- ],
- 'message' => [
- [ 'code' => 'mainpage', 'text' => $mainpageText, $C => 'text' ],
- $I => 'warning',
- ],
- 'foo' => [
- [ 'code' => 'mainpage', 'text' => $mainpageText, $C => 'text' ],
- [ 'code' => 'parentheses', 'text' => $parensText, $C => 'text' ],
- $I => 'warning',
- ],
+ [ 'code' => 'mainpage', 'text' => $mainpageText, 'module' => 'foo', $C => 'text' ],
+ [ 'code' => 'parentheses', 'text' => $parensText, 'module' => 'foo', $C => 'text' ],
+ [ 'code' => 'mainpage', 'text' => $mainpageText, 'module' => 'message', $C => 'text' ],
+ [ 'code' => 'overriddenCode', 'text' => $mainpageText,
+ 'data' => $overriddenData, 'module' => 'messageWithData', $C => 'text' ],
+ $I => 'warning',
],
],
[
'errors' => [
- 'status' => [
- [ 'code' => 'mainpage', 'text' => $mainpageText, $C => 'text' ],
- [ 'code' => 'parentheses', 'text' => $parensText, $C => 'text' ],
- $I => 'error',
- ],
+ [ 'code' => 'mainpage', 'text' => $mainpageText, 'module' => 'status', $C => 'text' ],
+ [ 'code' => 'parentheses', 'text' => $parensText, 'module' => 'status', $C => 'text' ],
+ $I => 'error',
],
'warnings' => [
- 'status' => [
- [ 'code' => 'mainpage', 'text' => $mainpageText, $C => 'text' ],
- [ 'code' => 'parentheses', 'text' => $parensText, $C => 'text' ],
- [ 'code' => 'overriddenCode', 'text' => $mainpageText,
- 'overriddenData' => true, $C => 'text' ],
- $I => 'warning',
- ],
+ [ 'code' => 'mainpage', 'text' => $mainpageText, 'module' => 'status', $C => 'text' ],
+ [ 'code' => 'parentheses', 'text' => $parensText, 'module' => 'status', $C => 'text' ],
+ [ 'code' => 'overriddenCode', 'text' => $mainpageText,
+ 'data' => $overriddenData, 'module' => 'status', $C => 'text' ],
+ $I => 'warning',
+ ],
+ ],
+ ],
+ [ 'plaintext' ] + $tmp, // For these messages, plaintext and wikitext are the same
+ [ 'html', 'en', true,
+ [
+ 'errors' => [
+ [ 'code' => 'mainpage', 'html' => $mainpageHTML, 'module' => 'err', $C => 'html' ],
+ $I => 'error',
+ ],
+ 'warnings' => [
+ [ 'code' => 'mainpage', 'html' => $mainpageHTML, 'module' => 'string', $C => 'html' ],
+ $I => 'warning',
+ ],
+ ],
+ [
+ 'errors' => [
+ [ 'code' => 'overriddenCode', 'html' => $mainpageHTML,
+ 'data' => $overriddenData, 'module' => 'errWithData', $C => 'html' ],
+ $I => 'error',
+ ],
+ 'warnings' => [
+ [ 'code' => 'mainpage', 'html' => $mainpageHTML, 'module' => 'foo', $C => 'html' ],
+ [ 'code' => 'parentheses', 'html' => $parensHTML, 'module' => 'foo', $C => 'html' ],
+ [ 'code' => 'mainpage', 'html' => $mainpageHTML, 'module' => 'message', $C => 'html' ],
+ [ 'code' => 'overriddenCode', 'html' => $mainpageHTML,
+ 'data' => $overriddenData, 'module' => 'messageWithData', $C => 'html' ],
+ $I => 'warning',
+ ],
+ ],
+ [
+ 'errors' => [
+ [ 'code' => 'mainpage', 'html' => $mainpageHTML, 'module' => 'status', $C => 'html' ],
+ [ 'code' => 'parentheses', 'html' => $parensHTML, 'module' => 'status', $C => 'html' ],
+ $I => 'error',
+ ],
+ 'warnings' => [
+ [ 'code' => 'mainpage', 'html' => $mainpageHTML, 'module' => 'status', $C => 'html' ],
+ [ 'code' => 'parentheses', 'html' => $parensHTML, 'module' => 'status', $C => 'html' ],
+ [ 'code' => 'overriddenCode', 'html' => $mainpageHTML,
+ 'data' => $overriddenData, 'module' => 'status', $C => 'html' ],
+ $I => 'warning',
],
],
],
[ 'raw', 'fr', true,
[
'errors' => [
- 'err' => [
- [
- 'code' => 'mainpage',
- 'key' => 'mainpage',
- 'params' => [ $I => 'param' ]
- ],
- $I => 'error',
+ [
+ 'code' => 'mainpage',
+ 'key' => 'mainpage',
+ 'params' => [ $I => 'param' ],
+ 'module' => 'err',
],
+ $I => 'error',
],
'warnings' => [
- 'string' => [
- [
- 'code' => 'mainpage',
- 'key' => 'mainpage',
- 'params' => [ $I => 'param' ]
- ],
- $I => 'warning',
+ [
+ 'code' => 'mainpage',
+ 'key' => 'mainpage',
+ 'params' => [ $I => 'param' ],
+ 'module' => 'string',
],
+ $I => 'warning',
],
],
[
'errors' => [
- 'errWithData' => [
- [
- 'code' => 'overriddenCode',
- 'key' => 'mainpage',
- 'params' => [ $I => 'param' ],
- 'overriddenData' => true
- ],
- $I => 'error',
+ [
+ 'code' => 'overriddenCode',
+ 'key' => 'mainpage',
+ 'params' => [ $I => 'param' ],
+ 'data' => $overriddenData,
+ 'module' => 'errWithData',
],
+ $I => 'error',
],
'warnings' => [
- 'messageWithData' => [
- [
- 'code' => 'overriddenCode',
- 'key' => 'mainpage',
- 'params' => [ $I => 'param' ],
- 'overriddenData' => true
- ],
- $I => 'warning',
+ [
+ 'code' => 'mainpage',
+ 'key' => 'mainpage',
+ 'params' => [ $I => 'param' ],
+ 'module' => 'foo',
+ ],
+ [
+ 'code' => 'parentheses',
+ 'key' => 'parentheses',
+ 'params' => [ 'foobar', $I => 'param' ],
+ 'module' => 'foo',
],
- 'message' => [
- [
- 'code' => 'mainpage',
- 'key' => 'mainpage',
- 'params' => [ $I => 'param' ]
- ],
- $I => 'warning',
+ [
+ 'code' => 'mainpage',
+ 'key' => 'mainpage',
+ 'params' => [ $I => 'param' ],
+ 'module' => 'message',
],
- 'foo' => [
- [
- 'code' => 'mainpage',
- 'key' => 'mainpage',
- 'params' => [ $I => 'param' ]
- ],
- [
- 'code' => 'parentheses',
- 'key' => 'parentheses',
- 'params' => [ 'foobar', $I => 'param' ]
- ],
- $I => 'warning',
+ [
+ 'code' => 'overriddenCode',
+ 'key' => 'mainpage',
+ 'params' => [ $I => 'param' ],
+ 'data' => $overriddenData,
+ 'module' => 'messageWithData',
],
+ $I => 'warning',
],
],
[
'errors' => [
- 'status' => [
- [
- 'code' => 'mainpage',
- 'key' => 'mainpage',
- 'params' => [ $I => 'param' ]
- ],
- [
- 'code' => 'parentheses',
- 'key' => 'parentheses',
- 'params' => [ 'foobar', $I => 'param' ]
- ],
- $I => 'error',
+ [
+ 'code' => 'mainpage',
+ 'key' => 'mainpage',
+ 'params' => [ $I => 'param' ],
+ 'module' => 'status',
+ ],
+ [
+ 'code' => 'parentheses',
+ 'key' => 'parentheses',
+ 'params' => [ 'foobar', $I => 'param' ],
+ 'module' => 'status',
],
+ $I => 'error',
],
'warnings' => [
- 'status' => [
- [
- 'code' => 'mainpage',
- 'key' => 'mainpage',
- 'params' => [ $I => 'param' ]
- ],
- [
- 'code' => 'parentheses',
- 'key' => 'parentheses',
- 'params' => [ 'foobar', $I => 'param' ]
- ],
- [
- 'code' => 'overriddenCode',
- 'key' => 'mainpage',
- 'params' => [ $I => 'param' ],
- 'overriddenData' => true
- ],
- $I => 'warning',
+ [
+ 'code' => 'mainpage',
+ 'key' => 'mainpage',
+ 'params' => [ $I => 'param' ],
+ 'module' => 'status',
],
+ [
+ 'code' => 'parentheses',
+ 'key' => 'parentheses',
+ 'params' => [ 'foobar', $I => 'param' ],
+ 'module' => 'status',
+ ],
+ [
+ 'code' => 'overriddenCode',
+ 'key' => 'mainpage',
+ 'params' => [ $I => 'param' ],
+ 'data' => $overriddenData,
+ 'module' => 'status',
+ ],
+ $I => 'warning',
],
],
],
[ 'none', 'fr', true,
[
'errors' => [
- 'err' => [
- [ 'code' => 'mainpage' ],
- $I => 'error',
- ],
+ [ 'code' => 'mainpage', 'module' => 'err' ],
+ $I => 'error',
],
'warnings' => [
- 'string' => [
- [ 'code' => 'mainpage' ],
- $I => 'warning',
- ],
+ [ 'code' => 'mainpage', 'module' => 'string' ],
+ $I => 'warning',
],
],
[
'errors' => [
- 'errWithData' => [
- [ 'code' => 'overriddenCode', 'overriddenData' => true ],
- $I => 'error',
- ],
+ [ 'code' => 'overriddenCode', 'data' => $overriddenData,
+ 'module' => 'errWithData' ],
+ $I => 'error',
],
'warnings' => [
- 'messageWithData' => [
- [ 'code' => 'overriddenCode', 'overriddenData' => true ],
- $I => 'warning',
- ],
- 'message' => [
- [ 'code' => 'mainpage' ],
- $I => 'warning',
- ],
- 'foo' => [
- [ 'code' => 'mainpage' ],
- [ 'code' => 'parentheses' ],
- $I => 'warning',
- ],
+ [ 'code' => 'mainpage', 'module' => 'foo' ],
+ [ 'code' => 'parentheses', 'module' => 'foo' ],
+ [ 'code' => 'mainpage', 'module' => 'message' ],
+ [ 'code' => 'overriddenCode', 'data' => $overriddenData,
+ 'module' => 'messageWithData' ],
+ $I => 'warning',
],
],
[
'errors' => [
- 'status' => [
- [ 'code' => 'mainpage' ],
- [ 'code' => 'parentheses' ],
- $I => 'error',
- ],
+ [ 'code' => 'mainpage', 'module' => 'status' ],
+ [ 'code' => 'parentheses', 'module' => 'status' ],
+ $I => 'error',
],
'warnings' => [
- 'status' => [
- [ 'code' => 'mainpage' ],
- [ 'code' => 'parentheses' ],
- [ 'code' => 'overriddenCode', 'overriddenData' => true ],
- $I => 'warning',
- ],
+ [ 'code' => 'mainpage', 'module' => 'status' ],
+ [ 'code' => 'parentheses', 'module' => 'status' ],
+ [ 'code' => 'overriddenCode', 'data' => $overriddenData, 'module' => 'status' ],
+ $I => 'warning',
],
],
],
@@ -302,7 +348,14 @@ class ApiErrorFormatterTest extends MediaWikiLangTestCase {
$result = new ApiResult( 8388608 );
$formatter = new ApiErrorFormatter_BackCompat( $result );
+ $this->assertSame( 'en', $formatter->getLanguage()->getCode() );
+
+ $this->assertSame( [], $formatter->arrayFromStatus( Status::newGood() ) );
+
$formatter->addWarning( 'string', 'mainpage' );
+ $formatter->addWarning( 'raw',
+ new RawMessage( 'Blah <kbd>kbd</kbd> <b>&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',
],
diff --git a/tests/phpunit/includes/api/ApiMainTest.php b/tests/phpunit/includes/api/ApiMainTest.php
index c111949d2fae..c9a3428da159 100644
--- a/tests/phpunit/includes/api/ApiMainTest.php
+++ b/tests/phpunit/includes/api/ApiMainTest.php
@@ -53,8 +53,8 @@ class ApiMainTest extends ApiTestCase {
'assert' => $assert,
], null, null, $user );
$this->assertFalse( $error ); // That no error was expected
- } catch ( UsageException $e ) {
- $this->assertEquals( $e->getCodeString(), $error );
+ } catch ( ApiUsageException $e ) {
+ $this->assertTrue( self::apiExceptionHasCode( $e, $error ) );
}
}
@@ -76,8 +76,8 @@ class ApiMainTest extends ApiTestCase {
'assertuser' => $user->getName() . 'X',
], null, null, $user );
$this->fail( 'Expected exception not thrown' );
- } catch ( UsageException $e ) {
- $this->assertEquals( $e->getCodeString(), 'assertnameduserfailed' );
+ } catch ( ApiUsageException $e ) {
+ $this->assertTrue( self::apiExceptionHasCode( $e, 'assertnameduserfailed' ) );
}
}
@@ -305,4 +305,274 @@ class ApiMainTest extends ApiTestCase {
$main = new ApiMain( new FauxRequest( [ 'action' => 'query', 'meta' => 'siteinfo' ] ) );
$this->assertTrue( $main->lacksSameOriginSecurity(), 'Hook, should lack security' );
}
+
+ /**
+ * Test proper creation of the ApiErrorFormatter
+ * @covers ApiMain::__construct
+ * @dataProvider provideApiErrorFormatterCreation
+ * @param array $request Request parameters
+ * @param array $expect Expected data
+ * - uselang: ApiMain language
+ * - class: ApiErrorFormatter class
+ * - lang: ApiErrorFormatter language
+ * - format: ApiErrorFormatter format
+ * - usedb: ApiErrorFormatter use-database flag
+ */
+ public function testApiErrorFormatterCreation( array $request, array $expect ) {
+ $context = new RequestContext();
+ $context->setRequest( new FauxRequest( $request ) );
+ $context->setLanguage( 'ru' );
+
+ $main = new ApiMain( $context );
+ $formatter = $main->getErrorFormatter();
+ $wrappedFormatter = TestingAccessWrapper::newFromObject( $formatter );
+
+ $this->assertSame( $expect['uselang'], $main->getLanguage()->getCode() );
+ $this->assertInstanceOf( $expect['class'], $formatter );
+ $this->assertSame( $expect['lang'], $formatter->getLanguage()->getCode() );
+ $this->assertSame( $expect['format'], $wrappedFormatter->format );
+ $this->assertSame( $expect['usedb'], $wrappedFormatter->useDB );
+ }
+
+ public static function provideApiErrorFormatterCreation() {
+ global $wgContLang;
+
+ return [
+ 'Default (BC)' => [ [], [
+ 'uselang' => 'ru',
+ 'class' => ApiErrorFormatter_BackCompat::class,
+ 'lang' => 'en',
+ 'format' => 'none',
+ 'usedb' => false,
+ ] ],
+ 'BC ignores fields' => [ [ 'errorlang' => 'de', 'errorsuselocal' => 1 ], [
+ 'uselang' => 'ru',
+ 'class' => ApiErrorFormatter_BackCompat::class,
+ 'lang' => 'en',
+ 'format' => 'none',
+ 'usedb' => false,
+ ] ],
+ 'Explicit BC' => [ [ 'errorformat' => 'bc' ], [
+ 'uselang' => 'ru',
+ 'class' => ApiErrorFormatter_BackCompat::class,
+ 'lang' => 'en',
+ 'format' => 'none',
+ 'usedb' => false,
+ ] ],
+ 'Basic' => [ [ 'errorformat' => 'wikitext' ], [
+ 'uselang' => 'ru',
+ 'class' => ApiErrorFormatter::class,
+ 'lang' => 'ru',
+ 'format' => 'wikitext',
+ 'usedb' => false,
+ ] ],
+ 'Follows uselang' => [ [ 'uselang' => 'fr', 'errorformat' => 'plaintext' ], [
+ 'uselang' => 'fr',
+ 'class' => ApiErrorFormatter::class,
+ 'lang' => 'fr',
+ 'format' => 'plaintext',
+ 'usedb' => false,
+ ] ],
+ 'Explicitly follows uselang' => [
+ [ 'uselang' => 'fr', 'errorlang' => 'uselang', 'errorformat' => 'plaintext' ],
+ [
+ 'uselang' => 'fr',
+ 'class' => ApiErrorFormatter::class,
+ 'lang' => 'fr',
+ 'format' => 'plaintext',
+ 'usedb' => false,
+ ]
+ ],
+ 'uselang=content' => [
+ [ 'uselang' => 'content', 'errorformat' => 'plaintext' ],
+ [
+ 'uselang' => $wgContLang->getCode(),
+ 'class' => ApiErrorFormatter::class,
+ 'lang' => $wgContLang->getCode(),
+ 'format' => 'plaintext',
+ 'usedb' => false,
+ ]
+ ],
+ 'errorlang=content' => [
+ [ 'errorlang' => 'content', 'errorformat' => 'plaintext' ],
+ [
+ 'uselang' => 'ru',
+ 'class' => ApiErrorFormatter::class,
+ 'lang' => $wgContLang->getCode(),
+ 'format' => 'plaintext',
+ 'usedb' => false,
+ ]
+ ],
+ 'Explicit parameters' => [
+ [ 'errorlang' => 'de', 'errorformat' => 'html', 'errorsuselocal' => 1 ],
+ [
+ 'uselang' => 'ru',
+ 'class' => ApiErrorFormatter::class,
+ 'lang' => 'de',
+ 'format' => 'html',
+ 'usedb' => true,
+ ]
+ ],
+ 'Explicit parameters override uselang' => [
+ [ 'errorlang' => 'de', 'uselang' => 'fr', 'errorformat' => 'raw' ],
+ [
+ 'uselang' => 'fr',
+ 'class' => ApiErrorFormatter::class,
+ 'lang' => 'de',
+ 'format' => 'raw',
+ 'usedb' => false,
+ ]
+ ],
+ 'Bogus language doesn\'t explode' => [
+ [ 'errorlang' => '<bogus1>', 'uselang' => '<bogus2>', 'errorformat' => 'none' ],
+ [
+ 'uselang' => 'en',
+ 'class' => ApiErrorFormatter::class,
+ 'lang' => 'en',
+ 'format' => 'none',
+ 'usedb' => false,
+ ]
+ ],
+ 'Bogus format doesn\'t explode' => [ [ 'errorformat' => 'bogus' ], [
+ 'uselang' => 'ru',
+ 'class' => ApiErrorFormatter_BackCompat::class,
+ 'lang' => 'en',
+ 'format' => 'none',
+ 'usedb' => false,
+ ] ],
+ ];
+ }
+
+ /**
+ * @covers ApiMain::errorMessagesFromException
+ * @covers ApiMain::substituteResultWithError
+ * @dataProvider provideExceptionErrors
+ * @param Exception $exception
+ * @param array $expectReturn
+ * @param array $expectResult
+ */
+ public function testExceptionErrors( $error, $expectReturn, $expectResult ) {
+ $context = new RequestContext();
+ $context->setRequest( new FauxRequest( [ 'errorformat' => 'plaintext' ] ) );
+ $context->setLanguage( 'en' );
+ $context->setConfig( new MultiConfig( [
+ new HashConfig( [ 'ShowHostnames' => true, 'ShowSQLErrors' => false ] ),
+ $context->getConfig()
+ ] ) );
+
+ $main = new ApiMain( $context );
+ $main->addWarning( new RawMessage( 'existing warning' ), 'existing-warning' );
+ $main->addError( new RawMessage( 'existing error' ), 'existing-error' );
+
+ $ret = TestingAccessWrapper::newFromObject( $main )->substituteResultWithError( $error );
+ $this->assertSame( $expectReturn, $ret );
+
+ // PHPUnit sometimes adds some SplObjectStorage garbage to the arrays,
+ // so let's try ->assertEquals().
+ $this->assertEquals(
+ $expectResult,
+ $main->getResult()->getResultData( [], [ 'Strip' => 'all' ] )
+ );
+ }
+
+ // Not static so $this->getMock() can be used
+ public function provideExceptionErrors() {
+ $reqId = WebRequest::getRequestId();
+ $doclink = wfExpandUrl( wfScript( 'api' ) );
+
+ $ex = new InvalidArgumentException( 'Random exception' );
+ $trace = wfMessage( 'api-exception-trace',
+ get_class( $ex ),
+ $ex->getFile(),
+ $ex->getLine(),
+ MWExceptionHandler::getRedactedTraceAsString( $ex )
+ )->inLanguage( 'en' )->useDatabase( false )->text();
+
+ $dbex = new DBQueryError( $this->getMock( 'IDatabase' ), 'error', 1234, 'SELECT 1', __METHOD__ );
+ $dbtrace = wfMessage( 'api-exception-trace',
+ get_class( $dbex ),
+ $dbex->getFile(),
+ $dbex->getLine(),
+ MWExceptionHandler::getRedactedTraceAsString( $dbex )
+ )->inLanguage( 'en' )->useDatabase( false )->text();
+
+ $apiEx1 = new ApiUsageException( null,
+ StatusValue::newFatal( new ApiRawMessage( 'An error', 'sv-error1' ) ) );
+ TestingAccessWrapper::newFromObject( $apiEx1 )->modulePath = 'foo+bar';
+ $apiEx1->getStatusValue()->warning( new ApiRawMessage( 'A warning', 'sv-warn1' ) );
+ $apiEx1->getStatusValue()->warning( new ApiRawMessage( 'Another warning', 'sv-warn2' ) );
+ $apiEx1->getStatusValue()->fatal( new ApiRawMessage( 'Another error', 'sv-error2' ) );
+
+ return [
+ [
+ $ex,
+ [ 'existing-error', 'internal_api_error_InvalidArgumentException' ],
+ [
+ 'warnings' => [
+ [ 'code' => 'existing-warning', 'text' => 'existing warning', 'module' => 'main' ],
+ ],
+ 'errors' => [
+ [ 'code' => 'existing-error', 'text' => 'existing error', 'module' => 'main' ],
+ [
+ 'code' => 'internal_api_error_InvalidArgumentException',
+ 'text' => "[$reqId] Exception caught: Random exception",
+ ]
+ ],
+ 'trace' => $trace,
+ 'servedby' => wfHostname(),
+ ]
+ ],
+ [
+ $dbex,
+ [ 'existing-error', 'internal_api_error_DBQueryError' ],
+ [
+ 'warnings' => [
+ [ 'code' => 'existing-warning', 'text' => 'existing warning', 'module' => 'main' ],
+ ],
+ 'errors' => [
+ [ 'code' => 'existing-error', 'text' => 'existing error', 'module' => 'main' ],
+ [
+ 'code' => 'internal_api_error_DBQueryError',
+ 'text' => "[$reqId] Database query error.",
+ ]
+ ],
+ 'trace' => $dbtrace,
+ 'servedby' => wfHostname(),
+ ]
+ ],
+ [
+ new UsageException( 'Usage exception!', 'ue', 0, [ 'foo' => 'bar' ] ),
+ [ 'existing-error', 'ue' ],
+ [
+ 'warnings' => [
+ [ 'code' => 'existing-warning', 'text' => 'existing warning', 'module' => 'main' ],
+ ],
+ 'errors' => [
+ [ 'code' => 'existing-error', 'text' => 'existing error', 'module' => 'main' ],
+ [ 'code' => 'ue', 'text' => "Usage exception!", 'data' => [ 'foo' => 'bar' ] ]
+ ],
+ 'docref' => "See $doclink for API usage.",
+ 'servedby' => wfHostname(),
+ ]
+ ],
+ [
+ $apiEx1,
+ [ 'existing-error', 'sv-error1', 'sv-error2' ],
+ [
+ 'warnings' => [
+ [ 'code' => 'existing-warning', 'text' => 'existing warning', 'module' => 'main' ],
+ [ 'code' => 'sv-warn1', 'text' => 'A warning', 'module' => 'foo+bar' ],
+ [ 'code' => 'sv-warn2', 'text' => 'Another warning', 'module' => 'foo+bar' ],
+ ],
+ 'errors' => [
+ [ 'code' => 'existing-error', 'text' => 'existing error', 'module' => 'main' ],
+ [ 'code' => 'sv-error1', 'text' => 'An error', 'module' => 'foo+bar' ],
+ [ 'code' => 'sv-error2', 'text' => 'Another error', 'module' => 'foo+bar' ],
+ ],
+ 'docref' => "See $doclink for API usage.",
+ 'servedby' => wfHostname(),
+ ]
+ ],
+ ];
+ }
}
diff --git a/tests/phpunit/includes/api/ApiMessageTest.php b/tests/phpunit/includes/api/ApiMessageTest.php
index 8764b4194f5b..e405b3b89592 100644
--- a/tests/phpunit/includes/api/ApiMessageTest.php
+++ b/tests/phpunit/includes/api/ApiMessageTest.php
@@ -24,6 +24,56 @@ class ApiMessageTest extends MediaWikiTestCase {
}
/**
+ * @covers ApiMessageTrait
+ */
+ public function testCodeDefaults() {
+ $msg = new ApiMessage( 'foo' );
+ $this->assertSame( 'foo', $msg->getApiCode() );
+
+ $msg = new ApiMessage( 'apierror-bar' );
+ $this->assertSame( 'bar', $msg->getApiCode() );
+
+ $msg = new ApiMessage( 'apiwarn-baz' );
+ $this->assertSame( 'baz', $msg->getApiCode() );
+
+ // BC case
+ $msg = new ApiMessage( 'actionthrottledtext' );
+ $this->assertSame( 'ratelimited', $msg->getApiCode() );
+
+ $msg = new ApiMessage( [ 'apierror-missingparam', 'param' ] );
+ $this->assertSame( 'noparam', $msg->getApiCode() );
+ }
+
+ /**
+ * @covers ApiMessageTrait
+ * @dataProvider provideInvalidCode
+ * @param mixed $code
+ */
+ public function testInvalidCode( $code ) {
+ $msg = new ApiMessage( 'foo' );
+ try {
+ $msg->setApiCode( $code );
+ $this->fail( 'Expected exception not thrown' );
+ } catch ( InvalidArgumentException $ex ) {
+ $this->assertTrue( true );
+ }
+
+ try {
+ new ApiMessage( 'foo', $code );
+ $this->fail( 'Expected exception not thrown' );
+ } catch ( InvalidArgumentException $ex ) {
+ $this->assertTrue( true );
+ }
+ }
+
+ public static function provideInvalidCode() {
+ return [
+ [ '' ],
+ [ 42 ],
+ ];
+ }
+
+ /**
* @covers ApiMessage
* @covers ApiMessageTrait
*/
@@ -105,14 +155,32 @@ class ApiMessageTest extends MediaWikiTestCase {
* @covers ApiMessage::create
*/
public function testApiMessageCreate() {
- $this->assertInstanceOf( 'ApiMessage', ApiMessage::create( new Message( 'mainpage' ) ) );
- $this->assertInstanceOf( 'ApiRawMessage', ApiMessage::create( new RawMessage( 'mainpage' ) ) );
- $this->assertInstanceOf( 'ApiMessage', ApiMessage::create( 'mainpage' ) );
+ $this->assertInstanceOf( ApiMessage::class, ApiMessage::create( new Message( 'mainpage' ) ) );
+ $this->assertInstanceOf(
+ ApiRawMessage::class, ApiMessage::create( new RawMessage( 'mainpage' ) )
+ );
+ $this->assertInstanceOf( ApiMessage::class, ApiMessage::create( 'mainpage' ) );
+
+ $msg = new ApiMessage( [ 'parentheses', 'foobar' ] );
+ $msg2 = new Message( 'parentheses', [ 'foobar' ] );
- $msg = new ApiMessage( 'mainpage' );
$this->assertSame( $msg, ApiMessage::create( $msg ) );
+ $this->assertEquals( $msg, ApiMessage::create( $msg2 ) );
+ $this->assertEquals( $msg, ApiMessage::create( [ 'parentheses', 'foobar' ] ) );
+ $this->assertEquals( $msg,
+ ApiMessage::create( [ 'message' => 'parentheses', 'params' => [ 'foobar' ] ] )
+ );
+ $this->assertSame( $msg,
+ ApiMessage::create( [ 'message' => $msg, 'params' => [ 'xxx' ] ] )
+ );
+ $this->assertEquals( $msg,
+ ApiMessage::create( [ 'message' => $msg2, 'params' => [ 'xxx' ] ] )
+ );
+ $this->assertSame( $msg,
+ ApiMessage::create( [ 'message' => $msg ] )
+ );
- $msg = new ApiRawMessage( 'mainpage' );
+ $msg = new ApiRawMessage( [ 'parentheses', 'foobar' ] );
$this->assertSame( $msg, ApiMessage::create( $msg ) );
}
diff --git a/tests/phpunit/includes/api/ApiOptionsTest.php b/tests/phpunit/includes/api/ApiOptionsTest.php
index 0a577c1cb692..ef70626120fe 100644
--- a/tests/phpunit/includes/api/ApiOptionsTest.php
+++ b/tests/phpunit/includes/api/ApiOptionsTest.php
@@ -30,7 +30,7 @@ class ApiOptionsTest extends MediaWikiLangTestCase {
$this->mUserMock->expects( $this->any() )
->method( 'getEffectiveGroups' )->will( $this->returnValue( [ '*', 'user' ] ) );
$this->mUserMock->expects( $this->any() )
- ->method( 'isAllowed' )->will( $this->returnValue( true ) );
+ ->method( 'isAllowedAny' )->will( $this->returnValue( true ) );
// Set up callback for User::getOptionKinds
$this->mUserMock->expects( $this->any() )
@@ -146,7 +146,7 @@ class ApiOptionsTest extends MediaWikiLangTestCase {
}
/**
- * @expectedException UsageException
+ * @expectedException ApiUsageException
*/
public function testNoToken() {
$request = $this->getSampleRequest( [ 'token' => null ] );
@@ -163,13 +163,11 @@ class ApiOptionsTest extends MediaWikiLangTestCase {
$request = $this->getSampleRequest();
$this->executeQuery( $request );
- } catch ( UsageException $e ) {
- $this->assertEquals( 'notloggedin', $e->getCodeString() );
- $this->assertEquals( 'Anonymous users cannot change preferences', $e->getMessage() );
-
+ } catch ( ApiUsageException $e ) {
+ $this->assertTrue( ApiTestCase::apiExceptionHasCode( $e, 'notloggedin' ) );
return;
}
- $this->fail( "UsageException was not thrown" );
+ $this->fail( "ApiUsageException was not thrown" );
}
public function testNoOptionname() {
@@ -177,13 +175,11 @@ class ApiOptionsTest extends MediaWikiLangTestCase {
$request = $this->getSampleRequest( [ 'optionvalue' => '1' ] );
$this->executeQuery( $request );
- } catch ( UsageException $e ) {
- $this->assertEquals( 'nooptionname', $e->getCodeString() );
- $this->assertEquals( 'The optionname parameter must be set', $e->getMessage() );
-
+ } catch ( ApiUsageException $e ) {
+ $this->assertTrue( ApiTestCase::apiExceptionHasCode( $e, 'nooptionname' ) );
return;
}
- $this->fail( "UsageException was not thrown" );
+ $this->fail( "ApiUsageException was not thrown" );
}
public function testNoChanges() {
@@ -200,13 +196,11 @@ class ApiOptionsTest extends MediaWikiLangTestCase {
$request = $this->getSampleRequest();
$this->executeQuery( $request );
- } catch ( UsageException $e ) {
- $this->assertEquals( 'nochanges', $e->getCodeString() );
- $this->assertEquals( 'No changes were requested', $e->getMessage() );
-
+ } catch ( ApiUsageException $e ) {
+ $this->assertTrue( ApiTestCase::apiExceptionHasCode( $e, 'nochanges' ) );
return;
}
- $this->fail( "UsageException was not thrown" );
+ $this->fail( "ApiUsageException was not thrown" );
}
public function testReset() {
@@ -400,7 +394,7 @@ class ApiOptionsTest extends MediaWikiLangTestCase {
'options' => 'success',
'warnings' => [
'options' => [
- 'warnings' => "Validation error for 'special': cannot be set by this module"
+ 'warnings' => "Validation error for \"special\": cannot be set by this module."
]
]
], $response );
@@ -423,7 +417,7 @@ class ApiOptionsTest extends MediaWikiLangTestCase {
'options' => 'success',
'warnings' => [
'options' => [
- 'warnings' => "Validation error for 'unknownOption': not a valid preference"
+ 'warnings' => "Validation error for \"unknownOption\": not a valid preference."
]
]
], $response );
diff --git a/tests/phpunit/includes/api/ApiParseTest.php b/tests/phpunit/includes/api/ApiParseTest.php
index b72a4f8a8b8b..f01a670b7115 100644
--- a/tests/phpunit/includes/api/ApiParseTest.php
+++ b/tests/phpunit/includes/api/ApiParseTest.php
@@ -23,12 +23,10 @@ class ApiParseTest extends ApiTestCase {
'page' => $somePage ] );
$this->fail( "API did not return an error when parsing a nonexistent page" );
- } catch ( UsageException $ex ) {
- $this->assertEquals(
- 'missingtitle',
- $ex->getCodeString(),
+ } catch ( ApiUsageException $ex ) {
+ $this->assertTrue( ApiTestCase::apiExceptionHasCode( $ex, 'missingtitle' ),
"Parse request for nonexistent page must give 'missingtitle' error: "
- . var_export( $ex->getMessageArray(), true )
+ . var_export( self::getErrorFormatter()->arrayFromStatus( $ex->getStatusValue() ), true )
);
}
}
diff --git a/tests/phpunit/includes/api/ApiQueryWatchlistIntegrationTest.php b/tests/phpunit/includes/api/ApiQueryWatchlistIntegrationTest.php
index eaeb3ae925c0..0a2cd83dd7a7 100644
--- a/tests/phpunit/includes/api/ApiQueryWatchlistIntegrationTest.php
+++ b/tests/phpunit/includes/api/ApiQueryWatchlistIntegrationTest.php
@@ -1498,7 +1498,7 @@ class ApiQueryWatchlistIntegrationTest extends ApiTestCase {
$otherUser->setOption( 'watchlisttoken', '1234567890' );
$otherUser->saveSettings();
- $this->setExpectedException( UsageException::class, 'Incorrect watchlist token provided' );
+ $this->setExpectedException( ApiUsageException::class, 'Incorrect watchlist token provided' );
$this->doListWatchlistRequest( [
'wlowner' => $otherUser->getName(),
@@ -1507,7 +1507,7 @@ class ApiQueryWatchlistIntegrationTest extends ApiTestCase {
}
public function testOwnerAndTokenParams_noWatchlistTokenSet() {
- $this->setExpectedException( UsageException::class, 'Incorrect watchlist token provided' );
+ $this->setExpectedException( ApiUsageException::class, 'Incorrect watchlist token provided' );
$this->doListWatchlistRequest( [
'wlowner' => $this->getNonLoggedInTestUser()->getName(),
diff --git a/tests/phpunit/includes/api/ApiQueryWatchlistRawIntegrationTest.php b/tests/phpunit/includes/api/ApiQueryWatchlistRawIntegrationTest.php
index d6f315d5b37e..0f01664e720a 100644
--- a/tests/phpunit/includes/api/ApiQueryWatchlistRawIntegrationTest.php
+++ b/tests/phpunit/includes/api/ApiQueryWatchlistRawIntegrationTest.php
@@ -503,7 +503,7 @@ class ApiQueryWatchlistRawIntegrationTest extends ApiTestCase {
$otherUser->setOption( 'watchlisttoken', '1234567890' );
$otherUser->saveSettings();
- $this->setExpectedException( UsageException::class, 'Incorrect watchlist token provided' );
+ $this->setExpectedException( ApiUsageException::class, 'Incorrect watchlist token provided' );
$this->doListWatchlistRawRequest( [
'wrowner' => $otherUser->getName(),
@@ -512,7 +512,7 @@ class ApiQueryWatchlistRawIntegrationTest extends ApiTestCase {
}
public function testOwnerAndTokenParams_userHasNoWatchlistToken() {
- $this->setExpectedException( UsageException::class, 'Incorrect watchlist token provided' );
+ $this->setExpectedException( ApiUsageException::class, 'Incorrect watchlist token provided' );
$this->doListWatchlistRawRequest( [
'wrowner' => $this->getNotLoggedInTestUser()->getName(),
diff --git a/tests/phpunit/includes/api/ApiTestCase.php b/tests/phpunit/includes/api/ApiTestCase.php
index 7e1f9d8775ef..6b299c98c80f 100644
--- a/tests/phpunit/includes/api/ApiTestCase.php
+++ b/tests/phpunit/includes/api/ApiTestCase.php
@@ -3,6 +3,8 @@
abstract class ApiTestCase extends MediaWikiLangTestCase {
protected static $apiUrl;
+ protected static $errorFormatter = null;
+
/**
* @var ApiTestContext
*/
@@ -196,6 +198,26 @@ abstract class ApiTestCase extends MediaWikiLangTestCase {
return $data[0]['tokens'];
}
+ protected static function getErrorFormatter() {
+ if ( self::$errorFormatter === null ) {
+ self::$errorFormatter = new ApiErrorFormatter(
+ new ApiResult( false ),
+ Language::factory( 'en' ),
+ 'none'
+ );
+ }
+ return self::$errorFormatter;
+ }
+
+ public static function apiExceptionHasCode( ApiUsageException $ex, $code ) {
+ return (bool)array_filter(
+ self::getErrorFormatter()->arrayFromStatus( $ex->getStatusValue() ),
+ function ( $e ) use ( $code ) {
+ return is_array( $e ) && $e['code'] === $code;
+ }
+ );
+ }
+
public function testApiTestGroup() {
$groups = PHPUnit_Util_Test::getGroups( get_class( $this ) );
$constraint = PHPUnit_Framework_Assert::logicalOr(
diff --git a/tests/phpunit/includes/api/ApiUnblockTest.php b/tests/phpunit/includes/api/ApiUnblockTest.php
index b63bf2ea3131..971b63c3d4bc 100644
--- a/tests/phpunit/includes/api/ApiUnblockTest.php
+++ b/tests/phpunit/includes/api/ApiUnblockTest.php
@@ -14,7 +14,7 @@ class ApiUnblockTest extends ApiTestCase {
}
/**
- * @expectedException UsageException
+ * @expectedException ApiUsageException
*/
public function testWithNoToken() {
$this->doApiRequest(
diff --git a/tests/phpunit/includes/api/ApiUploadTest.php b/tests/phpunit/includes/api/ApiUploadTest.php
index de2b56bde3f4..9b79e6c538a1 100644
--- a/tests/phpunit/includes/api/ApiUploadTest.php
+++ b/tests/phpunit/includes/api/ApiUploadTest.php
@@ -67,9 +67,9 @@ class ApiUploadTest extends ApiTestCaseUpload {
$this->doApiRequest( [
'action' => 'upload'
] );
- } catch ( UsageException $e ) {
+ } catch ( ApiUsageException $e ) {
$exception = true;
- $this->assertEquals( "The token parameter must be set", $e->getMessage() );
+ $this->assertEquals( 'The "token" parameter must be set', $e->getMessage() );
}
$this->assertTrue( $exception, "Got exception" );
}
@@ -83,7 +83,7 @@ class ApiUploadTest extends ApiTestCaseUpload {
$this->doApiRequestWithToken( [
'action' => 'upload',
], $session, self::$users['uploader']->getUser() );
- } catch ( UsageException $e ) {
+ } catch ( ApiUsageException $e ) {
$exception = true;
$this->assertEquals( "One of the parameters filekey, file, url is required",
$e->getMessage() );
@@ -129,7 +129,7 @@ class ApiUploadTest extends ApiTestCaseUpload {
try {
list( $result, , ) = $this->doApiRequestWithToken( $params, $session,
self::$users['uploader']->getUser() );
- } catch ( UsageException $e ) {
+ } catch ( ApiUsageException $e ) {
$exception = true;
}
$this->assertTrue( isset( $result['upload'] ) );
@@ -168,7 +168,7 @@ class ApiUploadTest extends ApiTestCaseUpload {
$exception = false;
try {
$this->doApiRequestWithToken( $params, $session, self::$users['uploader']->getUser() );
- } catch ( UsageException $e ) {
+ } catch ( ApiUsageException $e ) {
$this->assertContains( 'The file you submitted was empty', $e->getMessage() );
$exception = true;
}
@@ -218,7 +218,7 @@ class ApiUploadTest extends ApiTestCaseUpload {
try {
list( $result, , $session ) = $this->doApiRequestWithToken( $params, $session,
self::$users['uploader']->getUser() );
- } catch ( UsageException $e ) {
+ } catch ( ApiUsageException $e ) {
$exception = true;
}
$this->assertTrue( isset( $result['upload'] ) );
@@ -235,7 +235,7 @@ class ApiUploadTest extends ApiTestCaseUpload {
try {
list( $result, , ) = $this->doApiRequestWithToken( $params, $session,
self::$users['uploader']->getUser() ); // FIXME: leaks a temporary file
- } catch ( UsageException $e ) {
+ } catch ( ApiUsageException $e ) {
$exception = true;
}
$this->assertTrue( isset( $result['upload'] ) );
@@ -289,7 +289,7 @@ class ApiUploadTest extends ApiTestCaseUpload {
try {
list( $result, , $session ) = $this->doApiRequestWithToken( $params, $session,
self::$users['uploader']->getUser() );
- } catch ( UsageException $e ) {
+ } catch ( ApiUsageException $e ) {
$exception = true;
}
$this->assertTrue( isset( $result['upload'] ) );
@@ -314,7 +314,7 @@ class ApiUploadTest extends ApiTestCaseUpload {
try {
list( $result ) = $this->doApiRequestWithToken( $params, $session,
self::$users['uploader']->getUser() ); // FIXME: leaks a temporary file
- } catch ( UsageException $e ) {
+ } catch ( ApiUsageException $e ) {
$exception = true;
}
$this->assertTrue( isset( $result['upload'] ) );
@@ -371,7 +371,7 @@ class ApiUploadTest extends ApiTestCaseUpload {
try {
list( $result, , $session ) = $this->doApiRequestWithToken( $params, $session,
self::$users['uploader']->getUser() ); // FIXME: leaks a temporary file
- } catch ( UsageException $e ) {
+ } catch ( ApiUsageException $e ) {
$exception = true;
}
$this->assertFalse( $exception );
@@ -400,12 +400,12 @@ class ApiUploadTest extends ApiTestCaseUpload {
try {
list( $result ) = $this->doApiRequestWithToken( $params, $session,
self::$users['uploader']->getUser() );
- } catch ( UsageException $e ) {
+ } catch ( ApiUsageException $e ) {
$exception = true;
}
$this->assertTrue( isset( $result['upload'] ) );
$this->assertEquals( 'Success', $result['upload']['result'] );
- $this->assertFalse( $exception, "No UsageException exception." );
+ $this->assertFalse( $exception, "No ApiUsageException exception." );
// clean up
$this->deleteFileByFileName( $fileName );
@@ -476,7 +476,7 @@ class ApiUploadTest extends ApiTestCaseUpload {
try {
list( $result, , $session ) = $this->doApiRequestWithToken( $params, $session,
self::$users['uploader']->getUser() );
- } catch ( UsageException $e ) {
+ } catch ( ApiUsageException $e ) {
$this->markTestIncomplete( $e->getMessage() );
}
// Make sure we got a valid chunk continue:
@@ -504,7 +504,7 @@ class ApiUploadTest extends ApiTestCaseUpload {
try {
list( $result, , $session ) = $this->doApiRequestWithToken( $params, $session,
self::$users['uploader']->getUser() );
- } catch ( UsageException $e ) {
+ } catch ( ApiUsageException $e ) {
$this->markTestIncomplete( $e->getMessage() );
}
// Make sure we got a valid chunk continue:
@@ -544,7 +544,7 @@ class ApiUploadTest extends ApiTestCaseUpload {
try {
list( $result ) = $this->doApiRequestWithToken( $params, $session,
self::$users['uploader']->getUser() );
- } catch ( UsageException $e ) {
+ } catch ( ApiUsageException $e ) {
$exception = true;
}
$this->assertTrue( isset( $result['upload'] ) );
diff --git a/tests/phpunit/includes/api/ApiWatchTest.php b/tests/phpunit/includes/api/ApiWatchTest.php
index 19afc148b7ca..0cd270764002 100644
--- a/tests/phpunit/includes/api/ApiWatchTest.php
+++ b/tests/phpunit/includes/api/ApiWatchTest.php
@@ -146,11 +146,11 @@ class ApiWatchTest extends ApiTestCase {
$this->assertArrayHasKey( 'rollback', $data[0] );
$this->assertArrayHasKey( 'title', $data[0]['rollback'] );
- } catch ( UsageException $ue ) {
- if ( $ue->getCodeString() == 'onlyauthor' ) {
+ } catch ( ApiUsageException $ue ) {
+ if ( self::apiExceptionHasCode( $ue, 'onlyauthor' ) ) {
$this->markTestIncomplete( "Only one author to 'Help:UTPage', cannot test rollback" );
} else {
- $this->fail( "Received error '" . $ue->getCodeString() . "'" );
+ $this->fail( "Received error '" . $ue->getMessage() . "'" );
}
}
}
diff --git a/tests/phpunit/includes/api/MockApi.php b/tests/phpunit/includes/api/MockApi.php
index d7db538273fb..1407c10d9314 100644
--- a/tests/phpunit/includes/api/MockApi.php
+++ b/tests/phpunit/includes/api/MockApi.php
@@ -9,7 +9,11 @@ class MockApi extends ApiBase {
public function __construct() {
}
- public function setWarning( $warning ) {
+ public function getModulePath() {
+ return $this->getModuleName();
+ }
+
+ public function addWarning( $warning, $code = null, $data = null ) {
$this->warnings[] = $warning;
}
diff --git a/tests/phpunit/includes/api/MockApiQueryBase.php b/tests/phpunit/includes/api/MockApiQueryBase.php
index f5b50e5a5919..9915a38d0a45 100644
--- a/tests/phpunit/includes/api/MockApiQueryBase.php
+++ b/tests/phpunit/includes/api/MockApiQueryBase.php
@@ -12,4 +12,8 @@ class MockApiQueryBase extends ApiQueryBase {
public function getModuleName() {
return $this->name;
}
+
+ public function getModulePath() {
+ return 'query+' . $this->getModuleName();
+ }
}
diff --git a/tests/phpunit/includes/api/format/ApiFormatPhpTest.php b/tests/phpunit/includes/api/format/ApiFormatPhpTest.php
index 0028bbb0ace8..3aa1db301057 100644
--- a/tests/phpunit/includes/api/format/ApiFormatPhpTest.php
+++ b/tests/phpunit/includes/api/format/ApiFormatPhpTest.php
@@ -133,12 +133,10 @@ class ApiFormatPhpTest extends ApiFormatTestBase {
$printer->closePrinter();
ob_end_clean();
$this->fail( 'Expected exception not thrown' );
- } catch ( UsageException $ex ) {
+ } catch ( ApiUsageException $ex ) {
ob_end_clean();
- $this->assertSame(
- 'This response cannot be represented using format=php. ' .
- 'See https://phabricator.wikimedia.org/T68776',
- $ex->getMessage(),
+ $this->assertTrue(
+ $ex->getStatusValue()->hasMessage( 'apierror-formatphp' ),
'Expected exception'
);
}
diff --git a/tests/phpunit/includes/api/format/ApiFormatXmlTest.php b/tests/phpunit/includes/api/format/ApiFormatXmlTest.php
index 3fef0b0027cc..0f8c8ee6c125 100644
--- a/tests/phpunit/includes/api/format/ApiFormatXmlTest.php
+++ b/tests/phpunit/includes/api/format/ApiFormatXmlTest.php
@@ -105,11 +105,11 @@ class ApiFormatXmlTest extends ApiFormatTestBase {
[ 'includexmlnamespace' => 1 ] ],
// xslt param
- [ [], '<?xml version="1.0"?><api><warnings><xml xml:space="preserve">Invalid or non-existent stylesheet specified</xml></warnings></api>',
+ [ [], '<?xml version="1.0"?><api><warnings><xml xml:space="preserve">Invalid or non-existent stylesheet specified.</xml></warnings></api>',
[ 'xslt' => 'DoesNotExist' ] ],
[ [], '<?xml version="1.0"?><api><warnings><xml xml:space="preserve">Stylesheet should be in the MediaWiki namespace.</xml></warnings></api>',
[ 'xslt' => 'ApiFormatXmlTest' ] ],
- [ [], '<?xml version="1.0"?><api><warnings><xml xml:space="preserve">Stylesheet should have .xsl extension.</xml></warnings></api>',
+ [ [], '<?xml version="1.0"?><api><warnings><xml xml:space="preserve">Stylesheet should have &quot;.xsl&quot; extension.</xml></warnings></api>',
[ 'xslt' => 'MediaWiki:ApiFormatXmlTest' ] ],
[ [],
'<?xml version="1.0"?><?xml-stylesheet href="' .
diff --git a/tests/phpunit/includes/api/query/ApiQueryTest.php b/tests/phpunit/includes/api/query/ApiQueryTest.php
index 8cb2327dfbed..9407edfffa8f 100644
--- a/tests/phpunit/includes/api/query/ApiQueryTest.php
+++ b/tests/phpunit/includes/api/query/ApiQueryTest.php
@@ -99,11 +99,11 @@ class ApiQueryTest extends ApiTestCase {
$exceptionCaught = false;
try {
$this->assertEquals( $expected, $api->titlePartToKey( $titlePart, $namespace ) );
- } catch ( UsageException $e ) {
+ } catch ( ApiUsageException $e ) {
$exceptionCaught = true;
}
$this->assertEquals( $expectException, $exceptionCaught,
- 'UsageException thrown by titlePartToKey' );
+ 'ApiUsageException thrown by titlePartToKey' );
}
function provideTestTitlePartToKey() {
diff --git a/tests/phpunit/includes/upload/UploadFromUrlTest.php b/tests/phpunit/includes/upload/UploadFromUrlTest.php
index 6d17a68c7df0..62081aa35da3 100644
--- a/tests/phpunit/includes/upload/UploadFromUrlTest.php
+++ b/tests/phpunit/includes/upload/UploadFromUrlTest.php
@@ -58,7 +58,7 @@ class UploadFromUrlTest extends ApiTestCase {
$this->doApiRequest( [
'action' => 'upload',
] );
- } catch ( UsageException $e ) {
+ } catch ( ApiUsageException $e ) {
$exception = true;
$this->assertEquals( "The token parameter must be set", $e->getMessage() );
}
@@ -70,7 +70,7 @@ class UploadFromUrlTest extends ApiTestCase {
'action' => 'upload',
'token' => $token,
], $data );
- } catch ( UsageException $e ) {
+ } catch ( ApiUsageException $e ) {
$exception = true;
$this->assertEquals( "One of the parameters sessionkey, file, url is required",
$e->getMessage() );
@@ -84,7 +84,7 @@ class UploadFromUrlTest extends ApiTestCase {
'url' => 'http://www.example.com/test.png',
'token' => $token,
], $data );
- } catch ( UsageException $e ) {
+ } catch ( ApiUsageException $e ) {
$exception = true;
$this->assertEquals( "The filename parameter must be set", $e->getMessage() );
}
@@ -99,7 +99,7 @@ class UploadFromUrlTest extends ApiTestCase {
'filename' => 'UploadFromUrlTest.png',
'token' => $token,
], $data );
- } catch ( UsageException $e ) {
+ } catch ( ApiUsageException $e ) {
$exception = true;
$this->assertEquals( "Permission denied", $e->getMessage() );
}