* - limit: An integer or the string 'max'. Default lower limit is 0 (but
* see PARAM_MIN), and requires that PARAM_MAX and PARAM_MAX2 be
* specified. Cannot be used with PARAM_ISMULTI.
- * - namespace: An integer representing a MediaWiki namespace.
+ * - namespace: An integer representing a MediaWiki namespace. Forces PARAM_ALL = true to
+ * support easily specifying all namespaces.
* - NULL: Any string.
* - password: Any non-empty string. Input value is private or sensitive.
* <input type="password"> would be an appropriate HTML form field.
*/
const PARAM_SUBMODULE_PARAM_PREFIX = 16;
+ /**
+ * (boolean|string) When PARAM_TYPE has a defined set of values and PARAM_ISMULTI is true,
+ * this allows for an asterisk ('*') to be passed in place of a pipe-separated list of
+ * every possible value. If a string is set, it will be used in place of the asterisk.
+ * @since 1.29
+ */
+ const PARAM_ALL = 17;
+
/**@}*/
+ const ALL_DEFAULT_STRING = '*';
+
/** Fast query, standard limit. */
const LIMIT_BIG1 = 500;
/** Fast query, apihighlimits limit. */
* @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'] ] );
}
}
$dupes = false;
$deprecated = false;
$required = false;
+ $allowAll = false;
} else {
$default = isset( $paramSettings[self::PARAM_DFLT] )
? $paramSettings[self::PARAM_DFLT]
$required = isset( $paramSettings[self::PARAM_REQUIRED] )
? $paramSettings[self::PARAM_REQUIRED]
: false;
+ $allowAll = isset( $paramSettings[self::PARAM_ALL] )
+ ? $paramSettings[self::PARAM_ALL]
+ : false;
// When type is not given, and no choices, the type is the same as $default
if ( !isset( $type ) ) {
// 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}"
);
}
if ( isset( $value ) && $type == 'namespace' ) {
$type = MWNamespace::getValidNamespaces();
+ // By default, namespace parameters allow ALL_DEFAULT_STRING to be used to specify
+ // all namespaces.
+ $allowAll = true;
}
if ( isset( $value ) && $type == 'submodule' ) {
if ( isset( $paramSettings[self::PARAM_SUBMODULE_MAP] ) ) {
// 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' );
}
}
}
}
+ $allSpecifier = ( is_string( $allowAll ) ? $allowAll : self::ALL_DEFAULT_STRING );
+ if ( $allowAll && $multi && is_array( $type ) && in_array( $allSpecifier, $type, true ) ) {
+ ApiBase::dieDebug(
+ __METHOD__,
+ "For param $encParamName, PARAM_ALL collides with a possible value" );
+ }
if ( isset( $value ) && ( $multi || is_array( $type ) ) ) {
$value = $this->parseMultiValue(
$encParamName,
$value,
$multi,
- is_array( $type ) ? $type : null
+ is_array( $type ) ? $type : null,
+ $allowAll ? $allSpecifier : null
);
}
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 ] );
}
/**
* separated by '|'?
* @param string[]|null $allowedValues An array of values to check against. If
* null, all values are accepted.
+ * @param string|null $allSpecifier String to use to specify all allowed values, or null
+ * if this behavior should not be allowed
* @return string|string[] (allowMultiple ? an_array_of_values : a_single_value)
*/
- protected function parseMultiValue( $valueName, $value, $allowMultiple, $allowedValues ) {
+ protected function parseMultiValue( $valueName, $value, $allowMultiple, $allowedValues,
+ $allSpecifier = null
+ ) {
if ( ( trim( $value ) === '' || trim( $value ) === "\x1f" ) && $allowMultiple ) {
return [];
}
? self::LIMIT_SML2
: self::LIMIT_SML1;
+ if ( $allowMultiple && is_array( $allowedValues ) && $allSpecifier &&
+ count( $valuesList ) === 1 && $valuesList[0] === $allSpecifier
+ ) {
+ return $allowedValues;
+ }
+
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 ) );
+ }
+
/**@}*/
}