aboutsummaryrefslogtreecommitdiffstats
path: root/includes
diff options
context:
space:
mode:
Diffstat (limited to 'includes')
-rw-r--r--includes/AutoLoader.php1
-rw-r--r--includes/ParamValidator/TypeDef/NamespaceDef.php78
-rw-r--r--includes/ParamValidator/TypeDef/TagsDef.php74
-rw-r--r--includes/ParamValidator/TypeDef/UserDef.php169
-rw-r--r--includes/WebRequestUpload.php14
-rw-r--r--includes/api/ApiBase.php941
-rw-r--r--includes/api/ApiBlock.php3
-rw-r--r--includes/api/ApiFeedContributions.php18
-rw-r--r--includes/api/ApiHelp.php327
-rw-r--r--includes/api/ApiMain.php40
-rw-r--r--includes/api/ApiPageSet.php8
-rw-r--r--includes/api/ApiParamInfo.php176
-rw-r--r--includes/api/ApiQueryAllDeletedRevisions.php11
-rw-r--r--includes/api/ApiQueryAllImages.php7
-rw-r--r--includes/api/ApiQueryAllRevisions.php9
-rw-r--r--includes/api/ApiQueryBacklinks.php14
-rw-r--r--includes/api/ApiQueryBlocks.php2
-rw-r--r--includes/api/ApiQueryDeletedRevisions.php13
-rw-r--r--includes/api/ApiQueryDeletedrevs.php25
-rw-r--r--includes/api/ApiQueryLogEvents.php7
-rw-r--r--includes/api/ApiQueryRecentChanges.php13
-rw-r--r--includes/api/ApiQueryRevisions.php9
-rw-r--r--includes/api/ApiQueryRevisionsBase.php15
-rw-r--r--includes/api/ApiQueryUserContribs.php2
-rw-r--r--includes/api/ApiQueryWatchlist.php6
-rw-r--r--includes/api/ApiQueryWatchlistRaw.php4
-rw-r--r--includes/api/ApiResetPassword.php2
-rw-r--r--includes/api/ApiRollback.php12
-rw-r--r--includes/api/ApiUnblock.php9
-rw-r--r--includes/api/ApiUsageException.php14
-rw-r--r--includes/api/ApiUserrights.php5
-rw-r--r--includes/api/ApiValidatePassword.php2
-rw-r--r--includes/api/Validator/ApiParamValidator.php249
-rw-r--r--includes/api/Validator/ApiParamValidatorCallbacks.php136
-rw-r--r--includes/api/Validator/SubmoduleDef.php172
-rw-r--r--includes/api/i18n/en.json40
-rw-r--r--includes/api/i18n/qqq.json32
-rw-r--r--includes/changetags/ChangeTags.php5
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>&#x7B;$1&#x7D;</var> in the parameter's name should be replaced with values of <var>$2</var>",
"api-help-param-templated-var": "<var>&#x7B;$1&#x7D;</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 &lt;kbd&gt; 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 &lt;kbd&gt; 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;
}
/**