* @link https://getkirby.com * @copyright Bastian Allgeier GmbH * @license https://opensource.org/licenses/MIT */ class V { /** * An array with all installed validators * * @var array */ public static $validators = []; /** * Validates the given input with all passed rules * and returns an array with all error messages. * The array will be empty if the input is valid * * @param mixed $input * @param array $rules * @param array $messages * @return array */ public static function errors($input, array $rules, $messages = []): array { $errors = static::value($input, $rules, $messages, false); return $errors === true ? [] : $errors; } /** * Creates a useful error message for the given validator * and the arguments. This is used mainly internally * to create error messages * * @param string $validatorName * @param mixed ...$params * @return string|null */ public static function message(string $validatorName, ...$params): ?string { $validatorName = strtolower($validatorName); $translationKey = 'error.validation.' . $validatorName; $validators = array_change_key_case(static::$validators); $validator = $validators[$validatorName] ?? null; if ($validator === null) { return null; } $reflection = new ReflectionFunction($validator); $arguments = []; foreach ($reflection->getParameters() as $index => $parameter) { $value = $params[$index] ?? null; if (is_array($value) === true) { try { foreach ($value as $index => $item) { if (is_array($item) === true) { $value[$index] = implode('|', $item); } } $value = implode(', ', $value); } catch (Throwable $e) { $value = '-'; } } $arguments[$parameter->getName()] = $value; } return I18n::template($translationKey, 'The "' . $validatorName . '" validation failed', $arguments); } /** * Return the list of all validators * * @return array */ public static function validators(): array { return static::$validators; } /** * Validate a single value against * a set of rules, using all registered * validators * * @param mixed $value * @param array $rules * @param array $messages * @param boolean $fail * @return boolean|array */ public static function value($value, array $rules, array $messages = [], bool $fail = true) { $errors = []; foreach ($rules as $validatorName => $validatorOptions) { if (is_int($validatorName)) { $validatorName = $validatorOptions; $validatorOptions = []; } if (is_array($validatorOptions) === false) { $validatorOptions = [$validatorOptions]; } $validatorName = strtolower($validatorName); if (static::$validatorName($value, ...$validatorOptions) === false) { $message = $messages[$validatorName] ?? static::message($validatorName, $value, ...$validatorOptions); $errors[$validatorName] = $message; if ($fail === true) { throw new Exception($message); } } } return empty($errors) === true ? true : $errors; } /** * Validate an input array against * a set of rules, using all registered * validators * * @param array $input * @param array $rules * @return boolean */ public static function input(array $input, array $rules): bool { foreach ($rules as $fieldName => $fieldRules) { $fieldValue = $input[$fieldName] ?? null; // first check for required fields if (($fieldRules['required'] ?? false) === true && $fieldValue === null) { throw new Exception(sprintf('The "%s" field is missing', $fieldName)); } // remove the required rule unset($fieldRules['required']); // skip validation for empty fields if ($fieldValue === null) { continue; } try { V::value($fieldValue, $fieldRules); } catch (Exception $e) { throw new Exception(sprintf($e->getMessage() . ' for field "%s"', $fieldName)); } static::value($fieldValue, $fieldRules); } return true; } /** * Calls an installed validator and passes all arguments * * @param string $method * @param array $arguments * @return boolean */ public static function __callStatic(string $method, array $arguments): bool { $method = strtolower($method); $validators = array_change_key_case(static::$validators); // check for missing validators if (isset($validators[$method]) === false) { throw new Exception('The validator does not exist: ' . $method); } return call_user_func_array($validators[$method], $arguments); } } /** * Default set of validators */ V::$validators = [ /** * Valid: `'yes' | true | 1 | 'on'` */ 'accepted' => function ($value): bool { return V::in($value, [1, true, 'yes', 'true', '1', 'on'], true) === true; }, /** * Valid: `a-z | A-Z` */ 'alpha' => function ($value): bool { return V::match($value, '/^([a-z])+$/i') === true; }, /** * Valid: `a-z | A-Z | 0-9` */ 'alphanum' => function ($value): bool { return V::match($value, '/^[a-z0-9]+$/i') === true; }, /** * Checks for numbers within the given range */ 'between' => function ($value, $min, $max): bool { return V::min($value, $min) === true && V::max($value, $max) === true; }, /** * Checks if the given string contains the given value */ 'contains' => function ($value, $needle): bool { return Str::contains($value, $needle); }, /** * Checks for a valid date */ 'date' => function ($value): bool { $date = date_parse($value); return ($date !== false && $date['error_count'] === 0 && $date['warning_count'] === 0); }, /** * Valid: `'no' | false | 0 | 'off'` */ 'denied' => function ($value): bool { return V::in($value, [0, false, 'no', 'false', '0', 'off'], true) === true; }, /** * Checks for a value, which does not equal the given value */ 'different' => function ($value, $other, $strict = false): bool { if ($strict === true) { return $value !== $other; } return $value != $other; }, /** * Checks for valid email addresses */ 'email' => function ($value): bool { if (filter_var($value, FILTER_VALIDATE_EMAIL) === false) { try { $parts = Str::split($value, '@'); $address = $parts[0] ?? null; $domain = Idn::encode($parts[1] ?? ''); $email = $address . '@' . $domain; } catch (Throwable $e) { return false; } return filter_var($email, FILTER_VALIDATE_EMAIL) !== false; } return true; }, /** * Checks if the given string ends with the given value */ 'endsWith' => function (string $value, string $end): bool { return Str::endsWith($value, $end); }, /** * Checks for a valid filename */ 'filename' => function ($value): bool { return V::match($value, '/^[a-z0-9@._-]+$/i') === true && V::min($value, 2) === true; }, /** * Checks if the value exists in a list of given values */ 'in' => function ($value, array $in, bool $strict = false): bool { return in_array($value, $in, $strict) === true; }, /** * Checks for a valid integer */ 'integer' => function ($value, bool $strict = false): bool { if ($strict === true) { return is_int($value) === true; } return filter_var($value, FILTER_VALIDATE_INT) !== false; }, /** * Checks for a valid IP address */ 'ip' => function ($value): bool { return filter_var($value, FILTER_VALIDATE_IP) !== false; }, /** * Checks if the value is lower than the second value */ 'less' => function ($value, float $max): bool { return V::size($value, $max, '<') === true; }, /** * Checks if the value matches the given regular expression */ 'match' => function ($value, string $pattern): bool { return preg_match($pattern, $value) !== 0; }, /** * Checks if the value does not exceed the maximum value */ 'max' => function ($value, float $max): bool { return V::size($value, $max, '<=') === true; }, /** * Checks if the value is higher than the minimum value */ 'min' => function ($value, float $min): bool { return V::size($value, $min, '>=') === true; }, /** * Checks if the number of characters in the value equals or is below the given maximum */ 'maxLength' => function (string $value = null, $max): bool { return Str::length(trim($value)) <= $max; }, /** * Checks if the number of characters in the value equals or is greater than the given minimum */ 'minLength' => function (string $value = null, $min): bool { return Str::length(trim($value)) >= $min; }, /** * Checks if the number of words in the value equals or is below the given maximum */ 'maxWords' => function (string $value = null, $max): bool { return V::max(explode(' ', trim($value)), $max) === true; }, /** * Checks if the number of words in the value equals or is below the given maximum */ 'minWords' => function (string $value = null, $min): bool { return V::min(explode(' ', trim($value)), $min) === true; }, /** * Checks if the first value is higher than the second value */ 'more' => function ($value, float $min): bool { return V::size($value, $min, '>') === true; }, /** * Checks that the given string does not contain the second value */ 'notContains' => function ($value, $needle): bool { return V::contains($value, $needle) === false; }, /** * Checks that the given value is not in the given list of values */ 'notIn' => function ($value, $notIn): bool { return V::in($value, $notIn) === false; }, /** * Checks for a valid number / numeric value (float, int, double) */ 'num' => function ($value): bool { return is_numeric($value) === true; }, /** * Checks if the value is present in the given array */ 'required' => function ($key, array $array): bool { return isset($array[$key]) === true && V::notIn($array[$key], [null, '', []]) === true; }, /** * Checks that the first value equals the second value */ 'same' => function ($value, $other, bool $strict = false): bool { if ($strict === true) { return $value === $other; } return $value == $other; }, /** * Checks that the value has the given size */ 'size' => function ($value, $size, $operator = '=='): bool { if (is_numeric($value) === true) { $count = $value; } elseif (is_string($value) === true) { $count = Str::length(trim($value)); } elseif (is_array($value) === true) { $count = count($value); } elseif (is_object($value) === true) { if ($value instanceof \Countable) { $count = count($value); } elseif (method_exists($value, 'count') === true) { $count = $value->count(); } else { throw new Exception('$value is an uncountable object'); } } else { throw new Exception('$value is of type without size'); } switch ($operator) { case '<': return $count < $size; case '>': return $count > $size; case '<=': return $count <= $size; case '>=': return $count >= $size; default: return $count == $size; } }, /** * Checks that the string starts with the given start value */ 'startsWith' => function (string $value, string $start): bool { return Str::startsWith($value, $start); }, /** * Checks for valid time */ 'time' => function ($value): bool { return V::date($value); }, /** * Checks for a valid Url */ 'url' => function ($value): bool { // In search for the perfect regular expression: https://mathiasbynens.be/demo/url-regex $regex = '_^(?:(?:https?|ftp)://)(?:\S+(?::\S*)?@)?(?:(?!10(?:\.\d{1,3}){3})(?!127(?:\.\d{1,3}){3})(?!169\.254(?:\.\d{1,3}){2})(?!192\.168(?:\.\d{1,3}){2})(?!172\.(?:1[6-9]|2\d|3[0-1])(?:\.\d{1,3}){2})(?:[1-9]\d?|1\d\d|2[01]\d|22[0-3])(?:\.(?:1?\d{1,2}|2[0-4]\d|25[0-5])){2}(?:\.(?:[1-9]\d?|1\d\d|2[0-4]\d|25[0-4]))|(?:(?:[a-z\x{00a1}-\x{ffff}0-9]+-?)*[a-z\x{00a1}-\x{ffff}0-9]+)(?:\.(?:[a-z\x{00a1}-\x{ffff}0-9]+-?)*[a-z\x{00a1}-\x{ffff}0-9]+)*(?:\.(?:[a-z\x{00a1}-\x{ffff}]{2,})))(?::\d{2,5})?(?:/[^\s]*)?$_iu'; return preg_match($regex, $value) !== 0; } ];