6use InvalidArgumentException;
7use Wikimedia\Assert\Assert;
12use Wikimedia\ObjectFactory\ObjectFactory;
178 'boolean' => [
'class' => TypeDef\BooleanDef::class ],
179 'checkbox' => [
'class' => TypeDef\PresenceBooleanDef::class ],
180 'integer' => [
'class' => TypeDef\IntegerDef::class ],
181 'limit' => [
'class' => TypeDef\LimitDef::class ],
182 'float' => [
'class' => TypeDef\FloatDef::class ],
183 'double' => [
'class' => TypeDef\FloatDef::class ],
184 'string' => [
'class' => TypeDef\StringDef::class ],
185 'password' => [
'class' => TypeDef\PasswordDef::class ],
187 'class' => TypeDef\StringDef::class,
189 'allowEmptyWhenRequired' =>
true,
192 'timestamp' => [
'class' => TypeDef\TimestampDef::class ],
193 'upload' => [
'class' => TypeDef\UploadDef::class ],
194 'enum' => [
'class' => TypeDef\EnumDef::class ],
195 'expiry' => [
'class' => TypeDef\ExpiryDef::class ],
202 private $objectFactory;
205 private $typeDefs = [];
208 private $ismultiLimit1;
211 private $ismultiLimit2;
224 ObjectFactory $objectFactory,
227 $this->callbacks = $callbacks;
228 $this->objectFactory = $objectFactory;
230 $this->
addTypeDefs( $options[
'typeDefs'] ?? self::$STANDARD_TYPES );
231 $this->ismultiLimit1 = $options[
'ismultiLimits'][0] ?? 50;
232 $this->ismultiLimit2 = $options[
'ismultiLimits'][1] ?? 500;
240 return array_keys( $this->typeDefs );
250 foreach ( $typeDefs as $name => $def ) {
269 Assert::parameterType(
270 [ TypeDef::class,
'array' ],
275 if ( isset( $this->typeDefs[$name] ) ) {
276 throw new InvalidArgumentException(
"Type '$name' is already registered" );
278 $this->typeDefs[$name] = $typeDef;
288 Assert::parameterType(
289 [ TypeDef::class,
'array',
'null' ],
294 if ( $typeDef ===
null ) {
295 unset( $this->typeDefs[$name] );
297 $this->typeDefs[$name] = $typeDef;
307 return isset( $this->typeDefs[$name] );
316 if ( is_array(
$type ) ) {
320 if ( !isset( $this->typeDefs[
$type] ) ) {
324 $def = $this->typeDefs[
$type];
325 if ( !$def instanceof
TypeDef ) {
326 $def = $this->objectFactory->createObject( $def, [
327 'extraArgs' => [ $this->callbacks ],
328 'assertClass' => TypeDef::class,
330 $this->typeDefs[
$type] = $def;
341 private function normalizeSettingsInternal( $settings ) {
343 if ( !is_array( $settings ) ) {
345 self::PARAM_DEFAULT => $settings,
350 if ( !isset( $settings[self::PARAM_TYPE] ) ) {
351 $settings[
self::PARAM_TYPE] = gettype( $settings[self::PARAM_DEFAULT] ??
null );
364 $settings = $this->normalizeSettingsInternal( $settings );
366 $typeDef = $this->
getTypeDef( $settings[self::PARAM_TYPE] );
368 $settings = $typeDef->normalizeSettings( $settings );
392 public function checkSettings(
string $name, $settings, array $options ): array {
393 $settings = $this->normalizeSettingsInternal( $settings );
403 if ( !is_string(
$type ) && !is_array(
$type ) ) {
406 $typeDef = $this->
getTypeDef( $settings[self::PARAM_TYPE] );
408 if ( is_array(
$type ) ) {
415 if ( isset( $settings[self::PARAM_DEFAULT] ) ) {
418 $name, $settings[self::PARAM_DEFAULT], $settings, [
'is-default' =>
true ] + $options
420 }
catch ( ValidationException $ex ) {
422 . $ex->getFailureMessage()->getCode() .
')';
426 if ( !is_bool( $settings[self::PARAM_REQUIRED] ??
false ) ) {
428 . gettype( $settings[self::PARAM_REQUIRED] );
431 if ( !is_bool( $settings[self::PARAM_ISMULTI] ??
false ) ) {
433 . gettype( $settings[self::PARAM_ISMULTI] );
436 if ( !empty( $settings[self::PARAM_ISMULTI] ) ) {
437 $allowedKeys = array_merge( $allowedKeys, [
438 self::PARAM_ISMULTI_LIMIT1, self::PARAM_ISMULTI_LIMIT2,
439 self::PARAM_ALL, self::PARAM_ALLOW_DUPLICATES
444 if ( !is_int( $limit1 ) ) {
446 . gettype( $settings[self::PARAM_ISMULTI_LIMIT1] );
447 } elseif ( $limit1 <= 0 ) {
449 "PARAM_ISMULTI_LIMIT1 must be greater than 0, got $limit1";
451 if ( !is_int( $limit2 ) ) {
453 . gettype( $settings[self::PARAM_ISMULTI_LIMIT2] );
454 } elseif ( $limit2 < $limit1 ) {
456 'PARAM_ISMULTI_LIMIT2 must be greater than or equal to PARAM_ISMULTI_LIMIT1, but '
457 .
"$limit2 < $limit1";
461 if ( !is_string( $all ) && !is_bool( $all ) ) {
462 $issues[
self::PARAM_ALL] =
'PARAM_ALL must be a string or boolean, got ' . gettype( $all );
463 } elseif ( $all !==
false && $typeDef ) {
464 if ( $all ===
true ) {
467 $values = $typeDef->getEnumValues( $name, $settings, $options );
468 if ( !is_array( $values ) ) {
469 $issues[
self::PARAM_ALL] =
'PARAM_ALL cannot be used with non-enumerated types';
470 } elseif ( in_array( $all, $values,
true ) ) {
471 $issues[
self::PARAM_ALL] =
'Value for PARAM_ALL conflicts with an enumerated value';
475 if ( !is_bool( $settings[self::PARAM_ALLOW_DUPLICATES] ??
false ) ) {
477 . gettype( $settings[self::PARAM_ALLOW_DUPLICATES] );
481 if ( !is_bool( $settings[self::PARAM_SENSITIVE] ??
false ) ) {
483 . gettype( $settings[self::PARAM_SENSITIVE] );
486 if ( !is_bool( $settings[self::PARAM_DEPRECATED] ??
false ) ) {
488 . gettype( $settings[self::PARAM_DEPRECATED] );
491 if ( !is_bool( $settings[self::PARAM_IGNORE_UNRECOGNIZED_VALUES] ??
false ) ) {
493 .
'boolean, got ' . gettype( $settings[self::PARAM_IGNORE_UNRECOGNIZED_VALUES] );
496 $ret = [
'issues' => $issues,
'allowedKeys' => $allowedKeys,
'messages' => $messages ];
498 $ret = $typeDef->checkSettings( $name, $settings, $options, $ret );
515 public function getValue( $name, $settings, array $options = [] ) {
516 $settings = $this->normalizeSettings( $settings );
518 $typeDef = $this->getTypeDef( $settings[self::PARAM_TYPE] );
520 throw new DomainException(
521 "Param $name's type is unknown - {$settings[self::PARAM_TYPE]}"
525 $value = $typeDef->getValue( $name, $settings, $options );
527 if ( $value !==
null ) {
528 if ( !empty( $settings[self::PARAM_SENSITIVE] ) ) {
529 $strValue = $typeDef->stringifyValue( $name, $value, $settings, $options );
530 $this->callbacks->recordCondition(
531 DataMessageValue::new(
'paramvalidator-param-sensitive', [],
'param-sensitive' )
532 ->plaintextParams( $name, $strValue ),
533 $name, $value, $settings, $options
538 if ( !empty( $settings[self::PARAM_DEPRECATED] ) ) {
539 $strValue = $typeDef->stringifyValue( $name, $value, $settings, $options );
540 $this->callbacks->recordCondition(
541 DataMessageValue::new(
'paramvalidator-param-deprecated', [],
'param-deprecated' )
542 ->plaintextParams( $name, $strValue ),
543 $name, $value, $settings, $options
546 } elseif ( isset( $settings[self::PARAM_DEFAULT] ) ) {
547 $value = $settings[self::PARAM_DEFAULT];
548 $options[
'is-default'] =
true;
551 return $this->validateValue( $name, $value, $settings, $options );
567 public function validateValue( $name, $value, $settings, array $options = [] ) {
568 $settings = $this->normalizeSettings( $settings );
570 $typeDef = $this->getTypeDef( $settings[self::PARAM_TYPE] );
572 throw new DomainException(
573 "Param $name's type is unknown - {$settings[self::PARAM_TYPE]}"
577 if ( $value ===
null ) {
578 if ( !empty( $settings[self::PARAM_REQUIRED] ) ) {
580 DataMessageValue::new(
'paramvalidator-missingparam', [],
'missingparam' )
581 ->plaintextParams( $name ),
582 $name, $value, $settings
589 if ( empty( $settings[self::PARAM_ISMULTI] ) ) {
590 if ( is_string( $value ) && substr( $value, 0, 1 ) ===
"\x1f" ) {
592 DataMessageValue::new(
'paramvalidator-notmulti', [],
'badvalue' )
593 ->plaintextParams( $name, $value ),
594 $name, $value, $settings
597 return $typeDef->validate( $name, $value, $settings, $options );
601 $limit1 = $settings[self::PARAM_ISMULTI_LIMIT1] ?? $this->ismultiLimit1;
602 $limit2 = max( $limit1, $settings[self::PARAM_ISMULTI_LIMIT2] ?? $this->ismultiLimit2 );
603 $valuesList = is_array( $value ) ? $value : self::explodeMultiValue( $value, $limit2 + 1 );
606 $enumValues = $typeDef->getEnumValues( $name, $settings, $options );
607 if ( is_array( $enumValues ) && isset( $settings[self::PARAM_ALL] ) &&
608 count( $valuesList ) === 1
610 $allValue = is_string( $settings[self::PARAM_ALL] )
611 ? $settings[self::PARAM_ALL]
612 : self::ALL_DEFAULT_STRING;
613 if ( $valuesList[0] === $allValue ) {
620 $limit2 > $limit1 && count( $valuesList ) > $limit1 &&
621 $this->callbacks->useHighLimits( $options )
622 ) ? $limit2 : $limit1;
623 if ( count( $valuesList ) > $sizeLimit ) {
625 DataMessageValue::new(
'paramvalidator-toomanyvalues', [],
'toomanyvalues', [
626 'limit' => $sizeLimit,
627 'lowlimit' => $limit1,
628 'highlimit' => $limit2,
629 ] )->plaintextParams( $name )->numParams( $sizeLimit ),
630 $name, $valuesList, $settings
634 $options[
'values-list'] = $valuesList;
637 foreach ( $valuesList as $v ) {
639 $validValues[] = $typeDef->validate( $name, $v, $settings, $options );
642 empty( $settings[self::PARAM_IGNORE_UNRECOGNIZED_VALUES] )
646 $invalidValues[] = $v;
649 if ( $invalidValues ) {
650 if ( is_array( $value ) ) {
651 $value = self::implodeMultiValue( $value );
653 $this->callbacks->recordCondition(
654 DataMessageValue::new(
'paramvalidator-unrecognizedvalues', [],
'unrecognizedvalues', [
655 'values' => $invalidValues,
657 ->plaintextParams( $name, $value )
658 ->commaListParams( array_map(
static function ( $v ) {
660 }, $invalidValues ) )
661 ->numParams( count( $invalidValues ) ),
662 $name, $value, $settings, $options
667 if ( empty( $settings[self::PARAM_ALLOW_DUPLICATES] ) ) {
668 $validValues = array_values( array_unique( $validValues ) );
684 $settings = $this->normalizeSettings( $settings );
685 $typeDef = $this->getTypeDef( $settings[self::PARAM_TYPE] );
688 $info[
'type'] = $settings[self::PARAM_TYPE];
689 $info[
'required'] = !empty( $settings[self::PARAM_REQUIRED] );
690 if ( !empty( $settings[self::PARAM_DEPRECATED] ) ) {
691 $info[
'deprecated'] =
true;
693 if ( !empty( $settings[self::PARAM_SENSITIVE] ) ) {
694 $info[
'sensitive'] =
true;
696 if ( isset( $settings[self::PARAM_DEFAULT] ) ) {
697 $info[
'default'] = $settings[self::PARAM_DEFAULT];
699 $info[
'multi'] = !empty( $settings[self::PARAM_ISMULTI] );
700 if ( $info[
'multi'] ) {
701 $info[
'lowlimit'] = $settings[self::PARAM_ISMULTI_LIMIT1] ?? $this->ismultiLimit1;
702 $info[
'highlimit'] = max(
703 $info[
'lowlimit'], $settings[self::PARAM_ISMULTI_LIMIT2] ?? $this->ismultiLimit2
706 $info[
'highlimit'] > $info[
'lowlimit'] && $this->callbacks->useHighLimits( $options )
710 if ( !empty( $settings[self::PARAM_ALLOW_DUPLICATES] ) ) {
711 $info[
'allowsduplicates'] =
true;
714 $allSpecifier = $settings[self::PARAM_ALL] ??
false;
715 if ( $allSpecifier !==
false ) {
716 if ( !is_string( $allSpecifier ) ) {
717 $allSpecifier = self::ALL_DEFAULT_STRING;
719 $info[
'allspecifier'] = $allSpecifier;
724 $info = array_merge( $info, $typeDef->getParamInfo( $name, $settings, $options ) );
728 return array_filter( $info,
static function ( $v ) {
743 $settings = $this->normalizeSettings( $settings );
744 $typeDef = $this->getTypeDef( $settings[self::PARAM_TYPE] );
748 self::PARAM_DEPRECATED =>
null,
749 self::PARAM_REQUIRED =>
null,
750 self::PARAM_SENSITIVE =>
null,
751 self::PARAM_TYPE =>
null,
752 self::PARAM_ISMULTI =>
null,
753 self::PARAM_ISMULTI_LIMIT1 =>
null,
754 self::PARAM_ALL =>
null,
755 self::PARAM_DEFAULT =>
null,
758 if ( !empty( $settings[self::PARAM_DEPRECATED] ) ) {
759 $info[self::PARAM_DEPRECATED] = MessageValue::new(
'paramvalidator-help-deprecated' );
762 if ( !empty( $settings[self::PARAM_REQUIRED] ) ) {
763 $info[self::PARAM_REQUIRED] = MessageValue::new(
'paramvalidator-help-required' );
766 if ( !empty( $settings[self::PARAM_ISMULTI] ) ) {
767 $info[self::PARAM_ISMULTI] = MessageValue::new(
'paramvalidator-help-multi-separate' );
769 $lowcount = $settings[self::PARAM_ISMULTI_LIMIT1] ?? $this->ismultiLimit1;
770 $highcount = max( $lowcount, $settings[self::PARAM_ISMULTI_LIMIT2] ?? $this->ismultiLimit2 );
771 $values = $typeDef ? $typeDef->getEnumValues( $name, $settings, $options ) :
null;
774 $values ===
null || count( $values ) > $lowcount ||
775 !empty( $settings[self::PARAM_ALLOW_DUPLICATES] )
777 if ( $highcount > $lowcount ) {
778 $info[self::PARAM_ISMULTI_LIMIT1] = MessageValue::new(
'paramvalidator-help-multi-max' )
779 ->numParams( $lowcount, $highcount );
781 $info[self::PARAM_ISMULTI_LIMIT1] = MessageValue::new(
'paramvalidator-help-multi-max-simple' )
782 ->numParams( $lowcount );
786 $allSpecifier = $settings[self::PARAM_ALL] ??
false;
787 if ( $allSpecifier !==
false ) {
788 if ( !is_string( $allSpecifier ) ) {
789 $allSpecifier = self::ALL_DEFAULT_STRING;
791 $info[self::PARAM_ALL] = MessageValue::new(
'paramvalidator-help-multi-all' )
792 ->plaintextParams( $allSpecifier );
796 if ( isset( $settings[self::PARAM_DEFAULT] ) && $typeDef ) {
797 $value = $typeDef->stringifyValue( $name, $settings[self::PARAM_DEFAULT], $settings, $options );
798 if ( $value ===
'' ) {
799 $info[self::PARAM_DEFAULT] = MessageValue::new(
'paramvalidator-help-default-empty' );
800 } elseif ( $value !==
null ) {
801 $info[self::PARAM_DEFAULT] = MessageValue::new(
'paramvalidator-help-default' )
802 ->plaintextParams( $value );
807 $info = array_merge( $info, $typeDef->getHelpInfo( $name, $settings, $options ) );
811 $default = $info[self::PARAM_DEFAULT];
812 unset( $info[self::PARAM_DEFAULT] );
813 $info[self::PARAM_DEFAULT] = $default;
816 return array_filter( $info );
830 if ( $value ===
'' || $value ===
"\x1f" ) {
834 if ( substr( $value, 0, 1 ) ===
"\x1f" ) {
836 $value = substr( $value, 1 );
841 return explode( $sep, $value, $limit );
851 if ( $value === [
'' ] ) {
857 foreach ( $value as $v ) {
858 if ( strpos( $v,
'|' ) !==
false ) {
859 return "\x1f" . implode(
"\x1f", $value );
862 return implode(
'|', $value );