6use InvalidArgumentException;
7use Wikimedia\Assert\Assert;
12use Wikimedia\ObjectFactory\ObjectFactory;
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 ],
207 private $objectFactory;
210 private $typeDefs = [];
213 private $ismultiLimit1;
216 private $ismultiLimit2;
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;
245 return array_keys( $this->typeDefs );
255 foreach ( $typeDefs as $name => $def ) {
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;
293 Assert::parameterType(
294 [ TypeDef::class,
'array',
'null' ],
299 if ( $typeDef ===
null ) {
300 unset( $this->typeDefs[$name] );
302 $this->typeDefs[$name] = $typeDef;
312 return isset( $this->typeDefs[$name] );
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;
346 private function normalizeSettingsInternal( $settings ) {
348 if ( !is_array( $settings ) ) {
350 self::PARAM_DEFAULT => $settings,
355 if ( !isset( $settings[self::PARAM_TYPE] ) ) {
356 $settings[
self::PARAM_TYPE] = gettype( $settings[self::PARAM_DEFAULT] ??
null );
369 $settings = $this->normalizeSettingsInternal( $settings );
371 $typeDef = $this->
getTypeDef( $settings[self::PARAM_TYPE] );
373 $settings = $typeDef->normalizeSettings( $settings );
397 public function checkSettings(
string $name, $settings, array $options ): array {
398 $settings = $this->normalizeSettingsInternal( $settings );
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 ) ) {
420 if ( isset( $settings[self::PARAM_DEFAULT] ) ) {
423 $name, $settings[self::PARAM_DEFAULT], $settings, [
'is-default' =>
true ] + $options
425 }
catch ( ValidationException $ex ) {
427 . $ex->getFailureMessage()->getCode() .
')';
431 if ( !is_bool( $settings[self::PARAM_REQUIRED] ??
false ) ) {
433 . gettype( $settings[self::PARAM_REQUIRED] );
436 if ( !is_bool( $settings[self::PARAM_ISMULTI] ??
false ) ) {
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
449 if ( !is_int( $limit1 ) ) {
451 . gettype( $settings[self::PARAM_ISMULTI_LIMIT1] );
452 } elseif ( $limit1 <= 0 ) {
454 "PARAM_ISMULTI_LIMIT1 must be greater than 0, got $limit1";
456 if ( !is_int( $limit2 ) ) {
458 . gettype( $settings[self::PARAM_ISMULTI_LIMIT2] );
459 } elseif ( $limit2 < $limit1 ) {
461 'PARAM_ISMULTI_LIMIT2 must be greater than or equal to PARAM_ISMULTI_LIMIT1, but '
462 .
"$limit2 < $limit1";
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 ) {
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 ) ) {
482 . gettype( $settings[self::PARAM_ALLOW_DUPLICATES] );
486 if ( !is_bool( $settings[self::PARAM_SENSITIVE] ??
false ) ) {
488 . gettype( $settings[self::PARAM_SENSITIVE] );
491 if ( !is_bool( $settings[self::PARAM_DEPRECATED] ??
false ) ) {
493 . gettype( $settings[self::PARAM_DEPRECATED] );
496 if ( !is_bool( $settings[self::PARAM_IGNORE_UNRECOGNIZED_VALUES] ??
false ) ) {
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 );
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
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 );
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] ) ) {
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" ) {
597 DataMessageValue::new(
'paramvalidator-notmulti', [],
'badvalue' )
598 ->plaintextParams( $name, $value ),
599 $name, $value, $settings
606 if ( is_array( $value ) && !$typeDef->supportsArrays() ) {
608 DataMessageValue::new(
'paramvalidator-notmulti', [],
'badvalue' )
609 ->plaintextParams( $name, gettype( $value ) ),
610 $name, $value, $settings
614 return $typeDef->validate( $name, $value, $settings, $options );
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 ) {
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 ) {
651 $limit2 > $limit1 && count( $valuesList ) > $limit1 &&
652 $this->callbacks->useHighLimits( $options )
653 ) ? $limit2 : $limit1;
654 if ( count( $valuesList ) > $sizeLimit ) {
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 );
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
699 if ( empty( $settings[self::PARAM_ALLOW_DUPLICATES] ) ) {
700 $validValues = array_values( array_unique( $validValues ) );
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 ) );
760 return array_filter( $info,
static function ( $v ) {
775 $settings = $this->normalizeSettings( $settings );
776 $typeDef = $this->getTypeDef( $settings[self::PARAM_TYPE] );
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;
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 ) );
843 $default = $info[self::PARAM_DEFAULT];
844 unset( $info[self::PARAM_DEFAULT] );
845 $info[self::PARAM_DEFAULT] = $default;
848 return array_filter( $info );
862 if ( $value ===
'' || $value ===
"\x1f" ) {
866 if ( substr( $value, 0, 1 ) ===
"\x1f" ) {
868 $value = substr( $value, 1 );
873 return explode( $sep, $value, $limit );
883 if ( $value === [
'' ] ) {
889 foreach ( $value as $v ) {
890 if ( strpos( $v,
'|' ) !==
false ) {
891 return "\x1f" . implode(
"\x1f", $value );
894 return implode(
'|', $value );