MediaWiki  master
ParamValidator.php
Go to the documentation of this file.
1 <?php
2 
4 
13 
43 
63  const PARAM_DEFAULT = 'param-default';
64 
71  const PARAM_TYPE = 'param-type';
72 
79  const PARAM_REQUIRED = 'param-required';
80 
107  const PARAM_ISMULTI = 'param-ismulti';
108 
114  const PARAM_ISMULTI_LIMIT1 = 'param-ismulti-limit1';
115 
122  const PARAM_ISMULTI_LIMIT2 = 'param-ismulti-limit2';
123 
132  const PARAM_ALL = 'param-all';
133 
140  const PARAM_ALLOW_DUPLICATES = 'param-allow-duplicates';
141 
148  const PARAM_SENSITIVE = 'param-sensitive';
149 
156  const PARAM_DEPRECATED = 'param-deprecated';
157 
166  const PARAM_IGNORE_INVALID_VALUES = 'param-ignore-invalid-values';
167 
171  const ALL_DEFAULT_STRING = '*';
172 
174  public static $STANDARD_TYPES = [
175  'boolean' => [ 'class' => TypeDef\BooleanDef::class ],
176  'checkbox' => [ 'class' => TypeDef\PresenceBooleanDef::class ],
177  'integer' => [ 'class' => TypeDef\IntegerDef::class ],
178  'limit' => [ 'class' => TypeDef\LimitDef::class ],
179  'float' => [ 'class' => TypeDef\FloatDef::class ],
180  'double' => [ 'class' => TypeDef\FloatDef::class ],
181  'string' => [ 'class' => TypeDef\StringDef::class ],
182  'password' => [ 'class' => TypeDef\PasswordDef::class ],
183  'NULL' => [
184  'class' => TypeDef\StringDef::class,
185  'args' => [ [
186  'allowEmptyWhenRequired' => true,
187  ] ],
188  ],
189  'timestamp' => [ 'class' => TypeDef\TimestampDef::class ],
190  'upload' => [ 'class' => TypeDef\UploadDef::class ],
191  'enum' => [ 'class' => TypeDef\EnumDef::class ],
192  ];
193 
195  private $callbacks;
196 
198  private $objectFactory;
199 
201  private $typeDefs = [];
202 
204  private $ismultiLimit1;
205 
207  private $ismultiLimit2;
208 
218  public function __construct(
220  ObjectFactory $objectFactory,
221  array $options = []
222  ) {
223  $this->callbacks = $callbacks;
224  $this->objectFactory = $objectFactory;
225 
226  $this->addTypeDefs( $options['typeDefs'] ?? self::$STANDARD_TYPES );
227  $this->ismultiLimit1 = $options['ismultiLimits'][0] ?? 50;
228  $this->ismultiLimit2 = $options['ismultiLimits'][1] ?? 500;
229  }
230 
235  public function knownTypes() {
236  return array_keys( $this->typeDefs );
237  }
238 
245  public function addTypeDefs( array $typeDefs ) {
246  foreach ( $typeDefs as $name => $def ) {
247  $this->addTypeDef( $name, $def );
248  }
249  }
250 
264  public function addTypeDef( $name, $typeDef ) {
265  Assert::parameterType(
266  implode( '|', [ TypeDef::class, 'array' ] ),
267  $typeDef,
268  '$typeDef'
269  );
270 
271  if ( isset( $this->typeDefs[$name] ) ) {
272  throw new InvalidArgumentException( "Type '$name' is already registered" );
273  }
274  $this->typeDefs[$name] = $typeDef;
275  }
276 
283  public function overrideTypeDef( $name, $typeDef ) {
284  Assert::parameterType(
285  implode( '|', [ TypeDef::class, 'array', 'null' ] ),
286  $typeDef,
287  '$typeDef'
288  );
289 
290  if ( $typeDef === null ) {
291  unset( $this->typeDefs[$name] );
292  } else {
293  $this->typeDefs[$name] = $typeDef;
294  }
295  }
296 
302  public function hasTypeDef( $name ) {
303  return isset( $this->typeDefs[$name] );
304  }
305 
311  public function getTypeDef( $type ) {
312  if ( is_array( $type ) ) {
313  $type = 'enum';
314  }
315 
316  if ( !isset( $this->typeDefs[$type] ) ) {
317  return null;
318  }
319 
320  $def = $this->typeDefs[$type];
321  if ( !$def instanceof TypeDef ) {
322  $def = $this->objectFactory->createObject( $def, [
323  'extraArgs' => [ $this->callbacks ],
324  'assertClass' => TypeDef::class,
325  ] );
326  $this->typeDefs[$type] = $def;
327  }
328 
329  return $def;
330  }
331 
338  public function normalizeSettings( $settings ) {
339  // Shorthand
340  if ( !is_array( $settings ) ) {
341  $settings = [
342  self::PARAM_DEFAULT => $settings,
343  ];
344  }
345 
346  // When type is not given, determine it from the type of the PARAM_DEFAULT
347  if ( !isset( $settings[self::PARAM_TYPE] ) ) {
348  $settings[self::PARAM_TYPE] = gettype( $settings[self::PARAM_DEFAULT] ?? null );
349  }
350 
351  $typeDef = $this->getTypeDef( $settings[self::PARAM_TYPE] );
352  if ( $typeDef ) {
353  $settings = $typeDef->normalizeSettings( $settings );
354  }
355 
356  return $settings;
357  }
358 
369  public function getValue( $name, $settings, array $options = [] ) {
370  $settings = $this->normalizeSettings( $settings );
371 
372  $typeDef = $this->getTypeDef( $settings[self::PARAM_TYPE] );
373  if ( !$typeDef ) {
374  throw new DomainException(
375  "Param $name's type is unknown - {$settings[self::PARAM_TYPE]}"
376  );
377  }
378 
379  $value = $typeDef->getValue( $name, $settings, $options );
380 
381  if ( $value !== null ) {
382  if ( !empty( $settings[self::PARAM_SENSITIVE] ) ) {
383  $this->callbacks->recordCondition(
384  DataMessageValue::new( 'paramvalidator-param-sensitive', [], 'param-sensitive' )
385  ->plaintextParams( $name, $value ),
386  $name, $value, $settings, $options
387  );
388  }
389 
390  // Set a warning if a deprecated parameter has been passed
391  if ( !empty( $settings[self::PARAM_DEPRECATED] ) ) {
392  $this->callbacks->recordCondition(
393  DataMessageValue::new( 'paramvalidator-param-deprecated', [], 'param-deprecated' )
394  ->plaintextParams( $name, $value ),
395  $name, $value, $settings, $options
396  );
397  }
398  } elseif ( isset( $settings[self::PARAM_DEFAULT] ) ) {
399  $value = $settings[self::PARAM_DEFAULT];
400  }
401 
402  return $this->validateValue( $name, $value, $settings, $options );
403  }
404 
418  public function validateValue( $name, $value, $settings, array $options = [] ) {
419  $settings = $this->normalizeSettings( $settings );
420 
421  $typeDef = $this->getTypeDef( $settings[self::PARAM_TYPE] );
422  if ( !$typeDef ) {
423  throw new DomainException(
424  "Param $name's type is unknown - {$settings[self::PARAM_TYPE]}"
425  );
426  }
427 
428  if ( $value === null ) {
429  if ( !empty( $settings[self::PARAM_REQUIRED] ) ) {
430  throw new ValidationException(
431  DataMessageValue::new( 'paramvalidator-missingparam', [], 'missingparam' )
432  ->plaintextParams( $name ),
433  $name, $value, $settings
434  );
435  }
436  return null;
437  }
438 
439  // Non-multi
440  if ( empty( $settings[self::PARAM_ISMULTI] ) ) {
441  if ( substr( $value, 0, 1 ) === "\x1f" ) {
442  throw new ValidationException(
443  DataMessageValue::new( 'paramvalidator-notmulti', [], 'badvalue' )
444  ->plaintextParams( $name, $value ),
445  $name, $value, $settings
446  );
447  }
448  return $typeDef->validate( $name, $value, $settings, $options );
449  }
450 
451  // Split the multi-value and validate each parameter
452  $limit1 = $settings[self::PARAM_ISMULTI_LIMIT1] ?? $this->ismultiLimit1;
453  $limit2 = max( $limit1, $settings[self::PARAM_ISMULTI_LIMIT2] ?? $this->ismultiLimit2 );
454  $valuesList = is_array( $value ) ? $value : self::explodeMultiValue( $value, $limit2 + 1 );
455 
456  // Handle PARAM_ALL
457  $enumValues = $typeDef->getEnumValues( $name, $settings, $options );
458  if ( is_array( $enumValues ) && isset( $settings[self::PARAM_ALL] ) &&
459  count( $valuesList ) === 1
460  ) {
461  $allValue = is_string( $settings[self::PARAM_ALL] )
462  ? $settings[self::PARAM_ALL]
463  : self::ALL_DEFAULT_STRING;
464  if ( $valuesList[0] === $allValue ) {
465  return $enumValues;
466  }
467  }
468 
469  // Avoid checking useHighLimits() unless it's actually necessary
470  $sizeLimit = (
471  $limit2 > $limit1 && count( $valuesList ) > $limit1 &&
472  $this->callbacks->useHighLimits( $options )
473  ) ? $limit2 : $limit1;
474  if ( count( $valuesList ) > $sizeLimit ) {
475  if ( is_array( $value ) ) {
476  $value = self::implodeMultiValue( $value );
477  }
478  throw new ValidationException(
479  DataMessageValue::new( 'paramvalidator-toomanyvalues', [], 'toomanyvalues', [
480  'limit' => $sizeLimit,
481  'lowlimit' => $limit1,
482  'highlimit' => $limit2,
483  ] )->plaintextParams( $name, $value )->numParams( $sizeLimit ),
484  $name, $valuesList, $settings
485  );
486  }
487 
488  $options['values-list'] = $valuesList;
489  $validValues = [];
490  $invalidValues = [];
491  foreach ( $valuesList as $v ) {
492  try {
493  $validValues[] = $typeDef->validate( $name, $v, $settings, $options );
494  } catch ( ValidationException $ex ) {
495  if ( empty( $settings[self::PARAM_IGNORE_INVALID_VALUES] ) ) {
496  throw $ex;
497  }
498  $invalidValues[] = $v;
499  }
500  }
501  if ( $invalidValues ) {
502  $this->callbacks->recordCondition(
503  DataMessageValue::new( 'paramvalidator-unrecognizedvalues', [], 'unrecognizedvalues', [
504  'values' => $invalidValues,
505  ] )
506  ->plaintextParams( $name, $value )
507  ->commaListParams( array_map( function ( $v ) {
508  return new ScalarParam( ParamType::PLAINTEXT, $v );
509  }, $invalidValues ) )
510  ->numParams( count( $invalidValues ) ),
511  $name, $value, $settings, $options
512  );
513  }
514 
515  // Throw out duplicates if requested
516  if ( empty( $settings[self::PARAM_ALLOW_DUPLICATES] ) ) {
517  $validValues = array_values( array_unique( $validValues ) );
518  }
519 
520  return $validValues;
521  }
522 
532  public function getParamInfo( $name, $settings, array $options ) {
533  $settings = $this->normalizeSettings( $settings );
534  $typeDef = $this->getTypeDef( $settings[self::PARAM_TYPE] );
535  $info = [];
536 
537  $info['type'] = $settings[self::PARAM_TYPE];
538  $info['required'] = !empty( $settings[self::PARAM_REQUIRED] );
539  if ( !empty( $settings[self::PARAM_DEPRECATED] ) ) {
540  $info['deprecated'] = true;
541  }
542  if ( !empty( $settings[self::PARAM_SENSITIVE] ) ) {
543  $info['sensitive'] = true;
544  }
545  if ( isset( $settings[self::PARAM_DEFAULT] ) ) {
546  $info['default'] = $settings[self::PARAM_DEFAULT];
547  }
548  $info['multi'] = !empty( $settings[self::PARAM_ISMULTI] );
549  if ( $info['multi'] ) {
550  $info['lowlimit'] = $settings[self::PARAM_ISMULTI_LIMIT1] ?? $this->ismultiLimit1;
551  $info['highlimit'] = max(
552  $info['lowlimit'], $settings[self::PARAM_ISMULTI_LIMIT2] ?? $this->ismultiLimit2
553  );
554  $info['limit'] =
555  $info['highlimit'] > $info['lowlimit'] && $this->callbacks->useHighLimits( $options )
556  ? $info['highlimit']
557  : $info['lowlimit'];
558 
559  if ( !empty( $settings[self::PARAM_ALLOW_DUPLICATES] ) ) {
560  $info['allowsduplicates'] = true;
561  }
562 
563  $allSpecifier = $settings[self::PARAM_ALL] ?? false;
564  if ( $allSpecifier !== false ) {
565  if ( !is_string( $allSpecifier ) ) {
566  $allSpecifier = self::ALL_DEFAULT_STRING;
567  }
568  $info['allspecifier'] = $allSpecifier;
569  }
570  }
571 
572  if ( $typeDef ) {
573  $info = array_merge( $info, $typeDef->getParamInfo( $name, $settings, $options ) );
574  }
575 
576  // Filter out nulls (strictly)
577  return array_filter( $info, function ( $v ) {
578  return $v !== null;
579  } );
580  }
581 
591  public function getHelpInfo( $name, $settings, array $options ) {
592  $settings = $this->normalizeSettings( $settings );
593  $typeDef = $this->getTypeDef( $settings[self::PARAM_TYPE] );
594 
595  // Define ordering. Some are overwritten below, some expected from the TypeDef
596  $info = [
597  self::PARAM_DEPRECATED => null,
598  self::PARAM_REQUIRED => null,
599  self::PARAM_SENSITIVE => null,
600  self::PARAM_TYPE => null,
601  self::PARAM_ISMULTI => null,
602  self::PARAM_ISMULTI_LIMIT1 => null,
603  self::PARAM_ALL => null,
604  self::PARAM_DEFAULT => null,
605  ];
606 
607  if ( !empty( $settings[self::PARAM_DEPRECATED] ) ) {
608  $info[self::PARAM_DEPRECATED] = MessageValue::new( 'paramvalidator-help-deprecated' );
609  }
610 
611  if ( !empty( $settings[self::PARAM_REQUIRED] ) ) {
612  $info[self::PARAM_REQUIRED] = MessageValue::new( 'paramvalidator-help-required' );
613  }
614 
615  if ( !empty( $settings[self::PARAM_ISMULTI] ) ) {
616  $info[self::PARAM_ISMULTI] = MessageValue::new( 'paramvalidator-help-multi-sep' );
617 
618  $lowcount = $settings[self::PARAM_ISMULTI_LIMIT1] ?? $this->ismultiLimit1;
619  $highcount = max( $lowcount, $settings[self::PARAM_ISMULTI_LIMIT2] ?? $this->ismultiLimit2 );
620  $values = $typeDef ? $typeDef->getEnumValues( $name, $settings, $options ) : null;
621  if (
622  // Only mention the limits if they're likely to matter.
623  $values === null || count( $values ) > $lowcount ||
624  !empty( $settings[self::PARAM_ALLOW_DUPLICATES] )
625  ) {
626  if ( $highcount > $lowcount ) {
627  $info[self::PARAM_ISMULTI_LIMIT1] = MessageValue::new( 'paramvalidator-help-multi-max' )
628  ->numParams( $lowcount, $highcount );
629  } else {
630  $info[self::PARAM_ISMULTI_LIMIT1] = MessageValue::new( 'paramvalidator-help-multi-max-simple' )
631  ->numParams( $lowcount );
632  }
633  }
634 
635  $allSpecifier = $settings[self::PARAM_ALL] ?? false;
636  if ( $allSpecifier !== false ) {
637  if ( !is_string( $allSpecifier ) ) {
638  $allSpecifier = self::ALL_DEFAULT_STRING;
639  }
640  $info[self::PARAM_ALL] = MessageValue::new( 'paramvalidator-help-multi-all' )
641  ->plaintextParams( $allSpecifier );
642  }
643  }
644 
645  if ( isset( $settings[self::PARAM_DEFAULT] ) && $typeDef ) {
646  $value = $typeDef->stringifyValue( $name, $settings[self::PARAM_DEFAULT], $settings, $options );
647  if ( $value === '' ) {
648  $info[self::PARAM_DEFAULT] = MessageValue::new( 'paramvalidator-param-default-empty' );
649  } elseif ( $value !== null ) {
650  $info[self::PARAM_DEFAULT] = MessageValue::new( 'paramvalidator-param-default' )
651  ->plaintextParams( $value );
652  }
653  }
654 
655  if ( $typeDef ) {
656  $info = array_merge( $info, $typeDef->getHelpInfo( $name, $settings, $options ) );
657  }
658 
659  // Put the default at the very end (the TypeDef may have added extra messages)
660  $default = $info[self::PARAM_DEFAULT];
661  unset( $info[self::PARAM_DEFAULT] );
662  $info[self::PARAM_DEFAULT] = $default;
663 
664  // Filter out nulls
665  return array_filter( $info );
666  }
667 
678  public static function explodeMultiValue( $value, $limit ) {
679  if ( $value === '' || $value === "\x1f" ) {
680  return [];
681  }
682 
683  if ( substr( $value, 0, 1 ) === "\x1f" ) {
684  $sep = "\x1f";
685  $value = substr( $value, 1 );
686  } else {
687  $sep = '|';
688  }
689 
690  return explode( $sep, $value, $limit );
691  }
692 
699  public static function implodeMultiValue( array $value ) {
700  if ( $value === [ '' ] ) {
701  // There's no value that actually returns a single empty string.
702  // Best we can do is this that returns two, which will be deduplicated to one.
703  return '|';
704  }
705 
706  foreach ( $value as $v ) {
707  if ( strpos( $v, '|' ) !== false ) {
708  return "\x1f" . implode( "\x1f", $value );
709  }
710  }
711  return implode( '|', $value );
712  }
713 
714 }
const PARAM_ALLOW_DUPLICATES
(bool) Allow the same value to be set more than once when PARAM_ISMULTI is true?
int $ismultiLimit1
Default values for PARAM_ISMULTI_LIMIT1.
const PARAM_DEPRECATED
(bool) Indicate that a deprecated parameter was used.
const PARAM_REQUIRED
(bool) Indicate that the parameter is required.
getParamInfo( $name, $settings, array $options)
Describe parameter settings in a machine-readable format.
const PARAM_ISMULTI_LIMIT2
(int) Maximum number of multi-valued parameter values allowed for users allowed high limits...
static new( $key, $params=[], $code=null, array $data=null)
Static constructor for easier chaining of ->params() methods.
const PLAINTEXT
A text parameter which is substituted after formatter processing.
Definition: ParamType.php:64
normalizeSettings( $settings)
Normalize a parameter settings array.
Interface defining callbacks needed by ParamValidator.
Definition: Callbacks.php:21
const PARAM_ISMULTI_LIMIT1
(int) Maximum number of multi-valued parameter values allowed
TypeDef array [] $typeDefs
Map parameter type names to TypeDef objects or ObjectFactory specs.
static new( $key, $params=[])
Static constructor for easier chaining of ->params() methods.
validateValue( $name, $value, $settings, array $options=[])
Valiate a parameter value using a settings array.
__construct(Callbacks $callbacks, ObjectFactory $objectFactory, array $options=[])
const PARAM_ISMULTI
(bool) Indicate that the parameter is multi-valued.
getValue( $name, $settings, array $options=[])
Fetch and valiate a parameter value using a settings array.
overrideTypeDef( $name, $typeDef)
Register a type handler, overriding any existing handler.
addTypeDefs(array $typeDefs)
Register multiple type handlers.
const ALL_DEFAULT_STRING
Magic "all values" value when PARAM_ALL is true.
static implodeMultiValue(array $value)
Implode an array as a multi-valued parameter string, like implode()
Error reporting for ParamValidator.
getHelpInfo( $name, $settings, array $options)
Describe parameter settings in human-readable format.
addTypeDef( $name, $typeDef)
Register a type handler.
const PARAM_DEFAULT
(mixed) Default value of the parameter.
hasTypeDef( $name)
Test if a type is registered.
static explodeMultiValue( $value, $limit)
Split a multi-valued parameter string, like explode()
Service for formatting and validating API parameters.
int $ismultiLimit2
Default values for PARAM_ISMULTI_LIMIT2.
getTypeDef( $type)
Get the TypeDef for a type.
static $STANDARD_TYPES
A list of standard type names and types that may be passed as $typeDefs to __construct().
const PARAM_ALL
(bool|string) Whether a magic "all values" value exists for multi-valued enumerated types...
Value object representing a message parameter holding a single value.
Definition: ScalarParam.php:10
const PARAM_SENSITIVE
(bool) Indicate that the parameter&#39;s value should not be logged.
Base definition for ParamValidator types.
Definition: TypeDef.php:18
const PARAM_IGNORE_INVALID_VALUES
(bool) Whether to ignore invalid values.
const PARAM_TYPE
(string|array) Type of the parameter.