Files
lichterei-web/kirby/src/Toolkit/V.php
Bastian Allgeier 70b8439c49 Upgrade to 3.6.1
2021-12-07 12:39:43 +01:00

531 lines
16 KiB
PHP
Executable File

<?php
namespace Kirby\Toolkit;
use Exception;
use Kirby\Exception\InvalidArgumentException;
use Kirby\Http\Idn;
use ReflectionFunction;
use Throwable;
/**
* A set of validator methods
*
* @package Kirby Toolkit
* @author Bastian Allgeier <bastian@getkirby.com>
* @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 $key => $item) {
if (is_array($item) === true) {
$value[$key] = 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 bool $fail
* @return bool|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 bool
*/
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 {
static::value($fieldValue, $fieldRules);
} catch (Exception $e) {
throw new Exception(sprintf($e->getMessage() . ' for field "%s"', $fieldName));
}
}
return true;
}
/**
* Calls an installed validator and passes all arguments
*
* @param string $method
* @param array $arguments
* @return bool
*/
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 $unicode = false): bool {
return V::match($value, ($unicode === true ? '/^([\pL])+$/u' : '/^([a-z])+$/i')) === true;
},
/**
* Valid: `a-z | A-Z | 0-9`
*/
'alphanum' => function ($value, bool $unicode = false): bool {
return V::match($value, ($unicode === true ? '/^[\pL\pN]+$/u' : '/^([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 or compares two
* dates with each other.
*
* Pass only the first argument to check for a valid date.
* Pass an operator as second argument and another date as
* third argument to compare them.
*/
'date' => function (?string $value, string $operator = null, string $test = null): bool {
// make sure $value is a string
$value ??= '';
$args = func_get_args();
// simple date validation
if (count($args) === 1) {
$date = date_parse($value);
return $date !== false &&
$date['error_count'] === 0 &&
$date['warning_count'] === 0;
}
$value = strtotime($value);
$test = strtotime($test);
if (is_int($value) !== true || is_int($test) !== true) {
return false;
}
switch ($operator) {
case '!=':
return $value !== $test;
case '<':
return $value < $test;
case '>':
return $value > $test;
case '<=':
return $value <= $test;
case '>=':
return $value >= $test;
case '==':
return $value === $test;
}
throw new InvalidArgumentException('Invalid date comparison operator: "' . $operator . '". Allowed operators: "==", "!=", "<", "<=", ">", ">="');
},
/**
* 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 {
$email = Idn::encodeEmail($value);
} 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 value is field object, first convert it to a readable value
// it is important to check at the beginning as the value can be string or numeric
if (is_a($value, '\Kirby\Cms\Field') === true) {
$value = $value->value();
}
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
// Added localhost support and removed 127.*.*.* ip restriction
$regex = '_^(?:(?:https?|ftp):\/\/)(?:\S+(?::\S*)?@)?(?:(?!10(?:\.\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]))|(?:localhost)|(?:(?:[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;
}
];