3 namespace Wikimedia\ParamValidator
;
6 use InvalidArgumentException
;
7 use Wikimedia\Assert\Assert
;
8 use Wikimedia\Message\DataMessageValue
;
9 use Wikimedia\Message\MessageValue
;
10 use Wikimedia\Message\ParamType
;
11 use Wikimedia\Message\ScalarParam
;
12 use Wikimedia\ObjectFactory\ObjectFactory
;
15 * Service for formatting and validating API parameters
17 * A settings array is simply an array with keys being the relevant PARAM_*
18 * constants from this class, TypeDef, and its subclasses.
20 * As a general overview of the architecture here:
21 * - ParamValidator handles some general validation of the parameter,
22 * then hands off to a TypeDef subclass to validate the specific representation
23 * based on the parameter's type.
24 * - TypeDef subclasses handle conversion between the string representation
25 * submitted by the client and the output PHP data types, validating that the
26 * strings are valid representations of the intended type as they do so.
27 * - ValidationException is used to report fatal errors in the validation back
28 * to the caller, since the return value represents the successful result of
29 * the validation and might be any type or class.
30 * - The Callbacks interface allows ParamValidator to reach out and fetch data
31 * it needs to perform the validation. Currently that includes:
32 * - Fetching the value of the parameter being validated (largely since a generic
33 * caller cannot know whether it needs to fetch a string from $_GET/$_POST or
34 * an array from $_FILES).
35 * - Reporting of non-fatal warnings back to the caller.
36 * - Fetching the "high limits" flag when necessary, to avoid the need for loading
37 * the user unnecessarily.
42 class ParamValidator
{
44 // region Constants for parameter settings arrays
45 /** @name Constants for parameter settings arrays
46 * These constants are keys in the settings array that define how the
47 * parameters coming in from the request are to be interpreted.
49 * If a constant is associated with a failure code, the failure code
50 * and data are described. ValidationExceptions are typically thrown, but
51 * those indicated as "non-fatal" are instead passed to
52 * Callbacks::recordCondition().
54 * Additional constants may be defined by TypeDef subclasses, or by other
55 * libraries for controlling things like auto-generated parameter documentation.
56 * For purposes of namespacing the constants, the values of all constants
57 * defined by this library begin with 'param-'.
63 * (mixed) Default value of the parameter. If omitted, null is the default.
65 * TypeDef::validate() will be informed when the default value was used by the presence of
66 * 'is-default' in $options.
68 public const PARAM_DEFAULT
= 'param-default';
71 * (string|array) Type of the parameter.
72 * Must be a registered type or an array of enumerated values (in which case the "enum"
73 * type must be registered). If omitted, the default is the PHP type of the default value
74 * (see PARAM_DEFAULT).
76 public const PARAM_TYPE
= 'param-type';
79 * (bool) Indicate that the parameter is required.
82 * - 'missingparam': The parameter is omitted/empty (and no default was set). No data.
84 public const PARAM_REQUIRED
= 'param-required';
87 * (bool) Indicate that the parameter is multi-valued.
89 * A multi-valued parameter may be submitted in one of several formats. All
90 * of the following result in a value of `[ 'a', 'b', 'c' ]`.
91 * - "a|b|c", i.e. pipe-separated.
92 * - "\x1Fa\x1Fb\x1Fc", i.e. separated by U+001F, with a signalling U+001F at the start.
93 * - As a string[], e.g. from a query string like "foo[]=a&foo[]=b&foo[]=c".
95 * Each of the multiple values is passed individually to the TypeDef.
96 * $options will contain a 'values-list' key holding the entire list.
98 * By default duplicates are removed from the resulting parameter list. Use
99 * PARAM_ALLOW_DUPLICATES to override that behavior.
102 * - 'toomanyvalues': More values were supplied than are allowed. See
103 * PARAM_ISMULTI_LIMIT1, PARAM_ISMULTI_LIMIT2, and constructor option
104 * 'ismultiLimits'. Data:
105 * - 'limit': The limit currently in effect.
106 * - 'lowlimit': The limit when high limits are not allowed.
107 * - 'highlimit': The limit when high limits are allowed.
108 * - 'unrecognizedvalues': Non-fatal. Invalid values were passed and
109 * PARAM_IGNORE_UNRECOGNIZED_VALUES was set. Data:
110 * - 'values': The unrecognized values.
112 public const PARAM_ISMULTI
= 'param-ismulti';
115 * (int) Maximum number of multi-valued parameter values allowed
119 public const PARAM_ISMULTI_LIMIT1
= 'param-ismulti-limit1';
122 * (int) Maximum number of multi-valued parameter values allowed for users
123 * allowed high limits.
127 public const PARAM_ISMULTI_LIMIT2
= 'param-ismulti-limit2';
130 * (bool|string) Whether a magic "all values" value exists for multi-valued
131 * enumerated types, and if so what that value is.
133 * When PARAM_TYPE has a defined set of values and PARAM_ISMULTI is true,
134 * this allows for an asterisk ('*') to be passed in place of a pipe-separated list of
135 * every possible value. If a string is set, it will be used in place of the asterisk.
137 public const PARAM_ALL
= 'param-all';
140 * (bool) Allow the same value to be set more than once when PARAM_ISMULTI is true?
142 * If not truthy, the set of values will be passed through
143 * `array_values( array_unique() )`. The default is falsey.
145 public const PARAM_ALLOW_DUPLICATES
= 'param-allow-duplicates';
148 * (bool) Indicate that the parameter's value should not be logged.
150 * Failure codes: (non-fatal)
151 * - 'param-sensitive': Always recorded when the parameter is used.
153 public const PARAM_SENSITIVE
= 'param-sensitive';
156 * (bool) Indicate that a deprecated parameter was used.
158 * Failure codes: (non-fatal)
159 * - 'param-deprecated': Always recorded when the parameter is used.
161 public const PARAM_DEPRECATED
= 'param-deprecated';
164 * (bool) Whether to downgrade "badvalue" errors to non-fatal when validating multi-valued
168 public const PARAM_IGNORE_UNRECOGNIZED_VALUES
= 'param-ignore-unrecognized-values';
171 // endregion -- end of Constants for parameter settings arrays
174 * @see TypeDef::OPT_ENFORCE_JSON_TYPES
176 public const OPT_ENFORCE_JSON_TYPES
= TypeDef
::OPT_ENFORCE_JSON_TYPES
;
178 /** Magic "all values" value when PARAM_ALL is true. */
179 public const ALL_DEFAULT_STRING
= '*';
181 /** A list of standard type names and types that may be passed as `$typeDefs` to __construct(). */
182 public const STANDARD_TYPES
= [
183 'boolean' => [ 'class' => TypeDef\BooleanDef
::class ],
184 'checkbox' => [ 'class' => TypeDef\PresenceBooleanDef
::class ],
185 'integer' => [ 'class' => TypeDef\IntegerDef
::class ],
186 'limit' => [ 'class' => TypeDef\LimitDef
::class ],
187 'float' => [ 'class' => TypeDef\FloatDef
::class ],
188 'double' => [ 'class' => TypeDef\FloatDef
::class ],
189 'string' => [ 'class' => TypeDef\StringDef
::class ],
190 'password' => [ 'class' => TypeDef\PasswordDef
::class ],
192 'class' => TypeDef\StringDef
::class,
194 TypeDef\StringDef
::OPT_ALLOW_EMPTY
=> true,
197 'timestamp' => [ 'class' => TypeDef\TimestampDef
::class ],
198 'upload' => [ 'class' => TypeDef\UploadDef
::class ],
199 'enum' => [ 'class' => TypeDef\EnumDef
::class ],
200 'expiry' => [ 'class' => TypeDef\ExpiryDef
::class ],
203 /** @var Callbacks */
206 /** @var ObjectFactory */
207 private $objectFactory;
209 /** @var (TypeDef|array)[] Map parameter type names to TypeDef objects or ObjectFactory specs */
210 private $typeDefs = [];
212 /** @var int Default values for PARAM_ISMULTI_LIMIT1 */
213 private $ismultiLimit1;
215 /** @var int Default values for PARAM_ISMULTI_LIMIT2 */
216 private $ismultiLimit2;
219 * @param Callbacks $callbacks
220 * @param ObjectFactory $objectFactory To turn specs into TypeDef objects
221 * @param array $options Associative array of additional settings
222 * - 'typeDefs': (array) As for addTypeDefs(). If omitted, self::STANDARD_TYPES will be used.
223 * Pass an empty array if you want to start with no registered types.
224 * - 'ismultiLimits': (int[]) Two ints, being the default values for PARAM_ISMULTI_LIMIT1 and
225 * PARAM_ISMULTI_LIMIT2. If not given, defaults to `[ 50, 500 ]`.
227 public function __construct(
228 Callbacks
$callbacks,
229 ObjectFactory
$objectFactory,
232 $this->callbacks
= $callbacks;
233 $this->objectFactory
= $objectFactory;
235 $this->addTypeDefs( $options['typeDefs'] ?? self
::STANDARD_TYPES
);
236 $this->ismultiLimit1
= $options['ismultiLimits'][0] ??
50;
237 $this->ismultiLimit2
= $options['ismultiLimits'][1] ??
500;
241 * List known type names
244 public function knownTypes() {
245 return array_keys( $this->typeDefs
);
249 * Register multiple type handlers
252 * @param array $typeDefs Associative array mapping `$name` to `$typeDef`.
254 public function addTypeDefs( array $typeDefs ) {
255 foreach ( $typeDefs as $name => $def ) {
256 $this->addTypeDef( $name, $def );
261 * Register a type handler
263 * To allow code to omit PARAM_TYPE in settings arrays to derive the type
264 * from PARAM_DEFAULT, it is strongly recommended that the following types be
265 * registered: "boolean", "integer", "double", "string", "NULL", and "enum".
267 * When using ObjectFactory specs, the following extra arguments are passed:
268 * - The Callbacks object for this ParamValidator instance.
270 * @param string $name Type name
271 * @param TypeDef|array $typeDef Type handler or ObjectFactory spec to create one.
273 public function addTypeDef( $name, $typeDef ) {
274 Assert
::parameterType(
275 [ TypeDef
::class, 'array' ],
280 if ( isset( $this->typeDefs
[$name] ) ) {
281 throw new InvalidArgumentException( "Type '$name' is already registered" );
283 $this->typeDefs
[$name] = $typeDef;
287 * Register a type handler, overriding any existing handler
289 * @param string $name Type name
290 * @param TypeDef|array|null $typeDef As for addTypeDef, or null to unregister a type.
292 public function overrideTypeDef( $name, $typeDef ) {
293 Assert
::parameterType(
294 [ TypeDef
::class, 'array', 'null' ],
299 if ( $typeDef === null ) {
300 unset( $this->typeDefs
[$name] );
302 $this->typeDefs
[$name] = $typeDef;
307 * Test if a type is registered
308 * @param string $name Type name
311 public function hasTypeDef( $name ) {
312 return isset( $this->typeDefs
[$name] );
316 * Get the TypeDef for a type
317 * @param string|array $type Any array is considered equivalent to the string "enum".
318 * @return TypeDef|null
320 public function getTypeDef( $type ) {
321 if ( is_array( $type ) ) {
325 if ( !isset( $this->typeDefs
[$type] ) ) {
329 $def = $this->typeDefs
[$type];
330 if ( !$def instanceof TypeDef
) {
331 $def = $this->objectFactory
->createObject( $def, [
332 'extraArgs' => [ $this->callbacks
],
333 'assertClass' => TypeDef
::class,
335 $this->typeDefs
[$type] = $def;
342 * Logic shared by normalizeSettings() and checkSettings()
343 * @param array|mixed $settings
346 private function normalizeSettingsInternal( $settings ) {
348 if ( !is_array( $settings ) ) {
350 self
::PARAM_DEFAULT
=> $settings,
354 // When type is not given, determine it from the type of the PARAM_DEFAULT
355 if ( !isset( $settings[self
::PARAM_TYPE
] ) ) {
356 $settings[self
::PARAM_TYPE
] = gettype( $settings[self
::PARAM_DEFAULT
] ??
null );
363 * Normalize a parameter settings array
364 * @param array|mixed $settings Default value or an array of settings
365 * using PARAM_* constants.
368 public function normalizeSettings( $settings ) {
369 $settings = $this->normalizeSettingsInternal( $settings );
371 $typeDef = $this->getTypeDef( $settings[self
::PARAM_TYPE
] );
373 $settings = $typeDef->normalizeSettings( $settings );
380 * Validate a parameter settings array
382 * This is intended for validation of parameter settings during unit or
383 * integration testing, and should implement strict checks.
385 * The rest of the code should generally be more permissive.
387 * @param string $name Parameter name
388 * @param array|mixed $settings Default value or an array of settings
389 * using PARAM_* constants.
390 * @param array $options Options array, passed through to the TypeDef and Callbacks.
392 * - 'issues': (string[]) Errors detected in $settings, as English text. If the settings
393 * are valid, this will be the empty array.
394 * - 'allowedKeys': (string[]) ParamValidator keys that are allowed in `$settings`.
395 * - 'messages': (MessageValue[]) Messages to be checked for existence.
397 public function checkSettings( string $name, $settings, array $options ): array {
398 $settings = $this->normalizeSettingsInternal( $settings );
401 self
::PARAM_TYPE
, self
::PARAM_DEFAULT
, self
::PARAM_REQUIRED
, self
::PARAM_ISMULTI
,
402 self
::PARAM_SENSITIVE
, self
::PARAM_DEPRECATED
, self
::PARAM_IGNORE_UNRECOGNIZED_VALUES
,
406 $type = $settings[self
::PARAM_TYPE
];
408 if ( !is_string( $type ) && !is_array( $type ) ) {
409 $issues[self
::PARAM_TYPE
] = 'PARAM_TYPE must be a string or array, got ' . gettype( $type );
411 $typeDef = $this->getTypeDef( $settings[self
::PARAM_TYPE
] );
413 if ( is_array( $type ) ) {
416 $issues[self
::PARAM_TYPE
] = "Unknown/unregistered PARAM_TYPE \"$type\"";
420 if ( isset( $settings[self
::PARAM_DEFAULT
] ) ) {
422 $this->validateValue(
423 $name, $settings[self
::PARAM_DEFAULT
], $settings, [ 'is-default' => true ] +
$options
425 } catch ( ValidationException
$ex ) {
426 $issues[self
::PARAM_DEFAULT
] = 'Value for PARAM_DEFAULT does not validate (code '
427 . $ex->getFailureMessage()->getCode() . ')';
431 if ( !is_bool( $settings[self
::PARAM_REQUIRED
] ??
false ) ) {
432 $issues[self
::PARAM_REQUIRED
] = 'PARAM_REQUIRED must be boolean, got '
433 . gettype( $settings[self
::PARAM_REQUIRED
] );
436 if ( !is_bool( $settings[self
::PARAM_ISMULTI
] ??
false ) ) {
437 $issues[self
::PARAM_ISMULTI
] = 'PARAM_ISMULTI must be boolean, got '
438 . gettype( $settings[self
::PARAM_ISMULTI
] );
441 if ( !empty( $settings[self
::PARAM_ISMULTI
] ) ) {
442 $allowedKeys = array_merge( $allowedKeys, [
443 self
::PARAM_ISMULTI_LIMIT1
, self
::PARAM_ISMULTI_LIMIT2
,
444 self
::PARAM_ALL
, self
::PARAM_ALLOW_DUPLICATES
447 $limit1 = $settings[self
::PARAM_ISMULTI_LIMIT1
] ??
$this->ismultiLimit1
;
448 $limit2 = $settings[self
::PARAM_ISMULTI_LIMIT2
] ??
$this->ismultiLimit2
;
449 if ( !is_int( $limit1 ) ) {
450 $issues[self
::PARAM_ISMULTI_LIMIT1
] = 'PARAM_ISMULTI_LIMIT1 must be an integer, got '
451 . gettype( $settings[self
::PARAM_ISMULTI_LIMIT1
] );
452 } elseif ( $limit1 <= 0 ) {
453 $issues[self
::PARAM_ISMULTI_LIMIT1
] =
454 "PARAM_ISMULTI_LIMIT1 must be greater than 0, got $limit1";
456 if ( !is_int( $limit2 ) ) {
457 $issues[self
::PARAM_ISMULTI_LIMIT2
] = 'PARAM_ISMULTI_LIMIT2 must be an integer, got '
458 . gettype( $settings[self
::PARAM_ISMULTI_LIMIT2
] );
459 } elseif ( $limit2 < $limit1 ) {
460 $issues[self
::PARAM_ISMULTI_LIMIT2
] =
461 'PARAM_ISMULTI_LIMIT2 must be greater than or equal to PARAM_ISMULTI_LIMIT1, but '
462 . "$limit2 < $limit1";
465 $all = $settings[self
::PARAM_ALL
] ??
false;
466 if ( !is_string( $all ) && !is_bool( $all ) ) {
467 $issues[self
::PARAM_ALL
] = 'PARAM_ALL must be a string or boolean, got ' . gettype( $all );
468 } elseif ( $all !== false && $typeDef ) {
469 if ( $all === true ) {
470 $all = self
::ALL_DEFAULT_STRING
;
472 $values = $typeDef->getEnumValues( $name, $settings, $options );
473 if ( !is_array( $values ) ) {
474 $issues[self
::PARAM_ALL
] = 'PARAM_ALL cannot be used with non-enumerated types';
475 } elseif ( in_array( $all, $values, true ) ) {
476 $issues[self
::PARAM_ALL
] = 'Value for PARAM_ALL conflicts with an enumerated value';
480 if ( !is_bool( $settings[self
::PARAM_ALLOW_DUPLICATES
] ??
false ) ) {
481 $issues[self
::PARAM_ALLOW_DUPLICATES
] = 'PARAM_ALLOW_DUPLICATES must be boolean, got '
482 . gettype( $settings[self
::PARAM_ALLOW_DUPLICATES
] );
486 if ( !is_bool( $settings[self
::PARAM_SENSITIVE
] ??
false ) ) {
487 $issues[self
::PARAM_SENSITIVE
] = 'PARAM_SENSITIVE must be boolean, got '
488 . gettype( $settings[self
::PARAM_SENSITIVE
] );
491 if ( !is_bool( $settings[self
::PARAM_DEPRECATED
] ??
false ) ) {
492 $issues[self
::PARAM_DEPRECATED
] = 'PARAM_DEPRECATED must be boolean, got '
493 . gettype( $settings[self
::PARAM_DEPRECATED
] );
496 if ( !is_bool( $settings[self
::PARAM_IGNORE_UNRECOGNIZED_VALUES
] ??
false ) ) {
497 $issues[self
::PARAM_IGNORE_UNRECOGNIZED_VALUES
] = 'PARAM_IGNORE_UNRECOGNIZED_VALUES must be '
498 . 'boolean, got ' . gettype( $settings[self
::PARAM_IGNORE_UNRECOGNIZED_VALUES
] );
501 $ret = [ 'issues' => $issues, 'allowedKeys' => $allowedKeys, 'messages' => $messages ];
503 $ret = $typeDef->checkSettings( $name, $settings, $options, $ret );
510 * Fetch and validate a parameter value using a settings array
512 * @param string $name Parameter name
513 * @param array|mixed $settings Default value or an array of settings
514 * using PARAM_* constants.
515 * @param array $options Options array, passed through to the TypeDef and Callbacks.
516 * - An additional option, 'is-default', will be set when the value comes from PARAM_DEFAULT.
517 * @return mixed Validated parameter value
518 * @throws ValidationException if the value is invalid
520 public function getValue( $name, $settings, array $options = [] ) {
521 $settings = $this->normalizeSettings( $settings );
523 $typeDef = $this->getTypeDef( $settings[self
::PARAM_TYPE
] );
525 throw new DomainException(
526 "Param $name's type is unknown - {$settings[self::PARAM_TYPE]}"
530 $value = $typeDef->getValue( $name, $settings, $options );
532 if ( $value !== null ) {
533 if ( !empty( $settings[self
::PARAM_SENSITIVE
] ) ) {
534 $strValue = $typeDef->stringifyValue( $name, $value, $settings, $options );
535 $this->callbacks
->recordCondition(
536 DataMessageValue
::new( 'paramvalidator-param-sensitive', [], 'param-sensitive' )
537 ->plaintextParams( $name, $strValue ),
538 $name, $value, $settings, $options
542 // Set a warning if a deprecated parameter has been passed
543 if ( !empty( $settings[self
::PARAM_DEPRECATED
] ) ) {
544 $strValue = $typeDef->stringifyValue( $name, $value, $settings, $options );
545 $this->callbacks
->recordCondition(
546 DataMessageValue
::new( 'paramvalidator-param-deprecated', [], 'param-deprecated' )
547 ->plaintextParams( $name, $strValue ),
548 $name, $value, $settings, $options
551 } elseif ( isset( $settings[self
::PARAM_DEFAULT
] ) ) {
552 $value = $settings[self
::PARAM_DEFAULT
];
553 $options['is-default'] = true;
556 return $this->validateValue( $name, $value, $settings, $options );
560 * Validate a parameter value using a settings array
562 * @param string $name Parameter name
563 * @param null|mixed $value Parameter value
564 * @param array|mixed $settings Default value or an array of settings
565 * using PARAM_* constants.
566 * @param array $options Options array, passed through to the TypeDef and Callbacks.
567 * - An additional option, 'values-list', will be set when processing the
568 * values of a multi-valued parameter.
569 * @return mixed Validated parameter value(s)
570 * @throws ValidationException if the value is invalid
572 public function validateValue( $name, $value, $settings, array $options = [] ) {
573 $settings = $this->normalizeSettings( $settings );
575 $typeDef = $this->getTypeDef( $settings[self
::PARAM_TYPE
] );
577 throw new DomainException(
578 "Param $name's type is unknown - {$settings[self::PARAM_TYPE]}"
582 if ( $value === null ) {
583 if ( !empty( $settings[self
::PARAM_REQUIRED
] ) ) {
584 throw new ValidationException(
585 DataMessageValue
::new( 'paramvalidator-missingparam', [], 'missingparam' )
586 ->plaintextParams( $name ),
587 $name, $value, $settings
594 if ( empty( $settings[self
::PARAM_ISMULTI
] ) ) {
595 if ( is_string( $value ) && substr( $value, 0, 1 ) === "\x1f" ) {
596 throw new ValidationException(
597 DataMessageValue
::new( 'paramvalidator-notmulti', [], 'badvalue' )
598 ->plaintextParams( $name, $value ),
599 $name, $value, $settings
603 // T326764: If the type of the actual param value is different from
604 // the type that is defined via getParamSettings(), throw an exception
605 // because this is a type to value mismatch.
606 if ( is_array( $value ) && !$typeDef->supportsArrays() ) {
607 throw new ValidationException(
608 DataMessageValue
::new( 'paramvalidator-notmulti', [], 'badvalue' )
609 ->plaintextParams( $name, gettype( $value ) ),
610 $name, $value, $settings
614 return $typeDef->validate( $name, $value, $settings, $options );
617 // Split the multi-value and validate each parameter
618 $limit1 = $settings[self
::PARAM_ISMULTI_LIMIT1
] ??
$this->ismultiLimit1
;
619 $limit2 = max( $limit1, $settings[self
::PARAM_ISMULTI_LIMIT2
] ??
$this->ismultiLimit2
);
621 if ( is_array( $value ) ) {
622 $valuesList = $value;
623 } elseif ( $options[ self
::OPT_ENFORCE_JSON_TYPES
] ??
false ) {
624 throw new ValidationException(
625 DataMessageValue
::new(
626 'paramvalidator-multivalue-must-be-array',
628 'multivalue-must-be-array'
629 )->plaintextParams( $name ),
630 $name, $value, $settings
633 $valuesList = self
::explodeMultiValue( $value, $limit2 +
1 );
637 $enumValues = $typeDef->getEnumValues( $name, $settings, $options );
638 if ( is_array( $enumValues ) && isset( $settings[self
::PARAM_ALL
] ) &&
639 count( $valuesList ) === 1
641 $allValue = is_string( $settings[self
::PARAM_ALL
] )
642 ?
$settings[self
::PARAM_ALL
]
643 : self
::ALL_DEFAULT_STRING
;
644 if ( $valuesList[0] === $allValue ) {
649 // Avoid checking useHighLimits() unless it's actually necessary
651 $limit2 > $limit1 && count( $valuesList ) > $limit1 &&
652 $this->callbacks
->useHighLimits( $options )
653 ) ?
$limit2 : $limit1;
654 if ( count( $valuesList ) > $sizeLimit ) {
655 throw new ValidationException(
656 DataMessageValue
::new( 'paramvalidator-toomanyvalues', [], 'toomanyvalues', [
657 'parameter' => $name,
658 'limit' => $sizeLimit,
659 'lowlimit' => $limit1,
660 'highlimit' => $limit2,
661 ] )->plaintextParams( $name )->numParams( $sizeLimit ),
662 $name, $valuesList, $settings
666 $options['values-list'] = $valuesList;
669 foreach ( $valuesList as $v ) {
671 $validValues[] = $typeDef->validate( $name, $v, $settings, $options );
672 } catch ( ValidationException
$ex ) {
673 if ( $ex->getFailureMessage()->getCode() !== 'badvalue' ||
674 empty( $settings[self
::PARAM_IGNORE_UNRECOGNIZED_VALUES
] )
678 $invalidValues[] = $v;
681 if ( $invalidValues ) {
682 if ( is_array( $value ) ) {
683 $value = self
::implodeMultiValue( $value );
685 $this->callbacks
->recordCondition(
686 DataMessageValue
::new( 'paramvalidator-unrecognizedvalues', [], 'unrecognizedvalues', [
687 'values' => $invalidValues,
689 ->plaintextParams( $name, $value )
690 ->commaListParams( array_map( static function ( $v ) {
691 return new ScalarParam( ParamType
::PLAINTEXT
, $v );
692 }, $invalidValues ) )
693 ->numParams( count( $invalidValues ) ),
694 $name, $value, $settings, $options
698 // Throw out duplicates if requested
699 if ( empty( $settings[self
::PARAM_ALLOW_DUPLICATES
] ) ) {
700 $validValues = array_values( array_unique( $validValues ) );
707 * Describe parameter settings in a machine-readable format.
709 * @param string $name Parameter name.
710 * @param array|mixed $settings Default value or an array of settings
711 * using PARAM_* constants.
712 * @param array $options Options array.
715 public function getParamInfo( $name, $settings, array $options ) {
716 $settings = $this->normalizeSettings( $settings );
717 $typeDef = $this->getTypeDef( $settings[self
::PARAM_TYPE
] );
720 $info['type'] = $settings[self
::PARAM_TYPE
];
721 $info['required'] = !empty( $settings[self
::PARAM_REQUIRED
] );
722 if ( !empty( $settings[self
::PARAM_DEPRECATED
] ) ) {
723 $info['deprecated'] = true;
725 if ( !empty( $settings[self
::PARAM_SENSITIVE
] ) ) {
726 $info['sensitive'] = true;
728 if ( isset( $settings[self
::PARAM_DEFAULT
] ) ) {
729 $info['default'] = $settings[self
::PARAM_DEFAULT
];
731 $info['multi'] = !empty( $settings[self
::PARAM_ISMULTI
] );
732 if ( $info['multi'] ) {
733 $info['lowlimit'] = $settings[self
::PARAM_ISMULTI_LIMIT1
] ??
$this->ismultiLimit1
;
734 $info['highlimit'] = max(
735 $info['lowlimit'], $settings[self
::PARAM_ISMULTI_LIMIT2
] ??
$this->ismultiLimit2
738 $info['highlimit'] > $info['lowlimit'] && $this->callbacks
->useHighLimits( $options )
742 if ( !empty( $settings[self
::PARAM_ALLOW_DUPLICATES
] ) ) {
743 $info['allowsduplicates'] = true;
746 $allSpecifier = $settings[self
::PARAM_ALL
] ??
false;
747 if ( $allSpecifier !== false ) {
748 if ( !is_string( $allSpecifier ) ) {
749 $allSpecifier = self
::ALL_DEFAULT_STRING
;
751 $info['allspecifier'] = $allSpecifier;
756 $info = array_merge( $info, $typeDef->getParamInfo( $name, $settings, $options ) );
759 // Filter out nulls (strictly)
760 return array_filter( $info, static function ( $v ) {
766 * Describe parameter settings in human-readable format
768 * @param string $name Parameter name being described.
769 * @param array|mixed $settings Default value or an array of settings
770 * using PARAM_* constants.
771 * @param array $options Options array.
772 * @return MessageValue[]
774 public function getHelpInfo( $name, $settings, array $options ) {
775 $settings = $this->normalizeSettings( $settings );
776 $typeDef = $this->getTypeDef( $settings[self
::PARAM_TYPE
] );
778 // Define ordering. Some are overwritten below, some expected from the TypeDef
780 self
::PARAM_DEPRECATED
=> null,
781 self
::PARAM_REQUIRED
=> null,
782 self
::PARAM_SENSITIVE
=> null,
783 self
::PARAM_TYPE
=> null,
784 self
::PARAM_ISMULTI
=> null,
785 self
::PARAM_ISMULTI_LIMIT1
=> null,
786 self
::PARAM_ALL
=> null,
787 self
::PARAM_DEFAULT
=> null,
790 if ( !empty( $settings[self
::PARAM_DEPRECATED
] ) ) {
791 $info[self
::PARAM_DEPRECATED
] = MessageValue
::new( 'paramvalidator-help-deprecated' );
794 if ( !empty( $settings[self
::PARAM_REQUIRED
] ) ) {
795 $info[self
::PARAM_REQUIRED
] = MessageValue
::new( 'paramvalidator-help-required' );
798 if ( !empty( $settings[self
::PARAM_ISMULTI
] ) ) {
799 $info[self
::PARAM_ISMULTI
] = MessageValue
::new( 'paramvalidator-help-multi-separate' );
801 $lowcount = $settings[self
::PARAM_ISMULTI_LIMIT1
] ??
$this->ismultiLimit1
;
802 $highcount = max( $lowcount, $settings[self
::PARAM_ISMULTI_LIMIT2
] ??
$this->ismultiLimit2
);
803 $values = $typeDef ?
$typeDef->getEnumValues( $name, $settings, $options ) : null;
805 // Only mention the limits if they're likely to matter.
806 $values === null ||
count( $values ) > $lowcount ||
807 !empty( $settings[self
::PARAM_ALLOW_DUPLICATES
] )
809 if ( $highcount > $lowcount ) {
810 $info[self
::PARAM_ISMULTI_LIMIT1
] = MessageValue
::new( 'paramvalidator-help-multi-max' )
811 ->numParams( $lowcount, $highcount );
813 $info[self
::PARAM_ISMULTI_LIMIT1
] = MessageValue
::new( 'paramvalidator-help-multi-max-simple' )
814 ->numParams( $lowcount );
818 $allSpecifier = $settings[self
::PARAM_ALL
] ??
false;
819 if ( $allSpecifier !== false ) {
820 if ( !is_string( $allSpecifier ) ) {
821 $allSpecifier = self
::ALL_DEFAULT_STRING
;
823 $info[self
::PARAM_ALL
] = MessageValue
::new( 'paramvalidator-help-multi-all' )
824 ->plaintextParams( $allSpecifier );
828 if ( isset( $settings[self
::PARAM_DEFAULT
] ) && $typeDef ) {
829 $value = $typeDef->stringifyValue( $name, $settings[self
::PARAM_DEFAULT
], $settings, $options );
830 if ( $value === '' ) {
831 $info[self
::PARAM_DEFAULT
] = MessageValue
::new( 'paramvalidator-help-default-empty' );
832 } elseif ( $value !== null ) {
833 $info[self
::PARAM_DEFAULT
] = MessageValue
::new( 'paramvalidator-help-default' )
834 ->plaintextParams( $value );
839 $info = array_merge( $info, $typeDef->getHelpInfo( $name, $settings, $options ) );
842 // Put the default at the very end (the TypeDef may have added extra messages)
843 $default = $info[self
::PARAM_DEFAULT
];
844 unset( $info[self
::PARAM_DEFAULT
] );
845 $info[self
::PARAM_DEFAULT
] = $default;
848 return array_filter( $info );
852 * Split a multi-valued parameter string, like explode()
854 * Note that, unlike explode(), this will return an empty array when given
857 * @param string $value
861 public static function explodeMultiValue( $value, $limit ) {
862 if ( $value === '' ||
$value === "\x1f" ) {
866 if ( substr( $value, 0, 1 ) === "\x1f" ) {
868 $value = substr( $value, 1 );
873 return explode( $sep, $value, $limit );
877 * Implode an array as a multi-valued parameter string, like implode()
879 * @param array $value
882 public static function implodeMultiValue( array $value ) {
883 if ( $value === [ '' ] ) {
884 // There's no value that actually returns a single empty string.
885 // Best we can do is this that returns two, which will be deduplicated to one.
889 foreach ( $value as $v ) {
890 if ( strpos( $v, '|' ) !== false ) {
891 return "\x1f" . implode( "\x1f", $value );
894 return implode( '|', $value );