3 namespace MediaWiki\Api\Validator
;
6 use MediaWiki\Api\ApiBase
;
7 use MediaWiki\Api\ApiMain
;
8 use MediaWiki\Api\ApiMessage
;
9 use MediaWiki\Api\ApiUsageException
;
10 use MediaWiki\Message\Message
;
11 use MediaWiki\ParamValidator\TypeDef\NamespaceDef
;
12 use MediaWiki\ParamValidator\TypeDef\TagsDef
;
13 use MediaWiki\ParamValidator\TypeDef\TitleDef
;
14 use MediaWiki\ParamValidator\TypeDef\UserDef
;
15 use Wikimedia\Message\DataMessageValue
;
16 use Wikimedia\Message\MessageValue
;
17 use Wikimedia\ObjectFactory\ObjectFactory
;
18 use Wikimedia\ParamValidator\ParamValidator
;
19 use Wikimedia\ParamValidator\TypeDef\EnumDef
;
20 use Wikimedia\ParamValidator\TypeDef\ExpiryDef
;
21 use Wikimedia\ParamValidator\TypeDef\IntegerDef
;
22 use Wikimedia\ParamValidator\TypeDef\LimitDef
;
23 use Wikimedia\ParamValidator\TypeDef\PasswordDef
;
24 use Wikimedia\ParamValidator\TypeDef\PresenceBooleanDef
;
25 use Wikimedia\ParamValidator\TypeDef\StringDef
;
26 use Wikimedia\ParamValidator\TypeDef\TimestampDef
;
27 use Wikimedia\ParamValidator\TypeDef\UploadDef
;
28 use Wikimedia\ParamValidator\ValidationException
;
29 use Wikimedia\RequestTimeout\TimeoutException
;
32 * This wraps a bunch of the API-specific parameter validation logic.
34 * It's intended to be used in ApiMain by composition.
39 class ApiParamValidator
{
41 /** @var ParamValidator */
42 private $paramValidator;
44 /** Type defs for ParamValidator */
45 private const TYPE_DEFS
= [
46 'boolean' => [ 'class' => PresenceBooleanDef
::class ],
47 'enum' => [ 'class' => EnumDef
::class ],
48 'expiry' => [ 'class' => ExpiryDef
::class ],
49 'integer' => [ 'class' => IntegerDef
::class ],
50 'limit' => [ 'class' => LimitDef
::class ],
52 'class' => NamespaceDef
::class,
53 'services' => [ 'NamespaceInfo' ],
56 'class' => StringDef
::class,
58 StringDef
::OPT_ALLOW_EMPTY
=> true,
61 'password' => [ 'class' => PasswordDef
::class ],
62 // Unlike 'string', the 'raw' type will not be subject to Unicode
64 'raw' => [ 'class' => StringDef
::class ],
65 'string' => [ 'class' => StringDef
::class ],
66 'submodule' => [ 'class' => SubmoduleDef
::class ],
68 'class' => TagsDef
::class,
69 'services' => [ 'ChangeTagsStore' ],
71 'text' => [ 'class' => StringDef
::class ],
73 'class' => TimestampDef
::class,
75 'defaultFormat' => TS_MW
,
79 'class' => TitleDef
::class,
80 'services' => [ 'TitleFactory' ],
83 'class' => UserDef
::class,
84 'services' => [ 'UserIdentityLookup', 'TitleParser', 'UserNameUtils' ]
86 'upload' => [ 'class' => UploadDef
::class ],
91 * @param ApiMain $main
92 * @param ObjectFactory $objectFactory
94 public function __construct( ApiMain
$main, ObjectFactory
$objectFactory ) {
95 $this->paramValidator
= new ParamValidator(
96 new ApiParamValidatorCallbacks( $main ),
99 'typeDefs' => self
::TYPE_DEFS
,
100 'ismultiLimits' => [ ApiBase
::LIMIT_SML1
, ApiBase
::LIMIT_SML2
],
106 * List known type names
109 public function knownTypes(): array {
110 return $this->paramValidator
->knownTypes();
114 * Map deprecated styles for messages for ParamValidator
115 * @param array $settings
118 private function mapDeprecatedSettingsMessages( array $settings ): array {
119 if ( isset( $settings[EnumDef
::PARAM_DEPRECATED_VALUES
] ) ) {
120 foreach ( $settings[EnumDef
::PARAM_DEPRECATED_VALUES
] as &$v ) {
121 if ( $v === null ||
$v === true ||
$v instanceof MessageValue
) {
125 // Convert the message specification to a DataMessageValue. Flag in the data
126 // that it was so converted, so ApiParamValidatorCallbacks::recordCondition() can
127 // take that into account.
128 $msg = ApiMessage
::create( $v );
129 $v = DataMessageValue
::new(
133 [ '💩' => 'back-compat' ]
143 * Adjust certain settings where ParamValidator differs from historical Action API behavior
144 * @param array|mixed $settings
147 public function normalizeSettings( $settings ): array {
148 if ( is_array( $settings ) ) {
149 if ( !isset( $settings[ParamValidator
::PARAM_IGNORE_UNRECOGNIZED_VALUES
] ) ) {
150 $settings[ParamValidator
::PARAM_IGNORE_UNRECOGNIZED_VALUES
] = true;
153 if ( !isset( $settings[IntegerDef
::PARAM_IGNORE_RANGE
] ) ) {
154 $settings[IntegerDef
::PARAM_IGNORE_RANGE
] = empty( $settings[ApiBase
::PARAM_RANGE_ENFORCE
] );
157 $settings = $this->mapDeprecatedSettingsMessages( $settings );
160 return $this->paramValidator
->normalizeSettings( $settings );
164 * Check an API settings message
165 * @param ApiBase $module
167 * @param string|array|Message $value Message definition, see Message::newFromSpecifier()
170 private function checkSettingsMessage( ApiBase
$module, string $key, $value, array &$ret ): void
{
172 $msg = Message
::newFromSpecifier( $value );
173 $ret['messages'][] = MessageValue
::newFromSpecifier( $msg );
174 } catch ( TimeoutException
$e ) {
176 } catch ( Exception
$e ) {
177 $ret['issues'][] = "Message specification for $key is not valid";
182 * Check settings for the Action API.
183 * @param ApiBase $module
184 * @param array $params All module params to test
185 * @param string $name Parameter to test
186 * @param array $options Options array
187 * @return array As for ParamValidator::checkSettings()
189 public function checkSettings(
190 ApiBase
$module, array $params, string $name, array $options
192 $options['module'] = $module;
193 $settings = $params[$name];
194 if ( is_array( $settings ) ) {
195 $settings = $this->mapDeprecatedSettingsMessages( $settings );
197 $ret = $this->paramValidator
->checkSettings(
198 $module->encodeParamName( $name ), $settings, $options
201 $ret['allowedKeys'] = array_merge( $ret['allowedKeys'], [
202 ApiBase
::PARAM_RANGE_ENFORCE
, ApiBase
::PARAM_HELP_MSG
, ApiBase
::PARAM_HELP_MSG_APPEND
,
203 ApiBase
::PARAM_HELP_MSG_INFO
, ApiBase
::PARAM_HELP_MSG_PER_VALUE
, ApiBase
::PARAM_TEMPLATE_VARS
,
206 if ( !is_array( $settings ) ) {
210 if ( !is_bool( $settings[ApiBase
::PARAM_RANGE_ENFORCE
] ??
false ) ) {
211 $ret['issues'][ApiBase
::PARAM_RANGE_ENFORCE
] = 'PARAM_RANGE_ENFORCE must be boolean, got '
212 . gettype( $settings[ApiBase
::PARAM_RANGE_ENFORCE
] );
215 $path = $module->getModulePath();
216 $this->checkSettingsMessage(
217 $module, 'PARAM_HELP_MSG', $settings[ApiBase
::PARAM_HELP_MSG
] ??
"apihelp-$path-param-$name", $ret
220 if ( isset( $settings[ApiBase
::PARAM_HELP_MSG_APPEND
] ) ) {
221 if ( !is_array( $settings[ApiBase
::PARAM_HELP_MSG_APPEND
] ) ) {
222 $ret['issues'][ApiBase
::PARAM_HELP_MSG_APPEND
] = 'PARAM_HELP_MSG_APPEND must be an array, got '
223 . gettype( $settings[ApiBase
::PARAM_HELP_MSG_APPEND
] );
225 foreach ( $settings[ApiBase
::PARAM_HELP_MSG_APPEND
] as $k => $v ) {
226 $this->checkSettingsMessage( $module, "PARAM_HELP_MSG_APPEND[$k]", $v, $ret );
231 if ( isset( $settings[ApiBase
::PARAM_HELP_MSG_INFO
] ) ) {
232 if ( !is_array( $settings[ApiBase
::PARAM_HELP_MSG_INFO
] ) ) {
233 $ret['issues'][ApiBase
::PARAM_HELP_MSG_INFO
] = 'PARAM_HELP_MSG_INFO must be an array, got '
234 . gettype( $settings[ApiBase
::PARAM_HELP_MSG_INFO
] );
236 foreach ( $settings[ApiBase
::PARAM_HELP_MSG_INFO
] as $k => $v ) {
237 if ( !is_array( $v ) ) {
238 $ret['issues'][] = "PARAM_HELP_MSG_INFO[$k] must be an array, got " . gettype( $v );
239 } elseif ( !is_string( $v[0] ) ) {
240 $ret['issues'][] = "PARAM_HELP_MSG_INFO[$k][0] must be a string, got " . gettype( $v[0] );
242 $v[0] = "apihelp-{$path}-paraminfo-{$v[0]}";
243 $this->checkSettingsMessage( $module, "PARAM_HELP_MSG_INFO[$k]", $v, $ret );
249 if ( isset( $settings[ApiBase
::PARAM_HELP_MSG_PER_VALUE
] ) ) {
250 if ( !is_array( $settings[ApiBase
::PARAM_HELP_MSG_PER_VALUE
] ) ) {
251 $ret['issues'][ApiBase
::PARAM_HELP_MSG_PER_VALUE
] = 'PARAM_HELP_MSG_PER_VALUE must be an array,'
252 . ' got ' . gettype( $settings[ApiBase
::PARAM_HELP_MSG_PER_VALUE
] );
253 } elseif ( !is_array( $settings[ParamValidator
::PARAM_TYPE
] ??
'' ) ) {
254 $ret['issues'][ApiBase
::PARAM_HELP_MSG_PER_VALUE
] = 'PARAM_HELP_MSG_PER_VALUE can only be used '
255 . 'with PARAM_TYPE as an array';
257 $values = array_map( 'strval', $settings[ParamValidator
::PARAM_TYPE
] );
258 foreach ( $settings[ApiBase
::PARAM_HELP_MSG_PER_VALUE
] as $k => $v ) {
259 if ( !in_array( (string)$k, $values, true ) ) {
260 // Or should this be allowed?
261 $ret['issues'][] = "PARAM_HELP_MSG_PER_VALUE contains \"$k\", which is not in PARAM_TYPE.";
263 $this->checkSettingsMessage( $module, "PARAM_HELP_MSG_PER_VALUE[$k]", $v, $ret );
265 foreach ( $settings[ParamValidator
::PARAM_TYPE
] as $p ) {
266 if ( array_key_exists( $p, $settings[ApiBase
::PARAM_HELP_MSG_PER_VALUE
] ) ) {
269 $path = $module->getModulePath();
270 $this->checkSettingsMessage(
272 "PARAM_HELP_MSG_PER_VALUE[$p]",
273 "apihelp-$path-paramvalue-$name-$p",
280 if ( isset( $settings[ApiBase
::PARAM_TEMPLATE_VARS
] ) ) {
281 if ( !is_array( $settings[ApiBase
::PARAM_TEMPLATE_VARS
] ) ) {
282 $ret['issues'][ApiBase
::PARAM_TEMPLATE_VARS
] = 'PARAM_TEMPLATE_VARS must be an array,'
283 . ' got ' . gettype( $settings[ApiBase
::PARAM_TEMPLATE_VARS
] );
284 } elseif ( $settings[ApiBase
::PARAM_TEMPLATE_VARS
] === [] ) {
285 $ret['issues'][ApiBase
::PARAM_TEMPLATE_VARS
] = 'PARAM_TEMPLATE_VARS cannot be the empty array';
287 foreach ( $settings[ApiBase
::PARAM_TEMPLATE_VARS
] as $key => $target ) {
288 if ( !preg_match( '/^[^{}]+$/', $key ) ) {
289 $ret['issues'][] = "PARAM_TEMPLATE_VARS keys may not contain '{' or '}', got \"$key\"";
290 } elseif ( !str_contains( $name, '{' . $key . '}' ) ) {
291 $ret['issues'][] = "Parameter name must contain PARAM_TEMPLATE_VARS key {{$key}}";
293 if ( !is_string( $target ) && !is_int( $target ) ) {
294 $ret['issues'][] = "PARAM_TEMPLATE_VARS[$key] has invalid target type " . gettype( $target );
295 } elseif ( !isset( $params[$target] ) ) {
296 $ret['issues'][] = "PARAM_TEMPLATE_VARS[$key] target parameter \"$target\" does not exist";
298 $settings2 = $params[$target];
299 if ( empty( $settings2[ParamValidator
::PARAM_ISMULTI
] ) ) {
300 $ret['issues'][] = "PARAM_TEMPLATE_VARS[$key] target parameter \"$target\" must have "
301 . 'PARAM_ISMULTI = true';
303 if ( isset( $settings2[ApiBase
::PARAM_TEMPLATE_VARS
] ) ) {
304 if ( $target === $name ) {
305 $ret['issues'][] = "PARAM_TEMPLATE_VARS[$key] cannot target the parameter itself";
308 $settings2[ApiBase
::PARAM_TEMPLATE_VARS
],
309 $settings[ApiBase
::PARAM_TEMPLATE_VARS
]
311 $ret['issues'][] = "PARAM_TEMPLATE_VARS[$key]: Target's "
312 . 'PARAM_TEMPLATE_VARS must be a subset of the original';
318 $keys = implode( '|', array_map(
319 static function ( $key ) {
320 return preg_quote( $key, '/' );
322 array_keys( $settings[ApiBase
::PARAM_TEMPLATE_VARS
] )
324 if ( !preg_match( '/^(?>[^{}]+|\{(?:' . $keys . ')\})+$/', $name ) ) {
325 $ret['issues'][] = "Parameter name may not contain '{' or '}' other than '
326 . 'as defined by PARAM_TEMPLATE_VARS";
329 } elseif ( !preg_match( '/^[^{}]+$/', $name ) ) {
330 $ret['issues'][] = "Parameter name may not contain '{' or '}' without PARAM_TEMPLATE_VARS";
337 * Convert a ValidationException to an ApiUsageException
338 * @param ApiBase $module
339 * @param ValidationException $ex
340 * @throws ApiUsageException always
343 private function convertValidationException( ApiBase
$module, ValidationException
$ex ) {
344 $mv = $ex->getFailureMessage();
345 throw ApiUsageException
::newWithMessage(
356 * Get and validate a value
357 * @param ApiBase $module
358 * @param string $name Parameter name, unprefixed
359 * @param array|mixed $settings Default value or an array of settings
360 * using PARAM_* constants.
361 * @param array $options Options array
362 * @return mixed Validated parameter value
363 * @throws ApiUsageException if the value is invalid
365 public function getValue( ApiBase
$module, string $name, $settings, array $options = [] ) {
366 $options['module'] = $module;
367 $name = $module->encodeParamName( $name );
368 $settings = $this->normalizeSettings( $settings );
370 return $this->paramValidator
->getValue( $name, $settings, $options );
371 } catch ( ValidationException
$ex ) {
372 $this->convertValidationException( $module, $ex );
377 * Validate a parameter value using a settings array
379 * @param ApiBase $module
380 * @param string $name Parameter name, unprefixed
381 * @param mixed $value Parameter value
382 * @param array|mixed $settings Default value or an array of settings
383 * using PARAM_* constants.
384 * @param array $options Options array
385 * @return mixed Validated parameter value(s)
386 * @throws ApiUsageException if the value is invalid
388 public function validateValue(
389 ApiBase
$module, string $name, $value, $settings, array $options = []
391 $options['module'] = $module;
392 $name = $module->encodeParamName( $name );
393 $settings = $this->normalizeSettings( $settings );
395 return $this->paramValidator
->validateValue( $name, $value, $settings, $options );
396 } catch ( ValidationException
$ex ) {
397 $this->convertValidationException( $module, $ex );
402 * Describe parameter settings in a machine-readable format.
404 * @param ApiBase $module
405 * @param string $name Parameter name.
406 * @param array|mixed $settings Default value or an array of settings
407 * using PARAM_* constants.
408 * @param array $options Options array.
411 public function getParamInfo( ApiBase
$module, string $name, $settings, array $options ): array {
412 $options['module'] = $module;
413 $name = $module->encodeParamName( $name );
414 return $this->paramValidator
->getParamInfo( $name, $settings, $options );
418 * Describe parameter settings in human-readable format
420 * @param ApiBase $module
421 * @param string $name Parameter name being described.
422 * @param array|mixed $settings Default value or an array of settings
423 * using PARAM_* constants.
424 * @param array $options Options array.
427 public function getHelpInfo( ApiBase
$module, string $name, $settings, array $options ): array {
428 $options['module'] = $module;
429 $name = $module->encodeParamName( $name );
431 $ret = $this->paramValidator
->getHelpInfo( $name, $settings, $options );
432 foreach ( $ret as &$m ) {
434 $m = Message
::newFromSpecifier( $m );
435 if ( str_starts_with( $k, 'paramvalidator-help-' ) ) {
437 [ 'api-help-param-' . substr( $k, 20 ), $k ],
442 '@phan-var Message[] $ret'; // The above loop converts it