[ 'class' => TypeDef\BooleanDef::class ], 'checkbox' => [ 'class' => TypeDef\PresenceBooleanDef::class ], 'integer' => [ 'class' => TypeDef\IntegerDef::class ], 'limit' => [ 'class' => TypeDef\LimitDef::class ], 'float' => [ 'class' => TypeDef\FloatDef::class ], 'double' => [ 'class' => TypeDef\FloatDef::class ], 'string' => [ 'class' => TypeDef\StringDef::class ], 'password' => [ 'class' => TypeDef\PasswordDef::class ], 'NULL' => [ 'class' => TypeDef\StringDef::class, 'args' => [ [ 'allowEmptyWhenRequired' => true, ] ], ], 'timestamp' => [ 'class' => TypeDef\TimestampDef::class ], 'upload' => [ 'class' => TypeDef\UploadDef::class ], 'enum' => [ 'class' => TypeDef\EnumDef::class ], ]; /** @var Callbacks */ private $callbacks; /** @var ObjectFactory */ private $objectFactory; /** @var (TypeDef|array)[] Map parameter type names to TypeDef objects or ObjectFactory specs */ private $typeDefs = []; /** @var int Default values for PARAM_ISMULTI_LIMIT1 */ private $ismultiLimit1; /** @var int Default values for PARAM_ISMULTI_LIMIT2 */ private $ismultiLimit2; /** * @param Callbacks $callbacks * @param ObjectFactory $objectFactory To turn specs into TypeDef objects * @param array $options Associative array of additional settings * - 'typeDefs': (array) As for addTypeDefs(). If omitted, self::$STANDARD_TYPES will be used. * Pass an empty array if you want to start with no registered types. * - 'ismultiLimits': (int[]) Two ints, being the default values for PARAM_ISMULTI_LIMIT1 and * PARAM_ISMULTI_LIMIT2. If not given, defaults to `[ 50, 500 ]`. */ public function __construct( Callbacks $callbacks, ObjectFactory $objectFactory, array $options = [] ) { $this->callbacks = $callbacks; $this->objectFactory = $objectFactory; $this->addTypeDefs( $options['typeDefs'] ?? self::$STANDARD_TYPES ); $this->ismultiLimit1 = $options['ismultiLimits'][0] ?? 50; $this->ismultiLimit2 = $options['ismultiLimits'][1] ?? 500; } /** * List known type names * @return string[] */ public function knownTypes() { return array_keys( $this->typeDefs ); } /** * Register multiple type handlers * * @see addTypeDef() * @param array $typeDefs Associative array mapping `$name` to `$typeDef`. */ public function addTypeDefs( array $typeDefs ) { foreach ( $typeDefs as $name => $def ) { $this->addTypeDef( $name, $def ); } } /** * Register a type handler * * To allow code to omit PARAM_TYPE in settings arrays to derive the type * from PARAM_DEFAULT, it is strongly recommended that the following types be * registered: "boolean", "integer", "double", "string", "NULL", and "enum". * * When using ObjectFactory specs, the following extra arguments are passed: * - The Callbacks object for this ParamValidator instance. * * @param string $name Type name * @param TypeDef|array $typeDef Type handler or ObjectFactory spec to create one. */ public function addTypeDef( $name, $typeDef ) { Assert::parameterType( implode( '|', [ TypeDef::class, 'array' ] ), $typeDef, '$typeDef' ); if ( isset( $this->typeDefs[$name] ) ) { throw new InvalidArgumentException( "Type '$name' is already registered" ); } $this->typeDefs[$name] = $typeDef; } /** * Register a type handler, overriding any existing handler * @see addTypeDef * @param string $name Type name * @param TypeDef|array|null $typeDef As for addTypeDef, or null to unregister a type. */ public function overrideTypeDef( $name, $typeDef ) { Assert::parameterType( implode( '|', [ TypeDef::class, 'array', 'null' ] ), $typeDef, '$typeDef' ); if ( $typeDef === null ) { unset( $this->typeDefs[$name] ); } else { $this->typeDefs[$name] = $typeDef; } } /** * Test if a type is registered * @param string $name Type name * @return bool */ public function hasTypeDef( $name ) { return isset( $this->typeDefs[$name] ); } /** * Get the TypeDef for a type * @param string|array $type Any array is considered equivalent to the string "enum". * @return TypeDef|null */ public function getTypeDef( $type ) { if ( is_array( $type ) ) { $type = 'enum'; } if ( !isset( $this->typeDefs[$type] ) ) { return null; } $def = $this->typeDefs[$type]; if ( !$def instanceof TypeDef ) { $def = $this->objectFactory->createObject( $def, [ 'extraArgs' => [ $this->callbacks ], 'assertClass' => TypeDef::class, ] ); $this->typeDefs[$type] = $def; } return $def; } /** * Normalize a parameter settings array * @param array|mixed $settings Default value or an array of settings * using PARAM_* constants. * @return array */ public function normalizeSettings( $settings ) { // Shorthand if ( !is_array( $settings ) ) { $settings = [ self::PARAM_DEFAULT => $settings, ]; } // When type is not given, determine it from the type of the PARAM_DEFAULT if ( !isset( $settings[self::PARAM_TYPE] ) ) { $settings[self::PARAM_TYPE] = gettype( $settings[self::PARAM_DEFAULT] ?? null ); } $typeDef = $this->getTypeDef( $settings[self::PARAM_TYPE] ); if ( $typeDef ) { $settings = $typeDef->normalizeSettings( $settings ); } return $settings; } /** * Fetch and valiate a parameter value using a settings array * * @param string $name Parameter name * @param array|mixed $settings Default value or an array of settings * using PARAM_* constants. * @param array $options Options array, passed through to the TypeDef and Callbacks. * @return mixed Validated parameter value * @throws ValidationException if the value is invalid */ public function getValue( $name, $settings, array $options = [] ) { $settings = $this->normalizeSettings( $settings ); $typeDef = $this->getTypeDef( $settings[self::PARAM_TYPE] ); if ( !$typeDef ) { throw new DomainException( "Param $name's type is unknown - {$settings[self::PARAM_TYPE]}" ); } $value = $typeDef->getValue( $name, $settings, $options ); if ( $value !== null ) { if ( !empty( $settings[self::PARAM_SENSITIVE] ) ) { $this->callbacks->recordCondition( new ValidationException( $name, $value, $settings, 'param-sensitive', [] ), $options ); } // Set a warning if a deprecated parameter has been passed if ( !empty( $settings[self::PARAM_DEPRECATED] ) ) { $this->callbacks->recordCondition( new ValidationException( $name, $value, $settings, 'param-deprecated', [] ), $options ); } } elseif ( isset( $settings[self::PARAM_DEFAULT] ) ) { $value = $settings[self::PARAM_DEFAULT]; } return $this->validateValue( $name, $value, $settings, $options ); } /** * Valiate a parameter value using a settings array * * @param string $name Parameter name * @param null|mixed $value Parameter value * @param array|mixed $settings Default value or an array of settings * using PARAM_* constants. * @param array $options Options array, passed through to the TypeDef and Callbacks. * - An additional option, 'values-list', will be set when processing the * values of a multi-valued parameter. * @return mixed Validated parameter value(s) * @throws ValidationException if the value is invalid */ public function validateValue( $name, $value, $settings, array $options = [] ) { $settings = $this->normalizeSettings( $settings ); $typeDef = $this->getTypeDef( $settings[self::PARAM_TYPE] ); if ( !$typeDef ) { throw new DomainException( "Param $name's type is unknown - {$settings[self::PARAM_TYPE]}" ); } if ( $value === null ) { if ( !empty( $settings[self::PARAM_REQUIRED] ) ) { throw new ValidationException( $name, $value, $settings, 'missingparam', [] ); } return null; } // Non-multi if ( empty( $settings[self::PARAM_ISMULTI] ) ) { return $typeDef->validate( $name, $value, $settings, $options ); } // Split the multi-value and validate each parameter $limit1 = $settings[self::PARAM_ISMULTI_LIMIT1] ?? $this->ismultiLimit1; $limit2 = $settings[self::PARAM_ISMULTI_LIMIT2] ?? $this->ismultiLimit2; $valuesList = is_array( $value ) ? $value : self::explodeMultiValue( $value, $limit2 + 1 ); // Handle PARAM_ALL $enumValues = $typeDef->getEnumValues( $name, $settings, $options ); if ( is_array( $enumValues ) && isset( $settings[self::PARAM_ALL] ) && count( $valuesList ) === 1 ) { $allValue = is_string( $settings[self::PARAM_ALL] ) ? $settings[self::PARAM_ALL] : self::ALL_DEFAULT_STRING; if ( $valuesList[0] === $allValue ) { return $enumValues; } } // Avoid checking useHighLimits() unless it's actually necessary $sizeLimit = count( $valuesList ) > $limit1 && $this->callbacks->useHighLimits( $options ) ? $limit2 : $limit1; if ( count( $valuesList ) > $sizeLimit ) { throw new ValidationException( $name, $valuesList, $settings, 'toomanyvalues', [ 'limit' => $sizeLimit ] ); } $options['values-list'] = $valuesList; $validValues = []; $invalidValues = []; foreach ( $valuesList as $v ) { try { $validValues[] = $typeDef->validate( $name, $v, $settings, $options ); } catch ( ValidationException $ex ) { if ( empty( $settings[self::PARAM_IGNORE_INVALID_VALUES] ) ) { throw $ex; } $invalidValues[] = $v; } } if ( $invalidValues ) { $this->callbacks->recordCondition( new ValidationException( $name, $value, $settings, 'unrecognizedvalues', [ 'values' => $invalidValues, ] ), $options ); } // Throw out duplicates if requested if ( empty( $settings[self::PARAM_ALLOW_DUPLICATES] ) ) { $validValues = array_values( array_unique( $validValues ) ); } return $validValues; } /** * Split a multi-valued parameter string, like explode() * * Note that, unlike explode(), this will return an empty array when given * an empty string. * * @param string $value * @param int $limit * @return string[] */ public static function explodeMultiValue( $value, $limit ) { if ( $value === '' || $value === "\x1f" ) { return []; } if ( substr( $value, 0, 1 ) === "\x1f" ) { $sep = "\x1f"; $value = substr( $value, 1 ); } else { $sep = '|'; } return explode( $sep, $value, $limit ); } }