forkbb/app/Core/Validator.php
2020-09-12 23:22:32 +07:00

823 lines
24 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<?php
namespace ForkBB\Core;
use ForkBB\Core\Container;
use ForkBB\Core\File;
use ForkBB\Core\Validators;
use RuntimeException;
use function \ForkBB\__;
class Validator
{
/**
* Контейнер
* @var Container
*/
protected $c;
/**
* Массив валидаторов
* @var array
*/
protected $validators;
/**
* Массив правил для текущей проверки данных
* @var array
*/
protected $rules;
/**
* Массив результатов проверенных данных
* @var array
*/
protected $result;
/**
* Массив дополнительных аргументов для валидаторов и конкретных полей/правил
* @var array
*/
protected $arguments;
/**
* Массив сообщений об ошибках для конкретных полей/правил
* @var array
*/
protected $messages;
/**
* Массив псевдонимов имен полей для вывода в ошибках
* @var array
*/
protected $aliases;
/**
* Массив ошибок валидации
* @var array
*/
protected $errors;
/**
* Массив имен полей для обработки
* @var array
*/
protected $fields;
/**
* Массив состояний проверки полей
* @var array
*/
protected $status;
/**
* Массив входящих данных для обработки
* @var array
*/
protected $raw;
/**
* Данные для текущей обработки
* @var array
*/
protected $curData;
/**
* Флаг ошибки
* @var mixed
*/
protected $error;
public function __construct(Container $container)
{
$this->c = $container;
$this->reset();
}
/**
* Сбрасывает настройки к начальным состояниям
*/
public function reset(): Validator
{
$this->validators = [
'absent' => [$this, 'vAbsent'],
'array' => [$this, 'vArray'],
'checkbox' => [$this, 'vCheckbox'],
'date' => [$this, 'vDate'],
'file' => [$this, 'vFile'],
'image' => [$this, 'vImage'],
'in' => [$this, 'vIn'],
'integer' => [$this, 'vInteger'],
'max' => [$this, 'vMax'],
'min' => [$this, 'vMin'],
'numeric' => [$this, 'vNumeric'],
'not_in' => [$this, 'vNotIn'],
'password' => [$this, 'vPassword'],
'referer' => [$this, 'vReferer'],
'regex' => [$this, 'vRegex'],
'required' => [$this, 'vRequired'],
'required_with' => [$this, 'vRequiredWith'],
'same' => [$this, 'vSame'],
'string' => [$this, 'vString'],
'token' => [$this, 'vToken'],
];
$this->rules = [];
$this->result = [];
$this->arguments = [];
$this->messages = [];
$this->aliases = [];
$this->errors = [];
$this->fields = [];
$this->status = [];
return $this;
}
/**
* Добавляет валидаторы
*/
public function addValidators(array $validators): Validator
{
$this->validators = \array_replace($this->validators, $validators);
return $this;
}
/**
* Добавляет правила
*/
public function addRules(array $list): Validator
{
foreach ($list as $field => $raw) {
$suffix = null;
// правило для элементов массива
if (\strpos($field, '.') > 0) {
list($field, $suffix) = \explode('.', $field, 2);
}
$rules = [];
// перебор правил для текущего поля
foreach (\explode('|', $raw) as $rule) { //???? нужно экранирование для разделителей
$vs = \explode(':', $rule, 2);
if (empty($this->validators[$vs[0]])) {
try {
$validator = $this->c->{'VL' . $vs[0]};
} catch (Exception $e) {
$validator = null;
}
if ($validator instanceof Validators) {
$this->validators[$vs[0]] = [$validator, $vs[0]];
} else {
throw new RuntimeException($vs[0] . ' validator not found');
}
}
$rules[$vs[0]] = $vs[1] ?? '';
}
if (isset($suffix)) {
if (
isset($this->rules[$field]['array'])
&& ! \is_array($this->rules[$field]['array'])
) {
$this->rules[$field]['array'] = [];
}
$this->rules[$field]['array'][$suffix] = $rules;
} else {
$this->rules[$field] = $rules;
}
$this->fields[$field] = $field;
}
return $this;
}
/**
* Добавляет дополнительные аргументы для конкретных "имя поля"."имя правила".
*/
public function addArguments(array $arguments): Validator
{
$this->arguments = \array_replace($this->arguments, $arguments);
return $this;
}
/**
* Добавляет сообщения для конкретных "имя поля"."имя правила".
*/
public function addMessages(array $messages): Validator
{
$this->messages = \array_replace($this->messages, $messages);
return $this;
}
/**
* Добавляет псевдонимы имен полей для сообщений об ошибках
*/
public function addAliases(array $aliases): Validator
{
$this->aliases = \array_replace($this->aliases, $aliases);
return $this;
}
/**
* Проверяет данные
*/
public function validation(array $raw): bool
{
if (empty($this->rules)) {
throw new RuntimeException('Rules not found');
}
$this->errors = [];
$this->status = [];
$this->curData = [];
$this->raw = $raw;
foreach ($this->fields as $field) {
$this->__get($field);
}
$this->raw = null;
return empty($this->errors);
}
/**
* Проверяет наличие поля
*/
public function __isset(string $field): bool
{
return isset($this->result[$field]);
}
/**
* Проверяет поле согласно заданным правилам
* Возвращает значение запрашиваемого поля
*/
public function __get(string $field) /* : mixed */
{
if (isset($this->status[$field])) {
return $this->result[$field];
} elseif (empty($this->rules[$field])) {
throw new RuntimeException("No rules for '{$field}' field");
}
$value = null;
if (
! isset($this->raw[$field])
&& isset($this->rules[$field]['required'])
) {
$rules = ['required' => ''];
} else {
$rules = $this->rules[$field];
if (isset($this->raw[$field])) {
$value = $this->c->Secury->replInvalidChars($this->raw[$field]);
// пустое поле в соответствии с правилом 'required' должно быть равно null
if (
(
\is_string($value)
&& 0 === \strlen(\preg_replace('%^\s+|\s+$%u', '', $value))
)
|| (
\is_array($value)
&& empty($value)
)
) {
$value = null;
}
}
}
$value = $this->checkValue($value, $rules, $field);
$this->status[$field] = true !== $this->error; // в $this->error может быть состояние false
$this->result[$field] = $value;
return $value;
}
/**
* Проверяет значение списком правил
*/
protected function checkValue(/* mixed */ $value, array $rules, string $field) /* : mixed */
{
foreach ($rules as $validator => $attr) {
// данные для обработчика ошибок
$this->error = null;
$this->curData[] = [
'field' => $field,
'rule' => $validator,
'attr' => $attr,
];
$value = $this->validators[$validator]($this, $value, $attr, $this->getArguments($field, $validator));
\array_pop($this->curData);
if (null !== $this->error) {
break;
}
}
return $value;
}
/**
* Добавляет ошибку
*/
public function addError(/* bool|string */ $error, string $type = 'v'): void
{
if (empty($vars = \end($this->curData))) {
throw new RuntimeException('The array of variables is empty');
}
// нет ошибки, для выхода из цикла проверки правил
if (true === $error) {
$this->error = false;
return;
}
\extract($vars);
// псевдоним имени поля
$alias = $this->aliases[$field] ?? $field;
// текст ошибки
if (isset($this->messages[$field . '.' . $rule])) {
$error = $this->messages[$field . '.' . $rule];
} elseif (isset($this->messages[$field])) {
$error = $this->messages[$field];
}
if (\is_array($error)) {
list($type, $error) = $error;
}
$this->errors[$type][] = __($error, [':alias' => __($alias), ':attr' => $attr]);
$this->error = true;
}
/**
* Возвращает дополнительные аргументы
*/
protected function getArguments(string $field, string $rule) /* : mixed */
{
if (isset($this->arguments[$field . '.' . $rule])) {
return $this->arguments[$field . '.' . $rule];
} elseif (isset($this->arguments[$field])) {
return $this->arguments[$field];
} else {
return null;
}
}
/**
* Возвращает статус проверки поля
*/
public function getStatus(string $field): bool
{
if (! isset($this->status[$field])) {
$this->__get($field);
}
return $this->status[$field];
}
/**
* Возвращает проверенные данные
* Поля с ошибками содержат значения по умолчанию или значения с ошибками
*/
public function getData(bool $all = false): array
{
if (empty($this->status)) {
throw new RuntimeException('Data not found');
}
if ($all) {
return $this->result;
} else {
return \array_filter($this->result, function ($value) {
return null !== $value;
});
}
}
/**
* Возращает массив ошибок
*/
public function getErrors(): array
{
return $this->errors;
}
/**
* Выполняет проверку значения по правилу
*
* @param Validator $first ссылка на валидатор
* @param mixed $second проверяемое значение
* @param mixed $third атрибут правила
* @param mixed $fourth дополнительный аргумент
*
* @return mixed
*/
protected function vAbsent(Validator $v, $value, $attr)
{
if (null !== $value) {
$this->addError('The :alias should be absent');
}
if (isset($attr[0])) {
return $attr;
} else {
return null;
}
}
protected function vRequired(Validator $v, $value)
{
if (\is_string($value)) {
if (\strlen(\preg_replace('%^\s+|\s+$%u', '', $value)) > 0) {
return $value;
}
} elseif (\is_array($value)) {
if (! empty($value)) {
return $value;
}
} elseif (null !== $value) {
return $value;
}
$this->addError('The :alias is required');
return null;
}
protected function vRequiredWith(Validator $v, $value, $attr) //???????????????????????
{
foreach (\explode(',', $attr) as $field) {
if (null !== $this->__get($field)) { // если есть хотя бы одно поле,
return $this->vRequired($v, $value); // то проверяем данное поле
} // на обязательное наличие
}
if (null === $value) { // если данное поле отсутствует,
$this->addError(true); // то прерываем его проверку
}
return $value;
}
protected function vString(Validator $v, $value, $attr)
{
if (null === $value) { // для пустого поля типа string
if (\preg_match('%(?:^|,)trim(?:,|$)%', $attr)) { // при наличии действия trim
$this->addError(true); // прерываем его проверку
return ''; // и возвращаем пустую строку,
} else {
return null; // а не null
}
} elseif (\is_string($value)) {
foreach(\explode(',', $attr) as $action) {
switch ($action) {
case 'trim':
$value = \preg_replace('%^\s+|\s+$%u', '', $value);
break;
case 'lower':
$value = \mb_strtolower($value, 'UTF-8');
break;
case 'spaces':
$value = \preg_replace('%\s+%u', ' ', $value);
break;
case 'linebreaks':
$value = \str_replace(["\r\n", "\r"], "\n", $value);
break;
}
}
return $value;
} else {
$this->addError('The :alias must be string');
return null;
}
}
protected function vNumeric(Validator $v, $value)
{
if (null === $value) {
return null;
} elseif (\is_numeric($value)) {
return 0.0 + $value;
} else {
$this->addError('The :alias must be numeric');
return null;
}
}
protected function vInteger(Validator $v, $value)
{
if (null === $value) {
return null;
} elseif (
\is_numeric($value)
&& \is_int(0 + $value)
) {
return (int) $value;
} else {
$this->addError('The :alias must be integer');
return null;
}
}
protected function vArray(Validator $v, $value, $attr)
{
if (
null !== $value
&& ! \is_array($value)
) {
$this->addError('The :alias must be array');
return null;
} elseif (! $attr) {
return $value;
}
if (empty($vars = \end($this->curData))) {
throw new RuntimeException('The array of variables is empty');
}
$result = [];
foreach ($attr as $name => $rules) {
$this->recArray($value, $result, $name, $rules, $vars['field'] . '.' . $name);
}
return $result;
}
protected function recArray(&$value, &$result, $name, $rules, $field)
{
$idxs = \explode('.', $name);
$key = \array_shift($idxs);
$name = \implode('.', $idxs);
if ('*' === $key) {
if (! \is_array($value)) {
return; //????
}
foreach ($value as $i => $cur) {
if ('' === $name) {
$result[$i] = $this->checkValue($cur, $rules, $field);
} else {
$this->recArray($value[$i], $result[$i], $name, $rules, $field);
}
}
} else {
if (! \array_key_exists($key, $value)) {
return; //????
}
if ('' === $name) {
$result[$key] = $this->checkValue($value[$key], $rules, $field);
} else {
$this->recArray($value[$key], $result[$key], $name, $rules, $field);
}
}
}
protected function vMin(Validator $v, $value, $attr)
{
if (\is_string($value)) {
$isBytes = \strpos($attr, 'bytes');
if (
(
$isBytes
&& \strlen($value) < (int) $attr
)
|| (
! $isBytes
&& \mb_strlen($value, 'UTF-8') < $attr
)
) {
$this->addError('The :alias minimum is :attr characters');
}
} elseif (\is_numeric($value)) {
if (0 + $value < $attr) {
$this->addError('The :alias minimum is :attr');
}
} elseif (\is_array($value)) {
if (\count($value) < $attr) {
$this->addError('The :alias minimum is :attr elements');
}
} elseif (null !== $value) {
$this->addError('The :alias minimum is :attr');
return null;
}
return $value;
}
protected function vMax(Validator $v, $value, $attr)
{
if (\is_string($value)) {
$isBytes = \strpos($attr, 'bytes');
if (
(
$isBytes
&& \strlen($value) > (int) $attr
)
|| (
! $isBytes
&& \mb_strlen($value, 'UTF-8') > $attr
)
) {
$this->addError('The :alias maximum is :attr characters');
}
} elseif (\is_numeric($value)) {
if (0 + $value > $attr) {
$this->addError('The :alias maximum is :attr');
}
} elseif (\is_array($value)) {
if (\reset($value) instanceof File) {
$attr *= 1024;
foreach ($value as $file) {
if ($file->size() > $attr) {
$this->addError('The :alias contains too large a file');
return null;
}
}
} elseif (\count($value) > $attr) {
$this->addError('The :alias maximum is :attr elements');
}
} elseif ($value instanceof File) {
if ($value->size() > $attr * 1024) {
$this->addError('The :alias contains too large a file');
return null;
}
} elseif (null !== $value) {
$this->addError('The :alias maximum is :attr'); //????
return null;
}
return $value;
}
protected function vToken(Validator $v, $value, $attr, $args)
{
if (! \is_array($args)) {
$args = [];
}
if (
! \is_string($value)
|| ! $this->c->Csrf->verify($value, $attr, $args)
) {
$this->addError('Bad token', 'e');
return null;
} else {
return $value;
}
}
protected function vCheckbox(Validator $v, $value)
{
return null === $value ? false : (string) $value;
}
protected function vReferer(Validator $v, $value, $attr, $args)
{
if (! \is_array($args)) {
$args = [];
}
return $this->c->Router->validate($value, $attr, $args);
}
protected function vSame(Validator $v, $value, $attr)
{
if (
! $this->getStatus($attr)
|| $value === $this->__get($attr)
) {
return $value;
} else {
$this->addError('The :alias must be same with original');
return null;
}
}
protected function vRegex(Validator $v, $value, $attr)
{
if (
null !== $value
&& (
! \is_string($value)
|| ! \preg_match($attr, $value)
)
) {
$this->addError('The :alias is not valid format');
return null;
} else {
return $value;
}
}
protected function vPassword(Validator $v, $value)
{
return $this->vRegex($v, $value, '%[^\x20][\x20][^\x20]%');
}
protected function vIn(Validator $v, $value, $attr)
{
if (
null !== $value
&& ! \in_array($value, \explode(',', $attr))
) {
$this->addError('The :alias contains an invalid value');
}
return $value;
}
protected function vNotIn(Validator $v, $value, $attr)
{
if (
null !== $value
&& \in_array($value, \explode(',', $attr))
) {
$this->addError('The :alias contains an invalid value');
}
return $value;
}
protected function vFile(Validator $v, $value, $attr)
{
if (null === $value) {
return null;
}
if (! \is_array($value)) {
$this->addError('The :alias not contains file');
return null;
}
$value = $this->c->Files->upload($value);
if (null === $value) {
return null;
} elseif (false === $value) {
$this->addError($this->c->Files->error());
return null;
} elseif ('multiple' === $attr) {
if (! \is_array($value)) {
$value = [$value];
}
} elseif (\is_array($value)) {
$this->addError('The :alias contains more than one file');
return null;
}
return $value;
}
protected function vImage(Validator $v, $value, $attr)
{
$value = $this->vFile($v, $value, $attr);
if (\is_array($value)) {
foreach ($value as $file) {
if (null === $this->c->Files->isImage($file)) {
$this->addError('The :alias not contains image');
return null;
}
}
} elseif (
null !== $value
&& null === $this->c->Files->isImage($value)
) {
$this->addError('The :alias not contains image');
return null;
}
return $value;
}
public function vDate(Validator $v, $value)
{
if (null === $value) {
return null;
} elseif (
! \is_string($value)
|| false === \strtotime($value . ' UTC')
) {
$v->addError('The :alias does not contain a date');
return \is_string($value) ? $value : null;
}
return $value;
}
}