Files
ave-cms/system/App/Content/Fields/FieldValidator.php
T
2026-07-27 12:58:44 +03:00

461 lines
17 KiB
PHP

<?php
/*
|--------------------------------------------------------------------------------------
| AVE.cms
|--------------------------------------------------------------------------------------
| @package AVE.cms
| @file system/App/Content/Fields/FieldValidator.php
| @author AVE.cms <support@ave-cms.ru>
| @copyright 2007-2026 (c) AVE.cms
| @link https://ave-cms.ru
| @version 3.3
*/
namespace App\Content\Fields;
defined('BASEPATH') || die('Direct access to this location is not allowed.');
use App\Helpers\Valid;
use App\Helpers\Json;
use App\Common\Lifecycle;
/**
* Серверная валидация значений полей документа.
*
* Правила берутся из JSON-настроек поля (`rubric_field_settings`): либо
* `settings.rules` (список строк типа `required`, `minLength:3`, `max:100`),
* либо флаги-ключи (`required:true`, `min:0`). Ядро правил делегирует
* App\Helpers\Valid; несколько правил (count/media/allowed) — доменные.
*
* $field — строка legacy `{prefix}_rubric_fields` (rubric_field_type/title/
* alias/Id + rubric_field_settings). Значение — то, что пришло из формы.
*/
class FieldValidator
{
/** Проверить набор значений: $fields — строки rubric_fields, $values — по Id поля. */
public static function validateValues(array $fields, array $values, $conditionsEnabled = true)
{
$errors = array();
$states = $conditionsEnabled ? FieldConditionEvaluator::states($fields, $values) : array();
foreach ($fields as $field) {
$field = (array) $field;
$id = isset($field['Id']) ? (int) $field['Id'] : 0;
if (isset($states[$id]) && empty($states[$id]['visible'])) { continue; }
if (isset($states[$id])) {
$field['_condition_required'] = !empty($states[$id]['required']);
if (is_array($states[$id]['allowed_values'])) {
$field['_condition_allowed_values'] = $states[$id]['allowed_values'];
}
}
$value = array_key_exists($id, $values) ? $values[$id] : '';
$error = self::validateField($field, $value);
if ($error !== null) {
$errors['fields[' . $id . ']'] = $error;
}
}
return $errors;
}
public static function assertValues(array $fields, array $values)
{
$errors = self::validateValues($fields, $values);
if (!empty($errors)) {
throw new FieldValidationException($errors);
}
}
/** Проверить одно поле. Возвращает текст ошибки или null. */
public static function validateField(array $field, $value)
{
$id = isset($field['Id']) ? (int) $field['Id'] : 0;
$validating = Lifecycle::event('content.field.validating', 'field', 'validating', $id, array(
'type' => isset($field['rubric_field_type']) ? (string) $field['rubric_field_type'] : '',
'field' => $field,
'value' => $value,
), null, array(), 'field_validator');
if ($validating->cancelled()) {
return $validating->result() === null ? null : (string) $validating->result();
}
$field = $validating->value('field', $field);
$value = $validating->value('value', $value);
$error = self::validateNative($field, $value);
$validated = Lifecycle::event('content.field.validated', 'field', 'validated', $id, array(
'type' => isset($field['rubric_field_type']) ? (string) $field['rubric_field_type'] : '',
'field' => $field,
'value' => $value,
), $error, array('valid' => $error === null), 'field_validator');
return $validated->result() === null ? null : (string) $validated->result();
}
protected static function validateNative(array $field, $value)
{
if (isset($field['_condition_allowed_values'])
&& !self::allowedValueArray($value, (array) $field['_condition_allowed_values'])) {
return 'Поле «' . self::label($field) . '» содержит вариант, недоступный при текущих условиях';
}
if (isset($field['rubric_field_type']) && (string) $field['rubric_field_type'] === 'number'
&& !self::isEmpty($field, $value) && Types\NumberValue::normalize(self::scalarValue($value)) === '') {
return 'Поле «' . self::label($field) . '» должно быть числом';
}
if (isset($field['rubric_field_type']) && (string) $field['rubric_field_type'] === 'date_time'
&& !self::isEmpty($field, $value) && Types\DateTimeValue::normalize(self::scalarValue($value)) === '') {
return 'Поле «' . self::label($field) . '» должно содержать корректную дату и время';
}
if (isset($field['rubric_field_type']) && (string) $field['rubric_field_type'] === 'period') {
$period = FieldRegistry::get('period');
if ($period instanceof Types\PeriodValue && !$period->valid(new FieldContext($value, $field, 'save'))) {
return 'Поле «' . self::label($field) . '» содержит некорректный период';
}
}
if (isset($field['rubric_field_type']) && (string) $field['rubric_field_type'] === 'choice') {
$choice = FieldRegistry::get('choice');
if ($choice instanceof Types\ChoiceValue && !$choice->valid(new FieldContext($value, $field, 'save'))) {
return 'Поле «' . self::label($field) . '» содержит вариант, которого нет в настройках';
}
}
if (isset($field['rubric_field_type']) && (string) $field['rubric_field_type'] === 'contact') {
$contact = FieldRegistry::get('contact');
if ($contact instanceof Types\ContactValue && !$contact->valid(new FieldContext($value, $field, 'save'))) {
return 'Поле «' . self::label($field) . '» содержит некорректный контакт';
}
}
if (isset($field['rubric_field_type']) && (string) $field['rubric_field_type'] === 'color') {
$color = FieldRegistry::get('color');
if ($color instanceof Types\ColorValue && !$color->valid(new FieldContext($value, $field, 'save'))) {
return 'Поле «' . self::label($field) . '» должно содержать HEX-цвет';
}
}
if (isset($field['rubric_field_type']) && (string) $field['rubric_field_type'] === 'range') {
$range = FieldRegistry::get('range');
if ($range instanceof Types\RangeValue && !$range->valid(new FieldContext($value, $field, 'save'))) {
return 'Поле «' . self::label($field) . '» содержит некорректный диапазон';
}
}
if (isset($field['rubric_field_type']) && (string) $field['rubric_field_type'] === 'dimensions') {
$dimensions = FieldRegistry::get('dimensions');
if ($dimensions instanceof Types\DimensionsValue && !$dimensions->valid(new FieldContext($value, $field, 'save'))) {
return 'Поле «' . self::label($field) . '» содержит некорректные габариты';
}
}
if (isset($field['rubric_field_type']) && (string) $field['rubric_field_type'] === 'packages') {
$packages = FieldRegistry::get('packages');
if ($packages instanceof Types\PackagesValue && !$packages->valid(new FieldContext($value, $field, 'save'))) {
return 'Поле «' . self::label($field) . '» содержит некорректные упаковки';
}
}
if (isset($field['rubric_field_type']) && (string) $field['rubric_field_type'] === 'address') {
$address = FieldRegistry::get('address');
if ($address instanceof Types\AddressValue && !$address->valid(new FieldContext($value, $field, 'save'))) {
return 'Поле «' . self::label($field) . '» содержит некорректный адрес';
}
}
$rules = self::rules($field);
if (empty($rules)) {
return null;
}
$label = self::label($field);
foreach ($rules as $rule) {
$error = self::apply($field, $value, $rule, $label);
if ($error !== null) {
return $error;
}
}
return null;
}
protected static function label(array $field)
{
if (isset($field['rubric_field_title']) && trim((string) $field['rubric_field_title']) !== '') {
return (string) $field['rubric_field_title'];
}
if (isset($field['rubric_field_alias']) && (string) $field['rubric_field_alias'] !== '') {
return (string) $field['rubric_field_alias'];
}
return 'поле #' . (isset($field['Id']) ? (int) $field['Id'] : 0);
}
protected static function settings(array $field)
{
$settings = FieldSettings::effective($field);
if (array_key_exists('_condition_required', $field)) {
$settings['required'] = !empty($field['_condition_required']);
}
return $settings;
}
protected static function rules(array $field)
{
$settings = self::settings($field);
$out = array();
if (isset($settings['rules'])) {
$out = array_merge($out, self::normalizeRuleList($settings['rules']));
}
foreach ($settings as $key => $value) {
if ($key === 'rules' || $key === 'options' || $value === false || $value === null || $value === '') {
continue;
}
if ($key === 'required') {
if ($value) {
$out[] = 'required';
}
continue;
}
if (in_array($key, array('min', 'max', 'minLength', 'maxLength', 'length', 'minCount', 'maxCount',
'date', 'regex', 'allowedValues', 'inArray', 'allowedExtensions', 'email', 'url'), true)) {
if (is_array($value)) {
$value = implode(',', array_filter(array_map('trim', array_map('strval', $value)), 'strlen'));
}
$out[] = $key . ($value === true ? '' : ':' . (string) $value);
}
}
return array_values(array_unique(array_filter(array_map('trim', $out), 'strlen')));
}
protected static function normalizeRuleList($rules)
{
if (is_array($rules)) {
$out = array();
foreach ($rules as $rule) {
$rule = trim((string) $rule);
if ($rule !== '') {
$out[] = $rule;
}
}
return $out;
}
return array_filter(array_map('trim', explode('|', (string) $rules)), 'strlen');
}
protected static function apply(array $field, $value, $ruleItem, $label)
{
$rule = $ruleItem;
$param = '';
if (strpos($ruleItem, ':') !== false) {
list($rule, $param) = explode(':', $ruleItem, 2);
}
$rule = self::normalizeRuleName(trim($rule));
$param = trim((string) $param);
switch ($rule) {
case 'required':
return self::isEmpty($field, $value) ? 'Поле «' . $label . '» обязательно для заполнения' : null;
case 'numeric':
return self::isEmpty($field, $value) || is_numeric(self::numericScalarValue($field, $value))
? null : 'Поле «' . $label . '» должно быть числом';
case 'min':
$numericValue = self::numericScalarValue($field, $value);
return self::isEmpty($field, $value) || (is_numeric($numericValue) && (float) $numericValue >= (float) $param)
? null : 'Значение «' . $label . '» должно быть не меньше ' . $param;
case 'max':
$numericValue = self::numericScalarValue($field, $value);
return self::isEmpty($field, $value) || (is_numeric($numericValue) && (float) $numericValue <= (float) $param)
? null : 'Значение «' . $label . '» должно быть не больше ' . $param;
case 'minLength':
return self::isEmpty($field, $value) || self::length(self::scalarValue($value)) >= (int) $param
? null : 'Поле «' . $label . '» — не менее ' . (int) $param . ' символов';
case 'maxLength':
return self::isEmpty($field, $value) || self::length(self::scalarValue($value)) <= (int) $param
? null : 'Поле «' . $label . '» — не более ' . (int) $param . ' символов';
case 'minCount':
return self::countValue($field, $value) >= (int) $param
? null : 'Поле «' . $label . '» — не менее ' . (int) $param . ' знач.';
case 'maxCount':
return self::countValue($field, $value) <= (int) $param
? null : 'Поле «' . $label . '» — не более ' . (int) $param . ' знач.';
case 'allowedValues':
return self::allowedValues($field, $value, $param)
? null : 'Поле «' . $label . '» содержит недопустимое значение';
case 'allowedExtensions':
return self::allowedExtensions($value, $param)
? null : 'Поле «' . $label . '» содержит файл недопустимого формата';
case 'date':
return self::isEmpty($field, $value) || self::validDate(self::scalarValue($value), $param !== '' ? $param : 'd.m.Y')
? null : 'Поле «' . $label . '» должно быть корректной датой';
case 'regex':
return self::isEmpty($field, $value) || @preg_match($param, self::scalarValue($value)) === 1
? null : 'Поле «' . $label . '» имеет неверный формат';
default:
$data = array('value' => self::scalarValue($value));
$errors = Valid::check($data, array('value' => array($ruleItem)), array('value' => $label));
return isset($errors['value']) ? $errors['value'] : null;
}
}
protected static function normalizeRuleName($rule)
{
$map = array(
'min_count' => 'minCount', 'max_count' => 'maxCount',
'min_length' => 'minLength', 'max_length' => 'maxLength',
'allowed_values' => 'allowedValues', 'allowed_extensions' => 'allowedExtensions',
'in' => 'allowedValues', 'inArray' => 'allowedValues',
'number' => 'numeric', 'integer' => 'int',
);
return isset($map[$rule]) ? $map[$rule] : $rule;
}
protected static function isEmpty(array $field, $value)
{
return self::countValue($field, $value) === 0;
}
protected static function countValue(array $field, $value)
{
if (isset($field['rubric_field_type']) && (string) $field['rubric_field_type'] === 'choice'
&& is_array($value) && array_key_exists('choice_present', $value)) {
$value = isset($value['selected']) ? $value['selected'] : array();
}
if (isset($field['rubric_field_type']) && (string) $field['rubric_field_type'] === 'packages'
&& is_array($value) && array_key_exists('list_present', $value)) {
$value = isset($value['items']) ? $value['items'] : array();
}
if (is_array($value)) {
return count(array_filter(self::flatten($value), 'strlen'));
}
$text = trim((string) $value);
if ($text === '') {
return 0;
}
$json = Json::toArray($text);
if (!empty($json)) {
return count(array_filter(self::flatten($json), 'strlen'));
}
return 1;
}
protected static function scalarValue($value)
{
return is_array($value) ? implode("\n", self::flatten($value)) : (string) $value;
}
protected static function numericScalarValue(array $field, $value)
{
$value = self::scalarValue($value);
return isset($field['rubric_field_type']) && (string) $field['rubric_field_type'] === 'number'
? Types\NumberValue::normalize($value)
: $value;
}
protected static function length($value)
{
return function_exists('mb_strlen') ? mb_strlen((string) $value, 'UTF-8') : strlen((string) $value);
}
protected static function allowedValues($field, $value, $param)
{
$allowed = array_filter(array_map('trim', explode(',', (string) $param)), 'strlen');
// Без явного списка — берём допустимые значения из опций поля (choice-типы).
if (empty($allowed) && isset($field['options']) && is_array($field['options'])) {
foreach ($field['options'] as $opt) {
if (is_array($opt) && isset($opt['value']) && (string) $opt['value'] !== '') {
$allowed[] = (string) $opt['value'];
}
}
}
if (empty($allowed)) {
return true;
}
$values = is_array($value) ? self::flatten($value) : array_filter(array_map('trim', preg_split('/\s*,\s*/', (string) $value)), 'strlen');
foreach ($values as $v) {
if (!in_array((string) $v, $allowed, true)) {
return false;
}
}
return true;
}
protected static function allowedValueArray($value, array $allowed)
{
$allowed = array_values(array_unique(array_map('strval', $allowed)));
$values = is_array($value)
? self::flatten($value)
: array_filter(array_map('trim', preg_split('/\s*,\s*/', (string) $value)), 'strlen');
foreach ($values as $candidate) {
if ((string) $candidate !== '' && !in_array((string) $candidate, $allowed, true)) { return false; }
}
return true;
}
protected static function allowedExtensions($value, $param)
{
$exts = array_map('strtolower', array_filter(array_map('trim', explode(',', (string) $param)), 'strlen'));
if (empty($exts)) {
return true;
}
$text = self::scalarValue($value);
if (trim($text) === '') {
return true;
}
if (preg_match_all('#[^\s"|]+\.([a-z0-9]{2,5})#i', $text, $m)) {
foreach ($m[1] as $ext) {
if (!in_array(strtolower($ext), $exts, true)) {
return false;
}
}
}
return true;
}
protected static function validDate($value, $format)
{
$value = trim((string) $value);
if ($value === '') {
return true;
}
$dt = \DateTime::createFromFormat($format, $value);
return $dt !== false && $dt->format($format) === $value;
}
protected static function flatten(array $data)
{
$out = array();
array_walk_recursive($data, function ($v) use (&$out) {
if (is_scalar($v) && (string) $v !== '') {
$out[] = (string) $v;
}
});
return $out;
}
}