diff options
Diffstat (limited to 'includes')
38 files changed, 1381 insertions, 1283 deletions
diff --git a/includes/AutoLoader.php b/includes/AutoLoader.php index 006c4065515d..179b4b0a29f9 100644 --- a/includes/AutoLoader.php +++ b/includes/AutoLoader.php @@ -135,6 +135,7 @@ class AutoLoader { 'MediaWiki\\EditPage\\' => __DIR__ . '/editpage/', 'MediaWiki\\Linker\\' => __DIR__ . '/linker/', 'MediaWiki\\Message\\' => __DIR__ . '/Message', + 'MediaWiki\\ParamValidator\\' => __DIR__ . '/ParamValidator/', 'MediaWiki\\Permissions\\' => __DIR__ . '/Permissions/', 'MediaWiki\\Preferences\\' => __DIR__ . '/preferences/', 'MediaWiki\\Rest\\' => __DIR__ . '/Rest/', diff --git a/includes/ParamValidator/TypeDef/NamespaceDef.php b/includes/ParamValidator/TypeDef/NamespaceDef.php new file mode 100644 index 000000000000..9204ef4a9e64 --- /dev/null +++ b/includes/ParamValidator/TypeDef/NamespaceDef.php @@ -0,0 +1,78 @@ +<?php + +namespace MediaWiki\ParamValidator\TypeDef; + +use ApiResult; +use NamespaceInfo; +use Wikimedia\ParamValidator\Callbacks; +use Wikimedia\ParamValidator\ParamValidator; +use Wikimedia\ParamValidator\TypeDef\EnumDef; + +/** + * Type definition for namespace types + * + * A namespace type is an enum type that accepts MediaWiki namespace IDs. + * + * @since 1.35 + */ +class NamespaceDef extends EnumDef { + + /** + * (int[]) Additional namespace IDs to recognize. + * + * Generally this will be used to include NS_SPECIAL and/or NS_MEDIA. + */ + public const PARAM_EXTRA_NAMESPACES = 'param-extra-namespaces'; + + /** @var NamespaceInfo */ + private $nsInfo; + + public function __construct( Callbacks $callbacks, NamespaceInfo $nsInfo ) { + parent::__construct( $callbacks ); + $this->nsInfo = $nsInfo; + } + + public function validate( $name, $value, array $settings, array $options ) { + if ( !is_int( $value ) && preg_match( '/^[+-]?\d+$/D', $value ) ) { + // Convert to int since that's what getEnumValues() returns. + $value = (int)$value; + } + + return parent::validate( $name, $value, $settings, $options ); + } + + public function getEnumValues( $name, array $settings, array $options ) { + $namespaces = $this->nsInfo->getValidNamespaces(); + $extra = $settings[self::PARAM_EXTRA_NAMESPACES] ?? []; + if ( is_array( $extra ) && $extra !== [] ) { + $namespaces = array_merge( $namespaces, $extra ); + } + sort( $namespaces ); + return $namespaces; + } + + public function normalizeSettings( array $settings ) { + // Force PARAM_ALL + if ( !empty( $settings[ParamValidator::PARAM_ISMULTI] ) ) { + $settings[ParamValidator::PARAM_ALL] = true; + } + return parent::normalizeSettings( $settings ); + } + + public function getParamInfo( $name, array $settings, array $options ) { + $info = parent::getParamInfo( $name, $settings, $options ); + + $info['type'] = 'namespace'; + $extra = $settings[self::PARAM_EXTRA_NAMESPACES] ?? []; + if ( is_array( $extra ) && $extra !== [] ) { + $info['extranamespaces'] = array_values( $extra ); + if ( isset( $options['module'] ) ) { + // ApiResult metadata when used with the Action API. + ApiResult::setIndexedTagName( $info['extranamespaces'], 'ns' ); + } + } + + return $info; + } + +} diff --git a/includes/ParamValidator/TypeDef/TagsDef.php b/includes/ParamValidator/TypeDef/TagsDef.php new file mode 100644 index 000000000000..7c66f88880fb --- /dev/null +++ b/includes/ParamValidator/TypeDef/TagsDef.php @@ -0,0 +1,74 @@ +<?php + +namespace MediaWiki\ParamValidator\TypeDef; + +use ChangeTags; +use MediaWiki\Message\Converter as MessageConverter; +use Wikimedia\Message\DataMessageValue; +use Wikimedia\ParamValidator\Callbacks; +use Wikimedia\ParamValidator\TypeDef\EnumDef; +use Wikimedia\ParamValidator\ValidationException; + +/** + * Type definition for tags type + * + * A tags type is an enum type for selecting MediaWiki change tags. + * + * Failure codes: + * - 'badtags': The value was not a valid set of tags. Data: + * - 'disallowedtags': The tags that were disallowed. + * + * @since 1.35 + */ +class TagsDef extends EnumDef { + + /** @var MessageConverter */ + private $messageConverter; + + public function __construct( Callbacks $callbacks ) { + parent::__construct( $callbacks ); + $this->messageConverter = new MessageConverter(); + } + + public function validate( $name, $value, array $settings, array $options ) { + // Validate the full list of tags at once, because the caller will + // *probably* stop at the first exception thrown. + if ( isset( $options['values-list'] ) ) { + $ret = $value; + $tagsStatus = ChangeTags::canAddTagsAccompanyingChange( $options['values-list'] ); + } else { + // The 'tags' type always returns an array. + $ret = [ $value ]; + $tagsStatus = ChangeTags::canAddTagsAccompanyingChange( $ret ); + } + + if ( !$tagsStatus->isGood() ) { + $msg = $this->messageConverter->convertMessage( $tagsStatus->getMessage() ); + $data = []; + if ( $tagsStatus->value ) { + // Specific tags are not allowed. + $data['disallowedtags'] = $tagsStatus->value; + // @codeCoverageIgnoreStart + } else { + // All are disallowed, I guess + $data['disallowedtags'] = $settings['values-list'] ?? $ret; + } + // @codeCoverageIgnoreEnd + + // Only throw if $value is among the disallowed tags + if ( in_array( $value, $data['disallowedtags'], true ) ) { + throw new ValidationException( + DataMessageValue::new( $msg->getKey(), $msg->getParams(), 'badtags', $data ), + $name, $value, $settings + ); + } + } + + return $ret; + } + + public function getEnumValues( $name, array $settings, array $options ) { + return ChangeTags::listExplicitlyDefinedTags(); + } + +} diff --git a/includes/ParamValidator/TypeDef/UserDef.php b/includes/ParamValidator/TypeDef/UserDef.php new file mode 100644 index 000000000000..a243b5bc8296 --- /dev/null +++ b/includes/ParamValidator/TypeDef/UserDef.php @@ -0,0 +1,169 @@ +<?php + +namespace MediaWiki\ParamValidator\TypeDef; + +use ExternalUserNames; +// phpcs:ignore MediaWiki.Classes.UnusedUseStatement.UnusedUse +use MediaWiki\User\UserIdentity; +use MediaWiki\User\UserIdentityValue; +use Title; +use User; +use Wikimedia\IPUtils; +use Wikimedia\Message\MessageValue; +use Wikimedia\ParamValidator\ParamValidator; +use Wikimedia\ParamValidator\TypeDef; + +/** + * Type definition for user types + * + * Failure codes: + * - 'baduser': The value was not a valid MediaWiki user. No data. + * + * @since 1.35 + */ +class UserDef extends TypeDef { + + /** + * (string[]) Allowed types of user. + * + * One or more of the following values: + * - 'name': User names are allowed. + * - 'ip': IP ("anon") usernames are allowed. + * - 'cidr': IP ranges are allowed. + * - 'interwiki': Interwiki usernames are allowed. + * - 'id': Allow specifying user IDs, formatted like "#123". + * + * Default is `[ 'name', 'ip', 'cidr', 'interwiki' ]`. + * + * Avoid combining 'id' with PARAM_ISMULTI, as it may result in excessive + * DB lookups. If you do combine them, consider setting low values for + * PARAM_ISMULTI_LIMIT1 and PARAM_ISMULTI_LIMIT2 to mitigate it. + */ + public const PARAM_ALLOWED_USER_TYPES = 'param-allowed-user-types'; + + /** + * (bool) Whether to return a UserIdentity object. + * + * If false, the validated user name is returned as a string. Default is false. + * + * Avoid setting true with PARAM_ISMULTI, as it may result in excessive DB + * lookups. If you do combine them, consider setting low values for + * PARAM_ISMULTI_LIMIT1 and PARAM_ISMULTI_LIMIT2 to mitigate it. + */ + public const PARAM_RETURN_OBJECT = 'param-return-object'; + + public function validate( $name, $value, array $settings, array $options ) { + list( $type, $user ) = $this->processUser( $value ); + + if ( !$user || !in_array( $type, $settings[self::PARAM_ALLOWED_USER_TYPES], true ) ) { + $this->failure( 'baduser', $name, $value, $settings, $options ); + } + return empty( $settings[self::PARAM_RETURN_OBJECT] ) ? $user->getName() : $user; + } + + public function normalizeSettings( array $settings ) { + if ( isset( $settings[self::PARAM_ALLOWED_USER_TYPES] ) ) { + $settings[self::PARAM_ALLOWED_USER_TYPES] = array_values( array_intersect( + [ 'name', 'ip', 'cidr', 'interwiki', 'id' ], + $settings[self::PARAM_ALLOWED_USER_TYPES] + ) ); + } + if ( empty( $settings[self::PARAM_ALLOWED_USER_TYPES] ) ) { + $settings[self::PARAM_ALLOWED_USER_TYPES] = [ 'name', 'ip', 'cidr', 'interwiki' ]; + } + + return parent::normalizeSettings( $settings ); + } + + /** + * Process $value to a UserIdentity, if possible + * @param string $value + * @return array [ string $type, UserIdentity|null $user ] + * @phan-return array{0:string,1:UserIdentity|null} + */ + private function processUser( string $value ) : array { + // A user ID? + if ( preg_match( '/^#(\d+)$/D', $value, $m ) ) { + return [ 'id', User::newFromId( $m[1] ) ]; + } + + // An interwiki username? + if ( ExternalUserNames::isExternal( $value ) ) { + $name = User::getCanonicalName( $value, false ); + return [ + 'interwiki', + is_string( $name ) ? new UserIdentityValue( 0, $value, 0 ) : null + ]; + } + + // A valid user name? + $user = User::newFromName( $value, 'valid' ); + if ( $user ) { + return [ 'name', $user ]; + } + + // (T232672) Reproduce the normalization applied in User::getCanonicalName() when + // performing the checks below. + if ( strpos( $value, '#' ) !== false ) { + return [ '', null ]; + } + $t = Title::newFromText( $value ); // In case of explicit "User:" prefix, sigh. + if ( !$t || $t->getNamespace() !== NS_USER || $t->isExternal() ) { // likely + $t = Title::newFromText( "User:$value" ); + } + if ( !$t || $t->getNamespace() !== NS_USER || $t->isExternal() ) { + // If it wasn't a valid User-namespace title, fail. + return [ '', null ]; + } + $value = $t->getText(); + + // An IP? + $b = IPUtils::RE_IP_BYTE; + if ( IPUtils::isValid( $value ) || + // See comment for User::isIP. We don't just call that function + // here because it also returns true for things like + // 300.300.300.300 that are neither valid usernames nor valid IP + // addresses. + preg_match( "/^$b\.$b\.$b\.xxx$/D", $value ) + ) { + return [ 'ip', new UserIdentityValue( 0, IPUtils::sanitizeIP( $value ), 0 ) ]; + } + + // A range? + if ( IPUtils::isValidRange( $value ) ) { + return [ 'cidr', new UserIdentityValue( 0, IPUtils::sanitizeIP( $value ), 0 ) ]; + } + + // Fail. + return [ '', null ]; + } + + public function getParamInfo( $name, array $settings, array $options ) { + $info = parent::getParamInfo( $name, $settings, $options ); + + $info['subtypes'] = $settings[self::PARAM_ALLOWED_USER_TYPES]; + + return $info; + } + + public function getHelpInfo( $name, array $settings, array $options ) { + $info = parent::getParamInfo( $name, $settings, $options ); + + $isMulti = !empty( $settings[ParamValidator::PARAM_ISMULTI] ); + + $subtypes = []; + foreach ( $settings[self::PARAM_ALLOWED_USER_TYPES] as $st ) { + // Messages: paramvalidator-help-type-user-subtype-name, + // paramvalidator-help-type-user-subtype-ip, paramvalidator-help-type-user-subtype-cidr, + // paramvalidator-help-type-user-subtype-interwiki, paramvalidator-help-type-user-subtype-id + $subtypes[] = MessageValue::new( "paramvalidator-help-type-user-subtype-$st" ); + } + $info[ParamValidator::PARAM_TYPE] = MessageValue::new( 'paramvalidator-help-type-user' ) + ->params( $isMulti ? 2 : 1 ) + ->textListParams( $subtypes ) + ->numParams( count( $subtypes ) ); + + return $info; + } + +} diff --git a/includes/WebRequestUpload.php b/includes/WebRequestUpload.php index d3d2c79ad79d..1b0d6e896b51 100644 --- a/includes/WebRequestUpload.php +++ b/includes/WebRequestUpload.php @@ -102,6 +102,20 @@ class WebRequestUpload { } /** + * Return the client specified content type + * + * @since 1.35 + * @return string|null Type or null if non-existent + */ + public function getType() { + if ( !$this->exists() ) { + return null; + } + + return $this->fileInfo['type']; + } + + /** * Return the upload error. See link for explanation * https://www.php.net/manual/en/features.file-upload.errors.php * diff --git a/includes/api/ApiBase.php b/includes/api/ApiBase.php index 25fa1e2ac8a5..2044794ea015 100644 --- a/includes/api/ApiBase.php +++ b/includes/api/ApiBase.php @@ -20,12 +20,17 @@ * @file */ +use MediaWiki\Api\Validator\SubmoduleDef; use MediaWiki\Block\AbstractBlock; use MediaWiki\Block\DatabaseBlock; use MediaWiki\Linker\LinkTarget; use MediaWiki\MediaWikiServices; +use MediaWiki\ParamValidator\TypeDef\NamespaceDef; use MediaWiki\Permissions\PermissionManager; -use Wikimedia\IPUtils; +use Wikimedia\ParamValidator\ParamValidator; +use Wikimedia\ParamValidator\TypeDef\EnumDef; +use Wikimedia\ParamValidator\TypeDef\IntegerDef; +use Wikimedia\ParamValidator\TypeDef\StringDef; use Wikimedia\Rdbms\IDatabase; /** @@ -45,98 +50,57 @@ abstract class ApiBase extends ContextSource { use ApiBlockInfoTrait; /** - * @name Constants for ::getAllowedParams() arrays - * These constants are keys in the arrays returned by ::getAllowedParams() - * and accepted by ::getParameterFromSettings() that define how the - * parameters coming in from the request are to be interpreted. + * @name Old constants for ::getAllowedParams() arrays + * @deprecated since 1.35, use the equivalent ParamValidator or TypeDef constants instead. * @{ */ - /** (null|boolean|integer|string) Default value of the parameter. */ - public const PARAM_DFLT = 0; + public const PARAM_DFLT = ParamValidator::PARAM_DEFAULT; + public const PARAM_ISMULTI = ParamValidator::PARAM_ISMULTI; + public const PARAM_TYPE = ParamValidator::PARAM_TYPE; + public const PARAM_MAX = IntegerDef::PARAM_MAX; + public const PARAM_MAX2 = IntegerDef::PARAM_MAX2; + public const PARAM_MIN = IntegerDef::PARAM_MIN; + public const PARAM_ALLOW_DUPLICATES = ParamValidator::PARAM_ALLOW_DUPLICATES; + public const PARAM_DEPRECATED = ParamValidator::PARAM_DEPRECATED; + public const PARAM_REQUIRED = ParamValidator::PARAM_REQUIRED; + public const PARAM_SUBMODULE_MAP = SubmoduleDef::PARAM_SUBMODULE_MAP; + public const PARAM_SUBMODULE_PARAM_PREFIX = SubmoduleDef::PARAM_SUBMODULE_PARAM_PREFIX; + public const PARAM_ALL = ParamValidator::PARAM_ALL; + public const PARAM_EXTRA_NAMESPACES = NamespaceDef::PARAM_EXTRA_NAMESPACES; + public const PARAM_SENSITIVE = ParamValidator::PARAM_SENSITIVE; + public const PARAM_DEPRECATED_VALUES = EnumDef::PARAM_DEPRECATED_VALUES; + public const PARAM_ISMULTI_LIMIT1 = ParamValidator::PARAM_ISMULTI_LIMIT1; + public const PARAM_ISMULTI_LIMIT2 = ParamValidator::PARAM_ISMULTI_LIMIT2; + public const PARAM_MAX_BYTES = StringDef::PARAM_MAX_BYTES; + public const PARAM_MAX_CHARS = StringDef::PARAM_MAX_CHARS; + + /** + * (boolean) Inverse of IntegerDef::PARAM_IGNORE_RANGE + * @deprecated since 1.35 + */ + public const PARAM_RANGE_ENFORCE = 'api-param-range-enforce'; - /** (boolean) Accept multiple pipe-separated values for this parameter (e.g. titles)? */ - public const PARAM_ISMULTI = 1; - - /** - * (string|string[]) Either an array of allowed value strings, or a string - * type as described below. If not specified, will be determined from the - * type of PARAM_DFLT. - * - * Supported string types are: - * - boolean: A boolean parameter, returned as false if the parameter is - * omitted and true if present (even with a falsey value, i.e. it works - * like HTML checkboxes). PARAM_DFLT must be boolean false, if specified. - * Cannot be used with PARAM_ISMULTI. - * - integer: An integer value. See also PARAM_MIN, PARAM_MAX, and - * PARAM_RANGE_ENFORCE. - * - 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. 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. - * - string: Any non-empty string, not expected to be very long or contain newlines. - * <input type="text"> would be an appropriate HTML form field. - * - submodule: The name of a submodule of this module, see PARAM_SUBMODULE_MAP. - * - tags: A string naming an existing, explicitly-defined tag. Should usually be - * used with PARAM_ISMULTI. - * - text: Any non-empty string, expected to be very long or contain newlines. - * <textarea> would be an appropriate HTML form field. - * - timestamp: A timestamp in any format recognized by MWTimestamp, or the - * string 'now' representing the current timestamp. Will be returned in - * TS_MW format. - * - user: A MediaWiki username or IP. Will be returned normalized but not canonicalized. - * - upload: An uploaded file. Will be returned as a WebRequestUpload object. - * Cannot be used with PARAM_ISMULTI. - */ - public const PARAM_TYPE = 2; - - /** (integer) Max value allowed for the parameter, for PARAM_TYPE 'integer' and 'limit'. */ - public const PARAM_MAX = 3; - - /** - * (integer) Max value allowed for the parameter for users with the - * apihighlimits right, for PARAM_TYPE 'limit'. - */ - public const PARAM_MAX2 = 4; - - /** (integer) Lowest value allowed for the parameter, for PARAM_TYPE 'integer' and 'limit'. */ - public const PARAM_MIN = 5; - - /** (boolean) Allow the same value to be set more than once when PARAM_ISMULTI is true? */ - public const PARAM_ALLOW_DUPLICATES = 6; - - /** (boolean) Is the parameter deprecated (will show a warning)? */ - public const PARAM_DEPRECATED = 7; - - /** - * (boolean) Is the parameter required? - * @since 1.17 - */ - public const PARAM_REQUIRED = 8; + /** @} */ /** - * (boolean) For PARAM_TYPE 'integer', enforce PARAM_MIN and PARAM_MAX? - * @since 1.17 + * @name API-specific constants for ::getAllowedParams() arrays + * @{ */ - public const PARAM_RANGE_ENFORCE = 9; /** * (string|array|Message) Specify an alternative i18n documentation message * for this parameter. Default is apihelp-{$path}-param-{$param}. * @since 1.25 */ - public const PARAM_HELP_MSG = 10; + public const PARAM_HELP_MSG = 'api-param-help-msg'; /** * ((string|array|Message)[]) Specify additional i18n messages to append to * the normal message for this parameter. * @since 1.25 */ - public const PARAM_HELP_MSG_APPEND = 11; + public const PARAM_HELP_MSG_APPEND = 'api-param-help-msg-append'; /** * (array) Specify additional information tags for the parameter. Value is @@ -146,14 +110,14 @@ abstract class ApiBase extends ContextSource { * $1 = count, $2 = comma-joined list of values, $3 = module prefix. * @since 1.25 */ - public const PARAM_HELP_MSG_INFO = 12; + public const PARAM_HELP_MSG_INFO = 'api-param-help-msg-info'; /** - * (string[]) When PARAM_TYPE is an array, this may be an array mapping - * those values to page titles which will be linked in the help. + * Deprecated and unused. * @since 1.25 + * @deprecated since 1.35 */ - public const PARAM_VALUE_LINKS = 13; + public const PARAM_VALUE_LINKS = 'api-param-value-links'; /** * ((string|array|Message)[]) When PARAM_TYPE is an array, this is an array @@ -162,77 +126,7 @@ abstract class ApiBase extends ContextSource { * Specify an empty array to use the default message key for all values. * @since 1.25 */ - public const PARAM_HELP_MSG_PER_VALUE = 14; - - /** - * (string[]) When PARAM_TYPE is 'submodule', map parameter values to - * submodule paths. Default is to use all modules in - * $this->getModuleManager() in the group matching the parameter name. - * @since 1.26 - */ - public const PARAM_SUBMODULE_MAP = 15; - - /** - * (string) When PARAM_TYPE is 'submodule', used to indicate the 'g' prefix - * added by ApiQueryGeneratorBase (and similar if anything else ever does that). - * @since 1.26 - */ - public 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 - */ - public const PARAM_ALL = 17; - - /** - * (int[]) When PARAM_TYPE is 'namespace', include these as additional possible values. - * @since 1.29 - */ - public const PARAM_EXTRA_NAMESPACES = 18; - - /** - * (boolean) Is the parameter sensitive? Note 'password'-type fields are - * always sensitive regardless of the value of this field. - * @since 1.29 - */ - public const PARAM_SENSITIVE = 19; - - /** - * (array) When PARAM_TYPE is an array, this indicates which of the values are deprecated. - * Keys are the deprecated parameter values, values define the warning - * message to emit: either boolean true (to use a default message) or a - * $msg for ApiBase::makeMessage(). - * @since 1.30 - */ - public const PARAM_DEPRECATED_VALUES = 20; - - /** - * (integer) Maximum number of values, for normal users. Must be used with PARAM_ISMULTI. - * @since 1.30 - */ - public const PARAM_ISMULTI_LIMIT1 = 21; - - /** - * (integer) Maximum number of values, for users with the apihighimits right. - * Must be used with PARAM_ISMULTI. - * @since 1.30 - */ - public const PARAM_ISMULTI_LIMIT2 = 22; - - /** - * (integer) Maximum length of a string in bytes (in UTF-8 encoding). - * @since 1.31 - */ - public const PARAM_MAX_BYTES = 23; - - /** - * (integer) Maximum length of a string in characters (unicode codepoints). - * @since 1.31 - */ - public const PARAM_MAX_CHARS = 24; + public const PARAM_HELP_MSG_PER_VALUE = 'api-param-help-msg-per-value'; /** * (array) Indicate that this is a templated parameter, and specify replacements. Keys are the @@ -250,7 +144,7 @@ abstract class ApiBase extends ContextSource { * * @since 1.32 */ - public const PARAM_TEMPLATE_VARS = 25; + public const PARAM_TEMPLATE_VARS = 'param-template-vars'; /** @} */ @@ -1127,318 +1021,24 @@ abstract class ApiBase extends ContextSource { /** * Using the settings determine the value for the given parameter * - * @param string $paramName Parameter name - * @param array|mixed $paramSettings Default value or an array of settings + * @param string $name Parameter name + * @param array|mixed $settings Default value or an array of settings * using PARAM_* constants. * @param bool $parseLimit Whether to parse and validate 'limit' parameters * @return mixed Parameter value */ - protected function getParameterFromSettings( $paramName, $paramSettings, $parseLimit ) { - // Some classes may decide to change parameter names - $encParamName = $this->encodeParamName( $paramName ); - - // Shorthand - if ( !is_array( $paramSettings ) ) { - $paramSettings = [ - self::PARAM_DFLT => $paramSettings, - ]; - } - - $default = $paramSettings[self::PARAM_DFLT] ?? null; - $multi = $paramSettings[self::PARAM_ISMULTI] ?? false; - $multiLimit1 = $paramSettings[self::PARAM_ISMULTI_LIMIT1] ?? null; - $multiLimit2 = $paramSettings[self::PARAM_ISMULTI_LIMIT2] ?? null; - $type = $paramSettings[self::PARAM_TYPE] ?? null; - $dupes = $paramSettings[self::PARAM_ALLOW_DUPLICATES] ?? false; - $deprecated = $paramSettings[self::PARAM_DEPRECATED] ?? false; - $deprecatedValues = $paramSettings[self::PARAM_DEPRECATED_VALUES] ?? []; - $required = $paramSettings[self::PARAM_REQUIRED] ?? false; - $allowAll = $paramSettings[self::PARAM_ALL] ?? false; - - // When type is not given, and no choices, the type is the same as $default - if ( !isset( $type ) ) { - if ( isset( $default ) ) { - $type = gettype( $default ); - } else { - $type = 'NULL'; // allow everything - } - } - - if ( $type == 'password' || !empty( $paramSettings[self::PARAM_SENSITIVE] ) ) { - $this->getMain()->markParamsSensitive( $encParamName ); - } - - if ( $type == 'boolean' ) { - if ( isset( $default ) && $default !== false ) { - // Having a default value of anything other than 'false' is not allowed - self::dieDebug( - __METHOD__, - "Boolean param $encParamName's default is set to '$default'. " . - 'Boolean parameters must default to false.' - ); - } - - $value = $this->getMain()->getCheck( $encParamName ); - $provided = $value; - } elseif ( $type == 'upload' ) { - if ( isset( $default ) ) { - // Having a default value is not allowed - self::dieDebug( - __METHOD__, - "File upload param $encParamName's default is set to " . - "'$default'. File upload parameters may not have a default." ); - } - if ( $multi ) { - self::dieDebug( __METHOD__, "Multi-values not supported for $encParamName" ); - } - $value = $this->getMain()->getUpload( $encParamName ); - $provided = $value->exists(); - if ( !$value->exists() ) { - // This will get the value without trying to normalize it - // (because trying to normalize a large binary file - // accidentally uploaded as a field fails spectacularly) - $value = $this->getMain()->getRequest()->unsetVal( $encParamName ); - if ( $value !== null ) { - $this->dieWithError( - [ 'apierror-badupload', $encParamName ], - "badupload_{$encParamName}" - ); - } - } - } else { - $value = $this->getMain()->getVal( $encParamName, $default ); - $provided = $this->getMain()->getCheck( $encParamName ); - - if ( isset( $value ) && $type == 'namespace' ) { - $type = MediaWikiServices::getInstance()->getNamespaceInfo()-> - getValidNamespaces(); - if ( isset( $paramSettings[self::PARAM_EXTRA_NAMESPACES] ) && - is_array( $paramSettings[self::PARAM_EXTRA_NAMESPACES] ) - ) { - $type = array_merge( $type, $paramSettings[self::PARAM_EXTRA_NAMESPACES] ); - } - // Namespace parameters allow ALL_DEFAULT_STRING to be used to - // specify all namespaces irrespective of PARAM_ALL. - $allowAll = true; - } - if ( isset( $value ) && $type == 'submodule' ) { - if ( isset( $paramSettings[self::PARAM_SUBMODULE_MAP] ) ) { - $type = array_keys( $paramSettings[self::PARAM_SUBMODULE_MAP] ); - } else { - $type = $this->getModuleManager()->getNames( $paramName ); - } - } - - $request = $this->getMain()->getRequest(); - $rawValue = $request->getRawVal( $encParamName ); - if ( $rawValue === null ) { - $rawValue = $default; - } - - // Preserve U+001F for self::parseMultiValue(), or error out if that won't be called - if ( isset( $value ) && substr( $rawValue, 0, 1 ) === "\x1f" ) { - if ( $multi ) { - // This loses the potential checkTitleEncoding() transformation done by - // WebRequest for $_GET. Let's call that a feature. - $value = implode( "\x1f", $request->normalizeUnicode( explode( "\x1f", $rawValue ) ) ); - } else { - $this->dieWithError( 'apierror-badvalue-notmultivalue', 'badvalue_notmultivalue' ); - } - } - - // Check for NFC normalization, and warn - if ( $rawValue !== $value ) { - $this->handleParamNormalization( $paramName, $value, $rawValue ); - } - } - - $allSpecifier = ( is_string( $allowAll ) ? $allowAll : self::ALL_DEFAULT_STRING ); - if ( $allowAll && $multi && is_array( $type ) && in_array( $allSpecifier, $type, true ) ) { - self::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, - $allowAll ? $allSpecifier : null, - $multiLimit1, - $multiLimit2 - ); - } - - if ( isset( $value ) ) { - // More validation only when choices were not given - // choices were validated in parseMultiValue() - if ( !is_array( $type ) ) { - switch ( $type ) { - case 'NULL': // nothing to do - break; - case 'string': - case 'text': - case 'password': - if ( $required && $value === '' ) { - $this->dieWithError( [ 'apierror-missingparam', $encParamName ] ); - } - break; - case 'integer': // Force everything using intval() and optionally validate limits - $min = $paramSettings[self::PARAM_MIN] ?? null; - $max = $paramSettings[self::PARAM_MAX] ?? null; - $enforceLimits = $paramSettings[self::PARAM_RANGE_ENFORCE] ?? false; - - if ( is_array( $value ) ) { - $value = array_map( 'intval', $value ); - if ( $min !== null || $max !== null ) { - foreach ( $value as &$v ) { - $this->validateLimit( $paramName, $v, $min, $max, null, $enforceLimits ); - } - } - } else { - $value = (int)$value; - if ( $min !== null || $max !== null ) { - $this->validateLimit( $paramName, $value, $min, $max, null, $enforceLimits ); - } - } - break; - case 'limit': - // Must be a number or 'max' - if ( $value !== 'max' ) { - $value = (int)$value; - } - if ( $multi ) { - self::dieDebug( __METHOD__, "Multi-values not supported for $encParamName" ); - } - if ( !$parseLimit ) { - // Don't do min/max validation and don't parse 'max' - break; - } - if ( !isset( $paramSettings[self::PARAM_MAX] ) - || !isset( $paramSettings[self::PARAM_MAX2] ) - ) { - self::dieDebug( - __METHOD__, - "MAX1 or MAX2 are not defined for the limit $encParamName" - ); - } - if ( $value === 'max' ) { - $value = $this->getMain()->canApiHighLimits() - ? $paramSettings[self::PARAM_MAX2] - : $paramSettings[self::PARAM_MAX]; - $this->getResult()->addParsedLimit( $this->getModuleName(), $value ); - } else { - $this->validateLimit( - $paramName, - $value, - $paramSettings[self::PARAM_MIN] ?? 0, - $paramSettings[self::PARAM_MAX], - $paramSettings[self::PARAM_MAX2] - ); - } - break; - case 'boolean': - if ( $multi ) { - self::dieDebug( __METHOD__, "Multi-values not supported for $encParamName" ); - } - break; - case 'timestamp': - if ( is_array( $value ) ) { - foreach ( $value as $key => $val ) { - $value[$key] = $this->validateTimestamp( $val, $encParamName ); - } - } else { - $value = $this->validateTimestamp( $value, $encParamName ); - } - break; - case 'user': - if ( is_array( $value ) ) { - foreach ( $value as $key => $val ) { - $value[$key] = $this->validateUser( $val, $encParamName ); - } - } else { - $value = $this->validateUser( $value, $encParamName ); - } - break; - case 'upload': // nothing to do - break; - case 'tags': - // If change tagging was requested, check that the tags are valid. - if ( !is_array( $value ) && !$multi ) { - $value = [ $value ]; - } - $tagsStatus = ChangeTags::canAddTagsAccompanyingChange( $value ); - if ( !$tagsStatus->isGood() ) { - $this->dieStatus( $tagsStatus ); - } - break; - default: - self::dieDebug( __METHOD__, "Param $encParamName's type is unknown - $type" ); - } - } - - // Throw out duplicates if requested - if ( !$dupes && is_array( $value ) ) { - $value = array_unique( $value ); - } - - if ( in_array( $type, [ 'NULL', 'string', 'text', 'password' ], true ) ) { - foreach ( (array)$value as $val ) { - if ( isset( $paramSettings[self::PARAM_MAX_BYTES] ) - && strlen( $val ) > $paramSettings[self::PARAM_MAX_BYTES] - ) { - $this->dieWithError( [ 'apierror-maxbytes', $encParamName, - $paramSettings[self::PARAM_MAX_BYTES] ] ); - } - if ( isset( $paramSettings[self::PARAM_MAX_CHARS] ) - && mb_strlen( $val, 'UTF-8' ) > $paramSettings[self::PARAM_MAX_CHARS] - ) { - $this->dieWithError( [ 'apierror-maxchars', $encParamName, - $paramSettings[self::PARAM_MAX_CHARS] ] ); - } - } - } - - // Set a warning if a deprecated parameter has been passed - if ( $deprecated && $provided ) { - $feature = $encParamName; - $m = $this; - while ( !$m->isMain() ) { - $p = $m->getParent(); - $name = $m->getModuleName(); - $param = $p->encodeParamName( $p->getModuleManager()->getModuleGroup( $name ) ); - $feature = "{$param}={$name}&{$feature}"; - $m = $p; - } - $this->addDeprecation( [ 'apiwarn-deprecation-parameter', $encParamName ], $feature ); - } + protected function getParameterFromSettings( $name, $settings, $parseLimit ) { + $validator = $this->getMain()->getParamValidator(); + $value = $validator->getValue( $this, $name, $settings, [ + 'parse-limit' => $parseLimit, + ] ); - // Set a warning if a deprecated parameter value has been passed - $usedDeprecatedValues = $deprecatedValues && $provided - ? array_intersect( array_keys( $deprecatedValues ), (array)$value ) - : []; - if ( $usedDeprecatedValues ) { - $feature = "$encParamName="; - $m = $this; - while ( !$m->isMain() ) { - $p = $m->getParent(); - $name = $m->getModuleName(); - $param = $p->encodeParamName( $p->getModuleManager()->getModuleGroup( $name ) ); - $feature = "{$param}={$name}&{$feature}"; - $m = $p; - } - foreach ( $usedDeprecatedValues as $v ) { - // @phan-suppress-next-line PhanTypeArraySuspiciousNullable - $msg = $deprecatedValues[$v]; - if ( $msg === true ) { - $msg = [ 'apiwarn-deprecation-parameter', "$encParamName=$v" ]; - } - $this->addDeprecation( $msg, "$feature$v" ); - } - } - } elseif ( $required ) { - $this->dieWithError( [ 'apierror-missingparam', $encParamName ] ); + // @todo Deprecate and remove this, if possible. + if ( $parseLimit && isset( $settings[ParamValidator::PARAM_TYPE] ) && + $settings[ParamValidator::PARAM_TYPE] === 'limit' && + $this->getMain()->getVal( $this->encodeParamName( $name ) ) === 'max' + ) { + $this->getResult()->addParsedLimit( $this->getModuleName(), $value ); } return $value; @@ -1447,213 +1047,14 @@ abstract class ApiBase extends ContextSource { /** * Handle when a parameter was Unicode-normalized * @since 1.28 - * @param string $paramName Unprefixed parameter name + * @since 1.35 $paramName is prefixed + * @internal For overriding by subclasses and use by ApiParamValidatorCallbacks only. + * @param string $paramName Prefixed parameter name * @param string $value Input that will be used. * @param string $rawValue Input before normalization. */ - protected function handleParamNormalization( $paramName, $value, $rawValue ) { - $encParamName = $this->encodeParamName( $paramName ); - $this->addWarning( [ 'apiwarn-badutf8', $encParamName ] ); - } - - /** - * Split a multi-valued parameter string, like explode() - * @since 1.28 - * @param string $value - * @param int $limit - * @return string[] - */ - protected function explodeMultiValue( $value, $limit ) { - if ( substr( $value, 0, 1 ) === "\x1f" ) { - $sep = "\x1f"; - $value = substr( $value, 1 ); - } else { - $sep = '|'; - } - - return explode( $sep, $value, $limit ); - } - - /** - * Return an array of values that were given in a 'a|b|c' notation, - * after it optionally validates them against the list allowed values. - * - * @param string $valueName The name of the parameter (for error - * reporting) - * @param mixed $value The value being parsed - * @param bool $allowMultiple Can $value contain more than one value - * 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 - * @param int|null $limit1 Maximum number of values, for normal users. - * @param int|null $limit2 Maximum number of values, for users with the apihighlimits right. - * @return string|string[] (allowMultiple ? an_array_of_values : a_single_value) - */ - protected function parseMultiValue( $valueName, $value, $allowMultiple, $allowedValues, - $allSpecifier = null, $limit1 = null, $limit2 = null - ) { - if ( ( $value === '' || $value === "\x1f" ) && $allowMultiple ) { - return []; - } - $limit1 = $limit1 ?: self::LIMIT_SML1; - $limit2 = $limit2 ?: self::LIMIT_SML2; - - // This is a bit awkward, but we want to avoid calling canApiHighLimits() - // because it unstubs $wgUser - $valuesList = $this->explodeMultiValue( $value, $limit2 + 1 ); - $sizeLimit = count( $valuesList ) > $limit1 && $this->mMainModule->canApiHighLimits() - ? $limit2 - : $limit1; - - if ( $allowMultiple && is_array( $allowedValues ) && $allSpecifier && - count( $valuesList ) === 1 && $valuesList[0] === $allSpecifier - ) { - return $allowedValues; - } - - if ( count( $valuesList ) > $sizeLimit ) { - $this->dieWithError( - [ 'apierror-toomanyvalues', $valueName, $sizeLimit ], - "too-many-$valueName" - ); - } - - if ( !$allowMultiple && count( $valuesList ) != 1 ) { - // T35482 - Allow entries with | in them for non-multiple values - if ( in_array( $value, $allowedValues, true ) ) { - return $value; - } - - $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" ); - } - - if ( is_array( $allowedValues ) ) { - // Check for unknown values - $unknown = array_map( 'wfEscapeWikiText', array_diff( $valuesList, $allowedValues ) ); - if ( count( $unknown ) ) { - if ( $allowMultiple ) { - $this->addWarning( [ - 'apiwarn-unrecognizedvalues', - $valueName, - Message::listParam( $unknown, 'comma' ), - count( $unknown ), - ] ); - } else { - $this->dieWithError( - [ 'apierror-unrecognizedvalue', $valueName, wfEscapeWikiText( $valuesList[0] ) ], - "unknown_$valueName" - ); - } - } - // Now throw them out - $valuesList = array_intersect( $valuesList, $allowedValues ); - } - - return $allowMultiple ? $valuesList : $valuesList[0]; - } - - /** - * Validate the value against the minimum and user/bot maximum limits. - * Prints usage info on failure. - * @param string $paramName Parameter name - * @param int &$value Parameter value - * @param int|null $min Minimum value - * @param int|null $max Maximum value for users - * @param int|null $botMax Maximum value for sysops/bots - * @param bool $enforceLimits Whether to enforce (die) if value is outside limits - */ - protected function validateLimit( $paramName, &$value, $min, $max, $botMax = null, - $enforceLimits = false - ) { - if ( $min !== null && $value < $min ) { - $msg = ApiMessage::create( - [ 'apierror-integeroutofrange-belowminimum', - $this->encodeParamName( $paramName ), $min, $value ], - 'integeroutofrange', - [ 'min' => $min, 'max' => $max, 'botMax' => $botMax ?: $max ] - ); - // @phan-suppress-next-line PhanTypeMismatchArgument - $this->warnOrDie( $msg, $enforceLimits ); - $value = $min; - } - - // Minimum is always validated, whereas maximum is checked only if not - // running in internal call mode - if ( $this->getMain()->isInternalMode() ) { - return; - } - - // Optimization: do not check user's bot status unless really needed -- skips db query - // assumes $botMax >= $max - if ( $max !== null && $value > $max ) { - if ( $botMax !== null && $this->getMain()->canApiHighLimits() ) { - if ( $value > $botMax ) { - $msg = ApiMessage::create( - [ 'apierror-integeroutofrange-abovebotmax', - $this->encodeParamName( $paramName ), $botMax, $value ], - 'integeroutofrange', - [ 'min' => $min, 'max' => $max, 'botMax' => $botMax ?: $max ] - ); - // @phan-suppress-next-line PhanTypeMismatchArgument - $this->warnOrDie( $msg, $enforceLimits ); - $value = $botMax; - } - } else { - $msg = ApiMessage::create( - [ 'apierror-integeroutofrange-abovemax', - $this->encodeParamName( $paramName ), $max, $value ], - 'integeroutofrange', - [ 'min' => $min, 'max' => $max, 'botMax' => $botMax ?: $max ] - ); - // @phan-suppress-next-line PhanTypeMismatchArgument - $this->warnOrDie( $msg, $enforceLimits ); - $value = $max; - } - } - } - - /** - * Validate and normalize parameters of type 'timestamp' - * @param string $value Parameter value - * @param string $encParamName Parameter name - * @return string Validated and normalized parameter - */ - protected function validateTimestamp( $value, $encParamName ) { - // Confusing synonyms for the current time accepted by wfTimestamp() - // (wfTimestamp() also accepts various non-strings and the string of 14 - // ASCII NUL bytes, but those can't get here) - if ( !$value ) { - $this->addDeprecation( - [ 'apiwarn-unclearnowtimestamp', $encParamName, wfEscapeWikiText( $value ) ], - 'unclear-"now"-timestamp' - ); - return wfTimestamp( TS_MW ); - } - - // Explicit synonym for the current time - if ( $value === 'now' ) { - return wfTimestamp( TS_MW ); - } - - $timestamp = wfTimestamp( TS_MW, $value ); - if ( $timestamp === false ) { - $this->dieWithError( - [ 'apierror-badtimestamp', $encParamName, wfEscapeWikiText( $value ) ], - "badtimestamp_{$encParamName}" - ); - } - - return $timestamp; + public function handleParamNormalization( $paramName, $value, $rawValue ) { + $this->addWarning( [ 'apiwarn-badutf8', $paramName ] ); } /** @@ -1694,47 +1095,6 @@ abstract class ApiBase extends ContextSource { return false; } - /** - * Validate and normalize parameters of type 'user' - * @param string $value Parameter value - * @param string $encParamName Parameter name - * @return string Validated and normalized parameter - */ - private function validateUser( $value, $encParamName ) { - if ( ExternalUserNames::isExternal( $value ) && User::newFromName( $value, false ) ) { - return $value; - } - - $name = User::getCanonicalName( $value, 'valid' ); - if ( $name !== false ) { - return $name; - } - - if ( - IPUtils::isIPAddress( $value ) || - // We allow ranges as well, for blocks. - IPUtils::isValidRange( $value ) || - // See comment for User::isIP. We don't just call that function - // here because it also returns true for things like - // 300.300.300.300 that are neither valid usernames nor valid IP - // addresses. - preg_match( - '/^' . IPUtils::RE_IP_BYTE . - '\.' . IPUtils::RE_IP_BYTE . - '\.' . IPUtils::RE_IP_BYTE . - '\.xxx$/', - $value - ) - ) { - return IPUtils::sanitizeIP( $value ); - } - - $this->dieWithError( - [ 'apierror-baduser', $encParamName, wfEscapeWikiText( $value ) ], - "baduser_{$encParamName}" - ); - } - /** @} */ /************************************************************************//** @@ -2032,20 +1392,6 @@ abstract class ApiBase extends ContextSource { } /** - * 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. * @@ -2650,6 +1996,169 @@ abstract class ApiBase extends ContextSource { } /** @} */ + + /************************************************************************//** + * @name Deprecated methods + * @{ + */ + + /** + * Split a multi-valued parameter string, like explode() + * @since 1.28 + * @deprecated since 1.35, use ParamValidator::explodeMultiValue() instead + * @param string $value + * @param int $limit + * @return string[] + */ + protected function explodeMultiValue( $value, $limit ) { + wfDeprecated( __METHOD__, '1.35' ); + return ParamValidator::explodeMultiValue( $value, $limit ); + } + + /** + * Return an array of values that were given in a 'a|b|c' notation, + * after it optionally validates them against the list allowed values. + * + * @deprecated since 1.35, no replacement + * @param string $valueName The name of the parameter (for error + * reporting) + * @param mixed $value The value being parsed + * @param bool $allowMultiple Can $value contain more than one value + * 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 + * @param int|null $limit1 Maximum number of values, for normal users. + * @param int|null $limit2 Maximum number of values, for users with the apihighlimits right. + * @return string|string[] (allowMultiple ? an_array_of_values : a_single_value) + */ + protected function parseMultiValue( $valueName, $value, $allowMultiple, $allowedValues, + $allSpecifier = null, $limit1 = null, $limit2 = null + ) { + wfDeprecated( __METHOD__, '1.35' ); + + if ( ( $value === '' || $value === "\x1f" ) && $allowMultiple ) { + return []; + } + $limit1 = $limit1 ?: self::LIMIT_SML1; + $limit2 = $limit2 ?: self::LIMIT_SML2; + + // This is a bit awkward, but we want to avoid calling canApiHighLimits() + // because it unstubs $wgUser + $valuesList = $this->explodeMultiValue( $value, $limit2 + 1 ); + $sizeLimit = count( $valuesList ) > $limit1 && $this->mMainModule->canApiHighLimits() + ? $limit2 + : $limit1; + + if ( $allowMultiple && is_array( $allowedValues ) && $allSpecifier && + count( $valuesList ) === 1 && $valuesList[0] === $allSpecifier + ) { + return $allowedValues; + } + + if ( count( $valuesList ) > $sizeLimit ) { + $this->dieWithError( + [ 'apierror-toomanyvalues', $valueName, $sizeLimit ], + "too-many-$valueName" + ); + } + + if ( !$allowMultiple && count( $valuesList ) != 1 ) { + // T35482 - Allow entries with | in them for non-multiple values + if ( in_array( $value, $allowedValues, true ) ) { + return $value; + } + + $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" ); + } + + if ( is_array( $allowedValues ) ) { + // Check for unknown values + $unknown = array_map( 'wfEscapeWikiText', array_diff( $valuesList, $allowedValues ) ); + if ( count( $unknown ) ) { + if ( $allowMultiple ) { + $this->addWarning( [ + 'apiwarn-unrecognizedvalues', + $valueName, + Message::listParam( $unknown, 'comma' ), + count( $unknown ), + ] ); + } else { + $this->dieWithError( + [ 'apierror-unrecognizedvalue', $valueName, wfEscapeWikiText( $valuesList[0] ) ], + "unknown_$valueName" + ); + } + } + // Now throw them out + $valuesList = array_intersect( $valuesList, $allowedValues ); + } + + return $allowMultiple ? $valuesList : $valuesList[0]; + } + + /** + * Validate the value against the minimum and user/bot maximum limits. + * Prints usage info on failure. + * @deprecated since 1.35, use $this->getMain()->getParamValidator()->validateValue() instead. + * @param string $name Parameter name, unprefixed + * @param int &$value Parameter value + * @param int|null $min Minimum value + * @param int|null $max Maximum value for users + * @param int|null $botMax Maximum value for sysops/bots + * @param bool $enforceLimits Whether to enforce (die) if value is outside limits + */ + protected function validateLimit( $name, &$value, $min, $max, $botMax = null, + $enforceLimits = false + ) { + wfDeprecated( __METHOD__, '1.35' ); + $value = $this->getMain()->getParamValidator()->validateValue( + $this, $name, $value, [ + ParamValidator::PARAM_TYPE => 'limit', + IntegerDef::PARAM_MIN => $min, + IntegerDef::PARAM_MAX => $max, + IntegerDef::PARAM_MAX2 => $botMax, + IntegerDef::PARAM_IGNORE_RANGE => !$enforceLimits, + ] + ); + } + + /** + * Validate and normalize parameters of type 'timestamp' + * @deprecated since 1.35, use $this->getMain()->getParamValidator()->validateValue() instead. + * @param string $value Parameter value + * @param string $encParamName Parameter name + * @return string Validated and normalized parameter + */ + protected function validateTimestamp( $value, $encParamName ) { + wfDeprecated( __METHOD__, '1.35' ); + + // Sigh. + $name = $encParamName; + $p = (string)$this->getModulePrefix(); + $l = strlen( $p ); + if ( $l && substr( $name, 0, $l ) === $p ) { + $name = substr( $name, $l ); + } + + return $this->getMain()->getParamValidator()->validateValue( + $this, $name, $value, [ + ParamValidator::PARAM_TYPE => 'timestamp', + ] + ); + } + + /** @} */ + } /** diff --git a/includes/api/ApiBlock.php b/includes/api/ApiBlock.php index 30a9242c3b31..33fa38d698e6 100644 --- a/includes/api/ApiBlock.php +++ b/includes/api/ApiBlock.php @@ -21,6 +21,7 @@ */ use MediaWiki\Block\DatabaseBlock; +use MediaWiki\ParamValidator\TypeDef\UserDef; /** * API module that facilitates the blocking of users. Requires API write mode @@ -186,9 +187,11 @@ class ApiBlock extends ApiBase { $params = [ 'user' => [ ApiBase::PARAM_TYPE => 'user', + UserDef::PARAM_ALLOWED_USER_TYPES => [ 'name', 'ip', 'cidr', 'id' ], ], 'userid' => [ ApiBase::PARAM_TYPE => 'integer', + ApiBase::PARAM_DEPRECATED => true, ], 'expiry' => 'never', 'reason' => '', diff --git a/includes/api/ApiFeedContributions.php b/includes/api/ApiFeedContributions.php index 2647139013c3..212fe6de6299 100644 --- a/includes/api/ApiFeedContributions.php +++ b/includes/api/ApiFeedContributions.php @@ -21,6 +21,7 @@ */ use MediaWiki\MediaWikiServices; +use MediaWiki\ParamValidator\TypeDef\UserDef; use MediaWiki\Revision\RevisionAccessException; use MediaWiki\Revision\RevisionRecord; use MediaWiki\Revision\RevisionStore; @@ -69,17 +70,13 @@ class ApiFeedContributions extends ApiBase { $msg = wfMessage( 'Contributions' )->inContentLanguage()->text(); $feedTitle = $config->get( 'Sitename' ) . ' - ' . $msg . ' [' . $config->get( 'LanguageCode' ) . ']'; - $feedUrl = SpecialPage::getTitleFor( 'Contributions', $params['user'] )->getFullURL(); - try { - $target = $this->titleParser - ->parseTitle( $params['user'], NS_USER ) - ->getText(); - } catch ( MalformedTitleException $e ) { - $this->dieWithError( - [ 'apierror-baduser', 'user', wfEscapeWikiText( $params['user'] ) ], - 'baduser_' . $this->encodeParamName( 'user' ) - ); + $target = $params['user']; + if ( ExternalUserNames::isExternal( $target ) ) { + // Interwiki names make invalid titles, so put the target in the query instead. + $feedUrl = SpecialPage::getTitleFor( 'Contributions' )->getFullURL( [ 'target' => $target ] ); + } else { + $feedUrl = SpecialPage::getTitleFor( 'Contributions', $target )->getFullURL(); } $feed = new $feedClasses[$params['feedformat']] ( @@ -220,6 +217,7 @@ class ApiFeedContributions extends ApiBase { ], 'user' => [ ApiBase::PARAM_TYPE => 'user', + UserDef::PARAM_ALLOWED_USER_TYPES => [ 'name', 'ip', 'cidr', 'id', 'interwiki' ], ApiBase::PARAM_REQUIRED => true, ], 'namespace' => [ diff --git a/includes/api/ApiHelp.php b/includes/api/ApiHelp.php index fe0986138292..ab6c77345948 100644 --- a/includes/api/ApiHelp.php +++ b/includes/api/ApiHelp.php @@ -22,6 +22,7 @@ use HtmlFormatter\HtmlFormatter; use MediaWiki\MediaWikiServices; +use Wikimedia\ParamValidator\ParamValidator; /** * Class to output help for an API module @@ -253,6 +254,7 @@ class ApiHelp extends ApiBase { } foreach ( $modules as $module ) { + $paramValidator = $module->getMain()->getParamValidator(); $tocnumber[$level]++; $path = $module->getModulePath(); $module->setContext( $context ); @@ -448,8 +450,10 @@ class ApiHelp extends ApiBase { $descriptions = $module->getFinalParamDescription(); foreach ( $params as $name => $settings ) { - if ( !is_array( $settings ) ) { - $settings = [ ApiBase::PARAM_DFLT => $settings ]; + $settings = $paramValidator->normalizeSettings( $settings ); + + if ( $settings[ApiBase::PARAM_TYPE] === 'submodule' ) { + $groups[] = $name; } $help['parameters'] .= Html::rawElement( 'dt', null, @@ -464,13 +468,41 @@ class ApiHelp extends ApiBase { $description[] = $msg->parseAsBlock(); } } + if ( !array_filter( $description ) ) { + $description = [ self::wrap( + $context->msg( 'api-help-param-no-description' ), + 'apihelp-empty' + ) ]; + } + + // Add "deprecated" flag + if ( !empty( $settings[ApiBase::PARAM_DEPRECATED] ) ) { + $help['parameters'] .= Html::openElement( 'dd', + [ 'class' => 'info' ] ); + $help['parameters'] .= self::wrap( + $context->msg( 'api-help-param-deprecated' ), + 'apihelp-deprecated', 'strong' + ); + $help['parameters'] .= Html::closeElement( 'dd' ); + } + + if ( $description ) { + $description = implode( '', $description ); + $description = preg_replace( '!\s*</([oud]l)>\s*<\1>\s*!', "\n", $description ); + $help['parameters'] .= Html::rawElement( 'dd', + [ 'class' => 'description' ], $description ); + } // Add usage info $info = []; + $paramHelp = $paramValidator->getHelpInfo( $module, $name, $settings, [] ); - // Required? - if ( !empty( $settings[ApiBase::PARAM_REQUIRED] ) ) { - $info[] = $context->msg( 'api-help-param-required' )->parse(); + unset( $paramHelp[ParamValidator::PARAM_DEPRECATED] ); + + if ( isset( $paramHelp[ParamValidator::PARAM_REQUIRED] ) ) { + $paramHelp[ParamValidator::PARAM_REQUIRED]->setContext( $context ); + $info[] = $paramHelp[ParamValidator::PARAM_REQUIRED]; + unset( $paramHelp[ParamValidator::PARAM_REQUIRED] ); } // Custom info? @@ -500,288 +532,9 @@ class ApiHelp extends ApiBase { } // Type documentation - if ( !isset( $settings[ApiBase::PARAM_TYPE] ) ) { - $dflt = $settings[ApiBase::PARAM_DFLT] ?? null; - if ( is_bool( $dflt ) ) { - $settings[ApiBase::PARAM_TYPE] = 'boolean'; - } elseif ( is_string( $dflt ) || $dflt === null ) { - $settings[ApiBase::PARAM_TYPE] = 'string'; - } elseif ( is_int( $dflt ) ) { - $settings[ApiBase::PARAM_TYPE] = 'integer'; - } - } - if ( isset( $settings[ApiBase::PARAM_TYPE] ) ) { - $type = $settings[ApiBase::PARAM_TYPE]; - $multi = !empty( $settings[ApiBase::PARAM_ISMULTI] ); - $hintPipeSeparated = true; - $count = !empty( $settings[ApiBase::PARAM_ISMULTI_LIMIT2] ) - ? $settings[ApiBase::PARAM_ISMULTI_LIMIT2] + 1 - : ApiBase::LIMIT_SML2 + 1; - - if ( is_array( $type ) ) { - $count = count( $type ); - $deprecatedValues = $settings[ApiBase::PARAM_DEPRECATED_VALUES] ?? []; - $links = $settings[ApiBase::PARAM_VALUE_LINKS] ?? []; - $values = array_map( function ( $v ) use ( $links, $deprecatedValues ) { - $attr = []; - if ( $v !== '' ) { - // We can't know whether this contains LTR or RTL text. - $attr['dir'] = 'auto'; - } - if ( isset( $deprecatedValues[$v] ) ) { - $attr['class'] = 'apihelp-deprecated-value'; - } - $ret = $attr ? Html::element( 'span', $attr, $v ) : $v; - if ( isset( $links[$v] ) ) { - $ret = "[[{$links[$v]}|$ret]]"; - } - return $ret; - }, $type ); - $i = array_search( '', $type, true ); - if ( $i === false ) { - $values = $context->getLanguage()->commaList( $values ); - } else { - unset( $values[$i] ); - $values = $context->msg( 'api-help-param-list-can-be-empty' ) - ->numParams( count( $values ) ) - ->params( $context->getLanguage()->commaList( $values ) ) - ->parse(); - } - $info[] = $context->msg( 'api-help-param-list' ) - ->params( $multi ? 2 : 1 ) - ->params( $values ) - ->parse(); - $hintPipeSeparated = false; - } else { - switch ( $type ) { - case 'submodule': - $groups[] = $name; - - if ( isset( $settings[ApiBase::PARAM_SUBMODULE_MAP] ) ) { - $map = $settings[ApiBase::PARAM_SUBMODULE_MAP]; - $defaultAttrs = []; - } else { - $prefix = $module->isMain() ? '' : ( $module->getModulePath() . '+' ); - $map = []; - foreach ( $module->getModuleManager()->getNames( $name ) as $submoduleName ) { - $map[$submoduleName] = $prefix . $submoduleName; - } - $defaultAttrs = [ 'dir' => 'ltr', 'lang' => 'en' ]; - } - - $submodules = []; - $submoduleFlags = []; // for sorting: higher flags are sorted later - $submoduleNames = []; // for sorting: lexicographical, ascending - foreach ( $map as $v => $m ) { - $attrs = $defaultAttrs; - $flags = 0; - try { - $submod = $module->getModuleFromPath( $m ); - if ( $submod && $submod->isDeprecated() ) { - $attrs['class'][] = 'apihelp-deprecated-value'; - $flags |= 1; - } - if ( $submod && $submod->isInternal() ) { - $attrs['class'][] = 'apihelp-internal-value'; - $flags |= 2; - } - } catch ( ApiUsageException $ex ) { - // Ignore - } - $v = Html::element( 'span', $attrs, $v ); - $submodules[] = "[[Special:ApiHelp/{$m}|{$v}]]"; - $submoduleFlags[] = $flags; - $submoduleNames[] = $v; - } - // sort $submodules by $submoduleFlags and $submoduleNames - array_multisort( $submoduleFlags, $submoduleNames, $submodules ); - $count = count( $submodules ); - $info[] = $context->msg( 'api-help-param-list' ) - ->params( $multi ? 2 : 1 ) - ->params( $context->getLanguage()->commaList( $submodules ) ) - ->parse(); - $hintPipeSeparated = false; - // No type message necessary, we have a list of values. - $type = null; - break; - - case 'namespace': - $namespaces = MediaWikiServices::getInstance()-> - getNamespaceInfo()->getValidNamespaces(); - if ( isset( $settings[ApiBase::PARAM_EXTRA_NAMESPACES] ) && - is_array( $settings[ApiBase::PARAM_EXTRA_NAMESPACES] ) - ) { - $namespaces = array_merge( $namespaces, $settings[ApiBase::PARAM_EXTRA_NAMESPACES] ); - } - sort( $namespaces ); - $count = count( $namespaces ); - $info[] = $context->msg( 'api-help-param-list' ) - ->params( $multi ? 2 : 1 ) - ->params( $context->getLanguage()->commaList( $namespaces ) ) - ->parse(); - $hintPipeSeparated = false; - // No type message necessary, we have a list of values. - $type = null; - break; - - case 'tags': - $tags = ChangeTags::listExplicitlyDefinedTags(); - $count = count( $tags ); - $info[] = $context->msg( 'api-help-param-list' ) - ->params( $multi ? 2 : 1 ) - ->params( $context->getLanguage()->commaList( $tags ) ) - ->parse(); - $hintPipeSeparated = false; - $type = null; - break; - - case 'limit': - if ( isset( $settings[ApiBase::PARAM_MAX2] ) ) { - $info[] = $context->msg( 'api-help-param-limit2' ) - ->numParams( $settings[ApiBase::PARAM_MAX] ) - ->numParams( $settings[ApiBase::PARAM_MAX2] ) - ->parse(); - } else { - $info[] = $context->msg( 'api-help-param-limit' ) - ->numParams( $settings[ApiBase::PARAM_MAX] ) - ->parse(); - } - break; - - case 'integer': - // Possible messages: - // api-help-param-integer-min, - // api-help-param-integer-max, - // api-help-param-integer-minmax - $suffix = ''; - $min = $max = 0; - if ( isset( $settings[ApiBase::PARAM_MIN] ) ) { - $suffix .= 'min'; - $min = $settings[ApiBase::PARAM_MIN]; - } - if ( isset( $settings[ApiBase::PARAM_MAX] ) ) { - $suffix .= 'max'; - $max = $settings[ApiBase::PARAM_MAX]; - } - if ( $suffix !== '' ) { - $info[] = - $context->msg( "api-help-param-integer-$suffix" ) - ->params( $multi ? 2 : 1 ) - ->numParams( $min, $max ) - ->parse(); - } - break; - - case 'upload': - $info[] = $context->msg( 'api-help-param-upload' ) - ->parse(); - // No type message necessary, api-help-param-upload should handle it. - $type = null; - break; - - case 'string': - case 'text': - // Displaying a type message here would be useless. - $type = null; - break; - } - } - - // Add type. Messages for grep: api-help-param-type-limit - // api-help-param-type-integer api-help-param-type-boolean - // api-help-param-type-timestamp api-help-param-type-user - // api-help-param-type-password - if ( is_string( $type ) ) { - $msg = $context->msg( "api-help-param-type-$type" ); - if ( !$msg->isDisabled() ) { - $info[] = $msg->params( $multi ? 2 : 1 )->parse(); - } - } - - if ( $multi ) { - $extra = []; - $lowcount = !empty( $settings[ApiBase::PARAM_ISMULTI_LIMIT1] ) - ? $settings[ApiBase::PARAM_ISMULTI_LIMIT1] - : ApiBase::LIMIT_SML1; - $highcount = !empty( $settings[ApiBase::PARAM_ISMULTI_LIMIT2] ) - ? $settings[ApiBase::PARAM_ISMULTI_LIMIT2] - : ApiBase::LIMIT_SML2; - - if ( $hintPipeSeparated ) { - $extra[] = $context->msg( 'api-help-param-multi-separate' )->parse(); - } - if ( $count > $lowcount ) { - if ( $lowcount === $highcount ) { - $msg = $context->msg( 'api-help-param-multi-max-simple' ) - ->numParams( $lowcount ); - } else { - $msg = $context->msg( 'api-help-param-multi-max' ) - ->numParams( $lowcount, $highcount ); - } - $extra[] = $msg->parse(); - } - if ( $extra ) { - $info[] = implode( ' ', $extra ); - } - - $allowAll = $settings[ApiBase::PARAM_ALL] ?? false; - if ( $allowAll || $settings[ApiBase::PARAM_TYPE] === 'namespace' ) { - if ( $settings[ApiBase::PARAM_TYPE] === 'namespace' ) { - $allSpecifier = ApiBase::ALL_DEFAULT_STRING; - } else { - $allSpecifier = ( is_string( $allowAll ) ? $allowAll : ApiBase::ALL_DEFAULT_STRING ); - } - $info[] = $context->msg( 'api-help-param-multi-all' ) - ->params( $allSpecifier ) - ->parse(); - } - } - } - - if ( isset( $settings[self::PARAM_MAX_BYTES] ) ) { - $info[] = $context->msg( 'api-help-param-maxbytes' ) - ->numParams( $settings[self::PARAM_MAX_BYTES] ); - } - if ( isset( $settings[self::PARAM_MAX_CHARS] ) ) { - $info[] = $context->msg( 'api-help-param-maxchars' ) - ->numParams( $settings[self::PARAM_MAX_CHARS] ); - } - - // Add default - $default = $settings[ApiBase::PARAM_DFLT] ?? null; - if ( $default === '' ) { - $info[] = $context->msg( 'api-help-param-default-empty' ) - ->parse(); - } elseif ( $default !== null && $default !== false ) { - // We can't know whether this contains LTR or RTL text. - $info[] = $context->msg( 'api-help-param-default' ) - ->params( Html::element( 'span', [ 'dir' => 'auto' ], $default ) ) - ->parse(); - } - - if ( !array_filter( $description ) ) { - $description = [ self::wrap( - $context->msg( 'api-help-param-no-description' ), - 'apihelp-empty' - ) ]; - } - - // Add "deprecated" flag - if ( !empty( $settings[ApiBase::PARAM_DEPRECATED] ) ) { - $help['parameters'] .= Html::openElement( 'dd', - [ 'class' => 'info' ] ); - $help['parameters'] .= self::wrap( - $context->msg( 'api-help-param-deprecated' ), - 'apihelp-deprecated', 'strong' - ); - $help['parameters'] .= Html::closeElement( 'dd' ); - } - - if ( $description ) { - $description = implode( '', $description ); - $description = preg_replace( '!\s*</([oud]l)>\s*<\1>\s*!', "\n", $description ); - $help['parameters'] .= Html::rawElement( 'dd', - [ 'class' => 'description' ], $description ); + foreach ( $paramHelp as $m ) { + $m->setContext( $context ); + $info[] = $m; } foreach ( $info as $i ) { diff --git a/includes/api/ApiMain.php b/includes/api/ApiMain.php index c35f10a7f362..c22075f16ba4 100644 --- a/includes/api/ApiMain.php +++ b/includes/api/ApiMain.php @@ -21,8 +21,10 @@ * @defgroup API API */ +use MediaWiki\Api\Validator\ApiParamValidator; use MediaWiki\Logger\LoggerFactory; use MediaWiki\MediaWikiServices; +use MediaWiki\ParamValidator\TypeDef\UserDef; use MediaWiki\Session\SessionManager; use Wikimedia\Timestamp\TimestampException; @@ -144,7 +146,7 @@ class ApiMain extends ApiBase { */ private $mPrinter; - private $mModuleMgr, $mResult, $mErrorFormatter = null; + private $mModuleMgr, $mResult, $mErrorFormatter = null, $mParamValidator; /** @var ApiContinuationManager|null */ private $mContinuationManager; private $mAction; @@ -237,6 +239,10 @@ class ApiMain extends ApiBase { } } + $this->mParamValidator = new ApiParamValidator( + $this, MediaWikiServices::getInstance()->getObjectFactory() + ); + $this->mResult = new ApiResult( $this->getConfig()->get( 'APIMaxResultSize' ) ); // Setup uselang. This doesn't use $this->getParameter() @@ -383,6 +389,14 @@ class ApiMain extends ApiBase { } /** + * Get the parameter validator + * @return ApiParamValidator + */ + public function getParamValidator() : ApiParamValidator { + return $this->mParamValidator; + } + + /** * Get the API module object. Only works after executeAction() * * @return ApiBase @@ -1788,7 +1802,8 @@ class ApiMain extends ApiBase { * @return bool */ public function getCheck( $name ) { - return $this->getVal( $name, null ) !== null; + $this->mParamsUsed[$name] = true; + return $this->getRequest()->getCheck( $name ); } /** @@ -1888,6 +1903,7 @@ class ApiMain extends ApiBase { ], 'assertuser' => [ ApiBase::PARAM_TYPE => 'user', + UserDef::PARAM_ALLOWED_USER_TYPES => [ 'name' ], ], 'requestid' => null, 'servedby' => false, @@ -1990,7 +2006,25 @@ class ApiMain extends ApiBase { $headline = '<div id="main/datatypes"></div>' . $headline; } $help['datatypes'] .= $headline; - $help['datatypes'] .= $this->msg( 'api-help-datatypes' )->parseAsBlock(); + $help['datatypes'] .= $this->msg( 'api-help-datatypes-top' )->parseAsBlock(); + $help['datatypes'] .= '<dl>'; + foreach ( $this->getParamValidator()->knownTypes() as $type ) { + $m = $this->msg( "api-help-datatype-$type" ); + if ( !$m->isDisabled() ) { + $id = "main/datatype/$type"; + $help['datatypes'] .= '<dt id="' . htmlspecialchars( $id ) . '">'; + $encId = Sanitizer::escapeIdForAttribute( $id, Sanitizer::ID_PRIMARY ); + if ( $encId !== $id ) { + $help['datatypes'] .= '<span id="' . htmlspecialchars( $encId ) . '"></span>'; + } + $encId2 = Sanitizer::escapeIdForAttribute( $id, Sanitizer::ID_FALLBACK ); + if ( $encId2 !== $id && $encId2 !== $encId ) { + $help['datatypes'] .= '<span id="' . htmlspecialchars( $encId2 ) . '"></span>'; + } + $help['datatypes'] .= htmlspecialchars( $type ) . '</dt><dd>' . $m->parseAsBlock() . "</dd>"; + } + } + $help['datatypes'] .= '</dl>'; if ( !isset( $tocData['main/datatypes'] ) ) { $tocnumber[$level]++; $tocData['main/datatypes'] = [ diff --git a/includes/api/ApiPageSet.php b/includes/api/ApiPageSet.php index c2f1a43b2e5a..f637cb1cb3e6 100644 --- a/includes/api/ApiPageSet.php +++ b/includes/api/ApiPageSet.php @@ -19,7 +19,9 @@ * * @file */ + use MediaWiki\MediaWikiServices; +use Wikimedia\ParamValidator\ParamValidator; use Wikimedia\Rdbms\IDatabase; use Wikimedia\Rdbms\IResultWrapper; @@ -1471,15 +1473,15 @@ class ApiPageSet extends ApiBase { return $result; } - protected function handleParamNormalization( $paramName, $value, $rawValue ) { + public function handleParamNormalization( $paramName, $value, $rawValue ) { parent::handleParamNormalization( $paramName, $value, $rawValue ); if ( $paramName === 'titles' ) { // For the 'titles' parameter, we want to split it like ApiBase would // and add any changed titles to $this->mNormalizedTitles - $value = $this->explodeMultiValue( $value, self::LIMIT_SML2 + 1 ); + $value = ParamValidator::explodeMultiValue( $value, self::LIMIT_SML2 + 1 ); $l = count( $value ); - $rawValue = $this->explodeMultiValue( $rawValue, $l ); + $rawValue = ParamValidator::explodeMultiValue( $rawValue, $l ); for ( $i = 0; $i < $l; $i++ ) { if ( $value[$i] !== $rawValue[$i] ) { $this->mNormalizedTitles[$rawValue[$i]] = $value[$i]; diff --git a/includes/api/ApiParamInfo.php b/includes/api/ApiParamInfo.php index a8fe8334a7bf..86a8769db822 100644 --- a/includes/api/ApiParamInfo.php +++ b/includes/api/ApiParamInfo.php @@ -238,6 +238,7 @@ class ApiParamInfo extends ApiBase { private function getModuleInfo( $module ) { $ret = []; $path = $module->getModulePath(); + $paramValidator = $module->getMain()->getParamValidator(); $ret['name'] = $module->getModuleName(); $ret['classname'] = get_class( $module ); @@ -310,9 +311,7 @@ class ApiParamInfo extends ApiBase { $paramDesc = $module->getFinalParamDescription(); $index = 0; foreach ( $params as $name => $settings ) { - if ( !is_array( $settings ) ) { - $settings = [ ApiBase::PARAM_DFLT => $settings ]; - } + $settings = $paramValidator->normalizeSettings( $settings ); $item = [ 'index' => ++$index, @@ -328,175 +327,20 @@ class ApiParamInfo extends ApiBase { $this->formatHelpMessages( $item, 'description', $paramDesc[$name], true ); } - $item['required'] = !empty( $settings[ApiBase::PARAM_REQUIRED] ); - - if ( !empty( $settings[ApiBase::PARAM_DEPRECATED] ) ) { - $item['deprecated'] = true; + foreach ( $paramValidator->getParamInfo( $module, $name, $settings, [] ) as $k => $v ) { + $item[$k] = $v; } if ( $name === 'token' && $module->needsToken() ) { $item['tokentype'] = $module->needsToken(); } - if ( !isset( $settings[ApiBase::PARAM_TYPE] ) ) { - $dflt = $settings[ApiBase::PARAM_DFLT] ?? null; - if ( is_bool( $dflt ) ) { - $settings[ApiBase::PARAM_TYPE] = 'boolean'; - } elseif ( is_string( $dflt ) || $dflt === null ) { - $settings[ApiBase::PARAM_TYPE] = 'string'; - } elseif ( is_int( $dflt ) ) { - $settings[ApiBase::PARAM_TYPE] = 'integer'; - } - } - - if ( isset( $settings[ApiBase::PARAM_DFLT] ) ) { - switch ( $settings[ApiBase::PARAM_TYPE] ) { - case 'boolean': - $item['default'] = (bool)$settings[ApiBase::PARAM_DFLT]; - break; - case 'string': - case 'text': - case 'password': - $item['default'] = strval( $settings[ApiBase::PARAM_DFLT] ); - break; - case 'integer': - case 'limit': - $item['default'] = (int)$settings[ApiBase::PARAM_DFLT]; - break; - case 'timestamp': - $item['default'] = wfTimestamp( TS_ISO_8601, $settings[ApiBase::PARAM_DFLT] ); - break; - default: - $item['default'] = $settings[ApiBase::PARAM_DFLT]; - break; - } - } - - $item['multi'] = !empty( $settings[ApiBase::PARAM_ISMULTI] ); - if ( $item['multi'] ) { - $item['lowlimit'] = !empty( $settings[ApiBase::PARAM_ISMULTI_LIMIT1] ) - ? $settings[ApiBase::PARAM_ISMULTI_LIMIT1] - : ApiBase::LIMIT_SML1; - $item['highlimit'] = !empty( $settings[ApiBase::PARAM_ISMULTI_LIMIT2] ) - ? $settings[ApiBase::PARAM_ISMULTI_LIMIT2] - : ApiBase::LIMIT_SML2; - $item['limit'] = $this->getMain()->canApiHighLimits() - ? $item['highlimit'] - : $item['lowlimit']; - } - - if ( !empty( $settings[ApiBase::PARAM_ALLOW_DUPLICATES] ) ) { - $item['allowsduplicates'] = true; - } - - if ( isset( $settings[ApiBase::PARAM_TYPE] ) ) { - if ( $settings[ApiBase::PARAM_TYPE] === 'submodule' ) { - if ( isset( $settings[ApiBase::PARAM_SUBMODULE_MAP] ) ) { - $item['type'] = array_keys( $settings[ApiBase::PARAM_SUBMODULE_MAP] ); - $item['submodules'] = $settings[ApiBase::PARAM_SUBMODULE_MAP]; - } else { - $item['type'] = $module->getModuleManager()->getNames( $name ); - $prefix = $module->isMain() - ? '' : ( $module->getModulePath() . '+' ); - $item['submodules'] = []; - foreach ( $item['type'] as $v ) { - $item['submodules'][$v] = $prefix . $v; - } - } - if ( isset( $settings[ApiBase::PARAM_SUBMODULE_PARAM_PREFIX] ) ) { - $item['submoduleparamprefix'] = $settings[ApiBase::PARAM_SUBMODULE_PARAM_PREFIX]; - } - - $submoduleFlags = []; // for sorting: higher flags are sorted later - $submoduleNames = []; // for sorting: lexicographical, ascending - foreach ( $item['submodules'] as $v => $submodulePath ) { - try { - $submod = $this->getModuleFromPath( $submodulePath ); - } catch ( ApiUsageException $ex ) { - $submoduleFlags[] = 0; - $submoduleNames[] = $v; - continue; - } - $flags = 0; - if ( $submod && $submod->isDeprecated() ) { - $item['deprecatedvalues'][] = $v; - $flags |= 1; - } - if ( $submod && $submod->isInternal() ) { - $item['internalvalues'][] = $v; - $flags |= 2; - } - $submoduleFlags[] = $flags; - $submoduleNames[] = $v; - } - // sort $item['submodules'] and $item['type'] by $submoduleFlags and $submoduleNames - array_multisort( $submoduleFlags, $submoduleNames, $item['submodules'], $item['type'] ); - if ( isset( $item['deprecatedvalues'] ) ) { - sort( $item['deprecatedvalues'] ); - } - if ( isset( $item['internalvalues'] ) ) { - sort( $item['internalvalues'] ); - } - } elseif ( $settings[ApiBase::PARAM_TYPE] === 'tags' ) { - $item['type'] = ChangeTags::listExplicitlyDefinedTags(); - } else { - $item['type'] = $settings[ApiBase::PARAM_TYPE]; - } - if ( is_array( $item['type'] ) ) { - // To prevent sparse arrays from being serialized to JSON as objects - $item['type'] = array_values( $item['type'] ); - ApiResult::setIndexedTagName( $item['type'], 't' ); - } - - // Add 'allspecifier' if applicable - if ( $item['type'] === 'namespace' ) { - $allowAll = true; - $allSpecifier = ApiBase::ALL_DEFAULT_STRING; - } else { - $allowAll = $settings[ApiBase::PARAM_ALL] ?? false; - $allSpecifier = ( is_string( $allowAll ) ? $allowAll : ApiBase::ALL_DEFAULT_STRING ); - } - if ( $allowAll && $item['multi'] && - ( is_array( $item['type'] ) || $item['type'] === 'namespace' ) ) { - $item['allspecifier'] = $allSpecifier; - } - - if ( $item['type'] === 'namespace' && - isset( $settings[ApiBase::PARAM_EXTRA_NAMESPACES] ) && - is_array( $settings[ApiBase::PARAM_EXTRA_NAMESPACES] ) - ) { - $item['extranamespaces'] = $settings[ApiBase::PARAM_EXTRA_NAMESPACES]; - ApiResult::setArrayType( $item['extranamespaces'], 'array' ); - ApiResult::setIndexedTagName( $item['extranamespaces'], 'ns' ); - } - } - if ( isset( $settings[ApiBase::PARAM_MAX] ) ) { - $item['max'] = $settings[ApiBase::PARAM_MAX]; - } - if ( isset( $settings[ApiBase::PARAM_MAX2] ) ) { - $item['highmax'] = $settings[ApiBase::PARAM_MAX2]; - } - if ( isset( $settings[ApiBase::PARAM_MIN] ) ) { - $item['min'] = $settings[ApiBase::PARAM_MIN]; - } - if ( !empty( $settings[ApiBase::PARAM_RANGE_ENFORCE] ) ) { - $item['enforcerange'] = true; - } - if ( isset( $settings[self::PARAM_MAX_BYTES] ) ) { - $item['maxbytes'] = $settings[self::PARAM_MAX_BYTES]; - } - if ( isset( $settings[self::PARAM_MAX_CHARS] ) ) { - $item['maxchars'] = $settings[self::PARAM_MAX_CHARS]; - } - if ( !empty( $settings[ApiBase::PARAM_DEPRECATED_VALUES] ) ) { - $deprecatedValues = array_keys( $settings[ApiBase::PARAM_DEPRECATED_VALUES] ); - if ( is_array( $item['type'] ) ) { - $deprecatedValues = array_intersect( $deprecatedValues, $item['type'] ); - } - if ( $deprecatedValues ) { - $item['deprecatedvalues'] = array_values( $deprecatedValues ); - ApiResult::setIndexedTagName( $item['deprecatedvalues'], 'v' ); - } + if ( $item['type'] === 'NULL' ) { + // Munge "NULL" to "string" for historical reasons + $item['type'] = 'string'; + } elseif ( is_array( $item['type'] ) ) { + // Set indexed tag name, for historical reasons + ApiResult::setIndexedTagName( $item['type'], 't' ); } if ( !empty( $settings[ApiBase::PARAM_HELP_MSG_INFO] ) ) { diff --git a/includes/api/ApiQueryAllDeletedRevisions.php b/includes/api/ApiQueryAllDeletedRevisions.php index f8f1acb08b75..e709141a8956 100644 --- a/includes/api/ApiQueryAllDeletedRevisions.php +++ b/includes/api/ApiQueryAllDeletedRevisions.php @@ -24,6 +24,7 @@ */ use MediaWiki\MediaWikiServices; +use MediaWiki\ParamValidator\TypeDef\UserDef; use MediaWiki\Revision\RevisionRecord; use MediaWiki\Storage\NameTableAccessException; @@ -224,14 +225,14 @@ class ApiQueryAllDeletedRevisions extends ApiQueryRevisionsBase { if ( $params['user'] !== null ) { // Don't query by user ID here, it might be able to use the ar_usertext_timestamp index. $actorQuery = ActorMigration::newMigration() - ->getWhere( $db, 'ar_user', User::newFromName( $params['user'], false ), false ); + ->getWhere( $db, 'ar_user', $params['user'], false ); $this->addTables( $actorQuery['tables'] ); $this->addJoinConds( $actorQuery['joins'] ); $this->addWhere( $actorQuery['conds'] ); } elseif ( $params['excludeuser'] !== null ) { // Here there's no chance of using ar_usertext_timestamp. $actorQuery = ActorMigration::newMigration() - ->getWhere( $db, 'ar_user', User::newFromName( $params['excludeuser'], false ) ); + ->getWhere( $db, 'ar_user', $params['excludeuser'] ); $this->addTables( $actorQuery['tables'] ); $this->addJoinConds( $actorQuery['joins'] ); $this->addWhere( 'NOT(' . $actorQuery['conds'] . ')' ); @@ -402,7 +403,9 @@ class ApiQueryAllDeletedRevisions extends ApiQueryRevisionsBase { public function getAllowedParams() { $ret = parent::getAllowedParams() + [ 'user' => [ - ApiBase::PARAM_TYPE => 'user' + ApiBase::PARAM_TYPE => 'user', + UserDef::PARAM_ALLOWED_USER_TYPES => [ 'name', 'ip', 'id', 'interwiki' ], + UserDef::PARAM_RETURN_OBJECT => true, ], 'namespace' => [ ApiBase::PARAM_ISMULTI => true, @@ -435,6 +438,8 @@ class ApiQueryAllDeletedRevisions extends ApiQueryRevisionsBase { ], 'excludeuser' => [ ApiBase::PARAM_TYPE => 'user', + UserDef::PARAM_ALLOWED_USER_TYPES => [ 'name', 'ip', 'id', 'interwiki' ], + UserDef::PARAM_RETURN_OBJECT => true, ApiBase::PARAM_HELP_MSG_INFO => [ [ 'nonuseronly' ] ], ], 'tag' => null, diff --git a/includes/api/ApiQueryAllImages.php b/includes/api/ApiQueryAllImages.php index 747e602b0d2d..489293fdcf64 100644 --- a/includes/api/ApiQueryAllImages.php +++ b/includes/api/ApiQueryAllImages.php @@ -24,6 +24,7 @@ * @file */ +use MediaWiki\ParamValidator\TypeDef\UserDef; use Wikimedia\Rdbms\IDatabase; /** @@ -192,7 +193,7 @@ class ApiQueryAllImages extends ApiQueryGeneratorBase { // Image filters if ( $params['user'] !== null ) { $actorQuery = ActorMigration::newMigration() - ->getWhere( $db, 'img_user', User::newFromName( $params['user'], false ) ); + ->getWhere( $db, 'img_user', $params['user'] ); $this->addTables( $actorQuery['tables'] ); $this->addJoinConds( $actorQuery['joins'] ); $this->addWhere( $actorQuery['conds'] ); @@ -372,7 +373,9 @@ class ApiQueryAllImages extends ApiQueryGeneratorBase { 'sha1' => null, 'sha1base36' => null, 'user' => [ - ApiBase::PARAM_TYPE => 'user' + ApiBase::PARAM_TYPE => 'user', + UserDef::PARAM_ALLOWED_USER_TYPES => [ 'name', 'ip', 'id', 'interwiki' ], + UserDef::PARAM_RETURN_OBJECT => true, ], 'filterbots' => [ ApiBase::PARAM_DFLT => 'all', diff --git a/includes/api/ApiQueryAllRevisions.php b/includes/api/ApiQueryAllRevisions.php index e3948331b6f4..d4392f65d9ec 100644 --- a/includes/api/ApiQueryAllRevisions.php +++ b/includes/api/ApiQueryAllRevisions.php @@ -21,6 +21,7 @@ */ use MediaWiki\MediaWikiServices; +use MediaWiki\ParamValidator\TypeDef\UserDef; use MediaWiki\Revision\RevisionRecord; /** @@ -140,11 +141,11 @@ class ApiQueryAllRevisions extends ApiQueryRevisionsBase { if ( $params['user'] !== null ) { $actorQuery = ActorMigration::newMigration() - ->getWhere( $db, 'rev_user', User::newFromName( $params['user'], false ) ); + ->getWhere( $db, 'rev_user', $params['user'] ); $this->addWhere( $actorQuery['conds'] ); } elseif ( $params['excludeuser'] !== null ) { $actorQuery = ActorMigration::newMigration() - ->getWhere( $db, 'rev_user', User::newFromName( $params['excludeuser'], false ) ); + ->getWhere( $db, 'rev_user', $params['excludeuser'] ); $this->addWhere( 'NOT(' . $actorQuery['conds'] . ')' ); } @@ -265,6 +266,8 @@ class ApiQueryAllRevisions extends ApiQueryRevisionsBase { $ret = parent::getAllowedParams() + [ 'user' => [ ApiBase::PARAM_TYPE => 'user', + UserDef::PARAM_ALLOWED_USER_TYPES => [ 'name', 'ip', 'id', 'interwiki' ], + UserDef::PARAM_RETURN_OBJECT => true, ], 'namespace' => [ ApiBase::PARAM_ISMULTI => true, @@ -287,6 +290,8 @@ class ApiQueryAllRevisions extends ApiQueryRevisionsBase { ], 'excludeuser' => [ ApiBase::PARAM_TYPE => 'user', + UserDef::PARAM_ALLOWED_USER_TYPES => [ 'name', 'ip', 'id', 'interwiki' ], + UserDef::PARAM_RETURN_OBJECT => true, ], 'continue' => [ ApiBase::PARAM_HELP_MSG => 'api-help-param-continue', diff --git a/includes/api/ApiQueryBacklinks.php b/includes/api/ApiQueryBacklinks.php index 85dfee2e6eb3..5c83a669bf75 100644 --- a/includes/api/ApiQueryBacklinks.php +++ b/includes/api/ApiQueryBacklinks.php @@ -20,6 +20,9 @@ * @file */ +use Wikimedia\ParamValidator\ParamValidator; +use Wikimedia\ParamValidator\TypeDef\IntegerDef; + /** * This is a three-in-one module to query: * * backlinks - links pointing to the given page, @@ -352,8 +355,15 @@ class ApiQueryBacklinks extends ApiQueryGeneratorBase { $this->params['limit'] = $this->getMain()->canApiHighLimits() ? $botMax : $userMax; $result->addParsedLimit( $this->getModuleName(), $this->params['limit'] ); } else { - $this->params['limit'] = (int)$this->params['limit']; - $this->validateLimit( 'limit', $this->params['limit'], 1, $userMax, $botMax ); + $this->params['limit'] = $this->getMain()->getParamValidator()->validateValue( + $this, 'limit', (int)$this->params['limit'], [ + ParamValidator::PARAM_TYPE => 'limit', + IntegerDef::PARAM_MIN => 1, + IntegerDef::PARAM_MAX => $userMax, + IntegerDef::PARAM_MAX2 => $botMax, + IntegerDef::PARAM_IGNORE_RANGE => true, + ] + ); } $this->rootTitle = $this->getTitleFromTitleOrPageId( $this->params ); diff --git a/includes/api/ApiQueryBlocks.php b/includes/api/ApiQueryBlocks.php index 5c751eaf2632..c6072e0ed894 100644 --- a/includes/api/ApiQueryBlocks.php +++ b/includes/api/ApiQueryBlocks.php @@ -21,6 +21,7 @@ */ use MediaWiki\MediaWikiServices; +use MediaWiki\ParamValidator\TypeDef\UserDef; use Wikimedia\IPUtils; use Wikimedia\Rdbms\IResultWrapper; @@ -351,6 +352,7 @@ class ApiQueryBlocks extends ApiQueryBase { ], 'users' => [ ApiBase::PARAM_TYPE => 'user', + UserDef::PARAM_ALLOWED_USER_TYPES => [ 'name', 'ip', 'cidr' ], ApiBase::PARAM_ISMULTI => true ], 'ip' => [ diff --git a/includes/api/ApiQueryDeletedRevisions.php b/includes/api/ApiQueryDeletedRevisions.php index 6eabbbad179a..4590463f2aaa 100644 --- a/includes/api/ApiQueryDeletedRevisions.php +++ b/includes/api/ApiQueryDeletedRevisions.php @@ -24,6 +24,7 @@ */ use MediaWiki\MediaWikiServices; +use MediaWiki\ParamValidator\TypeDef\UserDef; use MediaWiki\Revision\RevisionRecord; use MediaWiki\Storage\NameTableAccessException; @@ -120,14 +121,14 @@ class ApiQueryDeletedRevisions extends ApiQueryRevisionsBase { if ( $params['user'] !== null ) { // Don't query by user ID here, it might be able to use the ar_usertext_timestamp index. $actorQuery = ActorMigration::newMigration() - ->getWhere( $db, 'ar_user', User::newFromName( $params['user'], false ), false ); + ->getWhere( $db, 'ar_user', $params['user'], false ); $this->addTables( $actorQuery['tables'] ); $this->addJoinConds( $actorQuery['joins'] ); $this->addWhere( $actorQuery['conds'] ); } elseif ( $params['excludeuser'] !== null ) { // Here there's no chance of using ar_usertext_timestamp. $actorQuery = ActorMigration::newMigration() - ->getWhere( $db, 'ar_user', User::newFromName( $params['excludeuser'], false ) ); + ->getWhere( $db, 'ar_user', $params['excludeuser'] ); $this->addTables( $actorQuery['tables'] ); $this->addJoinConds( $actorQuery['joins'] ); $this->addWhere( 'NOT(' . $actorQuery['conds'] . ')' ); @@ -277,10 +278,14 @@ class ApiQueryDeletedRevisions extends ApiQueryRevisionsBase { ], 'tag' => null, 'user' => [ - ApiBase::PARAM_TYPE => 'user' + ApiBase::PARAM_TYPE => 'user', + UserDef::PARAM_ALLOWED_USER_TYPES => [ 'name', 'ip', 'id', 'interwiki' ], + UserDef::PARAM_RETURN_OBJECT => true, ], 'excludeuser' => [ - ApiBase::PARAM_TYPE => 'user' + ApiBase::PARAM_TYPE => 'user', + UserDef::PARAM_ALLOWED_USER_TYPES => [ 'name', 'ip', 'id', 'interwiki' ], + UserDef::PARAM_RETURN_OBJECT => true, ], 'continue' => [ ApiBase::PARAM_HELP_MSG => 'api-help-param-continue', diff --git a/includes/api/ApiQueryDeletedrevs.php b/includes/api/ApiQueryDeletedrevs.php index 6b3cdf372c4e..01d48bc9bd13 100644 --- a/includes/api/ApiQueryDeletedrevs.php +++ b/includes/api/ApiQueryDeletedrevs.php @@ -21,9 +21,12 @@ */ use MediaWiki\MediaWikiServices; +use MediaWiki\ParamValidator\TypeDef\UserDef; use MediaWiki\Revision\RevisionRecord; use MediaWiki\Revision\SlotRecord; use MediaWiki\Storage\NameTableAccessException; +use Wikimedia\ParamValidator\ParamValidator; +use Wikimedia\ParamValidator\TypeDef\IntegerDef; /** * Query module to enumerate all deleted revisions. @@ -146,7 +149,15 @@ class ApiQueryDeletedrevs extends ApiQueryBase { $this->getResult()->addParsedLimit( $this->getModuleName(), $limit ); } - $this->validateLimit( 'limit', $limit, 1, $userMax, $botMax ); + $limit = $this->getMain()->getParamValidator()->validateValue( + $this, 'limit', $limit, [ + ParamValidator::PARAM_TYPE => 'limit', + IntegerDef::PARAM_MIN => 1, + IntegerDef::PARAM_MAX => $userMax, + IntegerDef::PARAM_MAX2 => $botMax, + IntegerDef::PARAM_IGNORE_RANGE => true, + ] + ); if ( $fld_token ) { // Undelete tokens are identical for all pages, so we cache one here @@ -181,14 +192,14 @@ class ApiQueryDeletedrevs extends ApiQueryBase { if ( $params['user'] !== null ) { // Don't query by user ID here, it might be able to use the ar_usertext_timestamp index. $actorQuery = ActorMigration::newMigration() - ->getWhere( $db, 'ar_user', User::newFromName( $params['user'], false ), false ); + ->getWhere( $db, 'ar_user', $params['user'], false ); $this->addTables( $actorQuery['tables'] ); $this->addJoinConds( $actorQuery['joins'] ); $this->addWhere( $actorQuery['conds'] ); } elseif ( $params['excludeuser'] !== null ) { // Here there's no chance of using ar_usertext_timestamp. $actorQuery = ActorMigration::newMigration() - ->getWhere( $db, 'ar_user', User::newFromName( $params['excludeuser'], false ) ); + ->getWhere( $db, 'ar_user', $params['excludeuser'] ); $this->addTables( $actorQuery['tables'] ); $this->addJoinConds( $actorQuery['joins'] ); $this->addWhere( 'NOT(' . $actorQuery['conds'] . ')' ); @@ -442,10 +453,14 @@ class ApiQueryDeletedrevs extends ApiQueryBase { ], 'tag' => null, 'user' => [ - ApiBase::PARAM_TYPE => 'user' + ApiBase::PARAM_TYPE => 'user', + UserDef::PARAM_ALLOWED_USER_TYPES => [ 'name', 'ip', 'id', 'interwiki' ], + UserDef::PARAM_RETURN_OBJECT => true, ], 'excludeuser' => [ - ApiBase::PARAM_TYPE => 'user' + ApiBase::PARAM_TYPE => 'user', + UserDef::PARAM_ALLOWED_USER_TYPES => [ 'name', 'ip', 'id', 'interwiki' ], + UserDef::PARAM_RETURN_OBJECT => true, ], 'prop' => [ ApiBase::PARAM_DFLT => 'user|comment', diff --git a/includes/api/ApiQueryLogEvents.php b/includes/api/ApiQueryLogEvents.php index 02b9812d0c42..d06f154f58b3 100644 --- a/includes/api/ApiQueryLogEvents.php +++ b/includes/api/ApiQueryLogEvents.php @@ -21,6 +21,7 @@ */ use MediaWiki\MediaWikiServices; +use MediaWiki\ParamValidator\TypeDef\UserDef; use MediaWiki\Storage\NameTableAccessException; /** @@ -179,9 +180,7 @@ class ApiQueryLogEvents extends ApiQueryBase { if ( $user !== null ) { // Note the joins in $q are the same as those from ->getJoin() above // so we only need to add 'conds' here. - $q = $actorMigration->getWhere( - $db, 'log_user', User::newFromName( $params['user'], false ) - ); + $q = $actorMigration->getWhere( $db, 'log_user', $params['user'] ); $this->addWhere( $q['conds'] ); // T71222: MariaDB's optimizer, at least 10.1.37 and .38, likes to choose a wildly bad plan for @@ -447,6 +446,8 @@ class ApiQueryLogEvents extends ApiQueryBase { ], 'user' => [ ApiBase::PARAM_TYPE => 'user', + UserDef::PARAM_ALLOWED_USER_TYPES => [ 'name', 'ip', 'id', 'interwiki' ], + UserDef::PARAM_RETURN_OBJECT => true, ], 'title' => null, 'namespace' => [ diff --git a/includes/api/ApiQueryRecentChanges.php b/includes/api/ApiQueryRecentChanges.php index 948b57e2b20e..d697948c06b2 100644 --- a/includes/api/ApiQueryRecentChanges.php +++ b/includes/api/ApiQueryRecentChanges.php @@ -21,6 +21,7 @@ */ use MediaWiki\MediaWikiServices; +use MediaWiki\ParamValidator\TypeDef\UserDef; use MediaWiki\Revision\RevisionRecord; use MediaWiki\Storage\NameTableAccessException; @@ -272,7 +273,7 @@ class ApiQueryRecentChanges extends ApiQueryGeneratorBase { if ( $params['user'] !== null ) { // Don't query by user ID here, it might be able to use the rc_user_text index. $actorQuery = ActorMigration::newMigration() - ->getWhere( $this->getDB(), 'rc_user', User::newFromName( $params['user'], false ), false ); + ->getWhere( $this->getDB(), 'rc_user', $params['user'], false ); $this->addTables( $actorQuery['tables'] ); $this->addJoinConds( $actorQuery['joins'] ); $this->addWhere( $actorQuery['conds'] ); @@ -281,7 +282,7 @@ class ApiQueryRecentChanges extends ApiQueryGeneratorBase { if ( $params['excludeuser'] !== null ) { // Here there's no chance to use the rc_user_text index, so allow ID to be used. $actorQuery = ActorMigration::newMigration() - ->getWhere( $this->getDB(), 'rc_user', User::newFromName( $params['excludeuser'], false ) ); + ->getWhere( $this->getDB(), 'rc_user', $params['excludeuser'] ); $this->addTables( $actorQuery['tables'] ); $this->addJoinConds( $actorQuery['joins'] ); $this->addWhere( 'NOT(' . $actorQuery['conds'] . ')' ); @@ -750,10 +751,14 @@ class ApiQueryRecentChanges extends ApiQueryGeneratorBase { ApiBase::PARAM_EXTRA_NAMESPACES => [ NS_MEDIA, NS_SPECIAL ], ], 'user' => [ - ApiBase::PARAM_TYPE => 'user' + ApiBase::PARAM_TYPE => 'user', + UserDef::PARAM_ALLOWED_USER_TYPES => [ 'name', 'ip', 'id', 'interwiki' ], + UserDef::PARAM_RETURN_OBJECT => true, ], 'excludeuser' => [ - ApiBase::PARAM_TYPE => 'user' + ApiBase::PARAM_TYPE => 'user', + UserDef::PARAM_ALLOWED_USER_TYPES => [ 'name', 'ip', 'id', 'interwiki' ], + UserDef::PARAM_RETURN_OBJECT => true, ], 'tag' => null, 'prop' => [ diff --git a/includes/api/ApiQueryRevisions.php b/includes/api/ApiQueryRevisions.php index 49dc8d866576..253e6fc1df20 100644 --- a/includes/api/ApiQueryRevisions.php +++ b/includes/api/ApiQueryRevisions.php @@ -21,6 +21,7 @@ */ use MediaWiki\MediaWikiServices; +use MediaWiki\ParamValidator\TypeDef\UserDef; use MediaWiki\Revision\RevisionRecord; use MediaWiki\Storage\NameTableAccessException; @@ -313,13 +314,13 @@ class ApiQueryRevisions extends ApiQueryRevisionsBase { if ( $params['user'] !== null ) { $actorQuery = ActorMigration::newMigration() - ->getWhere( $db, 'rev_user', User::newFromName( $params['user'], false ) ); + ->getWhere( $db, 'rev_user', $params['user'] ); $this->addTables( $actorQuery['tables'] ); $this->addJoinConds( $actorQuery['joins'] ); $this->addWhere( $actorQuery['conds'] ); } elseif ( $params['excludeuser'] !== null ) { $actorQuery = ActorMigration::newMigration() - ->getWhere( $db, 'rev_user', User::newFromName( $params['excludeuser'], false ) ); + ->getWhere( $db, 'rev_user', $params['excludeuser'] ); $this->addTables( $actorQuery['tables'] ); $this->addJoinConds( $actorQuery['joins'] ); $this->addWhere( 'NOT(' . $actorQuery['conds'] . ')' ); @@ -489,10 +490,14 @@ class ApiQueryRevisions extends ApiQueryRevisionsBase { ], 'user' => [ ApiBase::PARAM_TYPE => 'user', + UserDef::PARAM_ALLOWED_USER_TYPES => [ 'name', 'ip', 'id', 'interwiki' ], + UserDef::PARAM_RETURN_OBJECT => true, ApiBase::PARAM_HELP_MSG_INFO => [ [ 'singlepageonly' ] ], ], 'excludeuser' => [ ApiBase::PARAM_TYPE => 'user', + UserDef::PARAM_ALLOWED_USER_TYPES => [ 'name', 'ip', 'id', 'interwiki' ], + UserDef::PARAM_RETURN_OBJECT => true, ApiBase::PARAM_HELP_MSG_INFO => [ [ 'singlepageonly' ] ], ], 'tag' => null, diff --git a/includes/api/ApiQueryRevisionsBase.php b/includes/api/ApiQueryRevisionsBase.php index 3fbe79457303..41bbcf1f24d7 100644 --- a/includes/api/ApiQueryRevisionsBase.php +++ b/includes/api/ApiQueryRevisionsBase.php @@ -25,6 +25,8 @@ use MediaWiki\MediaWikiServices; use MediaWiki\Revision\RevisionAccessException; use MediaWiki\Revision\RevisionRecord; use MediaWiki\Revision\SlotRecord; +use Wikimedia\ParamValidator\ParamValidator; +use Wikimedia\ParamValidator\TypeDef\IntegerDef; /** * A base class for functions common to producing a list of revisions. @@ -182,10 +184,15 @@ abstract class ApiQueryRevisionsBase extends ApiQueryGeneratorBase { } } - if ( $this->limit === null ) { - $this->limit = 10; - } - $this->validateLimit( 'limit', $this->limit, 1, $userMax, $botMax ); + $this->limit = $this->getMain()->getParamValidator()->validateValue( + $this, 'limit', $this->limit ?? 10, [ + ParamValidator::PARAM_TYPE => 'limit', + IntegerDef::PARAM_MIN => 1, + IntegerDef::PARAM_MAX => $userMax, + IntegerDef::PARAM_MAX2 => $botMax, + IntegerDef::PARAM_IGNORE_RANGE => true, + ] + ); $this->needSlots = $this->fetchContent || $this->fld_contentmodel || $this->fld_slotsize || $this->fld_slotsha1; diff --git a/includes/api/ApiQueryUserContribs.php b/includes/api/ApiQueryUserContribs.php index a5ca2840e6fc..ac8018de4b5c 100644 --- a/includes/api/ApiQueryUserContribs.php +++ b/includes/api/ApiQueryUserContribs.php @@ -21,6 +21,7 @@ */ use MediaWiki\MediaWikiServices; +use MediaWiki\ParamValidator\TypeDef\UserDef; use MediaWiki\Revision\RevisionRecord; use MediaWiki\Storage\NameTableAccessException; @@ -588,6 +589,7 @@ class ApiQueryUserContribs extends ApiQueryBase { ], 'user' => [ ApiBase::PARAM_TYPE => 'user', + UserDef::PARAM_ALLOWED_USER_TYPES => [ 'name', 'ip', 'interwiki' ], ApiBase::PARAM_ISMULTI => true ], 'userids' => [ diff --git a/includes/api/ApiQueryWatchlist.php b/includes/api/ApiQueryWatchlist.php index 5954b5668f3c..d68f72d75118 100644 --- a/includes/api/ApiQueryWatchlist.php +++ b/includes/api/ApiQueryWatchlist.php @@ -21,6 +21,7 @@ */ use MediaWiki\MediaWikiServices; +use MediaWiki\ParamValidator\TypeDef\UserDef; use MediaWiki\Revision\RevisionRecord; /** @@ -447,9 +448,11 @@ class ApiQueryWatchlist extends ApiQueryGeneratorBase { ], 'user' => [ ApiBase::PARAM_TYPE => 'user', + UserDef::PARAM_ALLOWED_USER_TYPES => [ 'name', 'ip', 'id', 'interwiki' ], ], 'excludeuser' => [ ApiBase::PARAM_TYPE => 'user', + UserDef::PARAM_ALLOWED_USER_TYPES => [ 'name', 'ip', 'id', 'interwiki' ], ], 'dir' => [ ApiBase::PARAM_DFLT => 'older', @@ -510,7 +513,8 @@ class ApiQueryWatchlist extends ApiQueryGeneratorBase { ApiBase::PARAM_TYPE => RecentChange::getChangeTypes() ], 'owner' => [ - ApiBase::PARAM_TYPE => 'user' + ApiBase::PARAM_TYPE => 'user', + UserDef::PARAM_ALLOWED_USER_TYPES => [ 'name' ], ], 'token' => [ ApiBase::PARAM_TYPE => 'string', diff --git a/includes/api/ApiQueryWatchlistRaw.php b/includes/api/ApiQueryWatchlistRaw.php index 25724caf7961..e19179a73114 100644 --- a/includes/api/ApiQueryWatchlistRaw.php +++ b/includes/api/ApiQueryWatchlistRaw.php @@ -21,6 +21,7 @@ */ use MediaWiki\MediaWikiServices; +use MediaWiki\ParamValidator\TypeDef\UserDef; /** * This query action allows clients to retrieve a list of pages @@ -181,7 +182,8 @@ class ApiQueryWatchlistRaw extends ApiQueryGeneratorBase { ] ], 'owner' => [ - ApiBase::PARAM_TYPE => 'user' + ApiBase::PARAM_TYPE => 'user', + UserDef::PARAM_ALLOWED_USER_TYPES => [ 'name' ], ], 'token' => [ ApiBase::PARAM_TYPE => 'string', diff --git a/includes/api/ApiResetPassword.php b/includes/api/ApiResetPassword.php index 6f13af212aa4..a9b58e98d83b 100644 --- a/includes/api/ApiResetPassword.php +++ b/includes/api/ApiResetPassword.php @@ -21,6 +21,7 @@ */ use MediaWiki\MediaWikiServices; +use MediaWiki\ParamValidator\TypeDef\UserDef; /** * Reset password, with AuthManager @@ -101,6 +102,7 @@ class ApiResetPassword extends ApiBase { $ret = [ 'user' => [ ApiBase::PARAM_TYPE => 'user', + UserDef::PARAM_ALLOWED_USER_TYPES => [ 'name' ], ], 'email' => [ ApiBase::PARAM_TYPE => 'string', diff --git a/includes/api/ApiRollback.php b/includes/api/ApiRollback.php index 7c313d5cb76d..59c87b3a4af9 100644 --- a/includes/api/ApiRollback.php +++ b/includes/api/ApiRollback.php @@ -20,6 +20,8 @@ * @file */ +use MediaWiki\ParamValidator\TypeDef\UserDef; + /** * @ingroup API */ @@ -117,6 +119,8 @@ class ApiRollback extends ApiBase { ], 'user' => [ ApiBase::PARAM_TYPE => 'user', + UserDef::PARAM_ALLOWED_USER_TYPES => [ 'name', 'ip', 'id', 'interwiki' ], + UserDef::PARAM_RETURN_OBJECT => true, ApiBase::PARAM_REQUIRED => true ], 'summary' => '', @@ -151,13 +155,7 @@ class ApiRollback extends ApiBase { return $this->mUser; } - // We need to be able to revert IPs, but getCanonicalName rejects them - $this->mUser = User::isIP( $params['user'] ) - ? $params['user'] - : User::getCanonicalName( $params['user'] ); - if ( !$this->mUser ) { - $this->dieWithError( [ 'apierror-invaliduser', wfEscapeWikiText( $params['user'] ) ] ); - } + $this->mUser = $params['user']; return $this->mUser; } diff --git a/includes/api/ApiUnblock.php b/includes/api/ApiUnblock.php index 8895c8ab94c9..fc1667bd660d 100644 --- a/includes/api/ApiUnblock.php +++ b/includes/api/ApiUnblock.php @@ -21,6 +21,7 @@ */ use MediaWiki\Block\DatabaseBlock; +use MediaWiki\ParamValidator\TypeDef\UserDef; /** * API module that facilitates the unblocking of users. Requires API write mode @@ -109,9 +110,13 @@ class ApiUnblock extends ApiBase { 'id' => [ ApiBase::PARAM_TYPE => 'integer', ], - 'user' => null, + 'user' => [ + ApiBase::PARAM_TYPE => 'user', + UserDef::PARAM_ALLOWED_USER_TYPES => [ 'name', 'ip', 'cidr', 'id' ], + ], 'userid' => [ - ApiBase::PARAM_TYPE => 'integer' + ApiBase::PARAM_TYPE => 'integer', + ApiBase::PARAM_DEPRECATED => true, ], 'reason' => '', 'tags' => [ diff --git a/includes/api/ApiUsageException.php b/includes/api/ApiUsageException.php index 5d5339388b78..6c5a3465a595 100644 --- a/includes/api/ApiUsageException.php +++ b/includes/api/ApiUsageException.php @@ -34,9 +34,10 @@ class ApiUsageException extends MWException implements ILocalizedException { * @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 + * @param Throwable|null $previous Previous exception */ public function __construct( - ?ApiBase $module, StatusValue $status, $httpCode = 0 + ?ApiBase $module, StatusValue $status, $httpCode = 0, Throwable $previous = null ) { if ( $status->isOK() ) { throw new InvalidArgumentException( __METHOD__ . ' requires a fatal Status' ); @@ -49,7 +50,7 @@ class ApiUsageException extends MWException implements ILocalizedException { // customized by the local wiki. $enMsg = clone $this->getApiMessage(); $enMsg->inLanguage( 'en' )->useDatabase( false ); - parent::__construct( ApiErrorFormatter::stripMarkup( $enMsg->text() ), $httpCode ); + parent::__construct( ApiErrorFormatter::stripMarkup( $enMsg->text() ), $httpCode, $previous ); } /** @@ -58,15 +59,17 @@ class ApiUsageException extends MWException implements ILocalizedException { * @param string|null $code See ApiMessage::create() * @param array|null $data See ApiMessage::create() * @param int $httpCode HTTP error code to use + * @param Throwable|null $previous Previous exception * @return static */ public static function newWithMessage( - ?ApiBase $module, $msg, $code = null, $data = null, $httpCode = 0 + ?ApiBase $module, $msg, $code = null, $data = null, $httpCode = 0, Throwable $previous = null ) { return new static( $module, StatusValue::newFatal( ApiMessage::create( $msg, $code, $data ) ), - $httpCode + $httpCode, + $previous ); } @@ -119,7 +122,8 @@ class ApiUsageException extends MWException implements ILocalizedException { return get_class( $this ) . ": {$enMsg->getApiCode()}: {$text} " . "in {$this->getFile()}:{$this->getLine()}\n" - . "Stack trace:\n{$this->getTraceAsString()}"; + . "Stack trace:\n{$this->getTraceAsString()}" + . $this->getPrevious() ? "\n\nNext {$this->getPrevious()}" : ""; } } diff --git a/includes/api/ApiUserrights.php b/includes/api/ApiUserrights.php index bfb2256b8c9a..ee223872ffb7 100644 --- a/includes/api/ApiUserrights.php +++ b/includes/api/ApiUserrights.php @@ -23,6 +23,8 @@ * @file */ +use MediaWiki\ParamValidator\TypeDef\UserDef; + /** * @ingroup API */ @@ -170,9 +172,12 @@ class ApiUserrights extends ApiBase { $a = [ 'user' => [ ApiBase::PARAM_TYPE => 'user', + UserDef::PARAM_ALLOWED_USER_TYPES => [ 'name', 'id' ], + UserDef::PARAM_RETURN_OBJECT => true, ], 'userid' => [ ApiBase::PARAM_TYPE => 'integer', + ApiBase::PARAM_DEPRECATED => true, ], 'add' => [ ApiBase::PARAM_TYPE => $allGroups, diff --git a/includes/api/ApiValidatePassword.php b/includes/api/ApiValidatePassword.php index c36759ac9d93..3a2fce10a9ce 100644 --- a/includes/api/ApiValidatePassword.php +++ b/includes/api/ApiValidatePassword.php @@ -1,6 +1,7 @@ <?php use MediaWiki\Auth\AuthManager; +use MediaWiki\ParamValidator\TypeDef\UserDef; /** * @ingroup API @@ -61,6 +62,7 @@ class ApiValidatePassword extends ApiBase { ], 'user' => [ ApiBase::PARAM_TYPE => 'user', + UserDef::PARAM_ALLOWED_USER_TYPES => [ 'name', 'id' ], ], 'email' => null, 'realname' => null, diff --git a/includes/api/Validator/ApiParamValidator.php b/includes/api/Validator/ApiParamValidator.php new file mode 100644 index 000000000000..712295d89755 --- /dev/null +++ b/includes/api/Validator/ApiParamValidator.php @@ -0,0 +1,249 @@ +<?php + +namespace MediaWiki\Api\Validator; + +use ApiBase; +use ApiMain; +use ApiMessage; +use ApiUsageException; +use MediaWiki\Message\Converter as MessageConverter; +use MediaWiki\ParamValidator\TypeDef\NamespaceDef; +use MediaWiki\ParamValidator\TypeDef\TagsDef; +use MediaWiki\ParamValidator\TypeDef\UserDef; +use Message; +use Wikimedia\Message\DataMessageValue; +use Wikimedia\Message\MessageValue; +use Wikimedia\ObjectFactory; +use Wikimedia\ParamValidator\ParamValidator; +use Wikimedia\ParamValidator\TypeDef\EnumDef; +use Wikimedia\ParamValidator\TypeDef\IntegerDef; +use Wikimedia\ParamValidator\TypeDef\LimitDef; +use Wikimedia\ParamValidator\TypeDef\PasswordDef; +use Wikimedia\ParamValidator\TypeDef\PresenceBooleanDef; +use Wikimedia\ParamValidator\TypeDef\StringDef; +use Wikimedia\ParamValidator\TypeDef\TimestampDef; +use Wikimedia\ParamValidator\TypeDef\UploadDef; +use Wikimedia\ParamValidator\ValidationException; + +/** + * This wraps a bunch of the API-specific parameter validation logic. + * + * It's intended to be used in ApiMain by composition. + * + * @since 1.35 + * @ingroup API + */ +class ApiParamValidator { + + /** @var ParamValidator */ + private $paramValidator; + + /** @var MessageConverter */ + private $messageConverter; + + /** Type defs for ParamValidator */ + private const TYPE_DEFS = [ + 'boolean' => [ 'class' => PresenceBooleanDef::class ], + 'enum' => [ 'class' => EnumDef::class ], + 'integer' => [ 'class' => IntegerDef::class ], + 'limit' => [ 'class' => LimitDef::class ], + 'namespace' => [ + 'class' => NamespaceDef::class, + 'services' => [ 'NamespaceInfo' ], + ], + 'NULL' => [ + 'class' => StringDef::class, + 'args' => [ [ + 'allowEmptyWhenRequired' => true, + ] ], + ], + 'password' => [ 'class' => PasswordDef::class ], + 'string' => [ 'class' => StringDef::class ], + 'submodule' => [ 'class' => SubmoduleDef::class ], + 'tags' => [ 'class' => TagsDef::class ], + 'text' => [ 'class' => StringDef::class ], + 'timestamp' => [ + 'class' => TimestampDef::class, + 'args' => [ [ + 'defaultFormat' => TS_MW, + ] ], + ], + 'user' => [ 'class' => UserDef::class ], + 'upload' => [ 'class' => UploadDef::class ], + ]; + + /** + * @internal + * @param ApiMain $main + * @param ObjectFactory $objectFactory + */ + public function __construct( ApiMain $main, ObjectFactory $objectFactory ) { + $this->paramValidator = new ParamValidator( + new ApiParamValidatorCallbacks( $main ), + $objectFactory, + [ + 'typeDefs' => self::TYPE_DEFS, + 'ismultiLimits' => [ ApiBase::LIMIT_SML1, ApiBase::LIMIT_SML2 ], + ] + ); + $this->messageConverter = new MessageConverter(); + } + + /** + * List known type names + * @return string[] + */ + public function knownTypes() : array { + return $this->paramValidator->knownTypes(); + } + + /** + * Adjust certain settings where ParamValidator differs from historical Action API behavior + * @param array|mixed $settings + * @return array + */ + public function normalizeSettings( $settings ) : array { + $settings = $this->paramValidator->normalizeSettings( $settings ); + + if ( !isset( $settings[ParamValidator::PARAM_IGNORE_UNRECOGNIZED_VALUES] ) ) { + $settings[ParamValidator::PARAM_IGNORE_UNRECOGNIZED_VALUES] = true; + } + + if ( !isset( $settings[IntegerDef::PARAM_IGNORE_RANGE] ) ) { + $settings[IntegerDef::PARAM_IGNORE_RANGE] = empty( $settings[ApiBase::PARAM_RANGE_ENFORCE] ); + } + + if ( isset( $settings[EnumDef::PARAM_DEPRECATED_VALUES] ) ) { + foreach ( $settings[EnumDef::PARAM_DEPRECATED_VALUES] as &$v ) { + if ( $v === null || $v === true || $v instanceof MessageValue ) { + continue; + } + + // Convert the message specification to a DataMessageValue. Flag in the data + // that it was so converted, so ApiParamValidatorCallbacks::recordCondition() can + // take that into account. + // @phan-suppress-next-line PhanTypeMismatchArgument + $msg = $this->messageConverter->convertMessage( ApiMessage::create( $v ) ); + $v = DataMessageValue::new( + $msg->getKey(), + $msg->getParams(), + 'bogus', + [ '💩' => 'back-compat' ] + ); + } + unset( $v ); + } + + return $settings; + } + + /** + * Convert a ValidationException to an ApiUsageException + * @param ApiBase $module + * @param ValidationException $ex + * @throws ApiUsageException always + */ + private function convertValidationException( ApiBase $module, ValidationException $ex ) : array { + $mv = $ex->getFailureMessage(); + throw ApiUsageException::newWithMessage( + $module, + $this->messageConverter->convertMessageValue( $mv ), + $mv->getCode(), + $mv->getData(), + 0, + $ex + ); + } + + /** + * Get and validate a value + * @param ApiBase $module + * @param string $name Parameter name, unprefixed + * @param array|mixed $settings Default value or an array of settings + * using PARAM_* constants. + * @param array $options Options array + * @return mixed Validated parameter value + * @throws ApiUsageException if the value is invalid + */ + public function getValue( ApiBase $module, string $name, $settings, array $options = [] ) { + $options['module'] = $module; + $name = $module->encodeParamName( $name ); + $settings = $this->normalizeSettings( $settings ); + try { + return $this->paramValidator->getValue( $name, $settings, $options ); + } catch ( ValidationException $ex ) { + $this->convertValidationException( $module, $ex ); + } + } + + /** + * Valiate a parameter value using a settings array + * + * @param ApiBase $module + * @param string $name Parameter name, unprefixed + * @param mixed $value Parameter value + * @param array|mixed $settings Default value or an array of settings + * using PARAM_* constants. + * @param array $options Options array + * @return mixed Validated parameter value(s) + * @throws ApiUsageException if the value is invalid + */ + public function validateValue( + ApiBase $module, string $name, $value, $settings, array $options = [] + ) { + $options['module'] = $module; + $name = $module->encodeParamName( $name ); + $settings = $this->normalizeSettings( $settings ); + try { + return $this->paramValidator->validateValue( $name, $value, $settings, $options ); + } catch ( ValidationException $ex ) { + $this->convertValidationException( $module, $ex ); + } + } + + /** + * Describe parameter settings in a machine-readable format. + * + * @param ApiBase $module + * @param string $name Parameter name. + * @param array|mixed $settings Default value or an array of settings + * using PARAM_* constants. + * @param array $options Options array. + * @return array + */ + public function getParamInfo( ApiBase $module, string $name, $settings, array $options ) : array { + $options['module'] = $module; + $name = $module->encodeParamName( $name ); + return $this->paramValidator->getParamInfo( $name, $settings, $options ); + } + + /** + * Describe parameter settings in human-readable format + * + * @param ApiBase $module + * @param string $name Parameter name being described. + * @param array|mixed $settings Default value or an array of settings + * using PARAM_* constants. + * @param array $options Options array. + * @return Message[] + */ + public function getHelpInfo( ApiBase $module, string $name, $settings, array $options ) : array { + $options['module'] = $module; + $name = $module->encodeParamName( $name ); + + $ret = $this->paramValidator->getHelpInfo( $name, $settings, $options ); + foreach ( $ret as &$m ) { + $k = $m->getKey(); + $m = $this->messageConverter->convertMessageValue( $m ); + if ( substr( $k, 0, 20 ) === 'paramvalidator-help-' ) { + $m = new Message( + [ 'api-help-param-' . substr( $k, 20 ), $k ], + $m->getParams() + ); + } + } + '@phan-var Message[] $ret'; // The above loop converts it + + return $ret; + } +} diff --git a/includes/api/Validator/ApiParamValidatorCallbacks.php b/includes/api/Validator/ApiParamValidatorCallbacks.php new file mode 100644 index 000000000000..9618e1d71fb9 --- /dev/null +++ b/includes/api/Validator/ApiParamValidatorCallbacks.php @@ -0,0 +1,136 @@ +<?php + +namespace MediaWiki\Api\Validator; + +use ApiMain; +use MediaWiki\Message\Converter as MessageConverter; +use Wikimedia\Message\DataMessageValue; +use Wikimedia\ParamValidator\Callbacks; +use Wikimedia\ParamValidator\Util\UploadedFile; + +/** + * ParamValidator callbacks for the Action API + * @since 1.35 + * @ingroup API + */ +class ApiParamValidatorCallbacks implements Callbacks { + + /** @var ApiMain */ + private $apiMain; + + /** @var MessageConverter */ + private $messageConverter; + + /** + * @internal + * @param ApiMain $main + */ + public function __construct( ApiMain $main ) { + $this->apiMain = $main; + $this->messageConverter = new MessageConverter(); + } + + public function hasParam( $name, array $options ) { + return $this->apiMain->getCheck( $name ); + } + + public function getValue( $name, $default, array $options ) { + $value = $this->apiMain->getVal( $name, $default ); + $request = $this->apiMain->getRequest(); + $rawValue = $request->getRawVal( $name ); + + if ( is_string( $rawValue ) ) { + // Preserve U+001F for multi-values + if ( substr( $rawValue, 0, 1 ) === "\x1f" ) { + // This loses the potential checkTitleEncoding() transformation done by + // WebRequest for $_GET. Let's call that a feature. + $value = implode( "\x1f", $request->normalizeUnicode( explode( "\x1f", $rawValue ) ) ); + } + + // Check for NFC normalization, and warn + if ( $rawValue !== $value ) { + $options['module']->handleParamNormalization( $name, $value, $rawValue ); + } + } + + return $value; + } + + public function hasUpload( $name, array $options ) { + return $this->getUploadedFile( $name, $options ) !== null; + } + + public function getUploadedFile( $name, array $options ) { + $upload = $this->apiMain->getUpload( $name ); + if ( !$upload->exists() ) { + return null; + } + return new UploadedFile( [ + 'error' => $upload->getError(), + 'tmp_name' => $upload->getTempName(), + 'size' => $upload->getSize(), + 'name' => $upload->getName(), + 'type' => $upload->getType(), + ] ); + } + + public function recordCondition( + DataMessageValue $message, $name, $value, array $settings, array $options + ) { + $module = $options['module']; + + $code = $message->getCode(); + switch ( $code ) { + case 'param-deprecated': // @codeCoverageIgnore + case 'deprecated-value': // @codeCoverageIgnore + if ( $code === 'param-deprecated' ) { + $feature = $name; + } else { + $feature = $name . '=' . $value; + $data = $message->getData() ?? []; + if ( isset( $data['💩'] ) ) { + // This is from an old-style Message. Strip out ParamValidator's added params. + unset( $data['💩'] ); + $message = DataMessageValue::new( + $message->getKey(), + array_slice( $message->getParams(), 2 ), + $code, + $data + ); + } + } + + $m = $module; + while ( !$m->isMain() ) { + $p = $m->getParent(); + $mName = $m->getModuleName(); + $mParam = $p->encodeParamName( $p->getModuleManager()->getModuleGroup( $mName ) ); + $feature = "{$mParam}={$mName}&{$feature}"; + $m = $p; + } + $module->addDeprecation( + $this->messageConverter->convertMessageValue( $message ), + $feature, + $message->getData() + ); + break; + + case 'param-sensitive': // @codeCoverageIgnore + $module->getMain()->markParamsSensitive( $name ); + break; + + default: + $module->addWarning( + $this->messageConverter->convertMessageValue( $message ), + $message->getCode(), + $message->getData() + ); + break; + } + } + + public function useHighLimits( array $options ) { + return $this->apiMain->canApiHighLimits(); + } + +} diff --git a/includes/api/Validator/SubmoduleDef.php b/includes/api/Validator/SubmoduleDef.php new file mode 100644 index 000000000000..94ed96414a4d --- /dev/null +++ b/includes/api/Validator/SubmoduleDef.php @@ -0,0 +1,172 @@ +<?php + +namespace MediaWiki\Api\Validator; + +use ApiBase; +use ApiUsageException; +use Html; +use Wikimedia\ParamValidator\TypeDef\EnumDef; + +/** + * Type definition for submodule types + * + * A submodule type is an enum type for selecting Action API submodules. + * + * @since 1.35 + */ +class SubmoduleDef extends EnumDef { + + /** + * (string[]) Map parameter values to submodule paths. + * + * Default is to use all modules in $options['module']->getModuleManager() + * in the group matching the parameter name. + */ + public const PARAM_SUBMODULE_MAP = 'param-submodule-map'; + + /** + * (string) Used to indicate the 'g' prefix added by ApiQueryGeneratorBase + * (and similar if anything else ever does that). + */ + public const PARAM_SUBMODULE_PARAM_PREFIX = 'param-submodule-param-prefix'; + + public function getEnumValues( $name, array $settings, array $options ) { + if ( isset( $settings[self::PARAM_SUBMODULE_MAP] ) ) { + $modules = array_keys( $settings[self::PARAM_SUBMODULE_MAP] ); + } else { + $modules = $options['module']->getModuleManager()->getNames( $name ); + } + + return $modules; + } + + public function getParamInfo( $name, array $settings, array $options ) { + $info = parent::getParamInfo( $name, $settings, $options ); + $module = $options['module']; + + if ( isset( $settings[self::PARAM_SUBMODULE_MAP] ) ) { + $info['type'] = array_keys( $settings[self::PARAM_SUBMODULE_MAP] ); + $info['submodules'] = $settings[self::PARAM_SUBMODULE_MAP]; + } else { + $info['type'] = $module->getModuleManager()->getNames( $name ); + $prefix = $module->isMain() ? '' : ( $module->getModulePath() . '+' ); + $info['submodules'] = []; + foreach ( $info['type'] as $v ) { + $info['submodules'][$v] = $prefix . $v; + } + } + if ( isset( $settings[self::PARAM_SUBMODULE_PARAM_PREFIX] ) ) { + $info['submoduleparamprefix'] = $settings[self::PARAM_SUBMODULE_PARAM_PREFIX]; + } + + $submoduleFlags = []; // for sorting: higher flags are sorted later + $submoduleNames = []; // for sorting: lexicographical, ascending + foreach ( $info['submodules'] as $v => $submodulePath ) { + try { + $submod = $module->getModuleFromPath( $submodulePath ); + } catch ( ApiUsageException $ex ) { + $submoduleFlags[] = 0; + $submoduleNames[] = $v; + continue; + } + $flags = 0; + if ( $submod && $submod->isDeprecated() ) { + $info['deprecatedvalues'][] = $v; + $flags |= 1; + } + if ( $submod && $submod->isInternal() ) { + $info['internalvalues'][] = $v; + $flags |= 2; + } + $submoduleFlags[] = $flags; + $submoduleNames[] = $v; + } + // sort $info['submodules'] and $info['type'] by $submoduleFlags and $submoduleNames + array_multisort( $submoduleFlags, $submoduleNames, $info['submodules'], $info['type'] ); + if ( isset( $info['deprecatedvalues'] ) ) { + sort( $info['deprecatedvalues'] ); + } + if ( isset( $info['internalvalues'] ) ) { + sort( $info['internalvalues'] ); + } + + return $info; + } + + private function getSubmoduleMap( ApiBase $module, string $name, array $settings ) : array { + if ( isset( $settings[self::PARAM_SUBMODULE_MAP] ) ) { + $map = $settings[self::PARAM_SUBMODULE_MAP]; + } else { + $prefix = $module->isMain() ? '' : ( $module->getModulePath() . '+' ); + $map = []; + foreach ( $module->getModuleManager()->getNames( $name ) as $submoduleName ) { + $map[$submoduleName] = $prefix . $submoduleName; + } + } + + return $map; + } + + protected function sortEnumValues( + string $name, array $values, array $settings, array $options + ) : array { + $module = $options['module']; + $map = $this->getSubmoduleMap( $module, $name, $settings ); + + $submoduleFlags = []; // for sorting: higher flags are sorted later + foreach ( $values as $k => $v ) { + $flags = 0; + try { + $submod = isset( $map[$v] ) ? $module->getModuleFromPath( $map[$v] ) : null; + if ( $submod && $submod->isDeprecated() ) { + $flags |= 1; + } + if ( $submod && $submod->isInternal() ) { + $flags |= 2; + } + } catch ( ApiUsageException $ex ) { + // Ignore + } + $submoduleFlags[$k] = $flags; + } + array_multisort( $submoduleFlags, $values, SORT_NATURAL ); + + return $values; + } + + protected function getEnumValuesForHelp( $name, array $settings, array $options ) { + $module = $options['module']; + $map = $this->getSubmoduleMap( $module, $name, $settings ); + $defaultAttrs = [ 'dir' => 'ltr', 'lang' => 'en' ]; + + $values = []; + $submoduleFlags = []; // for sorting: higher flags are sorted later + $submoduleNames = []; // for sorting: lexicographical, ascending + foreach ( $map as $v => $m ) { + $attrs = $defaultAttrs; + $flags = 0; + try { + $submod = $module->getModuleFromPath( $m ); + if ( $submod && $submod->isDeprecated() ) { + $attrs['class'][] = 'apihelp-deprecated-value'; + $flags |= 1; + } + if ( $submod && $submod->isInternal() ) { + $attrs['class'][] = 'apihelp-internal-value'; + $flags |= 2; + } + } catch ( ApiUsageException $ex ) { + // Ignore + } + $v = Html::element( 'span', $attrs, $v ); + $values[] = "[[Special:ApiHelp/{$m}|{$v}]]"; + $submoduleFlags[] = $flags; + $submoduleNames[] = $v; + } + // sort $values by $submoduleFlags and $submoduleNames + array_multisort( $submoduleFlags, $submoduleNames, SORT_NATURAL, $values, SORT_NATURAL ); + + return $values; + } + +} diff --git a/includes/api/i18n/en.json b/includes/api/i18n/en.json index bfd582312db4..0eda344cff42 100644 --- a/includes/api/i18n/en.json +++ b/includes/api/i18n/en.json @@ -27,8 +27,8 @@ "apihelp-main-param-errorsuselocal": "If given, error texts will use locally-customized messages from the {{ns:MediaWiki}} namespace.", "apihelp-block-summary": "Block a user.", - "apihelp-block-param-user": "Username, IP address, or IP address range to block. Cannot be used together with <var>$1userid</var>", - "apihelp-block-param-userid": "User ID to block. Cannot be used together with <var>$1user</var>.", + "apihelp-block-param-user": "User to block.", + "apihelp-block-param-userid": "Specify <kbd>$1user=#<var>ID</var></kbd> instead.", "apihelp-block-param-expiry": "Expiry time. May be relative (e.g. <kbd>5 months</kbd> or <kbd>2 weeks</kbd>) or absolute (e.g. <kbd>2014-09-18T12:34:56Z</kbd>). If set to <kbd>infinite</kbd>, <kbd>indefinite</kbd>, or <kbd>never</kbd>, the block will never expire.", "apihelp-block-param-reason": "Reason for block.", "apihelp-block-param-anononly": "Block anonymous users only (i.e. disable anonymous edits for this IP address).", @@ -1500,9 +1500,9 @@ "apihelp-tokens-example-emailmove": "Retrieve an email token and a move token.", "apihelp-unblock-summary": "Unblock a user.", - "apihelp-unblock-param-id": "ID of the block to unblock (obtained through <kbd>list=blocks</kbd>). Cannot be used together with <var>$1user</var> or <var>$1userid</var>.", - "apihelp-unblock-param-user": "Username, IP address or IP address range to unblock. Cannot be used together with <var>$1id</var> or <var>$1userid</var>.", - "apihelp-unblock-param-userid": "User ID to unblock. Cannot be used together with <var>$1id</var> or <var>$1user</var>.", + "apihelp-unblock-param-id": "ID of the block to unblock (obtained through <kbd>list=blocks</kbd>). Cannot be used together with <var>$1user</var>.", + "apihelp-unblock-param-user": "User to unblock. Cannot be used together with <var>$1id</var>.", + "apihelp-unblock-param-userid": "Specify <kbd>$1user=#<var>ID</var></kbd> instead.", "apihelp-unblock-param-reason": "Reason for unblock.", "apihelp-unblock-param-tags": "Change tags to apply to the entry in the block log.", "apihelp-unblock-example-id": "Unblock block ID #<kbd>105</kbd>.", @@ -1545,8 +1545,8 @@ "apihelp-upload-example-filekey": "Complete an upload that failed due to warnings.", "apihelp-userrights-summary": "Change a user's group membership.", - "apihelp-userrights-param-user": "User name.", - "apihelp-userrights-param-userid": "User ID.", + "apihelp-userrights-param-user": "User.", + "apihelp-userrights-param-userid": "Specify <kbd>$1user=#<var>ID</var></kbd> instead.", "apihelp-userrights-param-add": "Add the user to these groups, or if they are already a member, update the expiry of their membership in that group.", "apihelp-userrights-param-expiry": "Expiry timestamps. May be relative (e.g. <kbd>5 months</kbd> or <kbd>2 weeks</kbd>) or absolute (e.g. <kbd>2014-09-18T12:34:56Z</kbd>). If only one timestamp is set, it will be used for all groups passed to the <var>$1add</var> parameter. Use <kbd>infinite</kbd>, <kbd>indefinite</kbd>, <kbd>infinity</kbd>, or <kbd>never</kbd> for a never-expiring user group.", "apihelp-userrights-param-remove": "Remove the user from these groups.", @@ -1629,33 +1629,21 @@ "api-help-parameters": "{{PLURAL:$1|Parameter|Parameters}}:", "api-help-param-deprecated": "Deprecated.", "api-help-param-internal": "Internal.", - "api-help-param-required": "This parameter is required.", "api-help-param-templated": "This is a [[Special:ApiHelp/main#main/templatedparams|templated parameter]]. When making the request, $2.", "api-help-param-templated-var-first": "<var>{$1}</var> in the parameter's name should be replaced with values of <var>$2</var>", "api-help-param-templated-var": "<var>{$1}</var> with values of <var>$2</var>", "api-help-datatypes-header": "Data types", - "api-help-datatypes": "Input to MediaWiki should be NFC-normalized UTF-8. MediaWiki may attempt to convert other input, but this may cause some operations (such as [[Special:ApiHelp/edit|edits]] with MD5 checks) to fail.\n\nSome parameter types in API requests need further explanation:\n;boolean\n:Boolean parameters work like HTML checkboxes: if the parameter is specified, regardless of value, it is considered true. For a false value, omit the parameter entirely.\n;timestamp\n:Timestamps may be specified in several formats, see [[mw:Special:MyLanguage/Timestamp|the Timestamp library input formats documented on mediawiki.org]] for details. ISO 8601 date and time is recommended: <kbd><var>2001</var>-<var>01</var>-<var>15</var>T<var>14</var>:<var>56</var>:<var>00</var>Z</kbd>. Additionally, the string <kbd>now</kbd> may be used to specify the current timestamp.\n;alternative multiple-value separator\n:Parameters that take multiple values are normally submitted with the values separated using the pipe character, e.g. <kbd>param=value1|value2</kbd> or <kbd>param=value1%7Cvalue2</kbd>. If a value must contain the pipe character, use U+001F (Unit Separator) as the separator ''and'' prefix the value with U+001F, e.g. <kbd>param=%1Fvalue1%1Fvalue2</kbd>.", + "api-help-datatypes-top": "Input to MediaWiki should be NFC-normalized UTF-8. MediaWiki may attempt to convert other input, but this may cause some operations (such as [[Special:ApiHelp/edit|edits]] with MD5 checks) to fail.\n\nParameters that take multiple values are normally submitted with the values separated using the pipe character, e.g. <kbd>param=value1|value2</kbd> or <kbd>param=value1%7Cvalue2</kbd>. If a value must contain the pipe character, use U+001F (Unit Separator) as the separator ''and'' prefix the value with U+001F, e.g. <kbd>param=%1Fvalue1%1Fvalue2</kbd>.\n\nSome parameter types in API requests need further explanation:", + "api-help-datatype-boolean": "Boolean parameters work like HTML checkboxes: if the parameter is specified, regardless of value, it is considered true. For a false value, omit the parameter entirely.", + "api-help-datatype-timestamp": "Timestamps may be specified in several formats, see [[mw:Special:MyLanguage/Timestamp|the Timestamp library input formats documented on mediawiki.org]] for details. ISO 8601 date and time is recommended: <kbd><var>2001</var>-<var>01</var>-<var>15</var>T<var>14</var>:<var>56</var>:<var>00</var>Z</kbd>. Additionally, the string <kbd>now</kbd> may be used to specify the current timestamp.", "api-help-templatedparams-header": "Templated parameters", "api-help-templatedparams": "Templated parameters support cases where an API module needs a value for each value of some other parameter. For example, if there were an API module to request fruit, it might have a parameter <var>fruits</var> to specify which fruits are being requested and a templated parameter <var>{fruit}-quantity</var> to specify how many of each fruit to request. An API client that wants 1 apple, 5 bananas, and 20 strawberries could then make a request like <kbd>fruits=apples|bananas|strawberries&apples-quantity=1&bananas-quantity=5&strawberries-quantity=20</kbd>.", "api-help-param-type-limit": "Type: integer or <kbd>max</kbd>", - "api-help-param-type-integer": "Type: {{PLURAL:$1|1=integer|2=list of integers}}", - "api-help-param-type-boolean": "Type: boolean ([[Special:ApiHelp/main#main/datatypes|details]])", - "api-help-param-type-password": "", - "api-help-param-type-timestamp": "Type: {{PLURAL:$1|1=timestamp|2=list of timestamps}} ([[Special:ApiHelp/main#main/datatypes|allowed formats]])", - "api-help-param-type-user": "Type: {{PLURAL:$1|1=user name|2=list of user names}}", - "api-help-param-list": "{{PLURAL:$1|1=One of the following values|2=Values (separate with <kbd>{{!}}</kbd> or [[Special:ApiHelp/main#main/datatypes|alternative]])}}: $2", - "api-help-param-list-can-be-empty": "{{PLURAL:$1|0=Must be empty|Can be empty, or $2}}", - "api-help-param-limit": "No more than $1 allowed.", - "api-help-param-limit2": "No more than $1 ($2 for bots) allowed.", - "api-help-param-integer-min": "The {{PLURAL:$1|1=value|2=values}} must be no less than $2.", - "api-help-param-integer-max": "The {{PLURAL:$1|1=value|2=values}} must be no greater than $3.", - "api-help-param-integer-minmax": "The {{PLURAL:$1|1=value|2=values}} must be between $2 and $3.", - "api-help-param-upload": "Must be posted as a file upload using multipart/form-data.", + "api-help-param-type-presenceboolean": "Type: boolean ([[Special:ApiHelp/main#main/datatype/boolean|details]])", + "api-help-param-type-timestamp": "Type: {{PLURAL:$1|1=timestamp|2=list of timestamps}} ([[Special:ApiHelp/main#main/datatype/timestamp|allowed formats]])", + "api-help-param-type-enum": "{{PLURAL:$1|1=One of the following values|2=Values (separate with <kbd>{{!}}</kbd> or [[Special:ApiHelp/main#main/datatypes|alternative]])}}: $2", "api-help-param-multi-separate": "Separate values with <kbd>|</kbd> or [[Special:ApiHelp/main#main/datatypes|alternative]].", - "api-help-param-multi-max": "Maximum number of values is {{PLURAL:$1|$1}} ({{PLURAL:$2|$2}} for bots).", - "api-help-param-multi-max-simple": "Maximum number of values is {{PLURAL:$1|$1}}.", "api-help-param-multi-all": "To specify all values, use <kbd>$1</kbd>.", - "api-help-param-default": "Default: $1", "api-help-param-default-empty": "Default: <span class=\"apihelp-empty\">(empty)</span>", "api-help-param-token": "A \"$1\" token retrieved from [[Special:ApiHelp/query+tokens|action=query&meta=tokens]]", "api-help-param-token-webui": "For compatibility, the token used in the web UI is also accepted.", @@ -1664,8 +1652,6 @@ "api-help-param-direction": "In which direction to enumerate:\n;newer:List oldest first. Note: $1start has to be before $1end.\n;older:List newest first (default). Note: $1start has to be later than $1end.", "api-help-param-continue": "When more results are available, use this to continue.", "api-help-param-no-description": "<span class=\"apihelp-empty\">(no description)</span>", - "api-help-param-maxbytes": "Cannot be longer than $1 {{PLURAL:$1|byte|bytes}}.", - "api-help-param-maxchars": "Cannot be longer than $1 {{PLURAL:$1|character|characters}}.", "api-help-examples": "{{PLURAL:$1|Example|Examples}}:", "api-help-permissions": "{{PLURAL:$1|Permission|Permissions}}:", "api-help-permissions-granted-to": "{{PLURAL:$1|Granted to}}: $2", diff --git a/includes/api/i18n/qqq.json b/includes/api/i18n/qqq.json index c4109148c88d..0ac68b25eec7 100644 --- a/includes/api/i18n/qqq.json +++ b/includes/api/i18n/qqq.json @@ -1519,34 +1519,22 @@ "api-help-parameters": "Label for the API help parameters section\n\nParameters:\n* $1 - Number of parameters to be displayed\n{{Identical|Parameter}}", "api-help-param-deprecated": "Displayed in the API help for any deprecated parameter\n{{Identical|Deprecated}}", "api-help-param-internal": "Displayed in the API help for any internal parameter", - "api-help-param-required": "Displayed in the API help for any required parameter", "api-help-param-templated": "Displayed in the API help for any templated parameter.\n\nParameters:\n* $1 - Count of template variables in the parameter name.\n* $2 - A list, composed using {{msg-mw|comma-separator}} and {{msg-mw|and}}, of the template variables in the parameter name. The first is formatted using {{msg-mw|api-help-param-templated-var-first|notext=1}} and the rest use {{msg-mw|api-help-param-templated-var|notext=1}}.\n\nSee also:\n* {{msg-mw|api-help-param-templated-var-first}}\n* {{msg-mw|api-help-param-templated-var}}", "api-help-param-templated-var-first": "Used with {{msg-mw|api-help-param-templated|notext=1}} to display templated parameter replacement variables. See that message for context.\n\nParameters:\n* $1 - Variable.\n* $2 - Parameter from which values are taken.\n\nSee also:\n* {{msg-mw|api-help-param-templated}}\n* {{msg-mw|api-help-param-templated-var}}", "api-help-param-templated-var": "Used with {{msg-mw|api-help-param-templated|notext=1}} to display templated parameter replacement variables. See that message for context.\n\nParameters:\n* $1 - Variable.\n* $2 - Parameter from which values are taken.\n\nSee also:\n* {{msg-mw|api-help-param-templated}}\n* {{msg-mw|api-help-param-templated-var-first}}", "api-help-datatypes-header": "Header for the data type section in the API help output", - "api-help-datatypes": "{{technical}} {{doc-important|Do not translate or reformat dates inside <nowiki><kbd></kbd></nowiki> or <nowiki><var></var></nowiki> tags}} Documentation of certain API data types\nSee also:\n* [[Special:PrefixIndex/MediaWiki:api-help-param-type]]", + "api-help-datatypes-top": "{{technical}} {{doc-important|Do not translate or reformat dates inside <nowiki><kbd></kbd></nowiki> or <nowiki><var></var></nowiki> tags}} General documentation of API data types\nSee also:\n* [[Special:PrefixIndex/MediaWiki:api-help-param-type]]", + "api-help-datatype-boolean": "{{technical}} {{doc-important|Do not translate or reformat dates inside <nowiki><kbd></kbd></nowiki> or <nowiki><var></var></nowiki> tags}} Documentation of API boolean data type\nSee also:\n* [[Special:PrefixIndex/MediaWiki:api-help-param-type]]", + "api-help-datatype-timestamp": "{{technical}} {{doc-important|Do not translate or reformat dates inside <nowiki><kbd></kbd></nowiki> or <nowiki><var></var></nowiki> tags}} Documentation of API timestamp data type\nSee also:\n* [[Special:PrefixIndex/MediaWiki:api-help-param-type]]", "api-help-templatedparams-header": "Header for the \"templated parameters\" section in the API help output.", "api-help-templatedparams": "{{technical}} {{doc-important|Unlike in other API messages, feel free to localize the words \"fruit\", \"fruits\", \"quantity\", \"apples\", \"bananas\", and \"strawberries\" in this message even when inside <nowiki><kbd></kbd></nowiki> or <nowiki><var></var></nowiki> tags. Do not change the punctuation, only the words.}} Documentation for the \"templated parameters\" feature.", - "api-help-param-type-limit": "{{technical}} {{doc-important|Do not translate text inside <kbd> tags}} Used to indicate that a parameter is a \"limit\" type.\n\nSee also:\n* {{msg-mw|api-help-datatypes}}\n* [[Special:PrefixIndex/MediaWiki:api-help-param-type]]\n{{Related|Api-help-param-type}}", - "api-help-param-type-integer": "{{technical}} Used to indicate that a parameter is an integer or list of integers. Parameters:\n* $1 - 1 if the parameter takes one value, 2 if the parameter takes a list of values.\nSee also:\n* {{msg-mw|api-help-datatypes}}\n* [[Special:PrefixIndex/MediaWiki:api-help-param-type]]\n{{Related|Api-help-param-type}}", - "api-help-param-type-boolean": "{{technical}} {{doc-important|Do not translate <code>Special:ApiHelp</code> in this message.}} Used to indicate that a parameter is a boolean. Parameters:\n* $1 - Always 1.\nSee also:\n* {{msg-mw|api-help-datatypes}}\n* [[Special:PrefixIndex/MediaWiki:api-help-param-type]]\n{{Related|Api-help-param-type}}", - "api-help-param-type-password": "{{ignored}}{{technical}} Used to indicate that a parameter is a password or list of passwords. Parameters:\n* $1 - 1 if the parameter takes one value, 2 if the parameter takes a list of values.\nSee also:\n* {{msg-mw|api-help-datatypes}}\n* [[Special:PrefixIndex/MediaWiki:api-help-param-type]]", - "api-help-param-type-timestamp": "{{technical}} {{doc-important|Do not translate <code>Special:ApiHelp</code> in this message.}} Used to indicate that a parameter is a timestamp or list of timestamps. Parameters:\n* $1 - 1 if the parameter takes one value, 2 if the parameter takes a list of values.\nSee also:\n* {{msg-mw|api-help-datatypes}}\n* [[Special:PrefixIndex/MediaWiki:api-help-param-type]]\n{{Related|Api-help-param-type}}", - "api-help-param-type-user": "{{technical}} Used to indicate that a parameter is a username or list of usernames. Parameters:\n* $1 - 1 if the parameter takes one value, 2 if the parameter takes a list of values.\nSee also:\n* {{msg-mw|api-help-datatypes}}\n* [[Special:PrefixIndex/MediaWiki:api-help-param-type]]\n{{Related|Api-help-param-type}}", - "api-help-param-list": "Used to display the possible values for a parameter taking a list of values\n\nParameters:\n* $1 - 1 if the parameter takes one value, 2 if the parameter takes any number of values\n* $2 - Comma-separated list of values, possibly formatted using {{msg-mw|api-help-param-list-can-be-empty}}\n{{Identical|Value}}", - "api-help-param-list-can-be-empty": "Used to indicate that one of the possible values in the list is the empty string.\n\nParameters:\n* $1 - Number of items in the rest of the list; may be 0\n* $2 - Remainder of the list as a comma-separated string", - "api-help-param-limit": "Used to display the maximum value of a limit parameter\n\nParameters:\n* $1 - Maximum value", - "api-help-param-limit2": "Used to display the maximum values of a limit parameter\n\nParameters:\n* $1 - Maximum value without the apihighlimits right\n* $2 - Maximum value with the apihighlimits right", - "api-help-param-integer-min": "Used to display an integer parameter with a minimum but no maximum value\n\nParameters:\n* $1 - 1 if the parameter takes one value, 2 if the parameter takes any number of values\n* $2 - Minimum value\n* $3 - unused\n\nSee also:\n* {{msg-mw|api-help-param-integer-max}}\n* {{msg-mw|api-help-param-integer-minmax}}", - "api-help-param-integer-max": "Used to display an integer parameter with a maximum but no minimum value.\n\nParameters:\n* $1 - 1 if the parameter takes one value, 2 if the parameter takes any number of values\n* $2 - (Unused)\n* $3 - Maximum value\nSee also:\n* {{msg-mw|Api-help-param-integer-min}}\n* {{msg-mw|Api-help-param-integer-minmax}}", - "api-help-param-integer-minmax": "Used to display an integer parameter with a maximum and minimum values\n\nParameters:\n* $1 - 1 if the parameter takes one value, 2 if the parameter takes any number of values\n* $2 - Minimum value\n* $3 - Maximum value\n\nSee also:\n* {{msg-mw|api-help-param-integer-min}}\n* {{msg-mw|api-help-param-integer-max}}", - "api-help-param-upload": "{{technical}} Used to indicate that an 'upload'-type parameter must be posted as a file upload using multipart/form-data", - "api-help-param-multi-separate": "Used to indicate how to separate multiple values. Not used with {{msg-mw|api-help-param-list}}.", - "api-help-param-multi-max": "Used to indicate the maximum number of values accepted for a multi-valued parameter when that value is influenced by the user having apihighlimits right (otherwise {{msg-mw|api-help-param-multi-max-simple}} is used).\n\nParameters:\n* $1 - Maximum value without the apihighlimits right\n* $2 - Maximum value with the apihighlimits right", - "api-help-param-multi-max-simple": "Used to indicate the maximum number of values accepted for a multi-valued parameter when that value is not influenced by the user having apihighlimits right (otherwise {{msg-mw|api-help-param-multi-max}} is used).\n\nParameters:\n* $1 - Maximum value", + "api-help-param-type-limit": "{{technical}} {{doc-important|Do not translate text inside <kbd> tags}} Used to indicate that a parameter is a \"limit\" type. Parameters:\n* $1 - Always 1.\nSee also:\n* [[Special:PrefixIndex/MediaWiki:api-help-param-type]]\n* [[Special:PrefixIndex/MediaWiki:paramvalidator-help-type]]\n{{Related|Api-help-param-type}}", + "api-help-param-type-presenceboolean": "{{technical}} {{doc-important|Do not translate <code>Special:ApiHelp</code> in this message.}} Used to indicate that a parameter is a boolean. Parameters:\n* $1 - Always 1.\nSee also:\n* {{msg-mw|api-help-datatype-boolean}}\n* [[Special:PrefixIndex/MediaWiki:api-help-param-type]]\n* [[Special:PrefixIndex/MediaWiki:paramvalidator-help-type]]\n{{Related|Api-help-param-type}}", + "api-help-param-type-timestamp": "{{technical}} {{doc-important|Do not translate <code>Special:ApiHelp</code> in this message.}} Used to indicate that a parameter is a timestamp or list of timestamps. Parameters:\n* $1 - 1 if the parameter takes one value, 2 if the parameter takes a list of values.\nSee also:\n* {{msg-mw|api-help-datatype-timestamp}}\n* [[Special:PrefixIndex/MediaWiki:api-help-param-type]]\n* [[Special:PrefixIndex/MediaWiki:paramvalidator-help-type]]\n{{Related|Api-help-param-type}}", + "api-help-param-type-enum": "Used to display the possible values for a parameter taking a list of values\n\nParameters:\n* $1 - 1 if the parameter takes one value, 2 if the parameter takes any number of values\n* $2 - Comma-separated list of values, possibly formatted using {{msg-mw|paramvalidator-help-type-enum-can-be-empty}}\n{{Identical|Value}}\n{{Related|Api-help-param-type}}", + "api-help-param-multi-separate": "Used to indicate how to separate multiple values. Not used with {{msg-mw|api-help-param-type-enum}}.", "api-help-param-multi-all": "Used to indicate what string can be used to specify all possible values of a multi-valued parameter. \n\nParameters:\n* $1 - String to specify all possible values of the parameter", - "api-help-param-default": "Used to display the default value for an API parameter\n\nParameters:\n* $1 - Default value\n\nSee also:\n* {{msg-mw|api-help-param-default-empty}}\n{{Identical|Default}}", - "api-help-param-default-empty": "Used to display the default value for an API parameter when that default is an empty value\n\nSee also:\n* {{msg-mw|api-help-param-default}}", + "api-help-param-default-empty": "Used to display the default value for an API parameter when that default is an empty value\n\nSee also:\n* {{msg-mw|paramvalidator-help-default}}", "api-help-param-token": "{{doc-apihelp-param|description=any 'token' parameter|paramstart=2|params=\n* $1 - Token type|noseealso=1}}", "api-help-param-token-webui": "{{doc-apihelp-param|description=additional text for any \"token\" parameter, explaining that web UI tokens are also accepted|noseealso=1}}", "api-help-param-disabled-in-miser-mode": "{{doc-apihelp-param|description=any parameter that is disabled when [[mw:Manual:$wgMiserMode|$wgMiserMode]] is set.|noseealso=1}}", @@ -1554,8 +1542,6 @@ "api-help-param-direction": "{{doc-apihelp-param|description=any standard \"dir\" parameter|noseealso=1}}", "api-help-param-continue": "{{doc-apihelp-param|description=any standard \"continue\" parameter, or other parameter with the same semantics|noseealso=1}}", "api-help-param-no-description": "Displayed on API parameters that lack any description", - "api-help-param-maxbytes": "Used to display the maximum allowed length of a parameter, in bytes.", - "api-help-param-maxchars": "Used to display the maximum allowed length of a parameter, in characters.", "api-help-examples": "Label for the API help examples section\n\nParameters:\n* $1 - Number of examples to be displayed\n{{Identical|Example}}", "api-help-permissions": "Label for the \"permissions\" section in the main module's help output.\n\nParameters:\n* $1 - Number of permissions displayed\n{{Identical|Permission}}", "api-help-permissions-granted-to": "Used to introduce the list of groups each permission is assigned to.\n\nParameters:\n* $1 - Number of groups\n* $2 - List of group names, comma-separated", diff --git a/includes/changetags/ChangeTags.php b/includes/changetags/ChangeTags.php index f40eb049439b..8ec4e61f66eb 100644 --- a/includes/changetags/ChangeTags.php +++ b/includes/changetags/ChangeTags.php @@ -504,9 +504,12 @@ class ChangeTags { */ protected static function restrictedTagError( $msgOne, $msgMulti, $tags ) { $lang = RequestContext::getMain()->getLanguage(); + $tags = array_values( $tags ); $count = count( $tags ); - return Status::newFatal( ( $count > 1 ) ? $msgMulti : $msgOne, + $status = Status::newFatal( ( $count > 1 ) ? $msgMulti : $msgOne, $lang->commaList( $tags ), $count ); + $status->value = $tags; + return $status; } /** |