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
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 ===
'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',
'ExplodeIterator' => __DIR__ . '/includes/libs/ExplodeIterator.php',
'ExportProgressFilter' => __DIR__ . '/maintenance/backup.inc',
'ExportSites' => __DIR__ . '/maintenance/exportSites.php',
+ 'ExtensionJsonValidationError' => __DIR__ . '/includes/registration/ExtensionJsonValidationError.php',
+ 'ExtensionJsonValidator' => __DIR__ . '/includes/registration/ExtensionJsonValidator.php',
'ExtensionLanguages' => __DIR__ . '/maintenance/language/languages.inc',
'ExtensionProcessor' => __DIR__ . '/includes/registration/ExtensionProcessor.php',
'ExtensionRegistry' => __DIR__ . '/includes/registration/ExtensionRegistry.php',
'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',
"ext-xml": "*",
"liuggio/statsd-php-client": "1.0.18",
"mediawiki/at-ease": "1.1.0",
- "oojs/oojs-ui": "0.18.1",
+ "oojs/oojs-ui": "0.18.2",
"oyejorge/less.php": "1.7.0.10",
"php": ">=5.5.9",
"psr/log": "1.0.0",
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
&$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
/**
* Modify options in the login template.
*
- * @param UserLoginTemplate $template
+ * @param BaseTemplate $template
* @param string $type 'signup' or 'login'. Added in 1.16.
*/
public function modifyUITemplate( &$template, &$type ) {
* @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 = []
* @file
*/
+use MediaWiki\MediaWikiServices;
+
/**
* List for revision table items for a single page
*/
* This is used to show the list in HTML form, by the special page.
*/
abstract public function getHTML();
+
+ /**
+ * Returns an instance of LinkRenderer
+ * @return \MediaWiki\Linker\LinkRenderer
+ */
+ protected function getLinkRenderer() {
+ return MediaWikiServices::getInstance()->getLinkRenderer();
+ }
}
class RevisionList extends RevisionListBase {
$fname = 'Setup.php';
$ps_setup = Profiler::instance()->scopedProfileIn( $fname );
-// If any extensions are still queued, force load them
+// Load queued extensions
ExtensionRegistry::getInstance()->loadFromQueue();
+// Don't let any other extensions load
+ExtensionRegistry::getInstance()->finish();
// Check to see if we are at the file scope
if ( !isset( $wgVersion ) ) {
$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();
}
$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}"
);
}
/**
* Call $manager->securitySensitiveOperationStatus()
* @param string $operation Operation being checked.
- * @throws UsageException
+ * @throws ApiUsageException
*/
public function securitySensitiveOperation( $operation ) {
$status = AuthManager::singleton()->securitySensitiveOperationStatus( $operation );
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\"" );
* @since 1.25
* @param string $path
* @return ApiBase|null
- * @throws UsageException
+ * @throws ApiUsageException
*/
public function getModuleFromPath( $path ) {
$module = $this->getMain();
$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'
);
}
/**
* 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;
+ }
}
/**
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' );
}
}
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 ),
+ ] );
}
}
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' ] ) ),
);
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' );
}
}
}
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 ) ]
);
}
}
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 ) {
}
$pageObj = WikiPage::newFromID( $params['pageid'], $load );
if ( !$pageObj ) {
- $this->dieUsageMsg( [ 'nosuchpageid', $params['pageid'] ] );
+ $this->dieWithError( [ 'apierror-nosuchpageid', $params['pageid'] ] );
}
}
// 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}"
);
}
// 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' );
}
}
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
// 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() ) {
$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;
*/
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 ] );
}
/**
}
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 ) {
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"
);
}
$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;
}
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;
}
// (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 );
}
$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}"
);
}
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}"
);
}
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();
}
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;
+ }
+
/**@}*/
/************************************************************************//**
*/
/**
- * 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 ] );
}
/**
/**
* 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 ) {
}
}
+ /**
+ * @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 ) );
+ }
+
/**@}*/
}
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() ) ]
);
}
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 = [
$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'] );
}
$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();
*
* @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, [
'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() {
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 );
$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 );
);
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 ) ) {
$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}"
);
}
$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();
$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 );
} 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() {
* @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 = []
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] );
$pageObj = $this->getTitleOrPageId( $params, 'fromdbmaster' );
if ( !$pageObj->exists() ) {
- $this->dieUsageMsg( 'notanarticle' );
+ $this->dieWithError( 'apierror-missingtitle' );
}
$titleObj = $pageObj->getTitle();
$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
$status = self::delete( $pageObj, $user, $reason, $params['tags'] );
}
- if ( is_array( $status ) ) {
- $this->dieUsageMsg( $status[0] );
- }
if ( !$status->isGood() ) {
$this->dieStatus( $status );
}
* @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();
$hasHistory = false;
$reason = $page->getAutoDeleteReason( $hasHistory );
if ( $reason === false ) {
- return [ [ 'cannotdelete', $title->getPrefixedText() ] ];
+ return Status::newFatal( 'cannotdelete', $title->getPrefixedText() );
}
}
* @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 = []
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' );
}
}
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() {
$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();
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;
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'] == '' ) {
}
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'] ) ) {
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 {
// @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' ) {
$content = $content->getSection( $section );
if ( !$content ) {
- $this->dieUsage( "There is no section {$section}.", 'nosuchsection' );
+ $this->dieWithError( [ 'apierror-nosuchsection', wfEscapeWikiText( $section ) ] );
}
}
}
}
$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() ] );
}
);
if ( !$newContent ) {
- $this->dieUsageMsg( 'undo-failure' );
+ $this->dieWithError( 'undo-failure', 'undofailure' );
}
if ( empty( $params['contentmodel'] )
&& empty( $params['contentformat'] )
// 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
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 {
return;
}
- $this->dieUsageMsg( 'hookaborted' );
+ $this->dieWithError( 'hookaborted' );
}
// Do the actual save
$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
}
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 );
// 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
$this->getConfig()
);
if ( $error ) {
- $this->dieUsageMsg( [ $error ] );
+ $this->dieWithError( $error );
}
$data = [
'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 );
}
* @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
$this->format = $format;
}
+ /**
+ * Fetch the Language for this formatter
+ * @since 1.29
+ * @return Language
+ */
+ public function getLanguage() {
+ return $this->lang;
+ }
+
/**
* Fetch a dummy title to set on Messages
* @return Title
/**
* 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;
}
$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 [];
}
$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(),
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;
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;
}
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 ];
$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'] );
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'] ) ] );
}
}
}
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' );
}
}
}
$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();
$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' );
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 );
$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
$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 );
}
}
$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(
$this->getResult()->addValue( null, $this->getModuleName(), $result );
}
- /**
- * Checks that the user has permissions to perform this revert.
- * Dies with usage message on inadequate permissions.
- * @param User $user The user to check.
- */
- protected function checkPermissions( $user ) {
- $title = $this->file->getTitle();
- $permissionErrors = array_merge(
- $title->getUserPermissionsErrors( 'edit', $user ),
- $title->getUserPermissionsErrors( 'upload', $user )
- );
-
- if ( $permissionErrors ) {
- $this->dieUsageMsg( $permissionErrors[0] );
- }
- }
-
/**
* Validate the user parameters and set $this->archiveName and $this->file.
* Throws an error if validation fails
// 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' );
}
}
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 );
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 ) );
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 );
public function getMimeType() {
$data = $this->getResult()->getResultData();
- if ( isset( $data['error'] ) ) {
+ if ( isset( $data['error'] ) || isset( $data['errors'] ) ) {
return $this->errorFallback->getMimeType();
}
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 );
public function closePrinter() {
$data = $this->getResult()->getResultData();
- if ( isset( $data['error'] ) ) {
+ if ( isset( $data['error'] ) || isset( $data['errors'] ) ) {
$this->errorFallback->closePrinter();
} else {
parent::closePrinter();
public function execute() {
$data = $this->getResult()->getResultData();
- if ( isset( $data['error'] ) ) {
+ if ( isset( $data['error'] ) || isset( $data['errors'] ) ) {
$this->errorFallback->execute();
return;
}
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;
}
$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;
}
$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;
}
$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;
}
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;
}
$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'],
} else {
$isUpload = true;
if ( !$user->isAllowed( 'importupload' ) ) {
- $this->dieUsageMsg( 'cantimport-upload' );
+ $this->dieWithError( 'apierror-cantimport-upload' );
}
$source = ImportStreamSource::newFromUpload( 'xml' );
}
try {
$importer->doImport();
} catch ( Exception $e ) {
- $this->dieUsageMsg( [ 'import-unknownerror', $e->getMessage() ] );
+ $this->dieWithError( [ 'apierror-import-unknownerror', wfEscapeWikiText( $e->getMessage() ) ] );
}
$resultData = $reporter->getData();
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();
$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}"
);
}
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();
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;
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':
// 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'
);
}
*/
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 $mPrinter;
- private $mModuleMgr, $mResult, $mErrorFormatter;
+ private $mModuleMgr, $mResult, $mErrorFormatter = null;
/** @var ApiContinuationManager|null */
private $mContinuationManager;
private $mAction;
}
}
- $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).
}
}
+ // 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' );
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;
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;
*/
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 );
Hooks::run( 'ApiMain::onException', [ $this, $e ] );
// Log it
- if ( !( $e instanceof UsageException ) ) {
+ if ( !( $e instanceof ApiUsageException || $e instanceof UsageException ) ) {
MWExceptionHandler::logException( $e );
}
// 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
// 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();
/**
* 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' ) ) {
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;
}
* 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();
}
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' );
}
}
$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' );
}
}
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 );
// Allow extensions to stop execution for arbitrary reasons.
$message = false;
if ( !Hooks::run( 'ApiCheckCanExecute', [ $module, $user, &$message ] ) ) {
- $this->dieUsageMsg( $message );
+ $this->dieWithError( $message );
}
}
"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)" ]
);
}
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;
}
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'] ) ]
);
}
}
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
( $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' );
}
}
];
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
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;
}
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 );
}
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 )
+ ] );
}
}
*/
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;
'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,
],
];
}
$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()
);
}
}
}
-/**
- * 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
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();
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'] ] );
}
}
/**
* 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();
/**
* 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 );
* @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 );
* @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 ) {
* - 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 ) {
} else {
parent::__construct( $msg );
}
- $this->apiCode = $code;
- $this->apiData = (array)$data;
+ $this->setApiCode( $code, $data );
}
}
} else {
parent::__construct( $msg );
}
- $this->apiCode = $code;
- $this->apiData = (array)$data;
+ $this->setApiCode( $code, $data );
}
}
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();
&& 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
$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 );
}
}
$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
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;
}
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 ]
);
}
*/
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'] ) {
$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() );
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 ] );
}
}
}
$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
}
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';
}
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'] );
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;
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';
/** @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 ) {
$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;
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();
$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?
$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'] ) ) {
} 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() ) {
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 = '';
// 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 ) {
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'] ) ) {
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'] ) ) {
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 );
// 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 );
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;
* 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;
}
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'] ] );
}
}
$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' ) ) ];
$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'];
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'] )
] );
$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] ) ) {
} 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;
}
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();
$page->doPurge( $flags );
$r['purged'] = true;
} else {
- $error = $this->parseMsg( [ 'actionthrottledtext' ] );
- $this->setWarning( $error['info'] );
+ $this->addWarning( 'apierror-ratelimited' );
}
if ( $forceLinkUpdate || $forceRecursiveLinkUpdate ) {
}
}
} else {
- $error = $this->parseMsg( [ 'actionthrottledtext' ] );
- $this->setWarning( $error['info'] );
+ $this->addWarning( 'apierror-ratelimited' );
$forceLinkUpdate = false;
}
}
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 ) ) {
}
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'] ) {
* @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 );
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'
+ );
}
}
}
$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' );
}
}
$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;
*/
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 );
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();
$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'
);
}
$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" ]
);
}
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 ) {
if ( !is_null( $params['mime'] ) ) {
if ( $this->getConfig()->get( 'MiserMode' ) ) {
- $this->dieUsage( 'MIME search disabled in Miser Mode', 'mimesearchdisabled' );
+ $this->dieWithError( 'apierror-mimesearchdisabled' );
}
$mimeConds = [];
$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' );
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'] );
}
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' );
*/
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 );
$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' ) {
}
}
- 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
// 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'
);
}
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(
$likeQuery = LinkFilter::makeLikeArray( $query, $protocol );
if ( !$likeQuery ) {
- $this->dieUsage( 'Invalid query', 'bad_query' );
+ $this->dieWithError( 'apierror-badquery' );
}
$likeQuery = LinkFilter::keepOneWildcard( $likeQuery );
$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
// 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 );
$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 ) ];
$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.
|| ( 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'] ) );
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;
}
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();
}
}
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' );
$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'] );
$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 {
$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 {
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();
$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 ) {
$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'];
}
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'] );
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
// 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' );
$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;
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() {
}
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();
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 ) {
$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'] ) ) {
$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
$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;
}
// 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 ) ]
+ );
}
}
// 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() ) ] );
}
}
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();
}
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;
}
$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'] ) ) {
$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
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 );
}
}
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"
);
}
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() );
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() ) );
$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
/** @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'] ];
|| ( isset( $show['patrolled'] ) && isset( $show['unpatrolled'] ) )
|| ( isset( $show['!patrolled'] ) && isset( $show['unpatrolled'] ) )
) {
- $this->dieUsageMsg( 'show' );
+ $this->dieWithError( 'apierror-show' );
}
// Check permissions
|| 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' );
}
}
);
}
- 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'] );
$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. */
$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;
}
}
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'
);
}
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(
// 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'] );
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;
}
&& $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
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;
}
}
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'
);
}
$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'
+ );
}
}
}
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;
}
$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 {
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 {
// 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
$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 ) {
$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();
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();
$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'] ) {
$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() ) ] );
}
}
];
if ( $this->lacksSameOriginSecurity() ) {
- $this->setWarning( 'Tokens may not be obtained when the same-origin policy is not applied' );
+ $this->addWarning( [ 'apiwarn-tokens-origin' ] );
return;
}
$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 ) ) {
} 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;
}
|| ( 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'] ) );
$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
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'] ) &&
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;
}
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;
}
if ( $this->fld_patrol ) {
if ( !$user->useRCPatrol() && !$user->useNPPatrol() ) {
- $this->dieUsage( 'patrol property is not available', 'patrol' );
+ $this->dieWithError( 'apierror-permissiondenied-patrolflag', 'patrol' );
}
}
}
/* Check for conflicting parameters. */
if ( $this->showParamsConflicting( $show ) ) {
- $this->dieUsageMsg( 'show' );
+ $this->dieWithError( 'apierror-show' );
}
// Check permissions.
|| 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' );
}
}
}
}
- 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'];
}
if ( isset( $show[WatchedItemQueryService::FILTER_CHANGED] )
&& isset( $show[WatchedItemQueryService::FILTER_NOT_CHANGED] )
) {
- $this->dieUsageMsg( 'show' );
+ $this->dieWithError( 'apierror-show' );
}
$options = [];
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();
}
);
if ( count( $reqs ) !== 1 ) {
- $this->dieUsage( 'Failed to create change request', 'badrequest' );
+ $this->dieWithError( 'apierror-changeauth-norequest', 'badrequest' );
}
$req = reset( $reqs );
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() + [
$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;
$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'] ),
}
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;
}
$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(
$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';
? $params['user']
: User::getCanonicalName( $params['user'] );
if ( !$this->mUser ) {
- $this->dieUsageMsg( [ 'invaliduser', $params['user'] ] );
+ $this->dieWithError( [ 'apierror-invaliduser', wfEscapeWikiText( $params['user'] ) ] );
}
return $this->mUser;
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;
$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' );
$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'
);
}
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 ) {
}
} 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 ) {
$params = $this->extractRequestParams();
if ( $user->isBot() ) { // sanity
- $this->dieUsage( 'This interface is not supported for bots', 'botsnotsupported' );
+ $this->dieWithError( 'apierror-botsnotsupported' );
}
$cache = ObjectCache::getLocalClusterInstance();
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'] ) ) {
$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(
// 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(
$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
$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 );
$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() );
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;
}
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 = [
$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;
}
$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() ) ]
);
}
$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();
$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
$params['tags']
);
if ( !is_array( $retval ) ) {
- $this->dieUsageMsg( 'cannotundelete' );
+ $this->dieWithError( 'apierror-cantundelete' );
}
if ( $retval[1] ) {
public function execute() {
// Check whether upload is enabled
if ( !UploadBase::isEnabled() ) {
- $this->dieUsageMsg( 'uploaddisabled' );
+ $this->dieWithError( 'uploaddisabled' );
}
$user = $this->getUser();
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
/** @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
if ( !$this->mParams['stash'] ) {
$permErrors = $this->mUpload->verifyTitlePermissions( $user );
if ( $permErrors !== true ) {
- $this->dieRecoverableError( $permErrors[0], 'filename' );
+ $this->dieRecoverableError( $permErrors, 'filename' );
}
}
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 );
// 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
// 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 ) {
$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(
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;
}
}
* 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 );
}
/**
* @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 );
}
/**
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' );
}
// 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'] ) {
$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
);
} 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
} 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() );
} 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;
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
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:
'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;
}
}
/**
* 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 );
}
}
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(),
$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';
}
--- /dev/null
+<?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()}";
+ }
+
+}
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();
} ) );
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 );
}
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;
"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>errorsuselocal</var> are ignored.",
+ "apihelp-main-param-errorlang": "Language to use for warnings and errors. <kbd>[[Special:ApiHelp/query+siteinfo|action=query&meta=siteinfo]]</kbd> with <kbd>siprop=languages</kbd> returns a list of language codes, or specify <kbd>content</kbd> to use this wiki's content language, or specify <kbd>uselang</kbd> to use the same value as the <var>uselang</var> parameter.",
+ "apihelp-main-param-errorsuselocal": "If given, error texts will use locally-customized messages from the {{ns:MediaWiki}} namespace.",
"apihelp-block-description": "Block a user.",
"apihelp-block-param-user": "Username, IP address, or IP address range to block.",
"apihelp-query+allmessages-param-prop": "Which properties to get.",
"apihelp-query+allmessages-param-enableparser": "Set to enable parser, will preprocess the wikitext of message (substitute magic words, handle templates, etc.).",
"apihelp-query+allmessages-param-nocontent": "If set, do not include the content of the messages in the output.",
- "apihelp-query+allmessages-param-includelocal": "Also include local messages, i.e. messages that don't exist in the software but do exist as a MediaWiki: page.\nThis lists all MediaWiki: pages, so it will also list those that aren't really messages such as [[MediaWiki:Common.js|Common.js]].",
+ "apihelp-query+allmessages-param-includelocal": "Also include local messages, i.e. messages that don't exist in the software but do exist as in the {{ns:MediaWiki}} namespace.\nThis lists all {{ns:MediaWiki}}-namespace pages, so it will also list those that aren't really messages such as [[MediaWiki:Common.js|Common.js]].",
"apihelp-query+allmessages-param-args": "Arguments to be substituted into message.",
"apihelp-query+allmessages-param-filter": "Return only messages with names that contain this string.",
"apihelp-query+allmessages-param-customised": "Return only messages in this customisation state.",
"apihelp-phpfm-description": "Output data in serialized PHP format (pretty-print in HTML).",
"apihelp-rawfm-description": "Output data, including debugging elements, in JSON format (pretty-print in HTML).",
"apihelp-xml-description": "Output data in XML format.",
- "apihelp-xml-param-xslt": "If specified, adds the named page as an XSL stylesheet. The value must be a title in the {{ns:mediawiki}} namespace ending in <code>.xsl</code>.",
+ "apihelp-xml-param-xslt": "If specified, adds the named page as an XSL stylesheet. The value must be a title in the {{ns:MediaWiki}} namespace ending in <code>.xsl</code>.",
"apihelp-xml-param-includexmlnamespace": "If specified, adds an XML namespace.",
"apihelp-xmlfm-description": "Output data in XML format (pretty-print in HTML).",
"api-help-authmanagerhelper-continue": "This request is a continuation after an earlier <samp>UI</samp> or <samp>REDIRECT</samp> response. Either this or <var>$1returnurl</var> is required.",
"api-help-authmanagerhelper-additional-params": "This module accepts additional parameters depending on the available authentication requests. Use <kbd>[[Special:ApiHelp/query+authmanagerinfo|action=query&meta=authmanagerinfo]]</kbd> with <kbd>amirequestsfor=$1</kbd> (or a previous response from this module, if applicable) to determine the requests available and the fields that they use.",
+ "apierror-allimages-redirect": "Use <kbd>gaifilterredir=nonredirects</kbd> instead of <var>redirects</var> when using <kbd>allimages</kbd> as a generator.",
+ "apierror-allpages-generator-redirects": "Use <kbd>gapfilterredir=nonredirects</kbd> instead of <var>redirects</var> when using <kbd>allpages</kbd> as a generator.",
+ "apierror-appendnotsupported": "Can't append to pages using content model $1.",
+ "apierror-articleexists": "The article you tried to create has been created already.",
+ "apierror-assertbotfailed": "Assertion that the user has the <code>bot</code> right failed.",
+ "apierror-assertnameduserfailed": "Assertion that the user is \"$1\" failed.",
+ "apierror-assertuserfailed": "Assertion that the user is logged in failed.",
+ "apierror-autoblocked": "Your IP address has been blocked automatically, because it was used by a blocked user.",
+ "apierror-badconfig-resulttoosmall": "The value of <code>$wgAPIMaxResultSize</code> on this wiki is too small to hold basic result information.",
+ "apierror-badcontinue": "Invalid continue param. You should pass the original value returned by the previous query.",
+ "apierror-baddiff": "The diff cannot be retrieved, one or both revisions do not exist or you do not have permission to view them.",
+ "apierror-baddiffto": "<var>$1diffto</var> must be set to a non-negative number, <kbd>prev</kbd>, <kbd>next</kbd> or <kbd>cur</kbd>.",
+ "apierror-badformat-generic": "The requested format $1 is not supported for content model $2.",
+ "apierror-badformat": "The requested format $1 is not supported for content model $2 used by $3.",
+ "apierror-badgenerator-notgenerator": "Module <kbd>$1</kbd> cannot be used as a generator.",
+ "apierror-badgenerator-unknown": "Unknown <kbd>generator=$1</kbd>.",
+ "apierror-badip": "IP parameter is not valid.",
+ "apierror-badmd5": "The supplied MD5 hash was incorrect.",
+ "apierror-badmodule-badsubmodule": "The module <kbd>$1</kbd> does not have a submodule \"$2\".",
+ "apierror-badmodule-nosubmodules": "The module <kbd>$1</kbd> has no submodules.",
+ "apierror-badparameter": "Invalid value for parameter <var>$1</var>.",
+ "apierror-badquery": "Invalid query.",
+ "apierror-badtimestamp": "Invalid value \"$2\" for timestamp parameter <var>$1</var>.",
+ "apierror-badtoken": "Invalid CSRF token.",
+ "apierror-badupload": "File upload parameter <var>$1</var> is not a file upload; be sure to use <code>multipart/form-data</code> for your POST and include a filename in the <code>Content-Disposition</code> header.",
+ "apierror-badurl": "Invalid value \"$2\" for URL parameter <var>$1</var>.",
+ "apierror-baduser": "Invalid value \"$2\" for user parameter <var>$1</var>.",
+ "apierror-badvalue-notmultivalue": "U+001F multi-value separation may only be used for multi-valued parameters.",
+ "apierror-bad-watchlist-token": "Incorrect watchlist token provided. Please set a correct token in [[Special:Preferences]].",
+ "apierror-blockedfrommail": "You have been blocked from sending email.",
+ "apierror-blocked": "You have been blocked from editing.",
+ "apierror-botsnotsupported": "This interface is not supported for bots.",
+ "apierror-cannotreauthenticate": "This action is not available as your identity cannot be verified.",
+ "apierror-cannotviewtitle": "You are not allowed to view $1.",
+ "apierror-cantblock-email": "You don't have permission to block users from sending email through the wiki.",
+ "apierror-cantblock": "You don't have permission to block users.",
+ "apierror-cantchangecontentmodel": "You don't have permission to change the content model of a page.",
+ "apierror-canthide": "You don't have permission to hide user names from the block log.",
+ "apierror-cantimport-upload": "You don't have permission to import uploaded pages.",
+ "apierror-cantimport": "You don't have permission to import pages.",
+ "apierror-cantoverwrite-sharedfile": "The target file exists on a shared repository and you do not have permission to override it.",
+ "apierror-cantsend": "You are not logged in, you do not have a confirmed email address, or you are not allowed to send email to other users, so you cannot send email.",
+ "apierror-cantundelete": "Couldn't undelete: the requested revisions may not exist, or may have been undeleted already.",
+ "apierror-changeauth-norequest": "Failed to create change request.",
+ "apierror-chunk-too-small": "Minimum chunk size is $1 {{PLURAL:$1|byte|bytes}} for non-final chunks.",
+ "apierror-cidrtoobroad": "$1 CIDR ranges broader than /$2 are not accepted.",
+ "apierror-compare-inputneeded": "A title, a page ID, or a revision number is needed for both the <var>from</var> and the <var>to</var> parameters.",
+ "apierror-contentserializationexception": "Content serialization failed: $1",
+ "apierror-contenttoobig": "The content you supplied exceeds the article size limit of $1 {{PLURAL:$1|kilobyte|kilobytes}}.",
+ "apierror-copyuploadbaddomain": "Uploads by URL are not allowed from this domain.",
+ "apierror-copyuploadbadurl": "Upload not allowed from this URL.",
+ "apierror-create-titleexists": "Existing titles can't be protected with <kbd>create</kbd>.",
+ "apierror-csp-report": "Error processing CSP report: $1.",
+ "apierror-databaseerror": "[$1] Database query error.",
+ "apierror-deletedrevs-param-not-1-2": "The <var>$1</var> parameter cannot be used in modes 1 or 2.",
+ "apierror-deletedrevs-param-not-3": "The <var>$1</var> parameter cannot be used in mode 3.",
+ "apierror-emptynewsection": "Creating empty new sections is not possible.",
+ "apierror-emptypage": "Creating new, empty pages is not allowed.",
+ "apierror-exceptioncaught": "[$1] Exception caught: $2",
+ "apierror-filedoesnotexist": "File does not exist.",
+ "apierror-fileexists-sharedrepo-perm": "The target file exists on a shared repository. Use the <var>ignorewarnings</var> parameter to override it.",
+ "apierror-filenopath": "Cannot get local file path.",
+ "apierror-filetypecannotberotated": "File type cannot be rotated.",
+ "apierror-formatphp": "This response cannot be represented using <kbd>format=php</kbd>. See https://phabricator.wikimedia.org/T68776.",
+ "apierror-imageusage-badtitle": "The title for <kbd>$1</kbd> must be a file.",
+ "apierror-import-unknownerror": "Unknown error on import: $1.",
+ "apierror-integeroutofrange-abovebotmax": "<var>$1</var> may not be over $2 (set to $3) for bots or sysops.",
+ "apierror-integeroutofrange-abovemax": "<var>$1</var> may not be over $2 (set to $3) for users.",
+ "apierror-integeroutofrange-belowminimum": "<var>$1</var> may not be less than $2 (set to $3).",
+ "apierror-invalidcategory": "The category name you entered is not valid.",
+ "apierror-invalid-chunk": "Offset plus current chunk is greater than claimed file size.",
+ "apierror-invalidexpiry": "Invalid expiry time \"$1\".",
+ "apierror-invalid-file-key": "Not a valid file key.",
+ "apierror-invalidlang": "Invalid language code for parameter <var>$1</var>.",
+ "apierror-invalidoldimage": "The oldimage parameter has invalid format.",
+ "apierror-invalidparammix-cannotusewith": "The <kbd>$1</kbd> parameter cannot be used with <kbd>$2</kbd>.",
+ "apierror-invalidparammix-mustusewith": "The <kbd>$1</kbd> parameter may only be used with <kbd>$2</kbd>.",
+ "apierror-invalidparammix-parse-new-section": "<kbd>section=new</kbd> cannot be combined with the <var>oldid</var>, <var>pageid</var> or <var>page</var> parameters. Please use <var>title</var> and <var>text</var>.",
+ "apierror-invalidparammix": "The {{PLURAL:$2|parameters}} $1 can not be used together.",
+ "apierror-invalidsection": "The section parameter must be a valid section ID or <kbd>new</kbd>.",
+ "apierror-invalidsha1base36hash": "The SHA1Base36 hash provided is not valid.",
+ "apierror-invalidsha1hash": "The SHA1 hash provided is not valid.",
+ "apierror-invalidtitle": "Bad title \"$1\".",
+ "apierror-invalidurlparam": "Invalid value for <var>$1urlparam</var> (<kbd>$2=$3</kbd>).",
+ "apierror-invaliduser": "Invalid username \"$1\".",
+ "apierror-maxlag-generic": "Waiting for a database server: $1 {{PLURAL:$1|second|seconds}} lagged.",
+ "apierror-maxlag": "Waiting for $2: $1 {{PLURAL:$1|second|seconds}} lagged.",
+ "apierror-mimesearchdisabled": "MIME search is disabled in Miser Mode.",
+ "apierror-missingcontent-pageid": "Missing content for page ID $1.",
+ "apierror-missingparam-at-least-one-of": "{{PLURAL:$2|The parameter|At least one of the parameters}} $1 is required.",
+ "apierror-missingparam-one-of": "{{PLURAL:$2|The parameter|One of the parameters}} $1 is required.",
+ "apierror-missingparam": "The <var>$1</var> parameter must be set.",
+ "apierror-missingrev-pageid": "No current revision of page ID $1.",
+ "apierror-missingtitle-createonly": "Missing titles can only be protected with <kbd>create</kbd>.",
+ "apierror-missingtitle": "The page you specified doesn't exist.",
+ "apierror-missingtitle-byname": "The page $1 doesn't exist.",
+ "apierror-moduledisabled": "The <kbd>$1</kbd> module has been disabled.",
+ "apierror-multival-only-one-of": "{{PLURAL:$3|Only|Only one of}} $2 is allowed for parameter <var>$1</var>.",
+ "apierror-multival-only-one": "Only one value is allowed for parameter <var>$1</var>.",
+ "apierror-multpages": "<var>$1</var> may only be used with a single page.",
+ "apierror-mustbeloggedin-changeauth": "You must be logged in to change authentication data.",
+ "apierror-mustbeloggedin-generic": "You must be logged in.",
+ "apierror-mustbeloggedin-linkaccounts": "You must be logged in to link accounts.",
+ "apierror-mustbeloggedin-removeauth": "You must be logged in to remove authentication data.",
+ "apierror-mustbeloggedin-uploadstash": "The upload stash is only available to logged-in users.",
+ "apierror-mustbeloggedin": "You must be logged in to $1.",
+ "apierror-mustbeposted": "The <kbd>$1</kbd> module requires a POST request.",
+ "apierror-mustpostparams": "The following {{PLURAL:$2|parameter was|parameters were}} found in the query string, but must be in the POST body: $1.",
+ "apierror-noapiwrite": "Editing of this wiki through the API is disabled. Make sure the <code>$wgEnableWriteAPI=true;</code> statement is included in the wiki's <code>LocalSettings.php</code> file.",
+ "apierror-nochanges": "No changes were requested.",
+ "apierror-nodeleteablefile": "No such old version of the file.",
+ "apierror-no-direct-editing": "Direct editing via API is not supported for content model $1 used by $2.",
+ "apierror-noedit-anon": "Anonymous users can't edit pages.",
+ "apierror-noedit": "You don't have permission to edit pages.",
+ "apierror-noimageredirect-anon": "Anonymous users can't create image redirects.",
+ "apierror-noimageredirect": "You don't have permission to create image redirects.",
+ "apierror-nosuchlogid": "There is no log entry with ID $1.",
+ "apierror-nosuchpageid": "There is no page with ID $1.",
+ "apierror-nosuchrcid": "There is no recent change with ID $1.",
+ "apierror-nosuchrevid": "There is no revision with ID $1.",
+ "apierror-nosuchsection": "There is no section $1.",
+ "apierror-nosuchsection-what": "There is no section $1 in $2.",
+ "apierror-notarget": "You have not specified a valid target for this action.",
+ "apierror-notpatrollable": "The revision r$1 can't be patrolled as it's too old.",
+ "apierror-nouploadmodule": "No upload module set.",
+ "apierror-opensearch-json-warnings": "Warnings cannot be represented in OpenSearch JSON format.",
+ "apierror-pagecannotexist": "Namespace doesn't allow actual pages.",
+ "apierror-pagedeleted": "The page has been deleted since you fetched its timestamp.",
+ "apierror-paramempty": "The parameter <var>$1</var> may not be empty.",
+ "apierror-parsetree-notwikitext": "<kbd>prop=parsetree</kbd> is only supported for wikitext content.",
+ "apierror-parsetree-notwikitext-title": "<kbd>prop=parsetree</kbd> is only supported for wikitext content. $1 uses content model $2.",
+ "apierror-pastexpiry": "Expiry time \"$1\" is in the past.",
+ "apierror-permissiondenied": "You don't have permission to $1.",
+ "apierror-permissiondenied-generic": "Permission denied.",
+ "apierror-permissiondenied-patrolflag": "You need the <code>patrol</code> or <code>patrolmarks</code> right to request the patrolled flag.",
+ "apierror-permissiondenied-unblock": "You don't have permission to unblock users.",
+ "apierror-prefixsearchdisabled": "Prefix search is disabled in Miser Mode.",
+ "apierror-promised-nonwrite-api": "The <code>Promise-Non-Write-API-Action</code> HTTP header cannot be sent to write-mode API modules.",
+ "apierror-protect-invalidaction": "Invalid protection type \"$1\".",
+ "apierror-protect-invalidlevel": "Invalid protection level \"$1\".",
+ "apierror-ratelimited": "You've exceeded your rate limit. Please wait some time and try again.",
+ "apierror-readapidenied": "You need read permission to use this module.",
+ "apierror-readonly": "The wiki is currently in read-only mode.",
+ "apierror-reauthenticate": "You have not authenticated recently in this session, please reauthenticate.",
+ "apierror-redirect-appendonly": "You have attempted to edit using the redirect-following mode, which must be used in conjuction with <kbd>section=new</kbd>, <var>prependtext</var>, or <var>appendtext</var>.",
+ "apierror-revdel-mutuallyexclusive": "The same field cannot be used in both <var>hide</var> and <var>show</var>.",
+ "apierror-revdel-needtarget": "A target title is required for this RevDel type.",
+ "apierror-revdel-paramneeded": "At least one value is required for <var>hide</var> and/or <var>show</var>.",
+ "apierror-revisions-norevids": "The <var>revids</var> parameter may not be used with the list options (<var>$1limit</var>, <var>$1startid</var>, <var>$1endid</var>, <kbd>$1dir=newer</kbd>, <var>$1user</var>, <var>$1excludeuser</var>, <var>$1start</var>, and <var>$1end</var>).",
+ "apierror-revisions-singlepage": "<var>titles</var>, <var>pageids</var> or a generator was used to supply multiple pages, but the <var>$1limit</var>, <var>$1startid</var>, <var>$1endid</var>, <kbd>$1dir=newer</kbd>, <var>$1user</var>, <var>$1excludeuser</var>, <var>$1start</var>, and <var>$1end</var> parameters may only be used on a single page.",
+ "apierror-revwrongpage": "r$1 is not a revision of $2.",
+ "apierror-searchdisabled": "<var>$1</var> search is disabled.",
+ "apierror-sectionreplacefailed": "Could not merge updated section.",
+ "apierror-sectionsnotsupported": "Sections are not supported for content model $1.",
+ "apierror-sectionsnotsupported-what": "Sections are not supported by $1.",
+ "apierror-show": "Incorrect parameter - mutually exclusive values may not be supplied.",
+ "apierror-siteinfo-includealldenied": "Cannot view all servers' info unless <var>$wgShowHostNames</var> is true.",
+ "apierror-sizediffdisabled": "Size difference is disabled in Miser Mode.",
+ "apierror-spamdetected": "Your edit was refused because it contained a spam fragment: <code>$1</code>.",
+ "apierror-specialpage-cantexecute": "You don't have permission to view the results of this special page.",
+ "apierror-stashedfilenotfound": "Could not find the file in the stash: $1.",
+ "apierror-stashedit-missingtext": "No stashed text found with the given hash.",
+ "apierror-stashfailed-complete": "Chunked upload is already completed, check status for details.",
+ "apierror-stashfailed-nosession": "No chunked upload session with this key.",
+ "apierror-stashfilestorage": "Could not store upload in the stash: $1",
+ "apierror-stashnosuchfilekey": "No such filekey: $1.",
+ "apierror-stashpathinvalid": "File key of improper format or otherwise invalid: $1.",
+ "apierror-stashwrongowner": "Wrong owner: $1",
+ "apierror-stashzerolength": "File is of zero length, and could not be stored in the stash: $1.",
+ "apierror-templateexpansion-notwikitext": "Template expansion is only supported for wikitext content. $1 uses content model $2.",
+ "apierror-toofewexpiries": "$1 expiry {{PLURAL:$1|timestamp was|timestamps were}} provided where $2 {{PLURAL:$2|was|were}} needed.",
+ "apierror-unknownaction": "The action specified, <kbd>$1</kbd>, is not recognized.",
+ "apierror-unknownerror-editpage": "Unknown EditPage error: $1.",
+ "apierror-unknownerror-nocode": "Unknown error.",
+ "apierror-unknownerror": "Unknown error: \"$1\".",
+ "apierror-unknownformat": "Unrecognized format \"$1\".",
+ "apierror-unrecognizedparams": "Unrecognized {{PLURAL:$2|parameter|parameters}}: $1.",
+ "apierror-unrecognizedvalue": "Unrecognized value for parameter <var>$1</var>: $2.",
+ "apierror-unsupportedrepo": "Local file repository does not support querying all images.",
+ "apierror-upload-filekeyneeded": "Must supply a <var>filekey</var> when <var>offset</var> is non-zero.",
+ "apierror-upload-filekeynotallowed": "Cannot supply a <var>filekey</var> when <var>offset</var> is 0.",
+ "apierror-upload-inprogress": "Upload from stash already in progress.",
+ "apierror-upload-missingresult": "No result in status data.",
+ "apierror-urlparamnormal": "Could not normalize image parameters for $1.",
+ "apierror-writeapidenied": "You're not allowed to edit this wiki through the API.",
+
+ "apiwarn-alldeletedrevisions-performance": "For better performance when generating titles, set <kbd>$1dir=newer</kbd>.",
+ "apiwarn-badurlparam": "Could not parse <var>$1urlparam</var> for $2. Using only width and height.",
+ "apiwarn-badutf8": "The value passed for <var>$1</var> contains invalid or non-normalized data. Textual data should be valid, NFC-normalized Unicode without C0 control characters other than HT (\\t), LF (\\n), and CR (\\r).",
+ "apiwarn-checktoken-percentencoding": "Check that symbols such as \"+\" in the token are properly percent-encoded in the URL.",
+ "apiwarn-deprecation-deletedrevs": "<kbd>list=deletedrevs</kbd> has been deprecated. Please use <kbd>prop=deletedrevisions</kbd> or <kbd>list=alldeletedrevisions</kbd> instead.",
+ "apiwarn-deprecation-expandtemplates-prop": "Because no values have been specified for the <var>prop</var> parameter, a legacy format has been used for the output. This format is deprecated, and in the future, a default value will be set for the <var>prop</var> parameter, causing the new format to always be used.",
+ "apiwarn-deprecation-httpsexpected": "HTTP used when HTTPS was expected.",
+ "apiwarn-deprecation-login-botpw": "Main-account login via <kbd>action=login</kbd> is deprecated and may stop working without warning. To continue login with <kbd>action=login</kbd>, see [[Special:BotPasswords]]. To safely continue using main-account login, see <kbd>action=clientlogin</kbd>.",
+ "apiwarn-deprecation-login-nobotpw": "Main-account login via <kbd>action=login</kbd> is deprecated and may stop working without warning. To safely log in, see <kbd>action=clientlogin</kbd>.",
+ "apiwarn-deprecation-login-token": "Fetching a token via <kbd>action=login</kbd> is deprecated. Use <kbd>action=query&meta=tokens&type=login</kbd> instead.",
+ "apiwarn-deprecation-parameter": "The parameter <var>$1</var> has been deprecated.",
+ "apiwarn-deprecation-parse-headitems": "<kbd>prop=headitems</kbd> is deprecated since MediaWiki 1.28. Use <kbd>prop=headhtml</kbd> when creating new HTML documents, or <kbd>prop=modules|jsconfigvars</kbd> when updating a document client-side.",
+ "apiwarn-deprecation-purge-get": "Use of <kbd>action=purge</kbd> via GET is deprecated. Use POST instead.",
+ "apiwarn-deprecation-withreplacement": "<kbd>$1</kbd> has been deprecated. Please use <kbd>$2</kbd> instead.",
+ "apiwarn-difftohidden": "Couldn't diff to r$1: content is hidden.",
+ "apiwarn-errorprinterfailed": "Error printer failed. Will retry without params.",
+ "apiwarn-errorprinterfailed-ex": "Error printer failed (will retry without params): $1",
+ "apiwarn-invalidcategory": "\"$1\" is not a category.",
+ "apiwarn-invalidtitle": "\"$1\" is not a valid title.",
+ "apiwarn-invalidxmlstylesheetext": "Stylesheet should have <code>.xsl</code> extension.",
+ "apiwarn-invalidxmlstylesheet": "Invalid or non-existent stylesheet specified.",
+ "apiwarn-invalidxmlstylesheetns": "Stylesheet should be in the {{ns:MediaWiki}} namespace.",
+ "apiwarn-moduleswithoutvars": "Property <kbd>modules</kbd> was set but not <kbd>jsconfigvars</kbd> or <kbd>encodedjsconfigvars</kbd>. Configuration variables are necessary for proper module usage.",
+ "apiwarn-notfile": "\"$1\" is not a file.",
+ "apiwarn-nothumb-noimagehandler": "Could not create thumbnail because $1 does not have an associated image handler.",
+ "apiwarn-parse-nocontentmodel": "No <var>title</var> or <var>contentmodel</var> was given, assuming $1.",
+ "apiwarn-parse-titlewithouttext": "<var>title</var> used without <var>text</var>, and parsed page properties were requested. Did you mean to use <var>page</var> instead of <var>title</var>?",
+ "apiwarn-redirectsandrevids": "Redirect resolution cannot be used together with the <var>revids</var> parameter. Any redirects the <var>revids</var> point to have not been resolved.",
+ "apiwarn-tokennotallowed": "Action \"$1\" is not allowed for the current user.",
+ "apiwarn-tokens-origin": "Tokens may not be obtained when the same-origin policy is not applied.",
+ "apiwarn-toomanyvalues": "Too many values supplied for parameter <var>$1</var>: the limit is $2.",
+ "apiwarn-truncatedresult": "This result was truncated because it would otherwise be larger than the limit of $1 bytes.",
+ "apiwarn-unclearnowtimestamp": "Passing \"$2\" for timestamp parameter <var>$1</var> has been deprecated. If for some reason you need to explicitly specify the current time without calculating it client-side, use <kbd>now<kbd>.",
+ "apiwarn-unrecognizedvalues": "Unrecognized {{PLURAL:$3|value|values}} for parameter <var>$1</var>: $2.",
+ "apiwarn-unsupportedarray": "Parameter <var>$1</var> uses unsupported PHP array syntax.",
+ "apiwarn-urlparamwidth": "Ignoring width value set in <var>$1urlparam</var> ($2) in favor of width value derived from <var>$1urlwidth</var>/<var>$1urlheight</var> ($3).",
+ "apiwarn-validationfailed-badchars": "invalid characters in key (only <code>a-z</code>, <code>A-Z</code>, <code>0-9</code>, <code>_</code>, and <code>-</code> are allowed).",
+ "apiwarn-validationfailed-badpref": "not a valid preference.",
+ "apiwarn-validationfailed-cannotset": "cannot be set by this module.",
+ "apiwarn-validationfailed-keytoolong": "key too long (no more than $1 bytes allowed).",
+ "apiwarn-validationfailed": "Validation error for <kbd>$1</kbd>: $2",
+ "apiwarn-wgDebugAPI": "<strong>Security Warning</strong>: <var>$wgDebugAPI</var> is enabled.",
+
+ "api-feed-error-title": "Error ($1)",
+ "api-usage-docref": "See $1 for API usage.",
+ "api-exception-trace": "$1 at $2($3)\n$4",
"api-credits-header": "Credits",
"api-credits": "API developers:\n* Yuri Astrakhan (creator, lead developer Sep 2006–Sep 2007)\n* Roan Kattouw (lead developer Sep 2007–2009)\n* Victor Vasiliev\n* Bryan Tong Minh\n* Sam Reed\n* Brad Jorsch (lead developer 2013–present)\n\nPlease send your comments, suggestions and questions to mediawiki-api@lists.wikimedia.org\nor file a bug report at https://phabricator.wikimedia.org/."
}
"apihelp-edit-param-tags": "Ganti tag untuk menerapkan ke revisi.",
"apihelp-edit-param-minor": "Suntingan kecil.",
"apihelp-edit-param-notminor": "Bukan suntingan kecil.",
- "apihelp-edit-param-bot": "Tandai suntingan ini sebagai bot.",
+ "apihelp-edit-param-bot": "Tandai suntingan ini sebagai suntingan bot.",
"apihelp-edit-param-basetimestamp": "Stempel waktu dari revisi asal, digunakan untuk mendeteksi konflik penyuntingan. Dapat ditemukan di [[Special:ApiHelp/query+revisions|action=query&prop=revisions&rvprop=timestamp]].",
"apihelp-edit-param-starttimestamp": "Stempel waktu ketika proses penyuntingan dimulai, digunakan untuk mendeteksi konflik penyuntingan. Nilai yang cocok dapat ditemukan dengan menggunakan <var>[[Special:ApiHelp/main|curtimestamp]]</var> ketika memulai proses penyuntingan (seperti ketika memuat isi konten yang akan disunting).",
"apihelp-edit-param-recreate": "Batalkan galat yang terjadi tentang halaman yang sudah dihapus pada saat itu.",
"apihelp-emailuser-param-subject": "Tajuk subjek.",
"apihelp-emailuser-param-text": "Badan pesan.",
"apihelp-emailuser-param-ccme": "Kirimkan salinan pesan ini kepada saya.",
- "apihelp-expandtemplates-description": "Tambahkan semua templat dalam teks wiki.",
+ "apihelp-expandtemplates-description": "Longgarkan semua templat dalam teks wiki.",
"apihelp-expandtemplates-param-title": "Judul halaman.",
"apihelp-expandtemplates-param-text": "Teks wiki yang akan diubah.",
"apihelp-expandtemplates-param-revid": "ID revisi, untuk <nowiki>{{REVISIONID}}</nowiki> dan variabel serupa.",
"apihelp-expandtemplates-param-prop": "Bagian informasi manakah yang ingin didapatkan.\n\nPerhatikan bahwa jika tidak ada nilai yang dipilih, hasilnya akan mengandung teks wiki, namun keluaran akan berupa format usang.",
"apihelp-login-example-login": "Masuk log.",
+ "apihelp-move-param-noredirect": "Jangan buat pengalihan.",
+ "apihelp-move-param-unwatch": "Hapus halaman dan pengalihan dari daftar pantauan pengguna ini.",
+ "apihelp-move-example-move": "Pindahkan <kbd>Judul buruk</kbd> ke <kbd>Judul benar</kbd> tanpa membuat pengalihan.",
+ "apihelp-opensearch-param-redirects": "Bagaimana menangani pengalihan:\n;return:Kembali ke pengalihan itu.\n;resolve:Kembali ke halaman tujuan. Mungkin hasil kembali kurang dari $1limit.\nUntuk alasan riwayat, nilai baku adalah \"kembali\" untuk $1format=json dan \"resolve\" untuk format lain.",
"apihelp-query+prefixsearch-param-profile": "Cari profil untuk digunakan.",
"apihelp-query+search-param-qiprofile": "Meminta profil independen untuk digunakan (berefek pada algoritma peringkat).",
"apihelp-revisiondelete-param-ids": "Penanda untuk perubahan yang akan dihapus",
"apihelp-main-param-curtimestamp": "{{doc-apihelp-param|main|curtimestamp}}",
"apihelp-main-param-origin": "{{doc-apihelp-param|main|origin}}",
"apihelp-main-param-uselang": "{{doc-apihelp-param|main|uselang}}",
+ "apihelp-main-param-errorformat": "{{doc-apihelp-param|main|errorformat}}",
+ "apihelp-main-param-errorlang": "{{doc-apihelp-param|main|errorlang}}",
+ "apihelp-main-param-errorsuselocal": "{{doc-apihelp-param|main|errorsuselocal}}",
+ "apihelp-main-param-responselanginfo": "{{doc-apihelp-param|main|responselanginfo}}",
"apihelp-block-description": "{{doc-apihelp-description|block}}",
"apihelp-block-param-user": "{{doc-apihelp-param|block|user}}",
"apihelp-block-param-expiry": "{{doc-apihelp-param|block|expiry}}\n{{doc-important|Do not translate \"5 months\", \"2 weeks\", \"infinite\", \"indefinite\" or \"never\"!}}",
"api-help-authmanagerhelper-returnurl": "{{doc-apihelp-param|description=the \"returnurl\" parameter for AuthManager-using API modules|noseealso=1}}",
"api-help-authmanagerhelper-continue": "{{doc-apihelp-param|description=the \"continue\" parameter for AuthManager-using API modules|noseealso=1}}",
"api-help-authmanagerhelper-additional-params": "Message to display for AuthManager modules that take additional parameters to populate AuthenticationRequests. Parameters:\n* $1 - AuthManager action used by this module\n* $2 - Module parameter prefix, e.g. \"login\"\n* $3 - Module name, e.g. \"clientlogin\"\n* $4 - Module path, e.g. \"clientlogin\"",
+ "apierror-allimages-redirect": "{{doc-apierror}}",
+ "apierror-allpages-generator-redirects": "{{doc-apierror}}",
+ "apierror-appendnotsupported": "{{doc-apierror}}\n\nParameters:\n* $1 - Content model",
+ "apierror-articleexists": "{{doc-apierror}}",
+ "apierror-assertbotfailed": "{{doc-apierror}}",
+ "apierror-assertnameduserfailed": "{{doc-apierror}}\n\nParameters:\n* $1 - User name passed in.",
+ "apierror-assertuserfailed": "{{doc-apierror}}",
+ "apierror-autoblocked": "{{doc-apierror}}",
+ "apierror-bad-watchlist-token": "{{doc-apierror}}",
+ "apierror-badconfig-resulttoosmall": "{{doc-apierror}}",
+ "apierror-badcontinue": "{{doc-apierror}}",
+ "apierror-baddiff": "{{doc-apierror}}",
+ "apierror-baddiffto": "{{doc-apierror}}\n\nParameters:\n* $1 - Module parameter prefix, e.g. \"bl\".",
+ "apierror-badformat": "{{doc-apierror}}\n\nParameters:\n* $1 - Content format.\n* $2 - Content model.\n* $3 - Title using the model.",
+ "apierror-badformat-generic": "{{doc-apierror}}\n\nParameters:\n* $1 - Content format.\n* $2 - Content model.",
+ "apierror-badgenerator-notgenerator": "{{doc-apierror}}\n\nParameters:\n* $1 - Generator module name.",
+ "apierror-badgenerator-unknown": "{{doc-apierror}}\n\nParameters:\n* $1 - Generator module name.",
+ "apierror-badip": "{{doc-apierror}}",
+ "apierror-badmd5": "{{doc-apierror}}",
+ "apierror-badmodule-badsubmodule": "{{doc-apierror}}\n\nParameters:\n* $1 - Module path.\n* $2 - Submodule name.",
+ "apierror-badmodule-nosubmodules": "{{doc-apierror}}\n\nParameters:\n* $1 - Module path.",
+ "apierror-badparameter": "{{doc-apierror}}\n\nParameters:\n* $1 - Parameter name.",
+ "apierror-badquery": "{{doc-apierror}}",
+ "apierror-badtimestamp": "{{doc-apierror}}\n\nParameters:\n* $1 - Parameter name.\n* $2 - Value of the parameter.",
+ "apierror-badtoken": "{{doc-apierror}}",
+ "apierror-badupload": "{{doc-apierror}}\n\nParameters:\n* $1 - Parameter name.",
+ "apierror-badurl": "{{doc-apierror}}\n\nParameters:\n* $1 - Parameter name.\n* $2 - Value of the parameter.",
+ "apierror-baduser": "{{doc-apierror}}\n\nParameters:\n* $1 - Parameter name.\n* $2 - Value of the parameter.",
+ "apierror-badvalue-notmultivalue": "{{doc-apierror}}",
+ "apierror-blocked": "{{doc-apierror}}",
+ "apierror-blockedfrommail": "{{doc-apierror}}",
+ "apierror-botsnotsupported": "{{doc-apierror}}",
+ "apierror-cannotreauthenticate": "{{doc-apierror}}",
+ "apierror-cannotviewtitle": "{{doc-apierror}}\n\nParameters:\n* $1 - Title.",
+ "apierror-cantblock": "{{doc-apierror}}",
+ "apierror-cantblock-email": "{{doc-apierror}}",
+ "apierror-cantchangecontentmodel": "{{doc-apierror}}",
+ "apierror-canthide": "{{doc-apierror}}",
+ "apierror-cantimport": "{{doc-apierror}}",
+ "apierror-cantimport-upload": "{{doc-apierror}}",
+ "apierror-cantoverwrite-sharedfile": "{{doc-apierror}}",
+ "apierror-cantsend": "{{doc-apierror}}",
+ "apierror-cantundelete": "{{doc-apierror}}",
+ "apierror-changeauth-norequest": "{{doc-apierror}}",
+ "apierror-chunk-too-small": "{{doc-apierror}}\n\nParameters:\n* $1 - Minimum size in bytes.",
+ "apierror-cidrtoobroad": "{{doc-apierror}}\n\nParameters:\n* $1 - \"IPv4\" or \"IPv6\"\n* $2 - Minimum CIDR mask length.",
+ "apierror-compare-inputneeded": "{{doc-apierror}}",
+ "apierror-contentserializationexception": "{{doc-apierror}}\n\nParameters:\n* $1 - Exception text, may end with punctuation. Currently this is probably English, hopefully we'll fix that in the future.",
+ "apierror-contenttoobig": "{{doc-apierror}}\n\nParameters:\n* $1 - Maximum article size in kilobytes.",
+ "apierror-copyuploadbaddomain": "{{doc-apierror}}",
+ "apierror-copyuploadbadurl": "{{doc-apierror}}",
+ "apierror-create-titleexists": "{{doc-apierror}}",
+ "apierror-csp-report": "{{doc-apierror}}\n\nParameters:\n* $1 - Error code, e.g. \"toobig\".",
+ "apierror-databaseerror": "{{doc-apierror}}\n\nParameters:\n* $1 - Exception log ID code. This is meaningless to the end user, but can be used by people with access to the logs to easily find the logged error.",
+ "apierror-deletedrevs-param-not-1-2": "{{doc-apierror}}\n\nParameters:\n* $1 - Parameter name.\n\nSee also:\n* {{msg-mw|apihelp-query+deletedrevs-description}}",
+ "apierror-deletedrevs-param-not-3": "{{doc-apierror}}\n\nParameters:\n* $1 - Parameter name.\n\nSee also:\n* {{msg-mw|apihelp-query+deletedrevs-description}}",
+ "apierror-emptynewsection": "{{doc-apierror}}",
+ "apierror-emptypage": "{{doc-apierror}}",
+ "apierror-exceptioncaught": "{{doc-apierror}}\n\nParameters:\n* $1 - Exception log ID code. This is meaningless to the end user, but can be used by people with access to the logs to easily find the logged error.\n* $2 - Exception message, which may end with punctuation. Probably in English.",
+ "apierror-filedoesnotexist": "{{doc-apierror}}",
+ "apierror-fileexists-sharedrepo-perm": "{{doc-apierror}}",
+ "apierror-filenopath": "{{doc-apierror}}",
+ "apierror-filetypecannotberotated": "{{doc-apierror}}",
+ "apierror-formatphp": "{{doc-apierror}}",
+ "apierror-imageusage-badtitle": "{{doc-apierror}}\n\nParameters:\n* $1 - Module name.",
+ "apierror-import-unknownerror": "{{doc-apierror}}\n\nParameters:\n* $1 - Error message returned by the import, probably in English.",
+ "apierror-integeroutofrange-abovebotmax": "{{doc-apierror}}\n\nParameters:\n* $1 - Parameter name\n* $2 - Maximum allowed value\n* $3 - Supplied value",
+ "apierror-integeroutofrange-abovemax": "{{doc-apierror}}\n\nParameters:\n* $1 - Parameter name\n* $2 - Maximum allowed value\n* $3 - Supplied value",
+ "apierror-integeroutofrange-belowminimum": "{{doc-apierror}}\n\nParameters:\n* $1 - Parameter name\n* $2 - Minimum allowed value\n* $3 - Supplied value",
+ "apierror-invalid-chunk": "{{doc-apierror}}",
+ "apierror-invalid-file-key": "{{doc-apierror}}",
+ "apierror-invalidcategory": "{{doc-apierror}}",
+ "apierror-invalidexpiry": "{{doc-apierror}}\n\nParameters:\n* $1 - Value provided.",
+ "apierror-invalidlang": "{{doc-apierror}}\n\nParameters:\n* $1 - Parameter name.",
+ "apierror-invalidoldimage": "{{doc-apierror}}",
+ "apierror-invalidparammix": "{{doc-apierror}}\n\nParameters:\n* $1 - List of parameter names or \"parameter=value\" text.\n* $2 - Number of parameters.",
+ "apierror-invalidparammix-cannotusewith": "{{doc-apierror}}\n\nParameters:\n* $1 - Parameter name or \"parameter=value\" text.\n* $2 - Parameter name or \"parameter=value\" text.",
+ "apierror-invalidparammix-mustusewith": "{{doc-apierror}}\n\nParameters:\n* $1 - Parameter name or \"parameter=value\" text.\n* $2 - Parameter name or \"parameter=value\" text.",
+ "apierror-invalidparammix-parse-new-section": "{{doc-apierror}}",
+ "apierror-invalidsection": "{{doc-apierror}}",
+ "apierror-invalidsha1base36hash": "{{doc-apierror}}",
+ "apierror-invalidsha1hash": "{{doc-apierror}}",
+ "apierror-invalidtitle": "{{doc-apierror}}\n\nParameters:\n* $1 - Title that is invalid",
+ "apierror-invalidurlparam": "{{doc-apierror}}\n\nParameters:\n* $1 - Module parameter prefix, e.g. \"bl\".\n* $2 - Key\n* $3 - Value.",
+ "apierror-invaliduser": "{{doc-apierror}}\n\nParameters:\n* $1 - User name that is invalid.",
+ "apierror-maxlag": "{{doc-apierror}}\n\nParameters:\n* $1 - Database lag in seconds.\n* $2 - Database server that is lagged.",
+ "apierror-maxlag-generic": "{{doc-apierror}}\n\nParameters:\n* $1 - Database is lag in seconds.",
+ "apierror-mimesearchdisabled": "{{doc-apierror}}",
+ "apierror-missingcontent-pageid": "{{doc-apierror}}\n\nParameters:\n* $1 - Page ID number.",
+ "apierror-missingparam": "{{doc-apierror}}\n\nParameters:\n* $1 - Parameter name.",
+ "apierror-missingparam-at-least-one-of": "{{doc-apierror}}\n\nParameters:\n* $1 - List of parameter names.\n* $2 - Number of parameters.",
+ "apierror-missingparam-one-of": "{{doc-apierror}}\n\nParameters:\n* $1 - List of parameter names.\n* $2 - Number of parameters.",
+ "apierror-missingrev-pageid": "{{doc-apierror}}\n\nParameters:\n* $1 - Page ID number.",
+ "apierror-missingtitle": "{{doc-apierror}}",
+ "apierror-missingtitle-byname": "{{doc-apierror}}",
+ "apierror-missingtitle-createonly": "{{doc-apierror}}",
+ "apierror-moduledisabled": "{{doc-apierror}}\n\nParameters:\n* $1 - Name of the module.",
+ "apierror-multival-only-one": "{{doc-apierror}}\n\nParameters:\n* $1 - Parameter name.",
+ "apierror-multival-only-one-of": "{{doc-apierror}}\n\nParameters:\n* $1 - Parameter name.\n* $2 - Possible values for the parameter.\n* $3 - Number of values.",
+ "apierror-multpages": "{{doc-apierror}}\n\nParameters:\n* $1 - Parameter name",
+ "apierror-mustbeloggedin": "{{doc-apierror}}\n\nParameters:\n* $1 - One of the action-* messages (for example {{msg-mw|action-edit}}) or other such messages tagged with {{tl|doc-action}} in their documentation\n\nPlease report at [[Support]] if you are unable to properly translate this message. Also see [[phab:T16246]] (now closed) for background.\n\nSee also:\n* {{msg-mw|apierror-permissiondenied}}\n* {{msg-mw|permissionserrorstext-withaction}}",
+ "apierror-mustbeloggedin-changeauth": "{{doc-apierror}}",
+ "apierror-mustbeloggedin-generic": "{{doc-apierror}}",
+ "apierror-mustbeloggedin-linkaccounts": "{{doc-apierror}}",
+ "apierror-mustbeloggedin-removeauth": "{{doc-apierror}}",
+ "apierror-mustbeloggedin-uploadstash": "{{doc-apierror}}",
+ "apierror-mustbeposted": "{{doc-apierror}}\n\nParameters:\n* $1 - Module name.",
+ "apierror-mustpostparams": "{{doc-apierror}}\n\nParameters:\n* $1 - Parameter names.\n* $2 - Number of parameters.",
+ "apierror-no-direct-editing": "{{doc-apierror}}\n\nParameters:\n* $1 - Content model.\n* $2 - Title using the model.",
+ "apierror-noapiwrite": "{{doc-apierror}}",
+ "apierror-nochanges": "{{doc-apierror}}",
+ "apierror-nodeleteablefile": "{{doc-apierror}}",
+ "apierror-noedit": "{{doc-apierror}}",
+ "apierror-noedit-anon": "{{doc-apierror}}",
+ "apierror-noimageredirect": "{{doc-apierror}}",
+ "apierror-noimageredirect-anon": "{{doc-apierror}}",
+ "apierror-nosuchlogid": "{{doc-apierror}}\n\nParameters:\n* $1 - Log ID number.",
+ "apierror-nosuchpageid": "{{doc-apierror}}\n\nParameters:\n* $1 - Page ID number.",
+ "apierror-nosuchrcid": "{{doc-apierror}}\n\nParameters:\n* $1 - RecentChanges ID number.",
+ "apierror-nosuchrevid": "{{doc-apierror}}\n\nParameters:\n* $1 - Revision ID number.",
+ "apierror-nosuchsection": "{{doc-apierror}}\n\nParameters:\n* $1 - Section identifier. Probably a number or \"T-\" followed by a number.",
+ "apierror-nosuchsection-what": "{{doc-apierror}}\n\nParameters:\n* $1 - Section identifier. Probably a number or \"T-\" followed by a number.\n* $2 - Page title, revision ID formatted with {{msg-mw|revid}}, or page ID formatted with {{msg-mw|pageid}}.",
+ "apierror-notarget": "{{doc-apierror}}",
+ "apierror-notpatrollable": "{{doc-apierror}}\n\nParameters:\n* $1 - Revision ID number.",
+ "apierror-nouploadmodule": "{{doc-apierror}}",
+ "apierror-opensearch-json-warnings": "{{doc-apierror}}",
+ "apierror-pagecannotexist": "{{doc-apierror}}",
+ "apierror-pagedeleted": "{{doc-apierror}}",
+ "apierror-paramempty": "{{doc-apierror}}\n\nParameters:\n* $1 - Parameter name.",
+ "apierror-parsetree-notwikitext": "{{doc-apierror}}",
+ "apierror-parsetree-notwikitext-title": "{{doc-apierror}}\n\nParameters:\n* $1 - Page title.\n* $2 - Content model.",
+ "apierror-pastexpiry": "{{doc-apierror}}\n\nParameters:\n* $1 - Supplied expiry time.",
+ "apierror-permissiondenied-generic": "{{doc-apierror}}",
+ "apierror-permissiondenied-patrolflag": "{{doc-apierror}}\n\nSee also:\n* {{msg-mw|apierror-permissiondenied}}",
+ "apierror-permissiondenied-unblock": "{{doc-apierror}}\n\nSee also:\n* {{msg-mw|apierror-permissiondenied}}",
+ "apierror-permissiondenied": "{{doc-apierror}}\n\nParameters:\n* $1 - One of the action-* messages (for example {{msg-mw|action-edit}}) or other such messages tagged with {{tl|doc-action}} in their documentation\n\nPlease report at [[Support]] if you are unable to properly translate this message. Also see [[phab:T16246]] (now closed) for background.\n\nSee also:\n* {{msg-mw|permissionserrorstext-withaction}}",
+ "apierror-prefixsearchdisabled": "{{doc-apierror}}",
+ "apierror-promised-nonwrite-api": "{{doc-apierror}}",
+ "apierror-protect-invalidaction": "{{doc-apierror}}\n\nParameters:\n* $1 - Supplied protection type.",
+ "apierror-protect-invalidlevel": "{{doc-apierror}}\n\nParameters:\n* $1 - Supplied protection level.",
+ "apierror-ratelimited": "{{doc-apierror}}",
+ "apierror-readapidenied": "{{doc-apierror}}",
+ "apierror-readonly": "{{doc-apierror}}",
+ "apierror-reauthenticate": "{{doc-apierror}}",
+ "apierror-redirect-appendonly": "{{doc-apierror}}",
+ "apierror-revdel-mutuallyexclusive": "{{doc-apierror}}",
+ "apierror-revdel-needtarget": "{{doc-apierror}}",
+ "apierror-revdel-paramneeded": "{{doc-apierror}}",
+ "apierror-revisions-norevids": "{{doc-apierror}}\n\nParameters:\n* $1 - Module parameter prefix, e.g. \"bl\".",
+ "apierror-revisions-singlepage": "{{doc-apierror}}\n\nParameters:\n* $1 - Module parameter prefix, e.g. \"bl\".",
+ "apierror-revwrongpage": "{{doc-apierror}}\n\nParameters:\n* $1 - Revision ID number.\n* $2 - Page title.",
+ "apierror-searchdisabled": "{{doc-apierror}}\n\nParameters:\n* $1 - Search parameter that is disabled.",
+ "apierror-sectionreplacefailed": "{{doc-apierror}}",
+ "apierror-sectionsnotsupported": "{{doc-apierror}}\n\nParameters:\n* $1 - Content model that doesn't support sections.",
+ "apierror-sectionsnotsupported-what": "{{doc-apierror}}\n\nParameters:\n* $1 - Page title, revision ID formatted with {{msg-mw|revid}}, or page ID formatted with {{msg-mw|pageid}}.",
+ "apierror-show": "{{doc-apierror}}",
+ "apierror-siteinfo-includealldenied": "{{doc-apierror}}",
+ "apierror-sizediffdisabled": "{{doc-apierror}}",
+ "apierror-spamdetected": "{{doc-apierror}}\n\nParameters:\n* $1 - Matching \"spam filter\".\n\nSee also:\n* {{msg-mw|spamprotectionmatch}}",
+ "apierror-specialpage-cantexecute": "{{doc-apierror}}",
+ "apierror-stashedfilenotfound": "{{doc-apierror}}\n\nParameters:\n* $1 - Exception text. Currently this is probably English, hopefully we'll fix that in the future.",
+ "apierror-stashedit-missingtext": "{{doc-apierror}}",
+ "apierror-stashfailed-complete": "{{doc-apierror}}",
+ "apierror-stashfailed-nosession": "{{doc-apierror}}",
+ "apierror-stashfilestorage": "{{doc-apierror}}\n\nParameters:\n* $1 - Exception text, which may already end with punctuation. Currently this is probably English, hopefully we'll fix that in the future.",
+ "apierror-stashnosuchfilekey": "{{doc-apierror}}\n\nParameters:\n* $1 - Exception text. Currently this is probably English, hopefully we'll fix that in the future.",
+ "apierror-stashpathinvalid": "{{doc-apierror}}\n\nParameters:\n* $1 - Exception text. Currently this is probably English, hopefully we'll fix that in the future.",
+ "apierror-stashwrongowner": "{{doc-apierror}}\n\nParameters:\n* $1 - Exception text, which should already end with punctuation. Currently this is probably English, hopefully we'll fix that in the future.",
+ "apierror-stashzerolength": "{{doc-apierror}}\n\nParameters:\n* $1 - Exception text. Currently this is probably English, hopefully we'll fix that in the future.",
+ "apierror-templateexpansion-notwikitext": "{{doc-apierror}}\n\nParameters:\n* $1 - Page title.\n* $2 - Content model.",
+ "apierror-toofewexpiries": "{{doc-apierror}}\n\nParameters:\n* $1 - Number provided.\n* $2 - Number needed.",
+ "apierror-unknownaction": "{{doc-apierror}}\n\nParameters:\n* $1 - Action provided.",
+ "apierror-unknownerror": "{{doc-apierror}}\n\nParameters:\n* $1 - Error code (possibly a message key) not handled by ApiBase::parseMsg().",
+ "apierror-unknownerror-editpage": "{{doc-apierror}}\n\nParameters:\n* $1 - Error code (an integer).",
+ "apierror-unknownerror-nocode": "{{doc-apierror}}",
+ "apierror-unknownformat": "{{doc-apierror}}\n\nParameters:\n* $1 - Format provided.",
+ "apierror-unrecognizedparams": "{{doc-apierror}}\n\nParameters:\n* $1 - List of parameters.\n* $2 - Number of parameters.",
+ "apierror-unrecognizedvalue": "{{doc-apierror}}\n\nParameters:\n* $1 - Parameter name.\n* $2 - Parameter value.",
+ "apierror-unsupportedrepo": "{{doc-apierror}}",
+ "apierror-upload-filekeyneeded": "{{doc-apierror}}",
+ "apierror-upload-filekeynotallowed": "{{doc-apierror}}",
+ "apierror-upload-inprogress": "{{doc-apierror}}",
+ "apierror-upload-missingresult": "{{doc-apierror}}",
+ "apierror-urlparamnormal": "{{doc-apierror}}\n\nParameters:\n* $1 - Image title.",
+ "apierror-writeapidenied": "{{doc-apierror}}",
+ "apiwarn-alldeletedrevisions-performance": "{{doc-apierror}}\n\nParameters:\n* $1 - Module parameter prefix, e.g. \"bl\".",
+ "apiwarn-badurlparam": "{{doc-apierror}}\n\nParameters:\n* $1 - Module parameter prefix, e.g. \"bl\".\n* $2 - Image title.",
+ "apiwarn-badutf8": "{{doc-apierror}}\n\nParameters:\n* $1 - Parameter name.",
+ "apiwarn-checktoken-percentencoding": "{{doc-apierror}}",
+ "apiwarn-deprecation-deletedrevs": "{{doc-apierror}}",
+ "apiwarn-deprecation-expandtemplates-prop": "{{doc-apierror}}",
+ "apiwarn-deprecation-httpsexpected": "{{doc-apierror}}",
+ "apiwarn-deprecation-login-botpw": "{{doc-apierror}}",
+ "apiwarn-deprecation-login-nobotpw": "{{doc-apierror}}",
+ "apiwarn-deprecation-login-token": "{{doc-apierror}}",
+ "apiwarn-deprecation-parameter": "{{doc-apierror}}\n\nParameters:\n* $1 - Parameter name.",
+ "apiwarn-deprecation-parse-headitems": "{{doc-apierror}}",
+ "apiwarn-deprecation-purge-get": "{{doc-apierror}}",
+ "apiwarn-deprecation-withreplacement": "{{doc-apierror}}\n\nParameters:\n* $1 - Query string fragment that is deprecated, e.g. \"action=tokens\".\n* $2 - Query string fragment to use instead, e.g. \"action=tokens\".",
+ "apiwarn-difftohidden": "{{doc-apierror}}\n\nParameters:\n* $1 - Revision ID number.",
+ "apiwarn-errorprinterfailed": "{{doc-apierror}}",
+ "apiwarn-errorprinterfailed-ex": "{{doc-apierror}}\n\nParameters:\n* $1 - Exception message, which may already end in punctuation. Probably in English.",
+ "apiwarn-invalidcategory": "{{doc-apierror}}\n\nParameters:\n* $1 - Supplied category name.",
+ "apiwarn-invalidtitle": "{{doc-apierror}}\n\nParameters:\n* $1 - Supplied title.",
+ "apiwarn-invalidxmlstylesheet": "{{doc-apierror}}",
+ "apiwarn-invalidxmlstylesheetext": "{{doc-apierror}}",
+ "apiwarn-invalidxmlstylesheetns": "{{doc-apierror}}",
+ "apiwarn-moduleswithoutvars": "{{doc-apierror}}",
+ "apiwarn-notfile": "{{doc-apierror}}\n\nParameters:\n* $1 - Supplied file name.",
+ "apiwarn-nothumb-noimagehandler": "{{doc-apierror}}\n\nParameters:\n* $1 - File name.",
+ "apiwarn-parse-nocontentmodel": "{{doc-apierror}}\n\nParameters:\n* $1 - Content model being assumed.",
+ "apiwarn-parse-titlewithouttext": "{{doc-apierror}}",
+ "apiwarn-redirectsandrevids": "{{doc-apierror}}",
+ "apiwarn-tokennotallowed": "{{doc-apierror}}\n\nParameters:\n* $1 - Token type being requested, typically named after the action requiring the token.",
+ "apiwarn-tokens-origin": "{{doc-apierror}}",
+ "apiwarn-toomanyvalues": "{{doc-apierror}}\n\nParameters:\n* $1 - Parameter name.\n* $2 - Maximum number of values allowed.",
+ "apiwarn-truncatedresult": "{{doc-apierror}}\n\nParameters:\n* $1 - Size limit in bytes.",
+ "apiwarn-unclearnowtimestamp": "{{doc-apierror}}\n\nParameters:\n* $1 - Parameter name.\n* $2 - Supplied value.",
+ "apiwarn-unrecognizedvalues": "{{doc-apierror}}\n\nParameters:\n* $1 - Parameter name.\n* $2 - List of unknown values supplied.\n* $3 - Number of unknown values.",
+ "apiwarn-unsupportedarray": "{{doc-apierror}}\n\nParameters:\n* $1 - Parameter name.",
+ "apiwarn-urlparamwidth": "{{doc-apierror}}\n\nParameters:\n* $1 - Module parameter prefix, e.g. \"bl\".\n* $2 - Width being ignored.\n* $3 - Width being used.",
+ "apiwarn-validationfailed": "{{doc-apierror}}\n\nParameters:\n* $1 - User preference name.\n* $2 - Failure message, such as {{msg-mw|apiwarn-validationfailed-badpref}}. Probably already ends with punctuation",
+ "apiwarn-validationfailed-badchars": "{{doc-apierror}}\n\nUsed with {{msg-mw|apiwarn-validationfailed}}.",
+ "apiwarn-validationfailed-badpref": "{{doc-apierror}}\n\nUsed with {{msg-mw|apiwarn-validationfailed}}.",
+ "apiwarn-validationfailed-cannotset": "{{doc-apierror}}\n\nUsed with {{msg-mw|apiwarn-validationfailed}}.",
+ "apiwarn-validationfailed-keytoolong": "{{doc-apierror}}\n\nUsed with {{msg-mw|apiwarn-validationfailed}}.\n\nParameters:\n* $1 - Maximum allowed key length in bytes.",
+ "apiwarn-wgDebugAPI": "{{doc-apierror}}",
+ "api-feed-error-title": "Used as a feed item title when an error occurs in <kbd>action=feedwatchlist</kbd>.\n\nParameters:\n* $1 - API error code",
+ "api-usage-docref": "\n\nParameters:\n* $1 - URL of the API auto-generated documentation.",
+ "api-exception-trace": "\n\nParameters:\n* $1 - Exception class.\n* $2 - File from which the exception was thrown.\n* $3 - Line number from which the exception was thrown.\n* $4 - Exception backtrace.",
"api-credits-header": "Header for the API credits section in the API help output\n{{Identical|Credit}}",
"api-credits": "API credits text, displayed in the API help output"
}
"config-help": "সাহায্য",
"config-help-tooltip": "প্রসারিত করতে ক্লিক করুন",
"mainpagetext": "<strong>মিডিয়াউইকি ইনস্টল করা হয়েছে।</strong>",
- "mainpagedocfooter": "কীভাবে উইকি সফটওয়্যারটি ব্যবহারকার করবেন, তা জানতে [https://www.mediawiki.org/wiki/Special:MyLanguage/Help:Contents ব্যবহারকারী সহায়িকা] দেখুন।\n\n== কোথা থেকে শুরু করবেন ==\n\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:Configuration_settings কনফিগারেশন সেটিংস তালিকা]\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:FAQ প্রশ্নোত্তরে মিডিয়াউইকি]\n* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-announce মিডিয়াউইকি মুক্তির মেইলিং লিস্ট]\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Localisation#Translation_resources আপনার ভাষার জন্য মিডিয়াউইকি স্থানীয়করণ করুন]\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:Combating_spam আপনার উইকিতে স্প্যামের সাথে লড়াই করার উপায় সম্পর্কে জানুন]"
+ "mainpagedocfooter": "কীভাবে উইকি সফটওয়্যারটি ব্যবহারকার করবেন, তা জানতে [https://www.mediawiki.org/wiki/Special:MyLanguage/Help:Contents ব্যবহারকারী সহায়িকা] দেখুন।\n\n== কোথা থেকে শুরু করবেন ==\n\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:Configuration_settings কনফিগারেশন সেটিং তালিকা]\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:FAQ প্রশ্নোত্তরে মিডিয়াউইকি]\n* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-announce মিডিয়াউইকি মুক্তির মেইলিং লিস্ট]\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Localisation#Translation_resources আপনার ভাষার জন্য মিডিয়াউইকি স্থানীয়করণ করুন]\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:Combating_spam আপনার উইকিতে স্প্যামের সাথে লড়াই করার উপায় সম্পর্কে জানুন]"
}
"config-nofile": "Le fichier « $1 » est introuvable. A-t-il été supprimé ?",
"config-extension-link": "Saviez-vous que votre wiki prend en charge [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:Extensions des extensions] ?\n\nVous pouvez consulter les [https://www.mediawiki.org/wiki/Special:MyLanguage/Category:Extensions_by_category extensions par catégorie] ou la [https://www.mediawiki.org/wiki/Extension_Matrix matrice des extensions] pour voir la liste complète des extensions.",
"mainpagetext": "<strong>MediaWiki a été installé.</strong>",
- "mainpagedocfooter": "Consultez le [https://meta.wikimedia.org/wiki/Help:Contents/fr Guide de l’utilisateur] pour plus d’informations sur l’utilisation de ce logiciel de wiki.\n\n== Pour démarrer ==\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:Configuration_settings Liste des paramètres de configuration]\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:FAQ/fr Questions courantes sur MediaWiki]\n* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-announce Liste de discussion sur les distributions de MediaWiki]\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Localisation#Translation_resources Adaptez MediaWiki dans votre langue]\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:Combating_spam Apprendre comment lutter contre le pourriel dans votre wiki]"
+ "mainpagedocfooter": "Consultez le [https://meta.wikimedia.org/wiki/Help:Contents/fr Guide de l’utilisateur du contenu] pour plus d’informations sur l’utilisation de ce logiciel de wiki.\n\n== Pour démarrer ==\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:Configuration_settings Liste des paramètres de configuration]\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:FAQ/fr Questions courantes sur MediaWiki]\n* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-announce Liste de discussion sur les distributions de MediaWiki]\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Localisation#Translation_resources Adaptez MediaWiki dans votre langue]\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:Combating_spam Apprendre comment lutter contre le pourriel dans votre wiki]"
}
* @return void
*/
public function set( $key, $value ) {
- if ( array_key_exists( $key, $this->cache ) ) {
+ if ( $this->has( $key ) ) {
$this->ping( $key );
} elseif ( count( $this->cache ) >= $this->maxCacheKeys ) {
reset( $this->cache );
* @return bool
*/
public function has( $key ) {
+ if ( !is_int( $key ) && !is_string( $key ) ) {
+ throw new MWException( __METHOD__ . ' called with invalid key. Must be string or integer.' );
+ }
return array_key_exists( $key, $this->cache );
}
* @return mixed Returns null if the key was not found
*/
public function get( $key ) {
- if ( !array_key_exists( $key, $this->cache ) ) {
+ if ( !$this->has( $key ) ) {
return null;
}
* <https://github.com/phacility/xhprof>. XHProf can be installed as a PECL
* package for use with PHP5 (Zend PHP) and is built-in to HHVM 3.3.0.
*
+ * This also supports using the Tideways profiler
+ * <https://github.com/tideways/php-profiler-extension>, which additionally
+ * has support for PHP7.
+ *
* @since 1.28
*/
class Xhprof {
*/
public static function enable( $flags = 0, $options = [] ) {
if ( self::isEnabled() ) {
- throw new Exception( 'Xhprof profiling is already enabled.' );
+ throw new Exception( 'Profiling is already enabled.' );
}
self::$enabled = true;
- xhprof_enable( $flags, $options );
+ if ( function_exists( 'xhprof_enable' ) ) {
+ xhprof_enable( $flags, $options );
+ } elseif ( function_exists( 'tideways_enable' ) ) {
+ tideways_enable( $flags, $options );
+ } else {
+ throw new Exception( "Neither xhprof nor tideways are installed" );
+ }
}
/**
public static function disable() {
if ( self::isEnabled() ) {
self::$enabled = false;
- return xhprof_disable();
+ if ( function_exists( 'xhprof_disable' ) ) {
+ return xhprof_disable();
+ } else {
+ // tideways
+ return tideways_disable();
+ }
}
}
}
if ( apcu_exists( $key . self::KEY_SUFFIX ) ) {
return apcu_inc( $key . self::KEY_SUFFIX, $value );
} else {
- return apcu_set( $key . self::KEY_SUFFIX, $value );
+ return false;
}
}
if ( apcu_exists( $key . self::KEY_SUFFIX ) ) {
return apcu_dec( $key . self::KEY_SUFFIX, $value );
} else {
- return apcu_set( $key . self::KEY_SUFFIX, -$value );
+ return false;
}
}
}
/** @var resource */
protected $mLastResult;
- /** @var $mConn PDO */
+ /** @var PDO */
protected $mConn;
/** @var FSLockManager (hopefully on the same server as the DB) */
class LoadBalancer implements ILoadBalancer {
/** @var array[] Map of (server index => server config array) */
private $mServers;
- /** @var array[] Map of (local/foreignUsed/foreignFree => server index => IDatabase array) */
+ /** @var IDatabase[][] Map of (local/foreignUsed/foreignFree => server index => IDatabase array) */
private $mConns;
/** @var float[] Map of (server index => weight) */
private $mLoads;
return $i;
}
+ /**
+ * @param DBMasterPos|false $pos
+ */
public function waitFor( $pos ) {
$this->mWaitForPos = $pos;
$i = $this->mReadIndex;
return $ok;
}
+ /**
+ * @param int $i
+ * @return IDatabase
+ */
public function getAnyOpenConnection( $i ) {
foreach ( $this->mConns as $connsByServer ) {
if ( !empty( $connsByServer[$i] ) ) {
}
}
+ /**
+ * @param IDatabase $conn
+ * @param DBMasterPos|false $pos
+ * @param int $timeout
+ */
public function safeWaitForMasterPos( IDatabase $conn, $pos = false, $timeout = 10 ) {
if ( $this->getServerCount() <= 1 || !$conn->getLBInfo( 'replica' ) ) {
return true; // server is not a replica DB
*/
use \MediaWiki\MediaWikiServices;
+use \Wikimedia\WaitConditionLoop;
/**
* Class to store objects in the database
return TitleArray::newFromResult( $res );
}
+
+ /**
+ * @since 1.28
+ * @return string
+ */
+ public function getWikiDisplayName() {
+ return $this->getFile()->getRepo()->getDisplayName();
+ }
+
+ /**
+ * @since 1.28
+ * @return string
+ */
+ public function getSourceURL() {
+ return $this->getFile()->getDescriptionUrl();
+ }
}
public function isLocal() {
return true;
}
+
+ /**
+ * The display name for the site this content
+ * come from. If a subclass overrides isLocal(),
+ * this could return something other than the
+ * current site name
+ *
+ * @since 1.28
+ * @return string
+ */
+ public function getWikiDisplayName() {
+ global $wgSitename;
+ return $wgSitename;
+ }
+
+ /**
+ * Get the source URL for the content on this page,
+ * typically the canonical URL, but may be a remote
+ * link if the content comes from another site
+ *
+ * @since 1.28
+ * @return string
+ */
+ public function getSourceURL() {
+ return $this->getTitle()->getCanonicalURL();
+ }
}
* ($wgProfiler['exclude']) containing an array of function names.
* Shell-style patterns are also accepted.
*
+ * It is also possible to use the Tideways PHP extension, which is mostly
+ * a drop-in replacement for Xhprof. Just change the XHPROF_FLAGS_* constants
+ * to TIDEWAYS_FLAGS_*.
+ *
* @author Bryan Davis <bd808@wikimedia.org>
* @copyright © 2014 Bryan Davis and Wikimedia Foundation.
* @ingroup Profiler
* @see Xhprof
* @see https://php.net/xhprof
* @see https://github.com/facebook/hhvm/blob/master/hphp/doc/profiling.md
+ * @see https://github.com/tideways/php-profiler-extension
*/
class ProfilerXhprof extends Profiler {
/**
--- /dev/null
+<?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
+ */
+class ExtensionJsonValidationError extends Exception {
+}
--- /dev/null
+<?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
+ */
+
+use Composer\Spdx\SpdxLicenses;
+use JsonSchema\Validator;
+
+/**
+ * @since 1.29
+ */
+class ExtensionJsonValidator {
+
+ /**
+ * @var callable
+ */
+ private $missingDepCallback;
+
+ /**
+ * @param callable $missingDepCallback
+ */
+ public function __construct( callable $missingDepCallback ) {
+ $this->missingDepCallback = $missingDepCallback;
+ }
+
+ /**
+ * @return bool
+ */
+ public function checkDependencies() {
+ if ( !class_exists( Validator::class ) ) {
+ call_user_func( $this->missingDepCallback,
+ 'The JsonSchema library cannot be found, please install it through composer.'
+ );
+ return false;
+ } elseif ( !class_exists( SpdxLicenses::class ) ) {
+ call_user_func( $this->missingDepCallback,
+ 'The spdx-licenses library cannot be found, please install it through composer.'
+ );
+ return false;
+ }
+
+ return true;
+ }
+
+ /**
+ * @param string $path file to validate
+ * @return bool true if passes validation
+ * @throws ExtensionJsonValidationError on any failure
+ */
+ public function validate( $path ) {
+ $data = json_decode( file_get_contents( $path ) );
+ if ( !is_object( $data ) ) {
+ throw new ExtensionJsonValidationError( "$path is not valid JSON" );
+ }
+
+ if ( !isset( $data->manifest_version ) ) {
+ throw new ExtensionJsonValidationError(
+ "$path does not have manifest_version set." );
+ }
+
+ $version = $data->manifest_version;
+ if ( $version !== ExtensionRegistry::MANIFEST_VERSION ) {
+ $schemaPath = __DIR__ . "/../../docs/extension.schema.v$version.json";
+ } else {
+ $schemaPath = __DIR__ . '/../../docs/extension.schema.json';
+ }
+
+ // Not too old
+ if ( $version < ExtensionRegistry::OLDEST_MANIFEST_VERSION ) {
+ throw new ExtensionJsonValidationError(
+ "$path is using a non-supported schema version"
+ );
+ } elseif ( $version > ExtensionRegistry::MANIFEST_VERSION ) {
+ throw new ExtensionJsonValidationError(
+ "$path is using a non-supported schema version"
+ );
+ }
+
+ $licenseError = false;
+ // Check if it's a string, if not, schema validation will display an error
+ if ( isset( $data->{'license-name'} ) && is_string( $data->{'license-name'} ) ) {
+ $licenses = new SpdxLicenses();
+ $valid = $licenses->validate( $data->{'license-name'} );
+ if ( !$valid ) {
+ $licenseError = '[license-name] Invalid SPDX license identifier, '
+ . 'see <https://spdx.org/licenses/>';
+ }
+ }
+
+ $validator = new Validator;
+ $validator->check( $data, (object)[ '$ref' => 'file://' . $schemaPath ] );
+ if ( $validator->isValid() && !$licenseError ) {
+ // All good.
+ return true;
+ } else {
+ $out = "$path did pass validation.\n";
+ foreach ( $validator->getErrors() as $error ) {
+ $out .= "[{$error['property']}] {$error['message']}\n";
+ }
+ if ( $licenseError ) {
+ $out .= "$licenseError\n";
+ }
+ throw new ExtensionJsonValidationError( $out );
+ }
+ }
+}
/**
* Things to be called once registration of these extensions are done
+ * keyed by the name of the extension that it belongs to
*
* @var callable[]
*/
$this->extractResourceLoaderModules( $dir, $info );
$this->extractServiceWiringFiles( $dir, $info );
$this->extractParserTestFiles( $dir, $info );
+ $name = $this->extractCredits( $path, $info );
if ( isset( $info['callback'] ) ) {
- $this->callbacks[] = $info['callback'];
+ $this->callbacks[$name] = $info['callback'];
}
- $this->extractCredits( $path, $info );
foreach ( $info as $key => $val ) {
if ( in_array( $key, self::$globalSettings ) ) {
$this->storeToArray( $path, "wg$key", $val, $this->globals );
/**
* @param string $path
* @param array $info
+ * @return string Name of thing
* @throws Exception
*/
protected function extractCredits( $path, array $info ) {
$this->credits[$name] = $credits;
$this->globals['wgExtensionCredits'][$credits['type']][] = $credits;
+
+ return $name;
}
/**
/**
* Bump whenever the registration cache needs resetting
*/
- const CACHE_VERSION = 3;
+ const CACHE_VERSION = 4;
/**
* Special key that defines the merge strategy
*/
protected $queued = [];
+ /**
+ * Whether we are done loading things
+ *
+ * @var bool
+ */
+ private $finished = false;
+
/**
* Items in the JSON file that aren't being
* set as globals
$this->queued[$path] = $mtime;
}
+ /**
+ * @throws MWException If the queue is already marked as finished (no further things should
+ * be loaded then).
+ */
public function loadFromQueue() {
global $wgVersion;
if ( !$this->queued ) {
return;
}
+ if ( $this->finished ) {
+ throw new MWException(
+ "The following paths tried to load late: "
+ . implode( ', ', array_keys( $this->queued ) )
+ );
+ }
+
// A few more things to vary the cache on
$versions = [
'registration' => self::CACHE_VERSION,
$this->queued = [];
}
+ /**
+ * After this is called, no more extensions can be loaded
+ *
+ * @since 1.29
+ */
+ public function finish() {
+ $this->finished = true;
+ }
+
/**
* Process a queue of extensions and return their extracted data
*
foreach ( $info['autoloaderPaths'] as $path ) {
require_once $path;
}
- foreach ( $info['callbacks'] as $cb ) {
- call_user_func( $cb );
- }
$this->loaded += $info['credits'];
if ( $info['attributes'] ) {
$this->attributes = array_merge_recursive( $this->attributes, $info['attributes'] );
}
}
+
+ foreach ( $info['callbacks'] as $name => $cb ) {
+ call_user_func( $cb, $info['credits'][$name] );
+ }
}
/**
}
protected function getRevisionLink() {
- $date = htmlspecialchars( $this->list->getLanguage()->userTimeAndDate(
- $this->revision->getTimestamp(), $this->list->getUser() ) );
+ $date = $this->list->getLanguage()->userTimeAndDate(
+ $this->revision->getTimestamp(), $this->list->getUser() );
if ( $this->isDeleted() && !$this->canViewContent() ) {
- return $date;
+ return htmlspecialchars( $date );
}
- return Linker::link(
+ return $this->getLinkRenderer()->makeLink(
SpecialPage::getTitleFor( 'Undelete' ),
$date,
[],
return $this->list->msg( 'diff' )->escaped();
}
- return Linker::link(
+ return $this->getLinkRenderer()->makeLink(
SpecialPage::getTitleFor( 'Undelete' ),
- $this->list->msg( 'diff' )->escaped(),
+ $this->list->msg( 'diff' )->text(),
[],
[
'target' => $this->list->title->getPrefixedText(),
}
protected function getLink() {
- $date = htmlspecialchars( $this->list->getLanguage()->userTimeAndDate(
- $this->file->getTimestamp(), $this->list->getUser() ) );
+ $date = $this->list->getLanguage()->userTimeAndDate(
+ $this->file->getTimestamp(), $this->list->getUser() );
# Hidden files...
if ( !$this->canViewContent() ) {
- $link = $date;
+ $link = htmlspecialchars( $date );
} else {
$undelete = SpecialPage::getTitleFor( 'Undelete' );
$key = $this->file->getKey();
- $link = Linker::link( $undelete, $date, [],
+ $link = $this->getLinkRenderer()->makeLink( $undelete, $date, [],
[
'target' => $this->list->title->getPrefixedText(),
'file' => $key,
* @return string
*/
protected function getLink() {
- $date = htmlspecialchars( $this->list->getLanguage()->userTimeAndDate(
- $this->file->getTimestamp(), $this->list->getUser() ) );
+ $date = $this->list->getLanguage()->userTimeAndDate(
+ $this->file->getTimestamp(), $this->list->getUser() );
if ( !$this->isDeleted() ) {
# Regular files...
- return Html::rawElement( 'a', [ 'href' => $this->file->getUrl() ], $date );
+ return Html::element( 'a', [ 'href' => $this->file->getUrl() ], $date );
}
# Hidden files...
if ( !$this->canViewContent() ) {
- $link = $date;
+ $link = htmlspecialchars( $date );
} else {
- $link = Linker::link(
+ $link = $this->getLinkRenderer()->makeLink(
SpecialPage::getTitleFor( 'Revisiondelete' ),
$date,
[],
$formatter->setAudience( LogFormatter::FOR_THIS_USER );
// Log link for this page
- $loglink = Linker::link(
+ $loglink = $this->getLinkRenderer()->makeLink(
SpecialPage::getTitleFor( 'Log' ),
- $this->list->msg( 'log' )->escaped(),
+ $this->list->msg( 'log' )->text(),
[],
[ 'page' => $title->getPrefixedText() ]
);
* @return string
*/
protected function getRevisionLink() {
- $date = htmlspecialchars( $this->list->getLanguage()->userTimeAndDate(
- $this->revision->getTimestamp(), $this->list->getUser() ) );
+ $date = $this->list->getLanguage()->userTimeAndDate(
+ $this->revision->getTimestamp(), $this->list->getUser() );
if ( $this->isDeleted() && !$this->canViewContent() ) {
- return $date;
+ return htmlspecialchars( $date );
}
- return Linker::linkKnown(
+ return $this->getLinkRenderer()->makeKnownLink(
$this->list->title,
$date,
[],
if ( $this->isDeleted() && !$this->canViewContent() ) {
return $this->list->msg( 'diff' )->escaped();
} else {
- return Linker::linkKnown(
+ return $this->getLinkRenderer()->makeKnownLink(
$this->list->title,
- $this->list->msg( 'diff' )->escaped(),
+ $this->list->msg( 'diff' )->text(),
[],
[
'diff' => $this->revision->getId(),
$content_navigation['views']['view']['redundant'] = true;
}
- $isForeignFile = $title->inNamespace( NS_FILE ) && $this->canUseWikiPage() &&
- $this->getWikiPage() instanceof WikiFilePage && !$this->getWikiPage()->isLocal();
+ $page = $this->canUseWikiPage() ? $this->getWikiPage() : false;
+ $isRemoteContent = $page && !$page->isLocal();
// If it is a non-local file, show a link to the file in its own repository
// @todo abstract this for remote content that isn't a file
- if ( $isForeignFile ) {
- $file = $this->getWikiPage()->getFile();
+ if ( $isRemoteContent ) {
$content_navigation['views']['view-foreign'] = [
'class' => '',
'text' => wfMessageFallback( "$skname-view-foreign", 'view-foreign' )->
setContext( $this->getContext() )->
- params( $file->getRepo()->getDisplayName() )->text(),
- 'href' => $file->getDescriptionUrl(),
+ params( $page->getWikiDisplayName() )->text(),
+ 'href' => $page->getSourceURL(),
'primary' => false,
];
}
&& $title->getDefaultMessageText() !== false
)
) {
- $msgKey = $isForeignFile ? 'edit-local' : 'edit';
+ $msgKey = $isRemoteContent ? 'edit-local' : 'edit';
} else {
- $msgKey = $isForeignFile ? 'create-local' : 'create';
+ $msgKey = $isRemoteContent ? 'create-local' : 'create';
}
$content_navigation['views']['edit'] = [
'class' => ( $isEditing && ( $section !== 'new' || !$showNewSection )
'text' => wfMessageFallback( "$skname-view-$msgKey", $msgKey )
->setContext( $this->getContext() )->text(),
'href' => $title->getLocalURL( $this->editUrlOptions() ),
- 'primary' => !$isForeignFile, // don't collapse this in vector
+ 'primary' => !$isRemoteContent, // don't collapse this in vector
];
// section link
$opts->add( 'hideminor', false );
$opts->add( 'hidebots', false );
+ $opts->add( 'hidehumans', false );
$opts->add( 'hideanons', false );
$opts->add( 'hideliu', false );
$opts->add( 'hidepatrolled', false );
+ $opts->add( 'hideunpatrolled', false );
$opts->add( 'hidemyself', false );
$opts->add( 'hidebyothers', false );
if ( $config->get( 'RCWatchCategoryMembership' ) ) {
$opts->add( 'hidecategorization', false );
}
+ $opts->add( 'hidepageedits', false );
+ $opts->add( 'hidenewpages', false );
+ $opts->add( 'hidelog', false );
$opts->add( 'namespace', '', FormOptions::INTNULL );
$opts->add( 'invert', false );
if ( $opts['hidebots'] ) {
$conds['rc_bot'] = 0;
}
- if ( $user->useRCPatrol() && $opts['hidepatrolled'] ) {
- $conds['rc_patrolled'] = 0;
+ if ( $opts['hidehumans'] ) {
+ $conds[] = 'rc_bot = 1';
+ }
+ if ( $user->useRCPatrol() ) {
+ if ( $opts['hidepatrolled'] ) {
+ $conds[] = 'rc_patrolled = 0';
+ }
+ if ( $opts['hideunpatrolled'] ) {
+ $conds[] = 'rc_patrolled = 1';
+ }
}
if ( $botsonly ) {
$conds['rc_bot'] = 1;
) {
$conds[] = 'rc_type != ' . $dbr->addQuotes( RC_CATEGORIZE );
}
+ if ( $opts['hidepageedits'] ) {
+ $conds[] = 'rc_type != ' . $dbr->addQuotes( RC_EDIT );
+ }
+ if ( $opts['hidenewpages'] ) {
+ $conds[] = 'rc_type != ' . $dbr->addQuotes( RC_NEW );
+ }
+ if ( $opts['hidelog'] ) {
+ $conds[] = 'rc_type != ' . $dbr->addQuotes( RC_LOG );
+ }
// Namespace filtering
if ( $opts['namespace'] !== '' ) {
protected function getGroupName() {
return 'changes';
}
+
+ /**
+ * Get filters that can be rendered.
+ *
+ * Filters with 'msg' => false can be used to filter data but won't
+ * be presented as show/hide toggles in the UI. They are not returned
+ * by this function.
+ *
+ * @param array $allFilters Map of filter URL param names to properties (msg/default)
+ * @return array Map of filter URL param names to properties (msg/default)
+ */
+ protected function getRenderableCustomFilters( $allFilters ) {
+ return array_filter(
+ $allFilters,
+ function( $filter ) {
+ return isset( $filter['msg'] ) && ( $filter['msg'] !== false );
+ }
+ );
+ }
}
$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()
* @ingroup SpecialPage
*/
class DeletedContributionsPage extends SpecialPage {
+ /** @var FormOptions */
+ protected $mOpts;
+
function __construct() {
- parent::__construct( 'DeletedContributions', 'deletedhistory',
- /*listed*/true, /*function*/false, /*file*/false );
+ parent::__construct( 'DeletedContributions', 'deletedhistory' );
}
/**
function execute( $par ) {
$this->setHeaders();
$this->outputHeader();
+ $this->checkPermissions();
$user = $this->getUser();
- if ( !$this->userCanExecute( $user ) ) {
- $this->displayRestrictionError();
-
- return;
- }
-
- $request = $this->getRequest();
$out = $this->getOutput();
$out->setPageTitle( $this->msg( 'deletedcontributions-title' ) );
- $options = [];
+ $opts = new FormOptions();
+
+ $opts->add( 'target', '' );
+ $opts->add( 'namespace', '' );
+ $opts->add( 'limit', 20 );
+
+ $opts->fetchValuesFromRequest( $this->getRequest() );
+ $opts->validateIntBounds( 'limit', 0, $this->getConfig()->get( 'QueryPageDefaultLimit' ) );
if ( $par !== null ) {
- $target = $par;
- } else {
- $target = $request->getVal( 'target' );
+ $opts->setValue( 'target', $par );
}
+ $ns = $opts->getValue( 'namespace' );
+ if ( $ns !== null && $ns !== '' ) {
+ $opts->setValue( 'namespace', intval( $ns ) );
+ }
+
+ $this->mOpts = $opts;
+
+ $target = $opts->getValue( 'target' );
if ( !strlen( $target ) ) {
- $out->addHTML( $this->getForm( '' ) );
+ $this->getForm();
return;
}
- $options['limit'] = $request->getInt( 'limit',
- $this->getConfig()->get( 'QueryPageDefaultLimit' ) );
- $options['target'] = $target;
-
$userObj = User::newFromName( $target, false );
if ( !$userObj ) {
- $out->addHTML( $this->getForm( '' ) );
+ $this->getForm();
return;
}
$target = $userObj->getName();
$out->addSubtitle( $this->getSubTitle( $userObj ) );
- $ns = $request->getVal( 'namespace', null );
- if ( $ns !== null && $ns !== '' ) {
- $options['namespace'] = intval( $ns );
- } else {
- $options['namespace'] = '';
- }
-
- $out->addHTML( $this->getForm( $options ) );
+ $this->getForm();
- $pager = new DeletedContribsPager( $this->getContext(), $target, $options['namespace'] );
+ $pager = new DeletedContribsPager( $this->getContext(), $target, $opts->getValue( 'namespace' ) );
if ( !$pager->getNumRows() ) {
$out->addWikiMsg( 'nocontribs' );
/**
* Generates the namespace selector form with hidden attributes.
- * @param array $options The options to be included.
- * @return string
*/
- function getForm( $options ) {
- $options['title'] = $this->getPageTitle()->getPrefixedText();
- if ( !isset( $options['target'] ) ) {
- $options['target'] = '';
- } else {
- $options['target'] = str_replace( '_', ' ', $options['target'] );
- }
-
- if ( !isset( $options['namespace'] ) ) {
- $options['namespace'] = '';
- }
-
- if ( !isset( $options['contribs'] ) ) {
- $options['contribs'] = 'user';
- }
-
- if ( $options['contribs'] == 'newbie' ) {
- $options['target'] = '';
- }
-
- $f = Xml::openElement( 'form', [ 'method' => 'get', 'action' => wfScript() ] );
-
- foreach ( $options as $name => $value ) {
- if ( in_array( $name, [ 'namespace', 'target', 'contribs' ] ) ) {
- continue;
- }
- $f .= "\t" . Html::hidden( $name, $value ) . "\n";
- }
+ function getForm() {
+ $opts = $this->mOpts;
+
+ $formDescriptor = [
+ 'target' => [
+ 'type' => 'user',
+ 'name' => 'target',
+ 'label-message' => 'sp-contributions-username',
+ 'default' => $opts->getValue( 'target' ),
+ 'ipallowed' => true,
+ ],
- $this->getOutput()->addModules( 'mediawiki.userSuggest' );
-
- $f .= Xml::openElement( 'fieldset' );
- $f .= Xml::element( 'legend', [], $this->msg( 'sp-contributions-search' )->text() );
- $f .= Xml::tags(
- 'label',
- [ 'for' => 'target' ],
- $this->msg( 'sp-contributions-username' )->parse()
- ) . ' ';
- $f .= Html::input(
- 'target',
- $options['target'],
- 'text',
- [
- 'size' => '20',
- 'required' => '',
- 'class' => [
- 'mw-autocomplete-user', // used by mediawiki.userSuggest
- ],
- ] + ( $options['target'] ? [] : [ 'autofocus' ] )
- ) . ' ';
- $f .= Html::namespaceSelector(
- [
- 'selected' => $options['namespace'],
+ 'namespace' => [
+ 'type' => 'namespaceselect',
+ 'name' => 'namespace',
+ 'label-message' => 'namespace',
'all' => '',
- 'label' => $this->msg( 'namespace' )->text()
],
- [
- 'name' => 'namespace',
- 'id' => 'namespace',
- 'class' => 'namespaceselector',
- ]
- ) . ' ';
- $f .= Xml::submitButton( $this->msg( 'sp-contributions-submit' )->text() );
- $f .= Xml::closeElement( 'fieldset' );
- $f .= Xml::closeElement( 'form' );
-
- return $f;
+ ];
+
+ $form = HTMLForm::factory( 'ooui', $formDescriptor, $this->getContext() )
+ ->setWrapperLegendMsg( 'sp-contributions-search' )
+ ->setSubmitTextMsg( 'sp-contributions-submit' )
+ // prevent setting subpage and 'target' parameter at the same time
+ ->setAction( $this->getPageTitle()->getLocalURL() )
+ ->setMethod( 'get' )
+ ->prepareForm()
+ ->displayForm( false );
}
/**
* @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() );
*
* @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();
$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 );
$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' ) ) {
}
/**
- * Get custom show/hide filters
+ * Get all custom filters
*
* @return array Map of filter URL param names to properties (msg/default)
*/
$showhide = [ 'show', 'hide' ];
- foreach ( $this->getCustomFilters() as $key => $params ) {
+ foreach ( $this->getRenderableCustomFilters( $this->getCustomFilters() ) as $key => $params ) {
$filters[$key] = $params['msg'];
}
+
// Disable some if needed
if ( !$user->useRCPatrol() ) {
unset( $filters['hidepatrolled'] );
* @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();
# Delete block
if ( !$block->delete() ) {
- return [ 'ipb_cant_unblock', htmlspecialchars( $block->getTarget() ) ];
+ return [ [ 'ipb_cant_unblock', htmlspecialchars( $block->getTarget() ) ] ];
}
# Unset _deleted fields as needed
Xml::element(
'legend',
[],
- $this->msg( 'userrights-editusergroup', $user->getName() )->text()
+ $this->msg(
+ $canChangeAny ? 'userrights-editusergroup' : 'userrights-viewusergroup',
+ $user->getName()
+ )->text()
) .
- $this->msg( 'editinguser' )->params( wfEscapeWikiText( $user->getName() ) )
+ $this->msg(
+ $canChangeAny ? 'editinguser' : 'viewinguserrights'
+ )->params( wfEscapeWikiText( $user->getName() ) )
->rawParams( $userToolLinks )->parse()
);
if ( $canChangeAny ) {
}
/**
- * Get custom show/hide filters
+ * Get all custom filters
*
* @return array Map of filter URL param names to properties (msg/default)
*/
$filters['hidecategorization'] = 'wlshowhidecategorization';
}
- foreach ( $this->getCustomFilters() as $key => $params ) {
+ foreach ( $this->getRenderableCustomFilters( $this->getCustomFilters() ) as $key => $params ) {
$filters[$key] = $params['msg'];
}
+
// Disable some if needed
if ( !$user->useRCPatrol() ) {
unset( $filters['hidepatrolled'] );
"contributions": "{{GENDER:$1|Ҡатнашыусы}} башҡарған эш",
"contributions-title": "$1 исемле ҡатнашыусы башҡарған эш",
"mycontris": "Башҡарған эштәр",
- "anoncontribs": "Ð\98Ò\93Ó\99нÓ\99ләр",
+ "anoncontribs": "Ð\91аÑ\88ҡаÑ\80Ò\93ан Ñ\8dÑ\88Ñ\82әр",
"contribsub2": "{{GENDER:$3|$1}} башҡарған эше ($2)",
"contributions-userdoesnotexist": "«$1» исемле иҫәп яҙыуы юҡ.",
"nocontribs": "Күрһәтелгән шарттарға яуап биргән үҙгәртеүҙәр табылманы.",
"nonunicodebrowser": "<strong>Папярэджаньне: ваш браўзэр не падтрымлівае Unicode-кадаваньне.</strong>\nУ выніку гэтага ўсе сымбалі ў полі рэдагаваньня, ня ўключаныя ў ASCII, будуць замененыя на іх шаснаццаткавыя коды.",
"editingold": "<strong>Папярэджаньне: вы рэдагуеце састарэлую вэрсію гэтай старонкі.</strong>\nКалі вы паспрабуеце захаваць яе, любыя зьмены, зробленыя пасьля гэтай вэрсіі, будуць страчаныя.",
"yourdiff": "Адрозьненьні",
- "copyrightwarning": "Ð\9aалÑ\96 лаÑ\81ка, зÑ\8cвÑ\8fÑ\80нÑ\96Ñ\86е Ñ\9eвагÑ\83 на Ñ\82ое, Ñ\88Ñ\82о Ñ\9eÑ\81е дадаÑ\82кÑ\96 Ñ\96 зÑ\8cменÑ\8b Ñ\9e {{GRAMMAR:меÑ\81нÑ\8b|{{SITENAME}}}} Ñ\80азглÑ\8fдаÑ\8eÑ\86Ñ\86а Ñ\8fк вÑ\8bдадзенÑ\8bÑ\8f Ñ\9e адпаведнаÑ\81Ñ\8cÑ\86Ñ\96 з Ñ\83мовамÑ\96 лÑ\96Ñ\86Ñ\8dнзÑ\96Ñ\96 $2 (глÑ\8fдзÑ\96Ñ\86е падÑ\80абÑ\8fзнаÑ\81Ñ\8cÑ\86Ñ\96 на $1). Ð\9aалÑ\96 Ð\92Ñ\8b Ñ\81Ñ\83пÑ\80аÑ\86Ñ\8c Ñ\82аго, каб Ð\92аÑ\88Ñ\8bÑ\8f маÑ\82Ñ\8dÑ\80Ñ\8bÑ\8fлÑ\8b неабмежавана Ñ\80Ñ\8dдагавалаÑ\81Ñ\8f Ñ\96 Ñ\80аÑ\81паÑ\9eÑ\81Ñ\8eджвалаÑ\81Ñ\8f, не дадавайÑ\86е Ñ\96Ñ\85.<br />\nÐ\92Ñ\8b Ñ\82акÑ\81ама абавÑ\8fзÑ\83еÑ\86еÑ\81Ñ\8f, Ñ\88Ñ\82о Ð\92аÑ\88 маÑ\82Ñ\8dÑ\80Ñ\8bÑ\8fл напÑ\96Ñ\81анÑ\8b аÑ\81абÑ\96Ñ\81Ñ\82а Ð\92амÑ\96 або зÑ\8cÑ\8fÑ\9eлÑ\8fеÑ\86Ñ\86а гÑ\80амадзкÑ\96м набÑ\8bÑ\82кам, алÑ\8cбо Ñ\9eзÑ\8fÑ\82Ñ\8b з падобнÑ\8bÑ\85 волÑ\8cнÑ\8bÑ\85 кÑ\80Ñ\8bнÑ\96Ñ\86аÑ\9e.\n'''Ð\9dÐ\95Ð\9bЬÐ\93Ð\90 Ð\91Ð\95Ð\97 Ð\94Ð\90Ð\97Ð\92Ð\9eÐ\9bУ Ð\94Ð\90Ð\94Ð\90Ð\92Ð\90ЦЬ Ð\9cÐ\90ТÐРЫЯÐ\9bЫ, Ð\90Ð\91Ð\90Ð Ð\9eÐ\9dÐ\95Ð\9dЫЯ Ð\90Ð\8eТÐ\90РСÐ\9aÐ\86Ð\9c Ð\9fÐ Ð\90Ð\92Ð\90Ð\9c!'''",
+ "copyrightwarning": "Ð\9aалÑ\96 лаÑ\81ка, зÑ\8cвÑ\8fÑ\80нÑ\96Ñ\86е Ñ\9eвагÑ\83 на Ñ\82ое, Ñ\88Ñ\82о Ñ\9eÑ\81е дадаÑ\82кÑ\96 Ñ\96 зÑ\8cменÑ\8b Ñ\9e {{GRAMMAR:меÑ\81нÑ\8b|{{SITENAME}}}} Ñ\80азглÑ\8fдаÑ\8eÑ\86Ñ\86а Ñ\8fк вÑ\8bдадзенÑ\8bÑ\8f Ñ\9e адпаведнаÑ\81Ñ\8cÑ\86Ñ\96 з Ñ\83мовамÑ\96 лÑ\96Ñ\86Ñ\8dнзÑ\96Ñ\96 $2 (глÑ\8fдзÑ\96Ñ\86е падÑ\80абÑ\8fзнаÑ\81Ñ\8cÑ\86Ñ\96 на $1). Ð\9aалÑ\96 вÑ\8b Ñ\81Ñ\83пÑ\80аÑ\86Ñ\8c Ñ\82аго, каб ваÑ\88Ñ\8bÑ\8f маÑ\82Ñ\8dÑ\80Ñ\8bÑ\8fлÑ\8b неабмежавана Ñ\80Ñ\8dдагавалаÑ\81Ñ\8f Ñ\96 Ñ\80аÑ\81паÑ\9eÑ\81Ñ\8eджвалаÑ\81Ñ\8f, не дадавайÑ\86е Ñ\96Ñ\85.<br />\nÐ\92Ñ\8b Ñ\82акÑ\81ама абавÑ\8fзÑ\83еÑ\86еÑ\81Ñ\8f, Ñ\88Ñ\82о ваÑ\88 маÑ\82Ñ\8dÑ\80Ñ\8bÑ\8fл напÑ\96Ñ\81анÑ\8b аÑ\81абÑ\96Ñ\81Ñ\82а вамÑ\96 або зÑ\8cÑ\8fÑ\9eлÑ\8fеÑ\86Ñ\86а гÑ\80амадзкÑ\96м набÑ\8bÑ\82кам, алÑ\8cбо Ñ\9eзÑ\8fÑ\82Ñ\8b з падобнÑ\8bÑ\85 волÑ\8cнÑ\8bÑ\85 кÑ\80Ñ\8bнÑ\96Ñ\86аÑ\9e.\n<strong>Ð\9dелÑ\8cга без дазволÑ\83 дадаваÑ\86Ñ\8c маÑ\82Ñ\8dÑ\80Ñ\8bÑ\8fлÑ\8b, абаÑ\80оненÑ\8bÑ\8f аÑ\9eÑ\82аÑ\80Ñ\81кÑ\96м пÑ\80авам!</strong>",
"copyrightwarning2": "Калі ласка, заўважце, што ўвесь унёсак ў {{GRAMMAR:вінавальны|{{SITENAME}}}} можа рэдагавацца, зьмяняцца і выдаляцца іншымі ўдзельнікамі.\nКалі Вы з гэтым ня згодныя, калі ласка, не зьмяшчайце сюды Вашыя тэксты.<br />\nРазьмяшчэньнем тут тэкстаў, Вы дэкляруеце, што Вы зьяўляецеся іх аўтарам, ці Вы скапіявалі іх з крыніцы, якая дазваляе вольнае выкарыстаньне сваіх тэкстаў (дзеля падрабязнасьцяў глядзіце $1).\n\n'''КАЛІ ЛАСКА, НЕ ЗЬМЯШЧАЙЦЕ ТУТ БЕЗ ДАЗВОЛУ МАТЭРЫЯЛЫ, ЯКІЯ АХОЎВАЮЦЦА АЎТАРСКІМ ПРАВАМ!'''",
"editpage-cannot-use-custom-model": "Мадэль зьместу гэтай старонкі ня можа быць зьмененая.",
"longpageerror": "'''Памылка: Аб’ём тэксту, які Вы спрабуеце запісаць складае $1 {{PLURAL:$1|кілябайт|кілябайты|кілябайтаў}}, што болей устаноўленага абмежаваньня на $2 {{PLURAL:$2|кілябайт|кілябайты|кілябайтаў}}.'''\nСтаронка ня можа быць захаваная.",
"emailccsubject": "Копія Вашага ліста да $1: $2",
"emailsent": "Ліст адасланы",
"emailsenttext": "Ваш ліст быў адасланы.",
- "emailuserfooter": "Гэты ліст быў дасланы {{GENDER:$1|ўдзельнікам|ўдзельніцай}} $1 да {{GENDER:$2|ўдзельніка|ўдзельніцы}} $2 з дапамогай функцыі «{{int:emailuser}}» {{GRAMMAR:родны|{{SITENAME}}}}.",
+ "emailuserfooter": "Гэты ліст быў дасланы {{GENDER:$1|ўдзельнікам|ўдзельніцай}} $1 да {{GENDER:$2|ўдзельніка|ўдзельніцы}} $2 з дапамогай функцыі «{{int:emailuser}}» {{GRAMMAR:родны|{{SITENAME}}}}. {{GENDER:$2|Ваш}} ліст у адказ будзе дасланы {{GENDER:$1|адпраўніку|адпраўніцы}}, і {{GENDER:$1|яму|ёй}} будзе бачны {{GENDER:$2|ваш}} адрас электроннай пошты.",
"usermessage-summary": "Паведамленьне пра выхад з сыстэмы.",
"usermessage-editor": "Дастаўка сыстэмных паведамленьняў",
"watchlist": "Сьпіс назіраньня",
"emailuserfooter": "এই ইমেইলটি {{SITENAME}} সাইটের \"{{int:emailuser}}\" সুবিধা ব্যবহার করে $1-এর পক্ষ থেকে {{GENDER:$2|$2}}-এর নিকট {{GENDER:$1|পাঠানো হয়েছে}}।",
"usermessage-summary": "বাদবাকি সিস্টেম বার্তা",
"usermessage-editor": "সিস্টেম ম্যাসেঞ্জার",
+ "usermessage-template": "MediaWiki:ব্যবহারকারী বার্তা",
"watchlist": "নজর তালিকা",
"mywatchlist": "নজর তালিকা",
"watchlistfor2": "$1 ($2)-এর জন্য",
"hijri-calendar-m11": "জ্বিলকদ",
"hijri-calendar-m12": "জ্বিলহজ্জ",
"hebrew-calendar-m1": "তিশরেই",
+ "hebrew-calendar-m2": "হেশভান",
+ "hebrew-calendar-m3": "কিসলেভ",
+ "hebrew-calendar-m4": "তেভেত",
+ "hebrew-calendar-m5": "শেভাত",
+ "hebrew-calendar-m6": "আদার",
+ "hebrew-calendar-m6a": "আদার ১",
+ "hebrew-calendar-m6b": "আদার ২",
+ "hebrew-calendar-m7": "নিসান",
+ "hebrew-calendar-m8": "ইয়্যার",
+ "hebrew-calendar-m9": "সিভান",
"hebrew-calendar-m10": "তামুয",
"hebrew-calendar-m11": "আভ",
"hebrew-calendar-m12": "এলুল",
+ "hebrew-calendar-m1-gen": "তিশরি",
+ "hebrew-calendar-m2-gen": "হেশভান",
+ "hebrew-calendar-m3-gen": "কিসলেভ",
+ "hebrew-calendar-m4-gen": "তেভেত",
+ "hebrew-calendar-m5-gen": "শেভাত",
+ "hebrew-calendar-m6-gen": "আদার",
+ "hebrew-calendar-m6a-gen": "আদার ১",
+ "hebrew-calendar-m6b-gen": "আদার ২",
"hebrew-calendar-m7-gen": "নিসান",
+ "hebrew-calendar-m8-gen": "ইয়্যার",
+ "hebrew-calendar-m9-gen": "সিভান",
+ "hebrew-calendar-m10-gen": "তামুয",
+ "hebrew-calendar-m11-gen": "আভ",
+ "hebrew-calendar-m12-gen": "এলুল",
"signature": "[[{{ns:user}}:$1|$2]] ([[{{ns:user_talk}}:$1|আলাপ]])",
"timezone-utc": "ইউটিসি",
"timezone-local": "স্থানীয়",
"mw-widgets-dateinput-no-date": "কোন তারিখ নির্বাচন করা হয়নি",
"mw-widgets-dateinput-placeholder-day": "বববব-মম-দদ",
"mw-widgets-dateinput-placeholder-month": "বববব-মম",
+ "mw-widgets-mediasearch-input-placeholder": "মিডিয়ার জন্য অনুসন্ধান",
+ "mw-widgets-mediasearch-noresults": "কোনো ফলাফল পাওয়া যায়নি।",
"mw-widgets-titleinput-description-new-page": "পাতা এখনো বিদ্যমান নয়",
"mw-widgets-titleinput-description-redirect": "$1-এ পুনঃনির্দেশিত",
"mw-widgets-categoryselector-add-category-placeholder": "একটি বিষয়শ্রেণী যোগ করুন...",
"userrights-user-editname": "Benutzername:",
"editusergroup": "Benutzergruppen laden",
"editinguser": "Ändere Benutzerrechte {{GENDER:$1|des Benutzers|der Benutzerin}} <strong>[[User:$1|$1]]</strong> $2",
+ "viewinguserrights": "Benutzerrechte {{GENDER:$1|des Benutzers|der Benutzerin}} <strong>[[User:$1|$1]]</strong> $2",
"userrights-editusergroup": "Benutzer-Gruppenzugehörigkeit bearbeiten",
+ "userrights-viewusergroup": "Benutzergruppen ansehen",
"saveusergroups": "{{GENDER:$1|Gruppenzugehörigkeit}} ändern",
"userrights-groupsmember": "Mitglied von:",
"userrights-groupsmember-auto": "Automatisch Mitglied von:",
"disclaimerpage": "Project:Redê mesulêtê pêroyi",
"edithelp": "Peştdariya vurnayışi",
"helppage-top-gethelp": "Peşti",
- "mainpage": "Perra Seri",
+ "mainpage": "Pela Seri",
"mainpage-description": "Pela seri",
"policy-url": "Project:Terzê hereketi",
"portal": "Portalê cemaeti",
"nstab-template": "Şablon",
"nstab-help": "Pela peşti",
"nstab-category": "Kategoriye",
- "mainpage-nstab": "Perra seri",
+ "mainpage-nstab": "Pela seri",
"nosuchaction": "Fealiyeto wınasi çıniyo",
"nosuchactiontext": "URL ra kar qebul nêbı.\nŞıma belka URL şaş nuşt, ya zi gıreyi şaş ra ameyi.\nKeyepelê {{SITENAME}} eşkeno xeta eşkera bıkero.",
"nosuchspecialpage": "Pella xısusi ya unasin çınya",
"token_suffix_mismatch": "'''Vurnayişê şıma tepeya ameyo çunke qutiyê imla xerıbya.\nVurnayişê şıma qey nêxerepyayişê peli tepeya geyra a.\nEke şıma servisê proksi yo anonim şuxulneni sebebê ey noyo.'''",
"edit_form_incomplete": "'''Qandê form dê vurnayışa tay wastera ma nêreşti; Vurnayışê ke şıma kerdê nêalızyayê, çım ra ravyarnê u fına bıcerbnê.'''",
"editing": "$1 vuriyeno",
- "creating": "$1 vıraziyeno",
+ "creating": "$1 vırazeno.",
"editingsection": "Per da $1 de şımaye kenê ke leti bıvurnê",
"editingcomment": "$1 vuryeno (qısmo newe)",
"editconflict": "Têverabiyayışê vurnayışi: $1",
"userrights-user-editname": "Enter a username:",
"editusergroup": "Load user groups",
"editinguser": "Changing user rights of {{GENDER:$1|user}} <strong>[[User:$1|$1]]</strong> $2",
+ "viewinguserrights": "Viewing user rights of {{GENDER:$1|user}} <strong>[[User:$1|$1]]</strong> $2",
"userrights-editusergroup": "Edit user groups",
+ "userrights-viewusergroup": "View user groups",
"saveusergroups": "Save {{GENDER:$1|user}} groups",
"userrights-groupsmember": "Member of:",
"userrights-groupsmember-auto": "Implicit member of:",
"action-upload_by_url": "upload this file from a URL",
"action-writeapi": "use the write API",
"action-delete": "delete this page",
- "action-deleterevision": "delete this revision",
- "action-deletedhistory": "view this page's deleted history",
+ "action-deleterevision": "delete revisions",
+ "action-deletelogentry": "delete log entries",
+ "action-deletedhistory": "view a page's deleted history",
+ "action-deletedtext": "view deleted revision text",
"action-browsearchive": "search deleted pages",
- "action-undelete": "undelete this page",
- "action-suppressrevision": "review and restore this hidden revision",
+ "action-undelete": "undelete pages",
+ "action-suppressrevision": "review and restore hidden revisions",
"action-suppressionlog": "view this private log",
"action-block": "block this user from editing",
"action-protect": "change protection levels for this page",
"action-userrights-interwiki": "edit user rights of users on other wikis",
"action-siteadmin": "lock or unlock the database",
"action-sendemail": "send emails",
+ "action-editmyoptions": "edit your preferences",
"action-editmywatchlist": "edit your watchlist",
"action-viewmywatchlist": "view your watchlist",
"action-viewmyprivateinfo": "view your private information",
"usercssispublic": "Please note: CSS subpages should not contain confidential data as they are viewable by other users.",
"restrictionsfield-badip": "Invalid IP address or range: $1",
"restrictionsfield-label": "Allowed IP ranges:",
- "restrictionsfield-help": "One IP address or CIDR range per line. To enable everything, use<br><code>0.0.0.0/0</code><br><code>::/0</code>"
+ "restrictionsfield-help": "One IP address or CIDR range per line. To enable everything, use<br><code>0.0.0.0/0</code><br><code>::/0</code>",
+ "revid": "r$1",
+ "pageid": "page ID $1"
}
"mainpage-nstab": "Esileht",
"nosuchaction": "Sellist toimingut pole.",
"nosuchactiontext": "Viki ei tunne internetiaadressile vastavat tegevust.\nVõimalik, et sa sisestasid aadressi valesti või kasutasid vigast linki.\nSamuti ei ole välistatud, et tarkvaras, mida {{SITENAME}} kasutatab, on viga.",
- "nosuchspecialpage": "Sellist erilehekülge pole.",
+ "nosuchspecialpage": "Sellist erilehekülge pole",
"nospecialpagetext": "<strong>Viki ei tunne erilehekülge, mille poole pöördusid.</strong>\n\nKäibel olevad erileheküljed on loetletud leheküljel [[Special:SpecialPages|{{int:specialpages}}]].",
"error": "Viga",
"databaseerror": "Andmebaasi viga",
"virus-scanfailed": "skaneerimine ebaõnnestus (veakood $1)",
"virus-unknownscanner": "tundmatu viirusetõrje:",
"logouttext": "<strong>Oled nüüd välja loginud.</strong>\n\nPane tähele, et seni, kuni sa pole veebilehitseja puhvrit tühjendanud, võidakse mõni lehekülg endiselt kuvada nii nagu oleksid ikka sisse logitud.",
+ "cannotlogoutnow-title": "Praegu ei saa välja logida",
+ "cannotlogoutnow-text": "Väljalogimine pole võimalik, kui kasutad $1.",
"welcomeuser": "Tere tulemast, $1!",
"welcomecreation-msg": "Sinu konto on loodud.\nÄra unusta seada oma {{GRAMMAR:genitive|{{SITENAME}}}} [[Special:Preferences|eelistusi]].",
"yourname": "Kasutajanimi:",
"createacct-yourpasswordagain-ph": "Sisesta uuesti parool",
"userlogin-remembermypassword": "Jää sisseloginuks",
"userlogin-signwithsecure": "Kasuta turvalist ühendust",
+ "cannotlogin-title": "Ei saa sisse logida",
+ "cannotlogin-text": "Sisselogimine pole võimalik.",
+ "cannotloginnow-title": "Praegu ei saa sisse logida",
+ "cannotloginnow-text": "Sisselogimine pole võimalik, kui kasutad $1.",
+ "cannotcreateaccount-title": "Ei saa kontosid luua",
+ "cannotcreateaccount-text": "Kontode käsitsi loomine pole selles vikis lubatud.",
"yourdomainname": "Sinu domeen:",
"password-change-forbidden": "Selles vikis ei saa paroole muuta.",
"externaldberror": "Esines autentimistõrge või sul pole õigust konto andmeid muuta.",
"emailccsubject": "Koopia sinu sõnumist kasutajale $1: $2",
"emailsent": "E-kiri saadetud",
"emailsenttext": "Sinu teade on e-kirjaga saadetud.",
- "emailuserfooter": "Selle e-kirja saatis $1 {{GRAMMAR:elative|{{SITENAME}}}} kasutajale $2 toimingu \"{{int:emailuser}}\" abil.",
+ "emailuserfooter": "Selle e-kirja saatis $1 {{GRAMMAR:elative|{{SITENAME}}}} kasutajale $2 toimingu \"{{int:emailuser}}\" abil. Sinu kiri saadetakse otse algse kirja saatjale, mistõttu saab ta sinu e-posti aadressi teada.",
"usermessage-summary": "Jätan süsteemiteate.",
"usermessage-editor": "Süsteemiteadete edastaja",
"watchlist": "Jälgimisloend",
"mw-widgets-dateinput-placeholder-month": "AAAA-KK",
"mw-widgets-titleinput-description-new-page": "lehekülge pole veel",
"mw-widgets-titleinput-description-redirect": "ümbersuunamine leheküljele \"$1\"",
+ "sessionprovider-generic": "klassi $1 seansse",
+ "sessionprovider-mediawiki-session-cookiesessionprovider": "küpsisepõhiseid seansse",
"randomrootpage": "Juhuslik juurlehekülg",
"log-action-filter-block": "Blokeeringu tüüp:",
"log-action-filter-contentmodel": "Sisumudeli muudatuse tüüp:",
"htmlform-user-not-exists": "Käyttäjää <strong>$1</strong> ei ole olemassa.",
"htmlform-user-not-valid": "<strong>$1</strong> ei ole kelvollinen käyttäjänimi.",
"logentry-delete-delete": "$1 {{GENDER:$2|poisti}} sivun $3",
+ "logentry-delete-delete_redir": "$1 {{GENDER:$2|poisti}} ohjaussivun $3 korvaamalla",
"logentry-delete-restore": "$1 {{GENDER:$2|palautti}} sivun $3",
"logentry-delete-event": "$1 {{GENDER:$2|muutti}} {{PLURAL:$5|lokitapahtuman|$5 lokitapahtuman}} näkyvyyttä kohteessa $3: $4",
"logentry-delete-revision": "$1 {{GENDER:$2|muutti}} {{PLURAL:$5|version|$5 version}} näkyvyyttä sivulla $3: $4",
"feedback-external-bug-report-button": "Signaler un bogue technique",
"feedback-dialog-title": "Soumettre un commentaire",
"feedback-dialog-intro": "Vous pouvez utiliser le simple formulaire ci-dessous pour faire parvenir vos commentaires. Votre commentaire sera ajouté à la page « $1 », ainsi que votre nom d’utilisateur.",
- "feedback-error1": "Erreur : Résultat de l'IPA non reconnu",
+ "feedback-error1": "Erreur : résultat de l'API non reconnu",
"feedback-error2": "Erreur : la modification a échoué",
"feedback-error3": "Erreur : aucune réponse de l'API",
"feedback-error4": "Erreur : Impossible de publier sous le titre d’avis donné",
"searchsuggest-search": "Rechercher sur {{SITENAME}}",
"searchsuggest-containing": "contenant...",
"api-error-autoblocked": "Votre adresse IP a été bloquée automatiquement, parce qu’elle a été utilisée par un utilisateur bloqué.",
- "api-error-badaccess-groups": "Vous n'êtes pas autorisé à verser des fichiers sur ce wiki.",
+ "api-error-badaccess-groups": "Vous n'êtes pas autorisé à téléverser des fichiers sur ce wiki.",
"api-error-badtoken": "Erreur interne : mauvais « jeton ».",
"api-error-blocked": "Vous avez été bloqué en édition.",
"api-error-copyuploaddisabled": "Les versements via URL sont désactivés sur ce serveur.",
"limitreport-expansiondepth": "Plus grande profondeur d’expansion",
"limitreport-expensivefunctioncount": "Nombre de fonctions d’analyse coûteuses",
"expandtemplates": "Expansion des modèles",
- "expand_templates_intro": "Cette page spéciale accepte un texte wiki source et permet de réaliser récursivement l’expansion des modèles qu’il contient.\nElle réalise aussi l’expansion des fonctions du parseur telles que\n<code><nowiki>{{</nowiki>#language:...}}</code> et des variables telles que\n<code><nowiki>{{</nowiki>CURRENTDAY}}</code>.\nEn fait, elle réalise l'expansion de pratiquement tout ce qui est encadré par des doubles accolades.",
+ "expand_templates_intro": "Cette page spéciale accepte un texte wiki source et permet de réaliser récursivement l’expansion de tous les modèles qu’il contient.\nElle réalise aussi l’expansion des fonctions supportées d'analyse telles que\n<code><nowiki>{{</nowiki>#language:...}}</code> et des variables telles que\n<code><nowiki>{{</nowiki>CURRENTDAY}}</code>.\nEn fait, elle réalise l'expansion de pratiquement tout ce qui est encadré par des doubles accolades.",
"expand_templates_title": "Titre de la page, si le code utilise {{FULLPAGENAME}}, etc. :",
"expand_templates_input": "Texte wiki source :",
"expand_templates_output": "Texte wiki obtenu après expansion",
"mw-widgets-dateinput-no-date": "Non se seleccionou ningunha data",
"mw-widgets-dateinput-placeholder-day": "AAAA-MM-DD",
"mw-widgets-dateinput-placeholder-month": "AAAA-MM",
+ "mw-widgets-mediasearch-input-placeholder": "Procurar ficheiros multimedia",
"mw-widgets-mediasearch-noresults": "Non se atopou ningún resultado.",
"mw-widgets-titleinput-description-new-page": "a páxina aínda non existe",
"mw-widgets-titleinput-description-redirect": "redirección cara a $1",
"activeusers": "רשימת משתמשים פעילים",
"activeusers-intro": "זוהי רשימת המשתמשים שביצעו פעולה כלשהי {{PLURAL:$1|ביום האחרון|ביומיים האחרונים|ב־$1 הימים האחרונים}}.",
"activeusers-count": "{{PLURAL:$1|פעולה אחת|$1 פעולות}} ב{{PLURAL:$3|יום האחרון|יומיים האחרונים|־$3 הימים האחרונים}}",
- "activeusers-from": "×\94צ×\92ת ×\9eשת×\9eש×\99×\9d ×\94×\97×\9c ×\9e:",
+ "activeusers-from": "×\94צ×\92ת ×\9eשת×\9eש×\99×\9d שש×\9e×\9d ×\9eת×\97×\99×\9c ×\91:",
"activeusers-groups": "הצגת משתמשים השייכים לקבוצות:",
"activeusers-excludegroups": "הסתרת משתמשים השייכים לקבוצות:",
"activeusers-noresult": "לא נמצאו משתמשים.",
"eauthentsent": "Sebuah surel untuk konfirmasi telah dikirim ke alamat surel. Sebelum surel lainnya dikirim ke akun tersebut, Anda harus mengikuti instruksi di dalam surel tersebut, untuk melakukan konfirmasi bahwa alamat tersebut adalah benar kepunyaan Anda.",
"throttled-mailpassword": "Suatu pengingat kata sandi telah dikirimkan dalam {{PLURAL:$1|$1 jam}} terakhir.\nUntuk menghindari penyalahgunaan, hanya satu kata sandi yang akan dikirimkan setiap {{PLURAL:$1|$1 jam}}.",
"mailerror": "Kesalahan dalam mengirimkan surel: $1",
- "acct_creation_throttle_hit": "Pengunjung wiki ini dengan alamat IP yang sama dengan Anda telah membuat {{PLURAL:$1|1 akun|$1 akun}} dalam satu hari terakhir, hingga jumlah maksimum yang diizinkan.\nKarenanya, pengunjung dengan alamat IP ini tidak dapat lagi membuat akun lain untuk sementara.",
+ "acct_creation_throttle_hit": "Pengunjung wiki ini dengan alamat IP yang sama dengan Anda telah membuat {{PLURAL:$1|1 akun|$1 akun}} dalam $2 terakhir, hingga jumlah maksimum yang diizinkan.\nKarenanya, pengunjung dengan alamat IP ini tidak dapat lagi membuat akun lain untuk sementara.",
"emailauthenticated": "Alamat surel Anda telah dikonfirmasi pada $3, $2.",
"emailnotauthenticated": "Alamat surel Anda belum dikonfirmasi.\nSebelum dikonfirmasi Anda tidak akan menerima surel dari fitur berikut.",
"noemailprefs": "Anda harus memasukkan alamat surel di preferensi Anda untuk dapat menggunakan fitur-fitur ini.",
"botpasswords-label-delete": "Hapus",
"botpasswords-label-resetpassword": "Setel ulang kata sandi",
"botpasswords-label-grants": "Akses yang dapat diberikan:",
- "botpasswords-help-grants": "Tiap izin memberikan akses ke hak-hak pengguna yang telah dimiliki suatu akun pengguna. Lihat [[Special:ListGrants|tabel izin]] untuk informasi lebih lanjut.",
+ "botpasswords-help-grants": "Izin ke akses tertentu telah dimiliki oleh akun pengguna Anda. Mengaktifkan sebuah hak di sini tidak memberikan akses ke akses lain yang tidak dimiliki oleh akun pengguna Anda. Lihat [[Special:ListGrants|daftar hak akses]] untuk informasi selengkapnya.",
"botpasswords-label-grants-column": "Izin diberikan",
"botpasswords-bad-appid": "Nama bot \"$1\" tidak valid.",
"botpasswords-insert-failed": "Gagal menambah nama bot \"$1\". Apakah sudah ditambahkan sebelum ini?",
"prefs-help-recentchangescount": "Opsi ini berlaku untuk perubahan terbaru, versi terdahulu halaman, dan log.",
"prefs-help-watchlist-token2": "Ini adalah kunci rahasia (token) ke umpan web dari daftar pantauan Anda.\nSiapa saja yang tahu akan dapat melihat daftar pantauan Anda, jadi jangan dibagikan. Jika diperlukan\n[[Special:ResetTokens|Anda dapat mengatur ulang kunci tersebut]].",
"savedprefs": "Preferensi Anda telah disimpan",
- "savedrights": "Hak pengguna {{GENDER:$1|$1}} telah disimpan.",
+ "savedrights": "Kelompok hak pengguna {{GENDER:$1|$1}} telah disimpan.",
"timezonelegend": "Zona waktu:",
"localtime": "Waktu setempat:",
"timezoneuseserverdefault": "Gunakan bawaan wiki ($1)",
"prefswarning-warning": "Perubahan preferensi anda belum tersimpan. Apabila anda meninggalkan halaman ini tanpa men-klik \"$1\" preferensi anda tidak akan diperbarui.",
"prefs-tabs-navigation-hint": "Tip: Anda dapat menggunakan tombol panah kiri dan kanan untuk bernavigasi antartab di dalam daftar tab.",
"userrights": "Manajemen hak pengguna",
- "userrights-lookup-user": "Mengatur kelompok pengguna",
+ "userrights-lookup-user": "Pilih seorang pengguna",
"userrights-user-editname": "Masukkan nama pengguna:",
- "editusergroup": "Sunting kelompok {{GENDER:$1|pengguna}}",
+ "editusergroup": "Muat kelompok pengguna",
"editinguser": "Mengubah hak pengguna untuk {{GENDER:$1|pengguna}} <strong>[[User:$1|$1]]</strong> $2",
"userrights-editusergroup": "Sunting kelompok pengguna",
"saveusergroups": "Simpan kelompok {{GENDER:$1|pengguna}}",
"emailccsubject": "Salinan pesan Anda untuk $1: $2",
"emailsent": "Surel terkirim",
"emailsenttext": "Surel Anda telah dikirimkan.",
- "emailuserfooter": "Email ini dikirimkan oleh $1 ke $2 dengan fungsi \"{{int:emailuser}}\" di {{SITENAME}}.",
+ "emailuserfooter": "Surel ini telah {{GENDER:$1|dikirim}} oleh $1 kepada {{GENDER:$2|$2}} dengan fungsi \"{{int:emailuser}}\" pada {{SITENAME}}. Surel {{GENDER:$2|Anda}} akan dikirim langsung kepada {{GENDER:$1|pengirim asal}}, dengan menampilkan alamat surel {{GENDER:$2|Anda}} kepada {{GENDER:$1|mereka}}.",
"usermessage-summary": "Tinggalkan pesan sistem.",
"usermessage-editor": "Penyampai pesan sistem",
"usermessage-template": "MediaWiki:UserMessage",
"undeletedrevisions": "$1 {{PLURAL:$1|revisi|revisi}} telah dikembalikan",
"undeletedrevisions-files": "$1 {{PLURAL:$1|revisi|revisi}} and $2 berkas dikembalikan",
"undeletedfiles": "$1 {{PLURAL:$1|berkas|berkas}} dikembalikan",
- "cannotundelete": "Pembatalan penghapusan gagal:\n$1",
+ "cannotundelete": "Beberapa pembatalan penghapusan gagal:\n$1",
"undeletedpage": "'''$1 berhasil dikembalikan'''\n\nLihat [[Special:Log/delete|log penghapusan]] untuk data penghapusan dan pengembalian.",
"undelete-header": "Lihat [[Special:Log/delete|log penghapusan]] untuk daftar halaman yang baru dihapus.",
"undelete-search-title": "Cari halaman yang dihapus",
"sp-contributions-newbies-sub": "Untuk pengguna baru",
"sp-contributions-newbies-title": "Kontribusi pengguna baru",
"sp-contributions-blocklog": "log pemblokiran",
- "sp-contributions-suppresslog": "kontribusi pengguna yang disembunyikan",
- "sp-contributions-deleted": "kontribusi pengguna yang dihapus",
+ "sp-contributions-suppresslog": "kontribusi {{GENDER:$1|pengguna}} yang disembunyikan",
+ "sp-contributions-deleted": "kontribusi {{GENDER:$1|pengguna}} yang dihapus",
"sp-contributions-uploads": "unggahan",
"sp-contributions-logs": "log",
"sp-contributions-talk": "bicara",
"tags-actions-header": "Tindakan",
"tags-active-yes": "Ya",
"tags-active-no": "Tidak",
- "tags-source-extension": "Ditetapkan oleh suatu ekstensi",
+ "tags-source-extension": "Ditetapkan oleh perangkat lunak",
"tags-source-manual": "Digunakan secara manual oleh pengguna dan bot",
"tags-source-none": "Tidak digunakan lagi",
"tags-edit": "sunting",
"tags-deactivate": "nonaktifkan",
"tags-hitcount": "$1 {{PLURAL:$1|perubahan}}",
"tags-manage-no-permission": "Anda tak memiliki hak akses untuk mengatur perubahan tag.",
- "tags-manage-blocked": "Anda tidak dapat mengganti tag ketika sedang diblokir.",
+ "tags-manage-blocked": "Anda tidak dapat mengatur perubahan tag ketika {{GENDER:$1|Anda}} diblokir.",
"tags-create-heading": "Buat sebuah tag baru",
"tags-create-explanation": "Secara baku, tag yang baru dibuat akan tersedia untuk digunakan oleh pengguna dan bot.",
"tags-create-tag-name": "Nama tag:",
"tags-activate-submit": "Aktifkan",
"tags-deactivate-reason": "Alasan:",
"tags-deactivate-submit": "Matikan",
- "tags-apply-blocked": "Anda tidak dapat menerapkan perubahan tag dan perubahan lainnya ketika sedang diblokir.",
- "tags-update-blocked": "Anda tidak dapat menambah atau menghapus tag ketika sedang diblokir.",
+ "tags-apply-blocked": "Anda tidak dapat menerapkan perubahan tag dengan perubahan Anda ketika {{GENDER:$1|Anda}} sedang diblokir.",
+ "tags-update-blocked": "Anda tidak dapat menambahkan atau menghapus perubahan tag ketika {{GENDER:$1|Anda}} sedang diblokir.",
"tags-edit-existing-tags": "Tag yang ada:",
"tags-edit-existing-tags-none": "<em>Tidak ada</em>",
"tags-edit-new-tags": "Tag baru:",
"htmlform-user-not-exists": "<strong>$1</strong> tidak ada.",
"htmlform-user-not-valid": "<strong>$1</strong> bukan merupakan nama pengguna sah.",
"logentry-delete-delete": "$1 {{GENDER:$2|menghapus}} halaman $3",
+ "logentry-delete-delete_redir": "$1 {{GENDER:$2|menghapus}} pengalihan $3 dengan penimpaan",
"logentry-delete-restore": "$1 {{GENDER:$2|mengembalikan}} halaman $3",
"logentry-delete-event": "$1 {{GENDER:$2|mengubah}} tampilan {{PLURAL:$5|$5 log peristiwa}} di $3: $4",
"logentry-delete-revision": "$1 {{GENDER:$2|mengubah}} tampilan {{PLURAL:$5|$5 revisi}} di halaman $3: $4",
"feedback-thanks": "Terima kasih! Umpan balik Anda telah dikirimkan ke halaman \"[$2 $1]\".",
"feedback-thanks-title": "Terima kasih!",
"feedback-useragent": "Agen pengguna:",
- "searchsuggest-search": "Cari",
+ "searchsuggest-search": "Cari {{SITENAME}}",
"searchsuggest-containing": "berisi...",
"api-error-autoblocked": "Alamat IP Anda telah diblokir secara otomatis, karena sebelumnya digunakan oleh pengguna yang diblokir.",
"api-error-badaccess-groups": "Anda tidak diizinkan mengunggah berkas ke wiki ini.",
"log-action-filter-newusers": "Jenis pembuatan akun:",
"log-action-filter-patrol": "Jenis patroli:",
"log-action-filter-protect": "Jenis perlindungan:",
- "log-action-filter-rights": "Jenis penggantian hak",
- "log-action-filter-suppress": "Jenis penyembunyian",
+ "log-action-filter-rights": "Jenis penggantian hak akses:",
+ "log-action-filter-suppress": "Jenis penyembunyian:",
"log-action-filter-upload": "Jenis pengunggahan:",
"log-action-filter-all": "Semua",
"log-action-filter-block-block": "Blokir",
"log-action-filter-contentmodel-change": "Ubah Modelkonten",
"log-action-filter-contentmodel-new": "Pembuatan halaman dengan Modelkonten yang tak baku",
"log-action-filter-delete-delete": "Penghapusan halaman",
+ "log-action-filter-delete-delete_redir": "Mengalihkan pengalihan",
"log-action-filter-delete-restore": "Pembatalan penghapusan halaman",
"log-action-filter-delete-event": "Log penghapusan",
"log-action-filter-delete-revision": "Penghapusan revisi",
"authmanager-authn-autocreate-failed": "Pembuatan otomatis dari akun lokal gagal: $1",
"authmanager-change-not-supported": "Kredensial yang diberikan tidak dapat diganti, karena tidak ada yang akan menggunakannya.",
"authmanager-create-disabled": "Pembuatan akun dimatikan.",
- "authmanager-create-from-login": "Untuk membuat akun Anda, silakan isi kolom di bawah.",
+ "authmanager-create-from-login": "Untuk membuat akun, silakan isi kolom di bawah.",
"authmanager-create-not-in-progress": "Pembuatan akun tidak dilanjutkan atau data sesi telah hilang. Ulang kembali dari awal.",
"authmanager-create-no-primary": "Kredensial yang diberikan tidak dapat digunakan untuk pembuatan akun.",
"authmanager-link-no-primary": "Kredensial yang diberikan tidak dapat digunakan untuk menautkan akun.",
"emailccsubject": "Copia del messaggio inviato a $1: $2",
"emailsent": "Messaggio inviato",
"emailsenttext": "Il messaggio e-mail è stato inviato.",
- "emailuserfooter": "Questa email è stata {{GENDER:$1|inviata}} da $1 a {{GENDER:$2|$2}} attraverso la funzione \"{{int:emailuser}}\" su {{SITENAME}}.",
+ "emailuserfooter": "Questa email è stata {{GENDER:$1|inviata}} da $1 a {{GENDER:$2|$2}} attraverso la funzione \"{{int:emailuser}}\" su {{SITENAME}}. La {{GENDER:$2|tua}} eventuale email di risposta sarà inviata direttamente al {{GENDER:$1|mittente originale}}, rivelando il {{GENDER:$2|tuo}} indirizzo di posta elettronica a {{GENDER:$1|lui|lei}}.",
"usermessage-summary": "Messaggio di sistema",
"usermessage-editor": "Messaggero di sistema",
"usermessage-template": "MediaWiki:MessaggioUtente",
"preview": "Pratuduh",
"showpreview": "Deleng pratuduh",
"showdiff": "Tuduhaké owahan",
- "anoneditwarning": "<strong>Penget:</strong> Panjenengan boten mlebet log. Alamat IP Panjenengan badhe katingal dening publik manawi Panjenengan ngayahi ewah-ewahan. Manawi Panjenengan <strong>[$1 mlebet log]</strong> utawai <strong>[$2 damel akun]</strong>, suntingan Panjenengan badhe kaatribusekaken dhumateng nama pangangge Panjenengan, lan rupi-rupi kauntungan sanesipun.",
+ "anoneditwarning": "<strong>Pènget:</strong> Panjenengan durung mlebu log. Alamat IP-né panjenengan bakal katon marang wong akèh manawa panjenengan mbesut. Manawa panjenengan <strong>[$1 mlebu log]</strong> utawa <strong>[$2 nggawé akun]</strong>, besutané panjenengan bakal dadi darbéné naragunané panjenengan lan uga ana kauntungan liya.",
"anonpreviewwarning": "''Sampéyan durung mlebu log. Nyimpen bakal nyathet alamat IP Sampéyan nèng riwayat sunting kaca iki.''",
"missingsummary": "'''Pènget:''' Panjenengan ora nglebokaké ringkesan panyuntingan. Menawa panjenengan mencèt tombol Simpen manèh, suntingan panjenengan bakal kasimpen tanpa ringkesan panyuntingan.",
"selfredirect": "<strong>Pélik:</strong> Sampéyan ngalih kaca iki iya nyang kaca iki dhéwé.\nSampéyan mungkin salah wènèh tujuan kanggo alihan utawa salah mbesut kaca.\nYèn sampéyan ngeklik \"{{int:savearticle}}\" manèh, kaca alihan bakal digawé.",
"movepage-moved": "<strong>\"$1\" wis dilih nyang \"$2\"</strong>",
"movepage-moved-redirect": "Kaca pengalihan wis kacipta.",
"movepage-moved-noredirect": "Kanggo gawé pengalihan wis ditahan.",
- "articleexists": "Satunggalipun kaca kanthi asma punika sampun wonten, utawi asma ingkang panjenengan pendhet mboten leres. Sumangga nyobi asma sanèsipun.",
+ "articleexists": "Kaca mawa jeneng mangkono wis ana utawa jeneng sing kokpilih ora valid.\nMangga pilih jeneng liya.",
"cantmove-titleprotected": "Panjenengan ora bisa mindhahaké kaca iki menyang lokasi iki, amerga irah-irahan tujuan lagi direksa; ora olèh digawé",
"movetalk": "Lih kaca parembugan sing magepokan",
"move-subpages": "Lih anak kaca (tekan $1)",
"passwordreset-emaildisabled": "Šajā viki ir atspējotas e-pasta iespējas.",
"passwordreset-username": "Lietotājvārds:",
"passwordreset-domain": "Domēns:",
- "passwordreset-capture": "Apskatīt izveidoto e-pastu?",
"passwordreset-email": "E-pasta adrese:",
"passwordreset-emailtitle": "Konta informācija {{SITENAME}}",
"passwordreset-emailelement": "Lietotājvārds: \n$1\n\nPagaidu parole: \n$2",
"userrights-reason": "Iemesls:",
"userrights-no-interwiki": "Tev nav atļaujas izmainīt dalībnieku tiesības citos wiki.",
"userrights-nodatabase": "Datubāze $1 neeksistē vai nav lokāla.",
- "userrights-nologin": "Tev ir [[Special:UserLogin|jāieiet iekšā]] kā adminam, lai varētu izmainīt dalībnieku grupas.",
- "userrights-notallowed": "Tev nav atļaujas pievienot vai noņemt dalībnieku tiesības.",
"userrights-changeable-col": "Grupas, kuras tu vari izmainīt",
"userrights-unchangeable-col": "Grupas, kuras tu nevari izmainīt",
"group": "Grupa:",
"right-userrights-interwiki": "Mainīt dalīnieku tiesības citās Vikipēdijās",
"right-siteadmin": "Bloķēt un atbloķēt datubāzi",
"right-sendemail": "Sūtīt e-pastu citiem dalībniekiem",
- "right-passwordreset": "Apskatīt paroles atiestatīšanas e-pasta ziņojumus",
"grant-group-email": "Sūtīt e-pastu",
"grant-createaccount": "Izveidot kontu",
"grant-editmywatchlist": "Labot uzraugāmo rakstu sarakstu",
"htmlform-cloner-create": "Pievienot vairāk",
"htmlform-cloner-delete": "Noņemt",
"logentry-delete-delete": "$1 {{GENDER:$2|izdzēsa}} lapu $3",
+ "logentry-delete-delete_redir": "$1 {{GENDER:$2|izdzēsa}} pāradresāciju $3 pārrakstot",
"logentry-delete-restore": "$1 {{GENDER:$2|atjaunoja}} lapu $3",
"revdelete-content-hid": "saturs slēpts",
"revdelete-summary-hid": "labojuma kopsavilkums slēpts",
"authmanager-realname-label": "Tavs īstais vārds",
"authmanager-realname-help": "Dalībnieka īstais vārds",
"authprovider-resetpass-skip-label": "Izlaist",
- "specialpage-securitylevel-not-allowed-title": "Nav atļauts",
- "edit-error-short": "Kļūda: $1",
- "edit-error-long": "Kļūdas:\n\n$1"
+ "specialpage-securitylevel-not-allowed-title": "Nav atļauts"
}
"views": "Afichatges",
"toolbox": "Aisinas",
"tool-link-userrights": "Modificar los gropes de {{GENDER:$1|l’utilizaire|l’utilizaira}}",
+ "tool-link-userrights-readonly": "Veire los {{GENDER:$1|gropes utilizaire}}",
"tool-link-emailuser": "Mandar un corrièr electronic a {{GENDER:$1|l’utilizaire|l’utilizaira}}",
"userpage": "Pagina d'utilizaire",
"projectpage": "Pagina meta",
"passwordreset-emaildisabled": "Las foncionalitats e-mail son estadas desactivadas sus aqueste wiki.",
"passwordreset-username": "Nom d'utilizaire :",
"passwordreset-domain": "Domeni:",
- "passwordreset-capture": "Veire lo corrièl resultant ?",
- "passwordreset-capture-help": "Se marcatz aquesta casa, lo corrièr electronic (amb lo senhal temporari) vos serà afichat al meteis temps que serà mandat a l'utilizaire.",
"passwordreset-email": "Adreça de corrièr electronic :",
"passwordreset-emailtitle": "Detailhs d'un compte per {{SITENAME}}",
"passwordreset-emailtext-ip": "Qualqu'un (probablament vos, dempuèi l'adreça IP $1) a demandat una reïnicializacion de vòstre senhal per {{SITENAME}} ($4). {{PLURAL:$3|Lo compte d'utilizaire seguent es associat|Los comptes d'utilizaires seguents son associats}} a aquesta adreça de corrièr electronic :\n\n$2\n\n{{PLURAL:$3|Aqueste senhal temporari expirarà|Aquestes senhals temporaris expiraràn}} dins {{PLURAL:$5|un jorn|$5 jorns}}. Ara, vos cal vos connectar e causir un senhal novèl. Se aquesta demanda proven pas de vos, o que vos sètz remembrat de vòstre senhal inicial, e que volètz pas mai lo modificar, podètz ignorar aqueste messatge e contunhar d'utilizar vòstre ancian senhal.",
"columns": "Colomnas :",
"searchresultshead": "Recèrca",
"stub-threshold": "Limit pel formatatge dels ligams d’esbòs ($1) :",
+ "stub-threshold-sample-link": "exemple",
"stub-threshold-disabled": "Desactivat",
"recentchangesdays": "Nombre de jorns d'afichar dins los darrièrs cambiaments :",
"recentchangesdays-max": "(maximum $1 {{PLURAL:$1|jorn|jorns}})",
"prefs-help-recentchangescount": "Aquò inclutz las modificacions recentas, las paginas d’istorics e los jornals.",
"prefs-help-watchlist-token2": "Aquí la clau secreta del flux Web de vòstra lista de seguiment.\nTota persona que la coneis poirà legir vòstra lista de seguiment, doncas, la comuniquetz pas.\n[[Special:ResetTokens|Clicatz aicí se la vos cal reïnicializar]].",
"savedprefs": "Las preferéncias son estadas salvadas.",
+ "savedrights": "Los dreits d'utilizaire de {{GENDER:$1|$1}} son estats enregistrats.",
"timezonelegend": "Fus orari :",
"localtime": "Ora locala :",
"timezoneuseserverdefault": "Utilizar la valor del servidor ($1)",
"badsig": "Signatura bruta incorrècta, verificatz vòstras balisas HTML.",
"badsiglength": "Vòstra signatura es tròp longa.\nDeu aver, al maximum $1 caractèr{{PLURAL:$1||s}}.",
"yourgender": "Cossí vos agrada mai d'èsser descrit ?",
- "gender-unknown": "M'agrada mai sens detalh",
+ "gender-unknown": "Quand farà mencion de vos, lo logicial utilizarà de mots de genre neutre, quand serà possible",
"gender-male": "Modifica de pagina del wiki",
"gender-female": "Modifica de paginas del wiki",
"prefs-help-gender": "Definir aquesta preferéncia es facultatiu.\nAqueste logicial utiliza sa valor per s’adreçar a vos e vos mencionar als autres en utilizant lo bon genre gramatical.\nAquesta informacion serà publica.",
"prefs-help-prefershttps": "Aquesta preferéncia serà efectiva al moment de vòstra connexion que ven.",
"prefs-tabs-navigation-hint": "Astúcia : Podètz utilizar las sagetas d'esquèrra e de dreita per navigar entre los onglets.",
"userrights": "Gestion dels dreits d'utilizaire",
- "userrights-lookup-user": "Gestion dels dreits d'utilizaire",
+ "userrights-lookup-user": "Seleccionar un utilizaire",
"userrights-user-editname": "Entrar un nom d’utilizaire :",
- "editusergroup": "Modificacion dels gropes d’{{GENDER:$1|utilizaires}}",
+ "editusergroup": "Cargar de gropes d’utilizaires",
"editinguser": "Modificacion dels dreits de l’{{GENDER:$1|utilizaire|utilizaira}} <strong>[[User:$1|$1]]</strong> $2",
"userrights-editusergroup": "Modificar los gropes de l’utilizaire",
"saveusergroups": "Enregistrar los gropes de l’{{GENDER:$1|utilizaire|utilizaira}}",
"userrights-reason": "Motiu :",
"userrights-no-interwiki": "Sètz pas abilitat per modificar los dreits dels utilizaires sus d'autres wikis.",
"userrights-nodatabase": "La basa de donadas « $1 » existís pas o es pas en local.",
- "userrights-nologin": "Vos cal [[Special:UserLogin|vos connectar]] amb un compte d'administrator per balhar los dreits d'utilizaire.",
- "userrights-notallowed": "Avètz pas la permission d'apondre o suprimir de dreits d'utilizaire.",
"userrights-changeable-col": "Los gropes que podètz cambiar",
"userrights-unchangeable-col": "Los gropes que podètz pas cambiar",
"userrights-conflict": "Conflicte de modificacion de dreits d'utilizaire ! Relegissètz e confirmatz vòstras modificacions.",
- "userrights-removed-self": "Avètz suprimit vòstres pròpris dreits. Del còp, podètz pas mai accedir a aquesta pagina.",
"group": "Grop :",
"group-user": "Utilizaires",
"group-autoconfirmed": "Utilizaires enregistrats",
"right-siteadmin": "Verrolhar e desverrolhar la basa de donadas",
"right-override-export-depth": "Exportar las paginas en incluent las paginas ligadas fins a una prigondor de 5 nivèls",
"right-sendemail": "Mandar un corrièl als autres utilizaires",
- "right-passwordreset": "Veire los corrièrs electronics de reïnicializacion dels senhals",
"right-applychangetags": "Aplicar [[Special:Tags|las balisas]] amb sas pròprias modificacions",
"grant-generic": "ensemble de dreits « $1 »",
+ "grant-group-email": "Mandar un corrièr electronic",
"grant-blockusers": "Blocar e desblocar d'utilizaires",
+ "grant-createaccount": "Crear de comptes",
+ "grant-createeditmovepage": "Crear, modificar e desplaçar de paginas",
"grant-patrol": "Verificar las modificacions de paginas",
+ "grant-basic": "Dreits de basa",
"newuserlogpage": "Istoric de las creacions de comptes",
"newuserlogpagetext": "Jornal de las creacions de comptes d'utilizaires.",
"rightslog": "Istoric de las modificacions d'estatut",
"recentchanges-label-plusminus": "La talha de la pagina a cambiat d'aqueste nombre d’octets.",
"recentchanges-legend-heading": "<strong>Legenda :</strong>",
"recentchanges-legend-newpage": "{{int:recentchanges-label-newpage}} (veire tanben la [[Special:NewPages|lista de las paginas novèlas]]).",
+ "recentchanges-submit": "Afichar",
"rcnotefrom": "Çaijós {{PLURAL:$5|la modificacion efectuada|las modificacions efectuadas}} dempuèi lo <strong>$3, $4</strong> (afichadas fins a <strong>$1</strong>).",
"rclistfrom": "Afichar las modificacions novèlas dempuèi lo $3 $2",
"rcshowhideminor": "$1 los cambiaments menors",
"rcshowhidemine": "$1 mas modificacions",
"rcshowhidemine-show": "Afichar",
"rcshowhidemine-hide": "Amagar",
+ "rcshowhidecategorization": "$1 la categorizacion de las paginas",
+ "rcshowhidecategorization-show": "Afichar",
+ "rcshowhidecategorization-hide": "Amagar",
"rclinks": "Afichar los $1 darrièrs cambiaments efectuats al cors dels $2 darrièrs jorns<br />$3.",
"diff": "dif",
"hist": "ist",
"recentchangeslinked-summary": "Aquesta pagina especiala fa veire los darrièrs cambiaments sus las paginas que son ligadas. Las paginas de [[Special:Watchlist|vòstra lista de seguimznt]] son '''en gras'''.",
"recentchangeslinked-page": "Nom de la pagina :",
"recentchangeslinked-to": "Afichar los cambiaments cap a las paginas ligadas al luòc de la pagina donada",
+ "recentchanges-page-added-to-category": "[[:$1]] apondut a la categoria",
"upload": "Importar un fichièr",
"uploadbtn": "Importar un fichièr",
"reuploaddesc": "Anullar lo cargament e tornar al formulari.",
"upload-too-many-redirects": "L'URL conten tròp de redireccions",
"upload-http-error": "Una error HTTP es intervenguda : $1",
"upload-copy-upload-invalid-domain": "La còpia dels telecargaments es pas disponibla dempuèi aqueste domeni.",
+ "upload-dialog-title": "Mandar un fichièr",
+ "upload-dialog-button-cancel": "Anullar",
+ "upload-dialog-button-back": "Retorn",
+ "upload-dialog-button-done": "Acabat",
+ "upload-dialog-button-save": "Enregistrar",
+ "upload-dialog-button-upload": "Mandar",
+ "upload-form-label-infoform-title": "Detalhs",
+ "upload-form-label-infoform-name": "Nom",
+ "upload-form-label-infoform-description": "Descripcion",
+ "upload-form-label-usage-title": "Utilizacion",
+ "upload-form-label-usage-filename": "Nom del fichièr",
+ "upload-form-label-own-work": "Soi l'autor d'aquesta òbra",
+ "upload-form-label-infoform-categories": "Categorias",
+ "upload-form-label-infoform-date": "Data",
"backend-fail-stream": "Impossible de legir lo fichièr $1.",
"backend-fail-backup": "Impossible de salvar lo fichièr $1.",
"backend-fail-notexists": "Lo fichièr $1 existís pas.",
"uploadstash-nofiles": "Avètz pas de fichièrs en cache d'impòrt.",
"uploadstash-errclear": "La supression dels fichièrs a fracassat.",
"uploadstash-refresh": "Actualizar la lista dels fichièrs",
+ "uploadstash-thumbnail": "afichar una miniatura",
"invalid-chunk-offset": "Offset de segment invalid",
"img-auth-accessdenied": "Accès refusat",
"img-auth-nopathinfo": "PATH_INFO mancant. Vòstre servidor es pas parametrat per passar aquesta informacion.\nBenlèu que fonciona en CGI e supòrta pas img_atuh. Consultatz https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:Image_Authorization.",
"mostrevisions": "Articles mai modificats",
"prefixindex": "Totas las paginas que començan per…",
"prefixindex-namespace": "Totas las paginas amb prefix (espaci de noms $1)",
+ "prefixindex-submit": "Afichar",
"prefixindex-strip": "Levar lo prefix dins la lista",
"shortpages": "Paginas brèvas",
"longpages": "Paginas longas",
"protectedpages-performer": "Proteccion de l’utilizaire",
"protectedpages-params": "Paramètres de proteccion",
"protectedpages-reason": "Motiu",
+ "protectedpages-submit": "Afichar las paginas",
"protectedpages-unknown-timestamp": "Desconegut",
"protectedpages-unknown-performer": "Utilizaire desconegut",
"protectedtitles": "Títols protegits",
"protectedtitlesempty": "Cap de títol es pas actualament protegit amb aquestes paramètres.",
+ "protectedtitles-submit": "Afichar los títols",
"listusers": "Lista dels participants",
"listusers-editsonly": "Far veire sonque los utilizaires qu'an al mens una contribucion",
"listusers-creationsort": "Triar per data de creacion",
"usereditcount": "$1 {{PLURAL:$1|cambiament|cambiaments}}",
"usercreated": "{{GENDER:$3|Creat}} lo $1 a $2",
"newpages": "Paginas novèlas",
+ "newpages-submit": "Afichar",
"newpages-username": "Nom d'utilizaire :",
"ancientpages": "Articles mai ancians",
"move": "Renomenar",
"apihelp-no-such-module": "Lo modul « $1 » es introbable.",
"apisandbox": "Nauc de sabla API",
"apisandbox-api-disabled": "API es desactivat sus aqueste site.",
+ "apisandbox-fullscreen": "Espandir lo panèl",
+ "apisandbox-unfullscreen": "Afichar la pagina",
"apisandbox-submit": "Far la demanda",
"apisandbox-reset": "Escafar",
+ "apisandbox-retry": "Ensajar tornarmai",
+ "apisandbox-helpurls": "Ligams d'ajuda",
"apisandbox-examples": "Exemples",
+ "apisandbox-dynamic-parameters": "Paramètres suplementaris",
+ "apisandbox-dynamic-parameters-add-label": "Apondon del paramètre",
+ "apisandbox-dynamic-parameters-add-placeholder": "Nom del paramètre",
+ "apisandbox-deprecated-parameters": "Paramètres obsolèts",
"apisandbox-results": "Resultats",
"apisandbox-request-url-label": "Requèsta URL :",
"apisandbox-request-time": "Durada de la demanda : {{PLURAL:$1|$1 ms}}",
+ "apisandbox-continue": "Contunhar",
+ "apisandbox-continue-clear": "Escafar",
"booksources": "Obratges de referéncia",
"booksources-search-legend": "Recercar demest d'obratges de referéncia",
"booksources-isbn": "ISBN :",
"specialloguserlabel": "Autor :",
"speciallogtitlelabel": "Cibla (títol o {{ns:user}}:nom d'utilizaire) :",
"log": "Jornals",
+ "logeventslist-submit": "Afichar",
"all-logs-page": "Totas las operacions publicas",
"alllogstext": "Afichatge combinat de totes los jornals de {{SITENAME}}.\nPodètz restrénher la vista en seleccionant un tipe de jornal, un nom d’utilizaire (cassa sensibla) o una pagina ciblada (idem).",
"logempty": "I a pas res dins l’istoric per aquesta pagina.",
"log-title-wildcard": "Recercar de títols que començan per aqueste tèxte",
"showhideselectedlogentries": "Afichar/amagar las entradas de jornal seleccionadas",
+ "checkbox-select": "Seleccionar : $1",
+ "checkbox-all": "Tot",
+ "checkbox-none": "Pas cap",
+ "checkbox-invert": "Inversar",
"allpages": "Totas las paginas",
"nextpage": "Pagina seguenta ($1)",
"prevpage": "Pagina precedenta ($1)",
"cachedspecial-viewing-cached-ttl": "Visualizatz una version d'aquesta pagina mesa en cache, que pòt èsser datada d’al mai $1.",
"cachedspecial-refresh-now": "Veire lo mai recent.",
"categories": "Categorias",
+ "categories-submit": "Afichar",
"categoriespagetext": "{{PLURAL:$1|La categoria seguenta es utilizada|Las categorias seguentas son utilizadas}} per de paginas o de fichièrs.\n[[Special:UnusedCategories|Las categorias inutilizadas]] son pas afichadas aicí.\nVejatz tanben [[Special:WantedCategories|las categorias demandadas]].",
"categoriesfrom": "Afichar las categorias que començan a :",
"deletedcontributions": "Contribucions suprimidas d’un utilizaire",
"listgrouprights-namespaceprotection-header": "Restriccions d'espaci de noms",
"listgrouprights-namespaceprotection-namespace": "Espaci de noms",
"listgrouprights-namespaceprotection-restrictedto": "Dreit(s) que permet(on) a l'utilizaire de modificar",
+ "listgrants": "Autorizacions",
+ "listgrants-grant": "Acordar",
+ "listgrants-rights": "Dreits",
"trackingcategories": "Categorias de seguiment",
"trackingcategories-msg": "Categoria de seguiment",
"trackingcategories-name": "Nom del messatge",
"wlheader-showupdated": "Las paginas que son estadas modificadas dempuèi vòstra darrièra visita son afichadas en '''gras'''.",
"wlnote": "Çaijós {{PLURAL:$1|figura la darrièra modificacion efectuada|figuran las <strong>$1</strong> darrièras modificacions efectuadas}} pendent {{PLURAL:$2|la darrièra ora|las <strong>$2</strong> darrièras oras}}, dempuèi $3, $4.",
"wlshowlast": "Far veire las darrièras $1 oras, los darrièrs $2 jorns",
+ "watchlist-hide": "Amagar",
+ "watchlist-submit": "Afichar",
"wlshowhideminor": "cambiaments menors",
+ "wlshowhidebots": "Robòts",
+ "wlshowhideliu": "utilizaires enregistrats",
+ "wlshowhideanons": "utilizaires anonims",
+ "wlshowhidepatr": "modificacions repassadas",
+ "wlshowhidemine": "mas modificacions",
"watchlist-options": "Opcions de la lista de seguiment",
"watching": "Seguit...",
"unwatching": "Fin del seguit...",
"delete-confirm": "Escafar «$1»",
"delete-legend": "Escafar",
"historywarning": "<strong>Atencion :</strong> la pagina que sètz a mand de suprimir a un istoric amb $1 {{PLURAL:$1|version|versions}} :",
+ "historyaction-submit": "Afichar",
"confirmdeletetext": "Sètz a mand de suprimir una pagina o un fichièr, e mai totas sas versions anterioras istorizadas.\nConfirmatz qu'es plan çò que volètz far, que ne comprenètz las consequéncias e que fasètz aquò en acòrdi amb las [[{{MediaWiki:Policy-url}}|règlas intèrnas]].",
"actioncomplete": "Accion efectuada",
"actionfailed": "L’accion a fracassat",
"changecontentmodel-title-label": "Títol de la pagina",
"changecontentmodel-model-label": "Novèl modèl de contengut",
"changecontentmodel-reason-label": "Motiu :",
+ "changecontentmodel-submit": "Modificar",
"logentry-contentmodel-change-revertlink": "restablir",
"logentry-contentmodel-change-revert": "restablir",
"protectlogpage": "Istoric de las proteccions",
"ipb-unblock": "Desblocar un compte d'utilizaire o una adreça IP",
"ipb-blocklist": "Vejatz los blocatges existents",
"ipb-blocklist-contribs": "Contribucions per {{GENDER:$1|$1}}",
+ "ipb-blocklist-duration-left": "$1 restant",
"unblockip": "Desblocar un utilizaire o una adreça IP",
"unblockiptext": "Utilizatz lo formulari çaijós per restablir l'accès en escritura\na partir d'una adreça IP precedentament blocada.",
"ipusubmit": "Suprimir aqueste blocatge",
"pageinfo-article-id": "Numèro de la pagina",
"pageinfo-language": "Lenga del contengut de la pagina",
"pageinfo-content-model": "Modèl de contengut de la pagina",
+ "pageinfo-content-model-change": "modificar",
"pageinfo-robot-policy": "Indexacion per robòts",
"pageinfo-robot-index": "Autorizada",
"pageinfo-robot-noindex": "Interdicha",
"pageinfo-category-pages": "Nombre de paginas",
"pageinfo-category-subcats": "Nombre de soscategorias",
"pageinfo-category-files": "Nombre de fichièrs",
+ "pageinfo-user-id": "ID de l'utilizaire",
"markaspatrolleddiff": "Marcar coma essent pas un vandalisme",
"markaspatrolledtext": "Marcar aqueste article coma pas vandalizat",
"markedaspatrolled": "Marcat coma pas vandalizat",
"patrol-log-header": "Vaquí un jornal de las versions patrolhadas.",
"log-show-hide-patrol": "$1 l'istoric de las relecturas",
"log-show-hide-tag": "$1 lo jornal de las balisas",
+ "confirm-markpatrolled-button": "D'acòrdi",
"deletedrevision": "La version anciana $1 es estada suprimida.",
"filedeleteerror-short": "Error al moment de la supression del fichièr : $1",
"filedeleteerror-long": "D'errors son estadas rencontradas al moment de la supression del fichièr :\n\n$1",
"confirm-watch-top": "Apondre aquesta pagina a vòstra lista de seguiment ?",
"confirm-unwatch-button": "D'acòrdi",
"confirm-unwatch-top": "Levar aquesta pagina de vòstra lista de seguiment ?",
+ "confirm-rollback-button": "D'acòrdi",
"colon-separator": " : ",
"quotation-marks": "« $1 »",
"imgmultipageprev": "← pagina precedenta",
"watchlisttools-edit": "Veire e modificar la lista de seguiment",
"watchlisttools-raw": "Modificar la lista (mòde brut)",
"signature": "[[{{ns:user}}:$1|$2]] ([[{{ns:user_talk}}:$1|discussion]])",
+ "timezone-local": "Local",
"duplicate-defaultsort": "Atencion : La clau de triada per defaut « $2 » espotís la mai recenta « $1 ».",
"duplicate-displaytitle": "<strong>Atencion :</strong> Lo títol d'afichatge «$2» remplaça l'ancian títol d'afichatge «$1».",
"version": "Version",
"redirect-page": "ID de pagina",
"redirect-revision": "Revision de la pagina",
"redirect-file": "Nom del fichièr",
+ "redirect-logid": "ID de jornal",
"redirect-not-exists": "Valor pas trobada",
"fileduplicatesearch": "Recèrca dels fichièrs en doble",
"fileduplicatesearch-summary": "Recèrca de las còpias de fichièrs identics d'aprèp lor emprenta de hachatge.",
"tags-actions-header": "Accions",
"tags-active-yes": "Òc",
"tags-active-no": "Non",
- "tags-source-extension": "Definida per una extension",
+ "tags-source-extension": "Definit pel logicial",
"tags-source-manual": "Aplicada manualament pels utilizaires e los bòts",
"tags-source-none": "Obsolèt",
"tags-edit": "modificar",
"htmlform-cloner-create": "Apondre encara",
"htmlform-cloner-delete": "Suprimir",
"htmlform-cloner-required": "Una valor al mens es obligatòria.",
+ "htmlform-date-placeholder": "AAAA-MM-JJ",
+ "htmlform-time-placeholder": "HH:MM:SS",
+ "htmlform-datetime-placeholder": "AAAA-MM-JJ HH:MM:SS",
"logentry-delete-delete": "$1 {{GENDER:$2|a suprimit}} la pagina $3",
"logentry-delete-restore": "$1 {{GENDER:$2|a restablit}} la pagina $3",
"logentry-delete-event": "$1 {{GENDER:$2|a modificat}} la visibilitat {{PLURAL:$5|d'un eveniment del jornal|de $5 eveniments del jornal}} sus $3 : $4",
"mediastatistics-header-text": "Textual",
"mediastatistics-header-executable": "Executables",
"mediastatistics-header-archive": "Formats compressats",
+ "mediastatistics-header-total": "Totes los fichièrs",
"json-error-state-mismatch": "JSON invalid o mal format",
"json-error-syntax": "Error de sintaxi",
"headline-anchor-title": "Ligam cap a aquesta seccion",
"randomrootpage": "Pagina raiç aleatòria",
"log-action-filter-rights": "Tipe de cambiament de dreits :",
"log-action-filter-suppress": "Tipe de supression :",
+ "log-action-filter-all": "Tot",
+ "log-action-filter-block-block": "Blocatge",
+ "log-action-filter-block-unblock": "Desblocar",
+ "authmanager-email-label": "Corrièr electronic",
+ "authmanager-email-help": "Adreça de corrièr electronic",
+ "authmanager-realname-label": "Nom vertadièr",
+ "authmanager-realname-help": "Nom real de l'utilizaire",
+ "authprovider-resetpass-skip-label": "Sautar",
"changecredentials": "Modificar las informacions d’identificacion"
}
"Cristofer Alves",
"Tark",
"O Andarilho",
- "Bruno.S.Alves 270"
+ "Bruno.S.Alves 270",
+ "!Silent"
]
},
"tog-underline": "Sublinhar links:",
"feedback-thanks": "Obrigado! O seu comentário foi adicionado à página \"[$2 $1]\".",
"feedback-thanks-title": "Obrigado!",
"feedback-useragent": "Agente de usuário:",
- "searchsuggest-search": "Pesquisa",
+ "searchsuggest-search": "Pesquisar em {{SITENAME}}",
"searchsuggest-containing": "páginas contendo…",
"api-error-badaccess-groups": "Você não tem permissão para enviar arquivos para este wiki.",
"api-error-badtoken": "Erro interno: token inválido.",
"Luan",
"Gato Preto",
"Jdforrester",
- "Mansil"
+ "Mansil",
+ "Ngl2016"
]
},
"tog-underline": "Sublinhar ligações:",
"search-external": "Pesquisa externa",
"searchdisabled": "Foi impossibilitada a realização de pesquisas na wiki {{SITENAME}}.\nEntretanto, pode realizar pesquisas através do Google.\nNote, no entanto, que a indexação da wiki {{SITENAME}} neste motor de busca pode estar desatualizada.",
"search-error": "Um erro ocorreu enquanto se efectuava a pesquisa: $1",
- "search-warning": "Foi assinalado um aviso ao pesquisar: $1",
+ "search-warning": "Ocorreu um aviso ao pesquisar: $1",
"preferences": "Preferências",
"mypreferences": "Preferências",
"prefs-edits": "Número de edições:",
"october-gen": "{{doc-months|10|genitive}}\n{{Identical|October}}",
"november-gen": "{{doc-months|11|genitive}}\n{{Identical|November}}",
"december-gen": "{{doc-months|12|genitive}}\n{{Identical|December}}",
- "jan": "{{doc-months|1|short}}",
- "feb": "{{doc-months|2|short}}",
- "mar": "{{doc-months|3|short}}",
- "apr": "{{doc-months|4|short}}",
- "may": "{{doc-months|5|short}}",
- "jun": "{{doc-months|6|short}}",
+ "jan": "{{doc-months|1|short}}\n{{Identical|January}}",
+ "feb": "{{doc-months|2|short}}\n{{Identical|February}}",
+ "mar": "{{doc-months|3|short}}\n{{Identical|March}}",
+ "apr": "{{doc-months|4|short}}\n{{Identical|April}}",
+ "may": "{{doc-months|5|short}}\n{{Identical|May}}",
+ "jun": "{{doc-months|6|short}}\n{{Identical|June}}",
"jul": "{{doc-months|7|short}}",
"aug": "{{doc-months|8|short}}",
"sep": "{{doc-months|9|short}}",
"userrights-lookup-user": "Label text when managing user rights ([[Special:UserRights]])",
"userrights-user-editname": "Displayed on [[Special:UserRights]].",
"editusergroup": "Button name, in page [[Special:Userrights]], in the section named {{MediaWiki:userrights-lookup-user}}. The username or gender of the user is not known when this message is displayed.",
- "editinguser": "Appears on [[Special:UserRights]]. Parameters:\n* $1 - a plaintext username\n* $2 - user tool links. e.g. \"(Talk | contribs | block | send email)\"",
- "userrights-editusergroup": "Parameter:\n* $1 - (Optional) a username, can be used for GENDER",
+ "editinguser": "Appears on [[Special:UserRights]]. Parameters:\n* $1 - a plaintext username\n* $2 - user tool links. e.g. \"(Talk | contribs | block | send email)\"\n\nRelated messages:\n* {{msg-mw|viewinguserrights}}",
+ "viewinguserrights": "Appears on [[Special:UserRights]]. Parameters:\n* $1 - a plaintext username\n* $2 - user tool links. e.g. \"(Talk | contribs | block | send email)\"\n\nRelated messages:\n* {{msg-mw|editinguser}}",
+ "userrights-editusergroup": "Parameter:\n* $1 - (Optional) a username, can be used for GENDER\n\nRelated messages:\n* {{msg-mw|userrights-viewusergroup}}",
+ "userrights-viewusergroup": "Parameter:\n* $1 - (Optional) a username, can be used for GENDER\n\nRelated messages:\n* {{msg-mw|userrights-editusergroup}}",
"saveusergroups": "Button text when editing user groups.\nParameters:\n* $1 - username, for GENDER support",
"userrights-groupsmember": "Used when editing user groups in [[Special:Userrights]].\n\nThe message is followed by a list of group names.\n\nParameters:\n* $1 - (Optional) the number of items in the list following the message, for PLURAL\n* $2 - (Optional) the user name, for GENDER",
"userrights-groupsmember-auto": "Used when editing user groups in [[Special:Userrights]]. The message is followed by a list of group names.\n\n\"Implicit\" is for groups that the user was automatically added to (such as \"autoconfirmed\"); cf. {{msg-mw|userrights-groupsmember}}\n\nParameters:\n* $1 - (Optional) the number of items in the list following the message, for PLURAL\n* $2 - (Optional) the user name, for GENDER",
"action-delete": "{{Doc-action|delete}}",
"action-deleterevision": "{{Doc-action|deleterevision}}",
"action-deletedhistory": "{{Doc-action|deletedhistory}}",
+ "action-deletedtext": "{{Doc-action|deletedtext}}",
+ "action-deletelogentry": "{{Doc-action|deletelogentry}}",
"action-browsearchive": "{{Doc-action|browsearchive}}",
"action-undelete": "{{Doc-action|undelete}}",
"action-suppressrevision": "{{Doc-action|suppressrevision}}",
"action-sendemail": "{{doc-action|sendemail}}\n{{Identical|E-mail}}",
"action-editmywatchlist": "{{doc-action|editmywatchlist}}\n{{Identical|Edit your watchlist}}",
"action-viewmywatchlist": "{{doc-action|viewmywatchlist}}\n{{Identical|View your watchlist}}",
+ "action-editmyoptions": "{{Doc-action|editmyoptions}}",
"action-viewmyprivateinfo": "{{doc-action|viewmyprivateinfo}}",
"action-editmyprivateinfo": "{{doc-action|editmyprivateinfo}}",
"action-editcontentmodel": "{{doc-action|editcontentmodel}}",
"usercssispublic": "A reminder to users that CSS subpages are not preferences but normal pages, and thus can be viewed by other users and the general public. This message is shown to a user whenever they are editing a subpage in their own user-space that ends in .css. See also {{msg-mw|userjsispublic}}",
"restrictionsfield-badip": "An error message shown when one entered an invalid IP address or range in a restrictions field (such as Special:BotPassword). $1 is the IP address.",
"restrictionsfield-label": "Field label shown for restriction fields (e.g. on Special:BotPassword).",
- "restrictionsfield-help": "Placeholder text displayed in restriction fields (e.g. on Special:BotPassword)."
+ "restrictionsfield-help": "Placeholder text displayed in restriction fields (e.g. on Special:BotPassword).",
+ "revid": "Used to format a revision ID number in text. Parameters:\n* $1 - Revision ID number.",
+ "pageid": "Used to format a page ID number in text. Parameters:\n* $1 - Page ID number."
}
"passwordreset-emaildisabled": "Funcțiile de e-mail au fost dezactivate de pe acest wiki.",
"passwordreset-username": "Nume de utilizator:",
"passwordreset-domain": "Domeniu:",
- "passwordreset-capture": "Vizualizați e-mailul rezultat?",
- "passwordreset-capture-help": "Dacă bifați această căsuță, e-mailul (conținând parola temperară) vă va fi afișat, dar va fi trimis și utilizatorului.",
"passwordreset-email": "Adresă de e-mail:",
"passwordreset-emailtitle": "Detalii despre cont pe {{SITENAME}}",
"passwordreset-emailtext-ip": "Cineva (probabil dumneavoastră, de la adresa IP $1) a solicitat resetarea parolei \npentru {{SITENAME}} ($4). {{PLURAL:$3|Următorul cont este asociat|Următoarele conturi sunt asociate}}\ncu această adresă de e-mail:\n\n$2\n\n{{PLURAL:$3|Această parolă temporară va|Aceste parole temporare vor}} expira {{PLURAL:$5|într-o zi|în $5 zile}}.\nAr trebui să vă autentificați și să schimbați parola acum. Dacă altcineva a făcut această cerere \nsau dacă v-ați reamintit parola inițială și nu mai doriți să o schimbați,\nputeți ignora acest mesaj, continuând să utilizați vechea parolă.",
"userrights-reason": "Motiv:",
"userrights-no-interwiki": "Nu aveți permisiunea de a modifica permisiunile utilizatorilor pe alte wiki.",
"userrights-nodatabase": "Baza de date $1 nu există sau nu este locală.",
- "userrights-nologin": "Trebuie să te [[Special:UserLogin|autentifici]] cu un cont de administrator pentru a atribui permisiuni utilizatorilor.",
- "userrights-notallowed": "Nu aveți permisiunea de a acorda sau elimina drepturi utilizatorilor.",
"userrights-changeable-col": "Grupuri pe care le puteți schimba",
"userrights-unchangeable-col": "Grupuri pe care nu le puteți schimba",
"userrights-conflict": "Conflict al schimbării drepturilor de utilizator! Reverificați și confirmați-vă modificările.",
- "userrights-removed-self": "V-ați eliminat propriile drepturi. Ca urmare, nu mai puteți accesa această pagină.",
"group": "Grup:",
"group-user": "Utilizatori",
"group-autoconfirmed": "Utilizatori autoconfirmați",
"right-siteadmin": "Blochează și deblochează baza de date",
"right-override-export-depth": "Exportă inclusiv paginile legate până la o adâncime de 5",
"right-sendemail": "Trimite e-mail altor utilizatori",
- "right-passwordreset": "Vizualizează e-mailurile de reinițializare a parolelor",
"right-managechangetags": "Creează și (dez)activează [[Special:Tags|etichete]]",
"right-applychangetags": "Aplică [[Special:Tags|etichete]] asociate modificărilor unui utilizator",
"right-changetags": "Adaugă și înlătură [[Special:Tags|etichete]] arbitrare din versiuni și intrări de jurnal individuale",
"grant-oversight": "Ascunde utilizatori și suprimă versiuni",
"grant-patrol": "Patrulează schimbările paginilor",
"grant-basic": "Drepturi de bază",
+ "grant-viewmywatchlist": "Vezi lista de pagini urmărite",
"newuserlogpage": "Jurnal utilizatori noi",
"newuserlogpagetext": "Acesta este jurnalul creărilor conturilor de utilizator.",
"rightslog": "Jurnal permisiuni de utilizator",
"tog-hidepatrolled": "Скрывать патрулированные правки в списке свежих правок",
"tog-newpageshidepatrolled": "Скрывать отпатрулированные страницы в списке новых страниц",
"tog-hidecategorization": "Скрывать категоризацию страниц",
- "tog-extendwatchlist": "Расширенный список наблюдения, включающий все изменения, а не только последние",
+ "tog-extendwatchlist": "Расширенный список наблюдения, включающий все изменения, а не только последние <small>(они могут быть сгруппированы настройкой на вкладке «[[Служебная:Настройки#mw-prefsection-rc|Свежие правки]]»)</small>",
"tog-usenewrc": "Группировать изменения в свежих правках и списке наблюдения",
"tog-numberheadings": "Автоматически нумеровать заголовки",
"tog-showtoolbar": "Показывать панель инструментов при редактировании",
"emailccsubject": "Эн суругуҥ куоппуйата $1: $2",
"emailsent": "Сурук барда",
"emailsenttext": "Эн суругуҥ ыытылынна.",
- "emailuserfooter": "Бу сурук $2 кыттааччыга $1 кыттааччыттан «Сурукта ыыт» диэн тэрил көмөтүнэн {{SITENAME}} ситим-сиртэн ыытыллыбыт.",
+ "emailuserfooter": "Бу сурук {{GENDER:$2|$2}} кыттааччыга {{GENDER:$1|$1}} кыттааччыттан «Сурукта ыыт» (\"{{int:emailuser}}\") диэн тэрил көмөтүнэн {{SITENAME}} ситим-сиртэн ыытыллыбыт. {{GENDER:$2|Эн}} электрон аадырыһыҥ ыыппыт {{GENDER:$1|киһигэр}} көстүөҕэ.",
"usermessage-summary": "Тиһилик биллэриитин хааллар.",
"usermessage-editor": "Тиһилик биллэрээччитэ",
"watchlist": "Кэтэбилим тиһигэ",
"prefs-diffs": "تفاوت",
"prefs-help-prefershttps": "هيءَ ترجيح توهان جي ايند داخل ٿيڻ تي عمل ۾ ايندي.",
"userrights": "يُوزر حقن جو بندوبست",
- "userrights-lookup-user": "يوزر گروپَ سنڀاليو",
+ "userrights-lookup-user": "ڪو يوزر چونڊيو",
"userrights-user-editname": "يُوزرنانءُ ڄاڻايو:",
- "editusergroup": "{{GENDER:$1|يوزر}} گروھ ترميميو",
+ "editusergroup": "يوزر گروھ اتاريو",
"userrights-editusergroup": "يوزر گروپَ سنواريو",
"saveusergroups": "{{GENDER:$1|يوزر}} گروھ سانڍيو",
"userrights-groupsmember": "برڪن:",
"unprotectthispage": "Та бамлэсь утемзэ воштыны",
"newpage": "Выль бам",
"talkpage": "Та бам сярысь вераськыны",
- "talkpagelinktext": "Ð\92ераськон",
+ "talkpagelinktext": "вераськон",
"specialpage": "Ваньмыз панель",
"personaltools": "Нимаз тӥрлыке",
"articlepage": "Статьяез учкыны",
"prefs-editing": "Тупатон",
"yourlanguage": "Интерфейслэн кылыз:",
"prefs-preview": "Бамез эскерон",
- "editusergroup": "Ð\93Ñ\80Ñ\83ппае {{GENDER:$1|пÑ\8bÑ\80иÑ\81Ñ\8c}} мÑ\83кеÑ\82",
+ "editusergroup": "Ð\92икиавÑ\82оÑ\80лÑ\8dÑ\81Ñ\8c гÑ\80Ñ\83ппаоÑ\81Ñ\81Ñ\8d возÑ\8cмаÑ\82Ñ\8bнÑ\8b",
"group-autoconfirmed": "Автоподтвержденный пыриськисьёс",
"group-bot": "Боты",
"group-sysop": "Администраторъёс",
"emailccsubject": "您发送给$1的消息的副本:$2",
"emailsent": "电子邮件已发送",
"emailsenttext": "您的电子邮件已经发出。",
- "emailuserfooter": "本电子邮件是通过{{SITENAME}}的“{{int:emailuser}}”功能被$1{{GENDER:$1|发送}}至{{GENDER:$2|$2}}的。",
+ "emailuserfooter": "本电子邮件是通过{{SITENAME}}的“{{int:emailuser}}”功能被$1{{GENDER:$1|发送}}至{{GENDER:$2|$2}}的。{{GENDER:$2|您}}的电子邮件将直接发送至{{GENDER:$1|原始发送者}},并向{{GENDER:$1|其}}显示{{GENDER:$2|您}}的电子邮件地址。",
"usermessage-summary": "留下系统消息。",
"usermessage-editor": "系统信息编辑器",
"watchlist": "监视列表",
/**
* The dependency-injected database to use.
*
- * @var DatabaseBase|null
+ * @var IDatabase|null
*
* @see self::setDB
*/
* @todo Fixme: the --server parameter is currently not respected, as it
* doesn't seem terribly easy to ask the load balancer for a particular
* connection by name.
- * @return DatabaseBase
+ * @return IDatabase
*/
function backupDb() {
if ( $this->forcedDb !== null ) {
* Force the dump to use the provided database connection for database
* operations, wherever possible.
*
- * @param DatabaseBase|null $db (Optional) the database connection to use. If null, resort to
+ * @param IDatabase|null $db (Optional) the database connection to use. If null, resort to
* use the globally provided ways to get database connections.
*/
function setDB( IDatabase $db = null ) {
require_once __DIR__ . '/Maintenance.php';
-use Composer\Spdx\SpdxLicenses;
-use JsonSchema\Validator;
-
class ValidateRegistrationFile extends Maintenance {
public function __construct() {
parent::__construct();
$this->addArg( 'path', 'Path to extension.json/skin.json file.', true );
}
public function execute() {
- if ( !class_exists( Validator::class ) ) {
- $this->error( 'The JsonSchema library cannot be found, please install it through composer.', 1 );
- } elseif ( !class_exists( SpdxLicenses::class ) ) {
- $this->error(
- 'The spdx-licenses library cannot be found, please install it through composer.', 1
- );
- }
-
+ $validator = new ExtensionJsonValidator( function( $msg ) {
+ $this->error( $msg, 1 );
+ } );
+ $validator->checkDependencies();
$path = $this->getArg( 0 );
- $data = json_decode( file_get_contents( $path ) );
- if ( !is_object( $data ) ) {
- $this->error( "$path is not a valid JSON file.", 1 );
- }
- if ( !isset( $data->manifest_version ) ) {
- $this->output( "Warning: No manifest_version set, assuming 1.\n" );
- // For backwards-compatability assume 1
- $data->manifest_version = 1;
- }
- $version = $data->manifest_version;
- if ( $version !== ExtensionRegistry::MANIFEST_VERSION ) {
- $schemaPath = dirname( __DIR__ ) . "/docs/extension.schema.v$version.json";
- } else {
- $schemaPath = dirname( __DIR__ ) . '/docs/extension.schema.json';
- }
-
- if ( $version < ExtensionRegistry::OLDEST_MANIFEST_VERSION
- || $version > ExtensionRegistry::MANIFEST_VERSION
- ) {
- $this->error( "Error: $path is using a non-supported schema version, it should use "
- . ExtensionRegistry::MANIFEST_VERSION, 1 );
- } elseif ( $version < ExtensionRegistry::MANIFEST_VERSION ) {
- $this->output( "Warning: $path is using a deprecated schema, and should be updated to "
- . ExtensionRegistry::MANIFEST_VERSION . "\n" );
- }
-
- $licenseError = false;
- // Check if it's a string, if not, schema validation will display an error
- if ( isset( $data->{'license-name'} ) && is_string( $data->{'license-name'} ) ) {
- $licenses = new SpdxLicenses();
- $valid = $licenses->validate( $data->{'license-name'} );
- if ( !$valid ) {
- $licenseError = '[license-name] Invalid SPDX license identifier, '
- . 'see <https://spdx.org/licenses/>';
- }
- }
-
- $validator = new Validator;
- $validator->check( $data, (object)[ '$ref' => 'file://' . $schemaPath ] );
- if ( $validator->isValid() && !$licenseError ) {
- $this->output( "$path validates against the version $version schema!\n" );
- } else {
- foreach ( $validator->getErrors() as $error ) {
- $this->output( "[{$error['property']}] {$error['message']}\n" );
- }
- if ( $licenseError ) {
- $this->output( "$licenseError\n" );
- }
- $this->error( "$path does not validate.", 1 );
+ try {
+ $validator->validate( $path );
+ $this->output( "$path validates against the schema!\n" );
+ } catch ( ExtensionJsonValidationError $e ) {
+ $this->error( $e->getMessage(), 1 );
}
}
}
/*!
- * OOjs UI v0.18.1
+ * OOjs UI v0.18.2
* https://www.mediawiki.org/wiki/OOjs_UI
*
* Copyright 2011–2016 OOjs UI Team and other contributors.
* Released under the MIT license
* http://oojs.mit-license.org
*
- * Date: 2016-11-29T22:57:37Z
+ * Date: 2016-12-06T23:32:53Z
*/
( function ( OO ) {
/*!
- * OOjs UI v0.18.1
+ * OOjs UI v0.18.2
* https://www.mediawiki.org/wiki/OOjs_UI
*
* Copyright 2011–2016 OOjs UI Team and other contributors.
* Released under the MIT license
* http://oojs.mit-license.org
*
- * Date: 2016-11-29T22:57:42Z
+ * Date: 2016-12-06T23:32:57Z
*/
.oo-ui-element-hidden {
display: none !important;
display: none;
}
.oo-ui-textInputWidget.oo-ui-iconElement > .oo-ui-iconElement-icon,
-.oo-ui-textInputWidget.oo-ui-indicatorElement > .oo-ui-indicatorElement-indicator {
+.oo-ui-textInputWidget.oo-ui-indicatorElement > .oo-ui-indicatorElement-indicator,
+.oo-ui-textInputWidget > .oo-ui-labelElement-label {
display: block;
position: absolute;
top: 0;
+}
+.oo-ui-textInputWidget.oo-ui-iconElement > .oo-ui-iconElement-icon,
+.oo-ui-textInputWidget.oo-ui-indicatorElement > .oo-ui-indicatorElement-indicator {
height: 100%;
-webkit-touch-callout: none;
-webkit-user-select: none;
-ms-user-select: none;
user-select: none;
}
+.oo-ui-textInputWidget > .oo-ui-iconElement-icon,
+.oo-ui-textInputWidget-labelPosition-before > .oo-ui-labelElement-label {
+ left: 0;
+}
+.oo-ui-textInputWidget > .oo-ui-indicatorElement-indicator,
+.oo-ui-textInputWidget-labelPosition-after > .oo-ui-labelElement-label {
+ right: 0;
+}
.oo-ui-textInputWidget.oo-ui-widget-enabled > .oo-ui-iconElement-icon,
-.oo-ui-textInputWidget.oo-ui-widget-enabled > .oo-ui-indicatorElement-indicator {
+.oo-ui-textInputWidget.oo-ui-widget-enabled > .oo-ui-indicatorElement-indicator,
+.oo-ui-textInputWidget.oo-ui-widget-enabled > .oo-ui-labelElement-label {
cursor: text;
}
.oo-ui-textInputWidget.oo-ui-widget-enabled.oo-ui-textInputWidget-type-search > .oo-ui-indicatorElement-indicator {
cursor: pointer;
}
.oo-ui-textInputWidget.oo-ui-widget-disabled input,
-.oo-ui-textInputWidget.oo-ui-widget-disabled textarea {
- -webkit-touch-callout: none;
- -webkit-user-select: none;
- -moz-user-select: none;
- -ms-user-select: none;
- user-select: none;
-}
+.oo-ui-textInputWidget.oo-ui-widget-disabled textarea,
.oo-ui-textInputWidget.oo-ui-widget-disabled .oo-ui-labelElement-label {
-webkit-touch-callout: none;
-webkit-user-select: none;
-ms-user-select: none;
user-select: none;
}
-.oo-ui-textInputWidget.oo-ui-labelElement > .oo-ui-labelElement-label {
- display: block;
-}
-.oo-ui-textInputWidget > .oo-ui-iconElement-icon,
-.oo-ui-textInputWidget-labelPosition-before > .oo-ui-labelElement-label {
- left: 0;
-}
-.oo-ui-textInputWidget > .oo-ui-indicatorElement-indicator,
-.oo-ui-textInputWidget-labelPosition-after > .oo-ui-labelElement-label {
- right: 0;
-}
-.oo-ui-textInputWidget > .oo-ui-labelElement-label {
- position: absolute;
- top: 0;
-}
.oo-ui-textInputWidget input,
.oo-ui-textInputWidget textarea {
padding: 0.5em;
/*!
- * OOjs UI v0.18.1
+ * OOjs UI v0.18.2
* https://www.mediawiki.org/wiki/OOjs_UI
*
* Copyright 2011–2016 OOjs UI Team and other contributors.
* Released under the MIT license
* http://oojs.mit-license.org
*
- * Date: 2016-11-29T22:57:42Z
+ * Date: 2016-12-06T23:32:57Z
*/
.oo-ui-element-hidden {
display: none !important;
.oo-ui-buttonElement-frameless.oo-ui-widget-enabled.oo-ui-buttonElement-pressed > input.oo-ui-buttonElement-button,
.oo-ui-buttonElement-frameless.oo-ui-widget-enabled.oo-ui-buttonElement-pressed > .oo-ui-buttonElement-button:active {
color: #000;
+ box-shadow: none;
}
.oo-ui-buttonElement-frameless.oo-ui-widget-enabled.oo-ui-flaggedElement-progressive > .oo-ui-buttonElement-button > .oo-ui-labelElement-label {
color: #36c;
color: #447ff5;
}
.oo-ui-buttonElement-frameless.oo-ui-widget-enabled.oo-ui-flaggedElement-progressive > .oo-ui-buttonElement-button:active > .oo-ui-labelElement-label,
+.oo-ui-buttonElement-frameless.oo-ui-widget-enabled.oo-ui-flaggedElement-progressive > .oo-ui-buttonElement-button:active:focus > .oo-ui-labelElement-label,
.oo-ui-buttonElement-frameless.oo-ui-widget-enabled.oo-ui-flaggedElement-progressive.oo-ui-buttonElement-pressed > .oo-ui-buttonElement-button > .oo-ui-labelElement-label {
color: #2a4b8d;
box-shadow: none;
color: #447ff5;
}
.oo-ui-buttonElement-frameless.oo-ui-widget-enabled.oo-ui-flaggedElement-constructive > .oo-ui-buttonElement-button:active > .oo-ui-labelElement-label,
+.oo-ui-buttonElement-frameless.oo-ui-widget-enabled.oo-ui-flaggedElement-constructive > .oo-ui-buttonElement-button:active:focus > .oo-ui-labelElement-label,
.oo-ui-buttonElement-frameless.oo-ui-widget-enabled.oo-ui-flaggedElement-constructive.oo-ui-buttonElement-pressed > .oo-ui-buttonElement-button > .oo-ui-labelElement-label {
color: #2a4b8d;
box-shadow: none;
color: #ff4242;
}
.oo-ui-buttonElement-frameless.oo-ui-widget-enabled.oo-ui-flaggedElement-destructive > .oo-ui-buttonElement-button:active > .oo-ui-labelElement-label,
+.oo-ui-buttonElement-frameless.oo-ui-widget-enabled.oo-ui-flaggedElement-destructive > .oo-ui-buttonElement-button:active:focus > .oo-ui-labelElement-label,
.oo-ui-buttonElement-frameless.oo-ui-widget-enabled.oo-ui-flaggedElement-destructive.oo-ui-buttonElement-pressed > .oo-ui-buttonElement-button > .oo-ui-labelElement-label {
color: #b32424;
box-shadow: none;
box-shadow: inset 0 0 0 1px #36c;
}
.oo-ui-buttonElement-framed.oo-ui-widget-enabled > .oo-ui-buttonElement-button:active,
+.oo-ui-buttonElement-framed.oo-ui-widget-enabled > .oo-ui-buttonElement-button:active:focus,
.oo-ui-buttonElement-framed.oo-ui-widget-enabled.oo-ui-buttonElement-pressed > .oo-ui-buttonElement-button {
background-color: #c8ccd1;
color: #000;
border-color: #72777d;
+ box-shadow: none;
}
.oo-ui-buttonElement-framed.oo-ui-widget-enabled.oo-ui-buttonElement-active > .oo-ui-buttonElement-button {
background-color: #2a4b8d;
}
.oo-ui-checkboxInputWidget {
position: relative;
- line-height: 1.6em;
+ line-height: 1.5625em;
white-space: nowrap;
}
.oo-ui-checkboxInputWidget * {
.oo-ui-checkboxInputWidget [type='checkbox'] {
position: relative;
max-width: none;
- width: 1.6em;
- height: 1.6em;
+ width: 1.5625em;
+ height: 1.5625em;
margin: 0;
opacity: 0;
z-index: 1;
box-sizing: border-box;
position: absolute;
left: 0;
- width: 1.6em;
- height: 1.6em;
+ width: 1.5625em;
+ height: 1.5625em;
border: 1px solid #72777d;
border-radius: 2px;
}
}
.oo-ui-radioInputWidget {
position: relative;
- line-height: 1.6em;
+ line-height: 1.5625em;
white-space: nowrap;
}
.oo-ui-radioInputWidget * {
.oo-ui-radioInputWidget [type='radio'] {
position: relative;
max-width: none;
- width: 1.6em;
- height: 1.6em;
+ width: 1.5625em;
+ height: 1.5625em;
margin: 0;
opacity: 0;
z-index: 1;
-webkit-box-sizing: border-box;
-moz-box-sizing: border-box;
box-sizing: border-box;
- width: 1.6em;
- height: 1.6em;
+ width: 1.5625em;
+ height: 1.5625em;
border: 1px solid #72777d;
border-radius: 100%;
}
border-radius: 100%;
}
.oo-ui-radioInputWidget [type='radio']:checked + span {
- border-width: 0.4em;
+ border-width: 0.390625em;
}
.oo-ui-radioInputWidget [type='radio']:checked:hover + span,
.oo-ui-radioInputWidget [type='radio']:checked:focus:hover + span {
- border-width: 0.4em;
+ border-width: 0.390625em;
}
.oo-ui-radioInputWidget [type='radio']:disabled + span {
background-color: #c8ccd1;
display: none;
}
.oo-ui-textInputWidget.oo-ui-iconElement > .oo-ui-iconElement-icon,
-.oo-ui-textInputWidget.oo-ui-indicatorElement > .oo-ui-indicatorElement-indicator {
+.oo-ui-textInputWidget.oo-ui-indicatorElement > .oo-ui-indicatorElement-indicator,
+.oo-ui-textInputWidget > .oo-ui-labelElement-label {
display: block;
position: absolute;
top: 0;
+}
+.oo-ui-textInputWidget.oo-ui-iconElement > .oo-ui-iconElement-icon,
+.oo-ui-textInputWidget.oo-ui-indicatorElement > .oo-ui-indicatorElement-indicator {
height: 100%;
-webkit-touch-callout: none;
-webkit-user-select: none;
-ms-user-select: none;
user-select: none;
}
+.oo-ui-textInputWidget > .oo-ui-iconElement-icon,
+.oo-ui-textInputWidget-labelPosition-before > .oo-ui-labelElement-label {
+ left: 0;
+}
+.oo-ui-textInputWidget > .oo-ui-indicatorElement-indicator,
+.oo-ui-textInputWidget-labelPosition-after > .oo-ui-labelElement-label {
+ right: 0;
+}
.oo-ui-textInputWidget.oo-ui-widget-enabled > .oo-ui-iconElement-icon,
-.oo-ui-textInputWidget.oo-ui-widget-enabled > .oo-ui-indicatorElement-indicator {
+.oo-ui-textInputWidget.oo-ui-widget-enabled > .oo-ui-indicatorElement-indicator,
+.oo-ui-textInputWidget.oo-ui-widget-enabled > .oo-ui-labelElement-label {
cursor: text;
}
.oo-ui-textInputWidget.oo-ui-widget-enabled.oo-ui-textInputWidget-type-search > .oo-ui-indicatorElement-indicator {
cursor: pointer;
}
.oo-ui-textInputWidget.oo-ui-widget-disabled input,
-.oo-ui-textInputWidget.oo-ui-widget-disabled textarea {
- -webkit-touch-callout: none;
- -webkit-user-select: none;
- -moz-user-select: none;
- -ms-user-select: none;
- user-select: none;
-}
+.oo-ui-textInputWidget.oo-ui-widget-disabled textarea,
.oo-ui-textInputWidget.oo-ui-widget-disabled .oo-ui-labelElement-label {
-webkit-touch-callout: none;
-webkit-user-select: none;
-ms-user-select: none;
user-select: none;
}
-.oo-ui-textInputWidget.oo-ui-labelElement > .oo-ui-labelElement-label {
- display: block;
-}
-.oo-ui-textInputWidget > .oo-ui-iconElement-icon,
-.oo-ui-textInputWidget-labelPosition-before > .oo-ui-labelElement-label {
- left: 0;
-}
-.oo-ui-textInputWidget > .oo-ui-indicatorElement-indicator,
-.oo-ui-textInputWidget-labelPosition-after > .oo-ui-labelElement-label {
- right: 0;
-}
-.oo-ui-textInputWidget > .oo-ui-labelElement-label {
- position: absolute;
- top: 0;
-}
.oo-ui-textInputWidget input,
.oo-ui-textInputWidget textarea {
- margin: 0;
font-size: inherit;
font-family: inherit;
background-color: #fff;
color: #000;
border: 1px solid #a2a9b1;
border-radius: 2px;
- padding: 0.625em 0.546875em 0.546875em;
}
.oo-ui-textInputWidget input {
+ padding: 0.625em 0.546875em 0.546875em;
line-height: 1.172em;
}
.oo-ui-textInputWidget textarea {
- line-height: 1.275;
+ padding: 0.46875em 0.546875em 0.546875em;
+ line-height: 1.4;
}
.oo-ui-textInputWidget .oo-ui-pendingElement-pending {
background-color: transparent;
border-color: #f00;
box-shadow: inset 0 0 0 0.1em #f00;
}
-.oo-ui-textInputWidget.oo-ui-widget-disabled input,
-.oo-ui-textInputWidget.oo-ui-widget-disabled textarea {
- background-color: #eaecf0;
- color: #72777d;
- text-shadow: 0 1px 1px #fff;
- border-color: #c8ccd1;
-}
-.oo-ui-textInputWidget.oo-ui-widget-disabled .oo-ui-iconElement-icon,
-.oo-ui-textInputWidget.oo-ui-widget-disabled .oo-ui-indicatorElement-indicator {
- opacity: 0.51;
-}
-.oo-ui-textInputWidget.oo-ui-widget-disabled .oo-ui-labelElement-label {
- color: #72777d;
- text-shadow: 0 1px 1px #fff;
-}
.oo-ui-textInputWidget.oo-ui-iconElement input,
.oo-ui-textInputWidget.oo-ui-iconElement textarea {
- padding-left: 2.875em;
+ padding-left: 2.65625em;
}
.oo-ui-textInputWidget.oo-ui-iconElement .oo-ui-iconElement-icon {
- left: 0;
- height: 100%;
- max-height: 2.375em;
- margin-left: 0.5em;
- background-position: right center;
+ max-height: 2.5em;
+ left: 0.46875em;
}
.oo-ui-textInputWidget.oo-ui-indicatorElement input,
.oo-ui-textInputWidget.oo-ui-indicatorElement textarea {
padding-right: 2.4875em;
}
.oo-ui-textInputWidget.oo-ui-indicatorElement .oo-ui-indicatorElement-indicator {
- height: 100%;
- max-height: 2.375em;
- margin: 0 0.775em;
+ max-height: 2.5em;
+ right: 0.625em;
}
.oo-ui-textInputWidget > .oo-ui-labelElement-label {
color: #72777d;
- padding: 0.4em;
- line-height: 1.5;
+ right: 0.625em;
+ border: 1px solid transparent;
+ border-width: 1px 0;
+ padding: 0.625em 0 0.546875em;
+ line-height: 1.172em;
}
.oo-ui-textInputWidget-labelPosition-after.oo-ui-indicatorElement > .oo-ui-labelElement-label {
- margin-right: 2.0875em;
+ right: 2.1875em;
}
.oo-ui-textInputWidget-labelPosition-before.oo-ui-iconElement > .oo-ui-labelElement-label {
- margin-left: 2.475em;
+ left: 2.65625em;
+}
+.oo-ui-textInputWidget.oo-ui-widget-disabled input,
+.oo-ui-textInputWidget.oo-ui-widget-disabled textarea {
+ background-color: #eaecf0;
+ color: #72777d;
+ text-shadow: 0 1px 1px #fff;
+ border-color: #c8ccd1;
+}
+.oo-ui-textInputWidget.oo-ui-widget-disabled .oo-ui-iconElement-icon,
+.oo-ui-textInputWidget.oo-ui-widget-disabled .oo-ui-indicatorElement-indicator {
+ opacity: 0.51;
+}
+.oo-ui-textInputWidget.oo-ui-widget-disabled .oo-ui-labelElement-label {
+ color: #72777d;
+ text-shadow: 0 1px 1px #fff;
}
.oo-ui-menuSelectWidget {
position: absolute;
/*!
- * OOjs UI v0.18.1
+ * OOjs UI v0.18.2
* https://www.mediawiki.org/wiki/OOjs_UI
*
* Copyright 2011–2016 OOjs UI Team and other contributors.
* Released under the MIT license
* http://oojs.mit-license.org
*
- * Date: 2016-11-29T22:57:37Z
+ * Date: 2016-12-06T23:32:53Z
*/
( function ( OO ) {
// Mixin constructors
OO.ui.mixin.IconElement.call( this, config );
- OO.ui.mixin.LabelElement.call( this, $.extend( {}, config, { $label: $( '<legend>' ) } ) );
+ OO.ui.mixin.LabelElement.call( this, $.extend( {}, config, { $label: $( '<div>' ) } ) );
OO.ui.mixin.GroupElement.call( this, config );
if ( config.help ) {
/*!
- * OOjs UI v0.18.1
+ * OOjs UI v0.18.2
* https://www.mediawiki.org/wiki/OOjs_UI
*
* Copyright 2011–2016 OOjs UI Team and other contributors.
* Released under the MIT license
* http://oojs.mit-license.org
*
- * Date: 2016-11-29T22:57:37Z
+ * Date: 2016-12-06T23:32:53Z
*/
( function ( OO ) {
/*!
- * OOjs UI v0.18.1
+ * OOjs UI v0.18.2
* https://www.mediawiki.org/wiki/OOjs_UI
*
* Copyright 2011–2016 OOjs UI Team and other contributors.
* Released under the MIT license
* http://oojs.mit-license.org
*
- * Date: 2016-11-29T22:57:42Z
+ * Date: 2016-12-06T23:32:57Z
*/
.oo-ui-popupTool .oo-ui-popupWidget-popup,
.oo-ui-popupTool .oo-ui-popupWidget-anchor {
/*!
- * OOjs UI v0.18.1
+ * OOjs UI v0.18.2
* https://www.mediawiki.org/wiki/OOjs_UI
*
* Copyright 2011–2016 OOjs UI Team and other contributors.
* Released under the MIT license
* http://oojs.mit-license.org
*
- * Date: 2016-11-29T22:57:42Z
+ * Date: 2016-12-06T23:32:57Z
*/
.oo-ui-tool.oo-ui-widget-enabled {
-webkit-transition: background-color 100ms;
/*!
- * OOjs UI v0.18.1
+ * OOjs UI v0.18.2
* https://www.mediawiki.org/wiki/OOjs_UI
*
* Copyright 2011–2016 OOjs UI Team and other contributors.
* Released under the MIT license
* http://oojs.mit-license.org
*
- * Date: 2016-11-29T22:57:37Z
+ * Date: 2016-12-06T23:32:53Z
*/
( function ( OO ) {
/*!
- * OOjs UI v0.18.1
+ * OOjs UI v0.18.2
* https://www.mediawiki.org/wiki/OOjs_UI
*
* Copyright 2011–2016 OOjs UI Team and other contributors.
* Released under the MIT license
* http://oojs.mit-license.org
*
- * Date: 2016-11-29T22:57:42Z
+ * Date: 2016-12-06T23:32:57Z
*/
.oo-ui-draggableElement-handle,
.oo-ui-draggableElement-handle.oo-ui-widget {
/*!
- * OOjs UI v0.18.1
+ * OOjs UI v0.18.2
* https://www.mediawiki.org/wiki/OOjs_UI
*
* Copyright 2011–2016 OOjs UI Team and other contributors.
* Released under the MIT license
* http://oojs.mit-license.org
*
- * Date: 2016-11-29T22:57:42Z
+ * Date: 2016-12-06T23:32:57Z
*/
.oo-ui-draggableElement-handle,
.oo-ui-draggableElement-handle.oo-ui-widget {
/*!
- * OOjs UI v0.18.1
+ * OOjs UI v0.18.2
* https://www.mediawiki.org/wiki/OOjs_UI
*
* Copyright 2011–2016 OOjs UI Team and other contributors.
* Released under the MIT license
* http://oojs.mit-license.org
*
- * Date: 2016-11-29T22:57:37Z
+ * Date: 2016-12-06T23:32:53Z
*/
( function ( OO ) {
/*!
- * OOjs UI v0.18.1
+ * OOjs UI v0.18.2
* https://www.mediawiki.org/wiki/OOjs_UI
*
* Copyright 2011–2016 OOjs UI Team and other contributors.
* Released under the MIT license
* http://oojs.mit-license.org
*
- * Date: 2016-11-29T22:57:42Z
+ * Date: 2016-12-06T23:32:57Z
*/
.oo-ui-actionWidget.oo-ui-pendingElement-pending {
background-image: /* @embed */ url(themes/apex/images/textures/pending.gif);
/*!
- * OOjs UI v0.18.1
+ * OOjs UI v0.18.2
* https://www.mediawiki.org/wiki/OOjs_UI
*
* Copyright 2011–2016 OOjs UI Team and other contributors.
* Released under the MIT license
* http://oojs.mit-license.org
*
- * Date: 2016-11-29T22:57:42Z
+ * Date: 2016-12-06T23:32:57Z
*/
.oo-ui-window {
background: transparent;
/*!
- * OOjs UI v0.18.1
+ * OOjs UI v0.18.2
* https://www.mediawiki.org/wiki/OOjs_UI
*
* Copyright 2011–2016 OOjs UI Team and other contributors.
* Released under the MIT license
* http://oojs.mit-license.org
*
- * Date: 2016-11-29T22:57:37Z
+ * Date: 2016-12-06T23:32:53Z
*/
( function ( OO ) {
<?xml version="1.0" encoding="utf-8"?>
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><style>* { fill: #fff }</style>
- <path d="M19 12c0-3.9-3.1-7-7-7s-7 3.1-7 7c0 1.4.4 2.6 1.1 3.7L12 23l5.9-7.3c.7-1.1 1.1-2.3 1.1-3.7zm-7 4c-2.2 0-4-1.8-4-4s1.8-4 4-4 4 1.8 4 4-1.8 4-4 4z"/>
+ <path d="M19 12c0-3.9-3.1-7-7-7s-7 3.1-7 7c0 1.4.4 2.6 1.1 3.7L12 23l5.9-7.3c.7-1.1 1.1-2.3 1.1-3.7zm-7 3c-1.6 0-3-1.4-3-3s1.4-3 3-3 3 1.4 3 3-1.4 3-3 3z"/>
</svg>
<?xml version="1.0" encoding="utf-8"?>
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><style>* { fill: #36c }</style>
- <path d="M19 12c0-3.9-3.1-7-7-7s-7 3.1-7 7c0 1.4.4 2.6 1.1 3.7L12 23l5.9-7.3c.7-1.1 1.1-2.3 1.1-3.7zm-7 4c-2.2 0-4-1.8-4-4s1.8-4 4-4 4 1.8 4 4-1.8 4-4 4z"/>
+ <path d="M19 12c0-3.9-3.1-7-7-7s-7 3.1-7 7c0 1.4.4 2.6 1.1 3.7L12 23l5.9-7.3c.7-1.1 1.1-2.3 1.1-3.7zm-7 3c-1.6 0-3-1.4-3-3s1.4-3 3-3 3 1.4 3 3-1.4 3-3 3z"/>
</svg>
<?xml version="1.0" encoding="utf-8"?>
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24">
- <path d="M19 12c0-3.9-3.1-7-7-7s-7 3.1-7 7c0 1.4.4 2.6 1.1 3.7L12 23l5.9-7.3c.7-1.1 1.1-2.3 1.1-3.7zm-7 4c-2.2 0-4-1.8-4-4s1.8-4 4-4 4 1.8 4 4-1.8 4-4 4z"/>
+ <path d="M19 12c0-3.9-3.1-7-7-7s-7 3.1-7 7c0 1.4.4 2.6 1.1 3.7L12 23l5.9-7.3c.7-1.1 1.1-2.3 1.1-3.7zm-7 3c-1.6 0-3-1.4-3-3s1.4-3 3-3 3 1.4 3 3-1.4 3-3 3z"/>
</svg>
<?xml version="1.0" encoding="utf-8"?>
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><style>* { fill: #fff }</style>
- <path d="M24 4h-4V0h-2v4h-4v2h4v4h2V6h4z"/>
- <path d="M18 11h-1V7.1l-.1-.1H13V5.1c-.3-.1-.7-.1-1-.1-3.9 0-7 3.1-7 7 0 1.4.4 2.6 1.1 3.7L12 23l5.9-7.3c.7-1.1 1.1-2.3 1.1-3.7 0-.3 0-.7-.1-1H18zm-6 5c-2.2 0-4-1.8-4-4s1.8-4 4-4 4 1.8 4 4-1.8 4-4 4z"/>
+ <path d="M24 4h-4V0h-2v4h-4v2h4v4h2V6h4V4z"/>
+ <path d="M18.9 11c.1.3.1.7.1 1 0 1.4-.4 2.6-1.1 3.7L12 23l-5.9-7.3C5.4 14.6 5 13.4 5 12c0-3.9 3.1-7 7-7 .3 0 .7 0 1 .1V7h3.9l.1.1V11h1.9zM15 12c0-1.6-1.4-3-3-3s-3 1.4-3 3 1.4 3 3 3 3-1.4 3-3z"/>
</svg>
+
<?xml version="1.0" encoding="utf-8"?>
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><style>* { fill: #36c }</style>
- <path d="M24 4h-4V0h-2v4h-4v2h4v4h2V6h4z"/>
- <path d="M18 11h-1V7.1l-.1-.1H13V5.1c-.3-.1-.7-.1-1-.1-3.9 0-7 3.1-7 7 0 1.4.4 2.6 1.1 3.7L12 23l5.9-7.3c.7-1.1 1.1-2.3 1.1-3.7 0-.3 0-.7-.1-1H18zm-6 5c-2.2 0-4-1.8-4-4s1.8-4 4-4 4 1.8 4 4-1.8 4-4 4z"/>
+ <path d="M24 4h-4V0h-2v4h-4v2h4v4h2V6h4V4z"/>
+ <path d="M18.9 11c.1.3.1.7.1 1 0 1.4-.4 2.6-1.1 3.7L12 23l-5.9-7.3C5.4 14.6 5 13.4 5 12c0-3.9 3.1-7 7-7 .3 0 .7 0 1 .1V7h3.9l.1.1V11h1.9zM15 12c0-1.6-1.4-3-3-3s-3 1.4-3 3 1.4 3 3 3 3-1.4 3-3z"/>
</svg>
+
<?xml version="1.0" encoding="utf-8"?>
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24">
- <path d="M24 4h-4V0h-2v4h-4v2h4v4h2V6h4z"/>
- <path d="M18 11h-1V7.1l-.1-.1H13V5.1c-.3-.1-.7-.1-1-.1-3.9 0-7 3.1-7 7 0 1.4.4 2.6 1.1 3.7L12 23l5.9-7.3c.7-1.1 1.1-2.3 1.1-3.7 0-.3 0-.7-.1-1H18zm-6 5c-2.2 0-4-1.8-4-4s1.8-4 4-4 4 1.8 4 4-1.8 4-4 4z"/>
+ <path d="M24 4h-4V0h-2v4h-4v2h4v4h2V6h4V4z"/>
+ <path d="M18.9 11c.1.3.1.7.1 1 0 1.4-.4 2.6-1.1 3.7L12 23l-5.9-7.3C5.4 14.6 5 13.4 5 12c0-3.9 3.1-7 7-7 .3 0 .7 0 1 .1V7h3.9l.1.1V11h1.9zM15 12c0-1.6-1.4-3-3-3s-3 1.4-3 3 1.4 3 3 3 3-1.4 3-3z"/>
</svg>
+
<?xml version="1.0" encoding="utf-8"?>
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><style>* { fill: #fff }</style>
- <path d="M0 4h4V0h2v4h4v2H6v4H4V6H0z"/>
- <path d="M6 11h1V7.1l.1-.1H11V5.1c.3-.1.7-.1 1-.1 3.9 0 7 3.1 7 7 0 1.4-.4 2.6-1.1 3.7L12 23l-5.9-7.3C5.4 14.6 5 13.4 5 12c0-.3 0-.7.1-1H6zm6 5c2.2 0 4-1.8 4-4s-1.8-4-4-4-4 1.8-4 4 1.8 4 4 4z"/>
+ <path d="M0 4h4V0h2v4h4v2H6v4H4V6H0"/>
+ <path d="M6 11h1V7.1l.1-.1H11V5.1c.3-.1.7-.1 1-.1 3.9 0 7 3.1 7 7 0 1.4-.4 2.6-1.1 3.7L12 23l-5.9-7.3C5.4 14.6 5 13.4 5 12c0-.3 0-.7.1-1H6zm6 4c1.65 0 3-1.35 3-3s-1.35-3-3-3-3 1.35-3 3 1.35 3 3 3z"/>
</svg>
<?xml version="1.0" encoding="utf-8"?>
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><style>* { fill: #36c }</style>
- <path d="M0 4h4V0h2v4h4v2H6v4H4V6H0z"/>
- <path d="M6 11h1V7.1l.1-.1H11V5.1c.3-.1.7-.1 1-.1 3.9 0 7 3.1 7 7 0 1.4-.4 2.6-1.1 3.7L12 23l-5.9-7.3C5.4 14.6 5 13.4 5 12c0-.3 0-.7.1-1H6zm6 5c2.2 0 4-1.8 4-4s-1.8-4-4-4-4 1.8-4 4 1.8 4 4 4z"/>
+ <path d="M0 4h4V0h2v4h4v2H6v4H4V6H0"/>
+ <path d="M6 11h1V7.1l.1-.1H11V5.1c.3-.1.7-.1 1-.1 3.9 0 7 3.1 7 7 0 1.4-.4 2.6-1.1 3.7L12 23l-5.9-7.3C5.4 14.6 5 13.4 5 12c0-.3 0-.7.1-1H6zm6 4c1.65 0 3-1.35 3-3s-1.35-3-3-3-3 1.35-3 3 1.35 3 3 3z"/>
</svg>
<?xml version="1.0" encoding="utf-8"?>
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24">
- <path d="M0 4h4V0h2v4h4v2H6v4H4V6H0z"/>
- <path d="M6 11h1V7.1l.1-.1H11V5.1c.3-.1.7-.1 1-.1 3.9 0 7 3.1 7 7 0 1.4-.4 2.6-1.1 3.7L12 23l-5.9-7.3C5.4 14.6 5 13.4 5 12c0-.3 0-.7.1-1H6zm6 5c2.2 0 4-1.8 4-4s-1.8-4-4-4-4 1.8-4 4 1.8 4 4 4z"/>
+ <path d="M0 4h4V0h2v4h4v2H6v4H4V6H0"/>
+ <path d="M6 11h1V7.1l.1-.1H11V5.1c.3-.1.7-.1 1-.1 3.9 0 7 3.1 7 7 0 1.4-.4 2.6-1.1 3.7L12 23l-5.9-7.3C5.4 14.6 5 13.4 5 12c0-.3 0-.7.1-1H6zm6 4c1.65 0 3-1.35 3-3s-1.35-3-3-3-3 1.35-3 3 1.35 3 3 3z"/>
</svg>
$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 ) {
const BLANK_VERSION = '09p30q0';
/**
- * @param string $lang
- * @param string $dir
+ * @param array|string $options Language code or options array
+ * - string 'lang' Language code
+ * - string 'dir' Language direction (ltr or rtl)
* @return ResourceLoaderContext
*/
- protected function getResourceLoaderContext( $lang = 'en', $dir = 'ltr' ) {
+ protected function getResourceLoaderContext( $options = [] ) {
+ if ( is_string( $options ) ) {
+ // Back-compat for extension tests
+ $options = [ 'lang' => $options ];
+ }
+ $options += [
+ 'lang' => 'en',
+ 'dir' => 'ltr',
+ ];
$resourceLoader = new ResourceLoader();
$request = new FauxRequest( [
- 'lang' => $lang,
+ 'lang' => $options['lang'],
'modules' => 'startup',
'only' => 'scripts',
'skin' => 'vector',
->setConstructorArgs( [ $resourceLoader, $request ] )
->setMethods( [ 'getDirection' ] )
->getMock();
- $ctx->method( 'getDirection' )->willReturn( $dir );
+ $ctx->method( 'getDirection' )->willReturn( $options['dir'] );
return $ctx;
}
->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 ]
}
/**
- * @expectedException UsageException
+ * @expectedException ApiUsageException
* @covers ApiBase::requireOnlyOneParameter
*/
public function testRequireOnlyOneParameterZero() {
}
/**
- * @expectedException UsageException
+ * @expectedException ApiUsageException
* @covers ApiBase::requireOnlyOneParameter
*/
public function testRequireOnlyOneParameterTrue() {
$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 {
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 = '';
'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' => [
];
}
+ 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 ) );
+ }
+
}
}
/**
- * @expectedException UsageException
- * @expectedExceptionMessage The token parameter must be set
+ * @expectedException ApiUsageException
+ * @expectedExceptionMessage The "token" parameter must be set
*/
public function testBlockingActionWithNoToken() {
$this->doApiRequest(
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'
);
}
'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' ) );
}
}
], 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' ) );
}
}
], 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' ) );
}
}
public function testCheckDirectApiEditingDisallowed_forNonTextContent() {
$this->setExpectedException(
- 'UsageException',
+ 'ApiUsageException',
'Direct editing via API is not supported for content model ' .
'testing used by Dummy:ApiEditPageTest_nonTextPageEdit'
);
*/
class ApiErrorFormatterTest extends MediaWikiLangTestCase {
+ /**
+ * @covers ApiErrorFormatter
+ */
+ public function testErrorFormatterBasics() {
+ $result = new ApiResult( 8388608 );
+ $formatter = new ApiErrorFormatter( $result, Language::factory( 'de' ), 'wikitext', false );
+ $this->assertSame( 'de', $formatter->getLanguage()->getCode() );
+
+ $formatter->addMessagesFromStatus( null, Status::newGood() );
+ $this->assertSame(
+ [ ApiResult::META_TYPE => 'assoc' ],
+ $result->getResultData()
+ );
+
+ $this->assertSame( [], $formatter->arrayFromStatus( Status::newGood() ) );
+
+ $wrappedFormatter = TestingAccessWrapper::newFromObject( $formatter );
+ $this->assertSame(
+ 'Blah "kbd" <X> 😊',
+ $wrappedFormatter->stripMarkup( 'Blah <kbd>kbd</kbd> <b><X></b> 😊' ),
+ 'stripMarkup'
+ );
+ }
+
/**
* @covers ApiErrorFormatter
* @dataProvider provideErrorFormatter
$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' );
$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' );
$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',
],
],
],
$result = new ApiResult( 8388608 );
$formatter = new ApiErrorFormatter_BackCompat( $result );
+ $this->assertSame( 'en', $formatter->getLanguage()->getCode() );
+
+ $this->assertSame( [], $formatter->arrayFromStatus( Status::newGood() ) );
+
$formatter->addWarning( 'string', 'mainpage' );
+ $formatter->addWarning( 'raw',
+ new RawMessage( 'Blah <kbd>kbd</kbd> <b><X></b> 😞' )
+ );
$formatter->addError( 'err', 'mainpage' );
$this->assertSame( [
'error' => [
'info' => $mainpagePlain,
],
'warnings' => [
+ 'raw' => [
+ 'warnings' => 'Blah "kbd" <X> 😞',
+ ApiResult::META_CONTENT => 'warnings',
+ ],
'string' => [
'warnings' => $mainpagePlain,
ApiResult::META_CONTENT => 'warnings',
$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',
'overriddenData' => true,
],
'warnings' => [
+ 'unknown' => [
+ 'warnings' => $mainpagePlain,
+ ApiResult::META_CONTENT => 'warnings',
+ ],
'messageWithData' => [
'warnings' => $mainpagePlain,
ApiResult::META_CONTENT => 'warnings',
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' );
$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',
],
$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',
],
'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 ) );
}
}
'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' ) );
}
}
$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(),
+ ]
+ ],
+ ];
+ }
}
);
}
+ /**
+ * @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
* @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 ) );
}
$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() )
}
/**
- * @expectedException UsageException
+ * @expectedException ApiUsageException
*/
public function testNoToken() {
$request = $this->getSampleRequest( [ 'token' => null ] );
$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() {
$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() {
$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() {
'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 );
'options' => 'success',
'warnings' => [
'options' => [
- 'warnings' => "Validation error for 'unknownOption': not a valid preference"
+ 'warnings' => "Validation error for \"unknownOption\": not a valid preference."
]
]
], $response );
'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 )
);
}
}
$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(),
}
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(),
$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(),
}
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(),
abstract class ApiTestCase extends MediaWikiLangTestCase {
protected static $apiUrl;
+ protected static $errorFormatter = null;
+
/**
* @var ApiTestContext
*/
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(
}
/**
- * @expectedException UsageException
+ * @expectedException ApiUsageException
*/
public function testWithNoToken() {
$this->doApiRequest(
$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" );
}
$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() );
try {
list( $result, , ) = $this->doApiRequestWithToken( $params, $session,
self::$users['uploader']->getUser() );
- } catch ( UsageException $e ) {
+ } catch ( ApiUsageException $e ) {
$exception = true;
}
$this->assertTrue( isset( $result['upload'] ) );
$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;
}
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'] ) );
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'] ) );
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'] ) );
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'] ) );
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 );
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 );
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:
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:
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->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() . "'" );
}
}
}
public function __construct() {
}
- public function setWarning( $warning ) {
+ public function getModulePath() {
+ return $this->getModuleName();
+ }
+
+ public function addWarning( $warning, $code = null, $data = null ) {
$this->warnings[] = $warning;
}
public function getModuleName() {
return $this->name;
}
+
+ public function getModulePath() {
+ return 'query+' . $this->getModuleName();
+ }
}
$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'
);
}
[ 'includexmlnamespace' => 1 ] ],
// xslt param
- [ [], '<?xml version="1.0"?><api><warnings><xml xml:space="preserve">Invalid or non-existent stylesheet specified</xml></warnings></api>',
+ [ [], '<?xml version="1.0"?><api><warnings><xml xml:space="preserve">Invalid or non-existent stylesheet specified.</xml></warnings></api>',
[ 'xslt' => 'DoesNotExist' ] ],
[ [], '<?xml version="1.0"?><api><warnings><xml xml:space="preserve">Stylesheet should be in the MediaWiki namespace.</xml></warnings></api>',
[ 'xslt' => 'ApiFormatXmlTest' ] ],
- [ [], '<?xml version="1.0"?><api><warnings><xml xml:space="preserve">Stylesheet should have .xsl extension.</xml></warnings></api>',
+ [ [], '<?xml version="1.0"?><api><warnings><xml xml:space="preserve">Stylesheet should have ".xsl" extension.</xml></warnings></api>',
[ 'xslt' => 'MediaWiki:ApiFormatXmlTest' ] ],
[ [],
'<?xml version="1.0"?><?xml-stylesheet href="' .
$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() {
$modules = self::getModules();
$rl = new ResourceLoaderFileModule( $modules[$name] );
$rl->setName( $name );
- $ctx = $this->getResourceLoaderContext( 'en', 'ltr' );
+ $ctx = $this->getResourceLoaderContext();
$this->assertEquals( $rl->getScript( $ctx ), $expected );
}
] );
$expectedModule->setName( 'testing' );
- $contextLtr = $this->getResourceLoaderContext( 'en', 'ltr' );
- $contextRtl = $this->getResourceLoaderContext( 'he', 'rtl' );
+ $contextLtr = $this->getResourceLoaderContext( [
+ 'lang' => 'en',
+ 'dir' => 'ltr',
+ ] );
+ $contextRtl = $this->getResourceLoaderContext( [
+ 'lang' => 'he',
+ 'dir' => 'rtl',
+ ] );
// Since we want to compare the effect of @noflip+@embed against the effect of just @embed, and
// the @noflip annotations are always preserved, we need to strip them first.
'File has leading BOM'
);
- $contextLtr = $this->getResourceLoaderContext( 'en', 'ltr' );
+ $context = $this->getResourceLoaderContext();
$this->assertEquals(
- $testModule->getStyles( $contextLtr ),
+ $testModule->getStyles( $context ),
[ 'all' => ".efbbbf_bom_char_at_start_of_file {}\n" ],
'Leading BOM removed when concatenating files'
);
static $contexts = [];
$image = $this->getTestImage( $imageName );
- $context = $this->getResourceLoaderContext( $languageCode, $dirMap[$languageCode] );
+ $context = $this->getResourceLoaderContext( [
+ 'lang' => $languageCode,
+ 'dir' => $dirMap[$languageCode],
+ ] );
$this->assertEquals( $image->getPath( $context ), $this->imagesPath . '/' . $path );
}
* @covers ResourceLoaderImage::massageSvgPathdata
*/
public function testGetImageData() {
- $context = $this->getResourceLoaderContext( 'en', 'ltr' );
+ $context = $this->getResourceLoaderContext();
$image = $this->getTestImage( 'remove' );
$data = file_get_contents( $this->imagesPath . '/remove.svg' );
);
$this->assertEquals(
- $expected,
- $queryConditions,
+ self::normalizeCondition( $expected ),
+ self::normalizeCondition( $queryConditions ),
$message
);
}
+ private static function normalizeCondition( $conds ) {
+ return array_map(
+ function ( $k, $v ) {
+ return is_numeric( $k ) ? $v : "$k = $v";
+ },
+ array_keys( $conds ),
+ $conds
+ );
+ }
+
/** return false if condition begin with 'rc_timestamp ' */
private static function filterOutRcTimestampCondition( $var ) {
return ( false === strpos( $var, 'rc_timestamp ' ) );
$this->assertConditions(
[ # expected
'rc_bot' => 0,
- 0 => "rc_type != '6'",
- 1 => "rc_namespace = '0'",
+ "rc_type != '6'",
+ "rc_namespace = '0'",
],
[
'namespace' => NS_MAIN,
$this->assertConditions(
[ # expected
'rc_bot' => 0,
- 0 => "rc_type != '6'",
- 1 => sprintf( "rc_namespace != '%s'", NS_MAIN ),
+ "rc_type != '6'",
+ "rc_namespace != '0'",
],
[
'namespace' => NS_MAIN,
$this->assertConditions(
[ # expected
'rc_bot' => 0,
- 0 => "rc_type != '6'",
- 1 => sprintf( "(rc_namespace = '%s' OR rc_namespace = '%s')", $ns1, $ns2 ),
+ "rc_type != '6'",
+ "(rc_namespace = '$ns1' OR rc_namespace = '$ns2')",
],
[
'namespace' => $ns1,
$this->assertConditions(
[ # expected
'rc_bot' => 0,
- 0 => "rc_type != '6'",
- 1 => sprintf( "(rc_namespace != '%s' AND rc_namespace != '%s')", $ns1, $ns2 ),
+ "rc_type != '6'",
+ "(rc_namespace != '$ns1' AND rc_namespace != '$ns2')",
],
[
'namespace' => $ns1,
$this->assertConditions(
[ # expected
'rc_bot' => 0,
- 0 => "rc_user != '{$user->getId()}'",
- 1 => "rc_type != '6'",
+ "rc_user != '{$user->getId()}'",
+ "rc_type != '6'",
],
[
'hidemyself' => 1,
$this->assertConditions(
[ # expected
'rc_bot' => 0,
- 0 => "rc_user_text != '10.11.12.13'",
- 1 => "rc_type != '6'",
+ "rc_user_text != '10.11.12.13'",
+ "rc_type != '6'",
],
[
'hidemyself' => 1,
$this->assertConditions(
[ # expected
'rc_bot' => 0,
- 0 => "rc_user = '{$user->getId()}'",
- 1 => "rc_type != '6'",
+ "rc_user = '{$user->getId()}'",
+ "rc_type != '6'",
],
[
'hidebyothers' => 1,
$this->assertConditions(
[ # expected
'rc_bot' => 0,
- 0 => "rc_user_text = '10.11.12.13'",
- 1 => "rc_type != '6'",
+ "rc_user_text = '10.11.12.13'",
+ "rc_type != '6'",
],
[
'hidebyothers' => 1,
$this->assertConditions(
[ # expected
'rc_bot' => 0,
- 0 => "rc_user != '{$user->getId()}'",
- 1 => "rc_user = '{$user->getId()}'",
- 2 => "rc_type != '6'",
+ "rc_user != '{$user->getId()}'",
+ "rc_user = '{$user->getId()}'",
+ "rc_type != '6'",
],
[
'hidemyself' => 1,
$user
);
}
+
+ public function testRcHidepageedits() {
+ $this->assertConditions(
+ [ # expected
+ 'rc_bot' => 0,
+ "rc_type != '6'",
+ "rc_type != '0'",
+ ],
+ [
+ 'hidepageedits' => 1,
+ ],
+ "rc conditions: hidepageedits=1"
+ );
+ }
+
+ public function testRcHidenewpages() {
+ $this->assertConditions(
+ [ # expected
+ 'rc_bot' => 0,
+ "rc_type != '6'",
+ "rc_type != '1'",
+ ],
+ [
+ 'hidenewpages' => 1,
+ ],
+ "rc conditions: hidenewpages=1"
+ );
+ }
+
+ public function testRcHidelog() {
+ $this->assertConditions(
+ [ # expected
+ 'rc_bot' => 0,
+ "rc_type != '6'",
+ "rc_type != '3'",
+ ],
+ [
+ 'hidelog' => 1,
+ ],
+ "rc conditions: hidelog=1"
+ );
+ }
+
+ public function testRcHidehumans() {
+ $this->assertConditions(
+ [ # expected
+ 'rc_bot' => 1,
+ "rc_type != '6'",
+ ],
+ [
+ 'hidebots' => 0,
+ 'hidehumans' => 1,
+ ],
+ "rc conditions: hidebots=0 hidehumans=1"
+ );
+ }
+
+ public function testRcHidepatrolledDisabledFilter() {
+ $user = $this->getTestUser()->getUser();
+ $this->assertConditions(
+ [ # expected
+ 'rc_bot' => 0,
+ "rc_type != '6'",
+ ],
+ [
+ 'hidepatrolled' => 1,
+ ],
+ "rc conditions: hidepatrolled=1 (user not allowed)",
+ $user
+ );
+ }
+
+ public function testRcHideunpatrolledDisabledFilter() {
+ $user = $this->getTestUser()->getUser();
+ $this->assertConditions(
+ [ # expected
+ 'rc_bot' => 0,
+ "rc_type != '6'",
+ ],
+ [
+ 'hideunpatrolled' => 1,
+ ],
+ "rc conditions: hideunpatrolled=1 (user not allowed)",
+ $user
+ );
+ }
+ public function testRcHidepatrolledFilter() {
+ $user = $this->getTestSysop()->getUser();
+ $this->assertConditions(
+ [ # expected
+ 'rc_bot' => 0,
+ "rc_patrolled = 0",
+ "rc_type != '6'",
+ ],
+ [
+ 'hidepatrolled' => 1,
+ ],
+ "rc conditions: hidepatrolled=1",
+ $user
+ );
+ }
+
+ public function testRcHideunpatrolledFilter() {
+ $user = $this->getTestSysop()->getUser();
+ $this->assertConditions(
+ [ # expected
+ 'rc_bot' => 0,
+ "rc_patrolled = 1",
+ "rc_type != '6'",
+ ],
+ [
+ 'hideunpatrolled' => 1,
+ ],
+ "rc conditions: hideunpatrolled=1",
+ $user
+ );
+ }
+
+ // This is probably going to change when we do auto-fix of
+ // filters combinations that don't make sense but for now
+ // it's the behavior therefore it's the test.
+ public function testRcHidepatrolledHideunpatrolledFilter() {
+ $user = $this->getTestSysop()->getUser();
+ $this->assertConditions(
+ [ # expected
+ 'rc_bot' => 0,
+ "rc_patrolled = 0",
+ "rc_patrolled = 1",
+ "rc_type != '6'",
+ ],
+ [
+ 'hidepatrolled' => 1,
+ 'hideunpatrolled' => 1,
+ ],
+ "rc conditions: hidepatrolled=1 hideunpatrolled=1",
+ $user
+ );
+ }
}
$this->doApiRequest( [
'action' => 'upload',
] );
- } catch ( UsageException $e ) {
+ } catch ( ApiUsageException $e ) {
$exception = true;
$this->assertEquals( "The token parameter must be set", $e->getMessage() );
}
'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() );
'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() );
}
'filename' => 'UploadFromUrlTest.png',
'token' => $token,
], $data );
- } catch ( UsageException $e ) {
+ } catch ( ApiUsageException $e ) {
$exception = true;
$this->assertEquals( "Permission denied", $e->getMessage() );
}
*/
class ExtensionJsonValidationTest extends PHPUnit_Framework_TestCase {
+ /**
+ * @var ExtensionJsonValidator
+ */
+ protected $validator;
+
public function setUp() {
parent::setUp();
- if ( !class_exists( Validator::class ) ) {
- $this->markTestSkipped(
- 'The JsonSchema library cannot be found,' .
- ' please install it through composer to run extension.json validation tests.'
- );
- }
+
+ $this->validator = new ExtensionJsonValidator( [ $this, 'markTestSkipped' ] );
+ $this->validator->checkDependencies();
if ( !ExtensionRegistry::getInstance()->getAllThings() ) {
$this->markTestSkipped(
* @param string $path Path to thing's json file
*/
public function testPassesValidation( $path ) {
- $data = json_decode( file_get_contents( $path ) );
- $this->assertInstanceOf( 'stdClass', $data, "$path is not valid JSON" );
-
- $this->assertObjectHasAttribute( 'manifest_version', $data,
- "$path does not have manifest_version set." );
- $version = $data->manifest_version;
- if ( $version !== ExtensionRegistry::MANIFEST_VERSION ) {
- $schemaPath = __DIR__ . "/../../../docs/extension.schema.v$version.json";
- } else {
- $schemaPath = __DIR__ . '/../../../docs/extension.schema.json';
- }
-
- // Not too old
- $this->assertTrue(
- $version >= ExtensionRegistry::OLDEST_MANIFEST_VERSION,
- "$path is using a non-supported schema version"
- );
- // Not too new
- $this->assertTrue(
- $version <= ExtensionRegistry::MANIFEST_VERSION,
- "$path is using a non-supported schema version"
- );
-
- $licenseError = false;
- if ( class_exists( SpdxLicenses::class ) && isset( $data->{'license-name'} )
- // Check if it's a string, if not, schema validation will display an error
- && is_string( $data->{'license-name'} )
- ) {
- $licenses = new SpdxLicenses();
- $valid = $licenses->validate( $data->{'license-name'} );
- if ( !$valid ) {
- $licenseError = '[license-name] Invalid SPDX license identifier, '
- . 'see <https://spdx.org/licenses/>';
- }
- }
-
- $validator = new Validator;
- $validator->check( $data, (object)[ '$ref' => 'file://' . $schemaPath ] );
- if ( $validator->isValid() && !$licenseError ) {
- // All good.
+ try {
+ $this->validator->validate( $path );
+ // All good
$this->assertTrue( true );
- } else {
- $out = "$path did pass validation.\n";
- foreach ( $validator->getErrors() as $error ) {
- $out .= "[{$error['property']}] {$error['message']}\n";
- }
- if ( $licenseError ) {
- $out .= "$licenseError\n";
- }
- $this->assertTrue( false, $out );
+ } catch ( ExtensionJsonValidationError $e ) {
+ $this->assertEquals( false, $e->getMessage() );
}
}
}
mw.config.set( 'wgUserLanguage', 'en' );
assert.equal( mw.language.convertNumber( 1800 ), '1.800', 'formatting' );
- assert.equal( mw.language.convertNumber( "1.800", true ), '1800', 'unformatting' );
+ assert.equal( mw.language.convertNumber( '1.800', true ), '1800', 'unformatting' );
} );
function grammarTest( langCode, test ) {