PluginProbe ʕ •ᴥ•ʔ
Kirki – Freeform Page Builder, Website Builder & Customizer / 6.1.0
Kirki – Freeform Page Builder, Website Builder & Customizer v6.1.0
6.1.1 6.1.0 6.0.14 6.0.13 6.0.12 6.0.11 6.0.10 6.0.9 6.0.8 6.0.7 6.0.6 6.0.5 6.0.4 6.0.3 6.0.2 6.0.1 3.1.3 3.1.4 3.1.5 3.1.6 3.1.7 3.1.8 3.1.9 4.0.19 4.0.20 4.0.21 4.0.22 4.0.23 4.0.24 4.1 4.2.0 5.0.0 5.1.0 5.1.1 5.2.0 5.2.1 5.2.2 5.2.3 6.0.0 trunk 3.0.40 3.0.41 3.0.42 3.0.43 3.0.44 3.0.45 3.1.0 3.1.1 3.1.2
kirki / libraries / framework / Validation / Validator.php
kirki / libraries / framework / Validation Last commit date
Constants 3 weeks ago Rules 3 weeks ago Validator.php 3 weeks ago
Validator.php
464 lines
1 <?php
2
3 /**
4 * Handles data validation against defined rules.
5 *
6 * @package Framework
7 * @subpackage Validation
8 * @since 1.0.0
9 */
10 namespace Kirki\Framework\Validation;
11
12 \defined('ABSPATH') || exit;
13 use Kirki\Framework\Validation\Constants\Validation;
14 use Kirki\Framework\Contracts\Rule;
15 use Kirki\Framework\Exceptions\InvalidValidationRuleException;
16 use Kirki\Framework\Exceptions\ValidationException;
17 use Kirki\Framework\Validation\Rules\BaseRule;
18 use Closure;
19 use InvalidArgumentException;
20 use function Kirki\Framework\message;
21 use function Kirki\Framework\value;
22 class Validator
23 {
24 /**
25 * The input data to validate.
26 *
27 * @var array
28 *
29 * @since 1.0.0
30 */
31 protected $data = [];
32 /**
33 * The validated data after passing all rules.
34 *
35 * @var array
36 *
37 * @since 1.0.0
38 */
39 protected $validated_data = [];
40 /**
41 * The validation rules for the data.
42 *
43 * @var array
44 *
45 * @since 1.0.0
46 */
47 protected $rules = [];
48 /**
49 * The errors encountered during validation.
50 *
51 * @var array
52 *
53 * @since 1.0.0
54 */
55 protected $errors = [];
56 /**
57 * The messages for the validation errors.
58 *
59 * @var array
60 *
61 * @since 1.0.0
62 */
63 protected $messages = [];
64 /**
65 * Create a new Validator instance.
66 *
67 * @param array $data The input data.
68 * @param array $rules The validation rules.
69 *
70 * @return void
71 *
72 * @since 1.0.0
73 */
74 public function __construct(array $data, array $rules, array $messages = [])
75 {
76 $this->data = $data;
77 $this->rules = $rules;
78 $this->messages = $messages;
79 }
80 /**
81 * Static factory method for creating a Validator instance.
82 *
83 * @param array $data The data payload.
84 * @param array $rules The rules.
85 *
86 * @return static
87 *
88 * @since 1.0.0
89 */
90 public static function make(array $data, array $rules, array $messages = [])
91 {
92 return new static($data, $rules, $messages);
93 }
94 /**
95 * Apply if we need to validate a field rules if the callback is resolved.
96 *
97 * @param string|array $field The fields to validate
98 * @param string $rules The validation rules to applied if $callback returns true.
99 * @param callable $callback The callback to check if the field should be validated
100 *
101 * @return static
102 *
103 * @throws \InvalidArgumentException
104 *
105 * @since 1.0.0
106 */
107 public function apply_if($field, string $rules, callable $callback)
108 {
109 if (empty($field) || empty($rules)) {
110 throw new InvalidArgumentException('Field and rules are required for sometimes validation.');
111 }
112 if (!\is_array($field)) {
113 $field = [$field];
114 }
115 if (!$callback($this->data)) {
116 return $this;
117 }
118 foreach ($field as $item) {
119 $this->rules[$item] = $rules;
120 }
121 return $this;
122 }
123 /**
124 * Determine if the data passed validation.
125 *
126 * @return bool
127 *
128 * @since 1.0.0
129 */
130 public function is_valid()
131 {
132 $this->run_validation();
133 return empty($this->errors);
134 }
135 /**
136 * Determine if the validation failed
137 *
138 * @return bool
139 *
140 * @since 1.0.0
141 */
142 public function is_failed()
143 {
144 $this->run_validation();
145 return !empty($this->errors);
146 }
147 /**
148 * Validate the data against the defined rules.
149 *
150 * @return bool
151 *
152 * @since 1.0.0
153 */
154 public function validate()
155 {
156 if ($this->is_failed()) {
157 throw ValidationException::with_errors($this->errors);
158 }
159 return \true;
160 }
161 /**
162 * Get the validated data.
163 *
164 * @return array
165 *
166 * @since 1.0.0
167 */
168 public function validated()
169 {
170 return $this->validated_data;
171 }
172 /**
173 * Get validation errors, if any.
174 *
175 * @return array|null
176 *
177 * @since 1.0.0
178 */
179 public function get_errors()
180 {
181 return !empty($this->errors) ? $this->errors : null;
182 }
183 /**
184 * Run validation on the data using defined rules.
185 *
186 * @return void
187 *
188 * @since 1.0.0
189 */
190 protected function run_validation()
191 {
192 foreach ($this->rules as $field => $field_rules) {
193 if (\is_string($field_rules)) {
194 $field_rules = \explode('|', $field_rules);
195 }
196 $key_segments = \explode('.', $field);
197 $this->traverse_and_validate($this->data, $key_segments, [], $field_rules);
198 }
199 }
200 /**
201 * Recursively traverses the input data structure according to a dot-notated rule key,
202 * handling wildcard segments (e.g., *) to apply validation rules at dynamic levels.
203 *
204 * @param mixed $current_data The current level of data being inspected. This is a portion
205 * @param array $key_segments The remaining parts (split by '.') of the rule key that
206 * @param array $traversed_path_stack The stack of key_segments already traversed so far. This is used
207 * @param array $rules An array of validation rules to apply (e.g., ['required', 'string']).
208 *
209 * @return void
210 *
211 * @since 1.0.0
212 */
213 protected function traverse_and_validate($current_data, $key_segments, $traversed_path_stack, $rules)
214 {
215 // when all segments have been traversed
216 if (empty($key_segments)) {
217 $traversed_key = \implode('.', $traversed_path_stack);
218 $this->apply_rules($rules, $traversed_key, $current_data);
219 return;
220 }
221 $segment = \array_shift($key_segments);
222 if ($segment === '*') {
223 if (\is_null($current_data)) {
224 return;
225 }
226 if (!\is_array($current_data)) {
227 $traversed_key = \implode('.', $traversed_path_stack);
228 $this->errors[$traversed_key][] = $this->resolve_expected_array_message($traversed_key);
229 return;
230 }
231 foreach ($current_data as $index => $item) {
232 $this->traverse_and_validate($item, $key_segments, \array_merge($traversed_path_stack, [$index]), $rules);
233 }
234 } elseif (\is_array($current_data) && \array_key_exists($segment, $current_data)) {
235 $this->traverse_and_validate($current_data[$segment], $key_segments, \array_merge($traversed_path_stack, [$segment]), $rules);
236 } else {
237 // If the field is missing and there are still segments left,
238 // it means an intermediate key is missing. We should stop here.
239 // The validation for the intermediate key itself (if any) is handled by its own rule.
240 if (!empty($key_segments)) {
241 return;
242 }
243 // Field missing, still run rules to trigger "required" failure
244 $traversed_key = \implode('.', \array_merge($traversed_path_stack, [$segment]));
245 $this->apply_rules($rules, $traversed_key, null, \true);
246 }
247 }
248 /**
249 * Applies a set of rules to a given value.
250 *
251 * @param array $rules An array of validation rules to apply.
252 * @param string $traversed_key The dot-notated key of the value being validated.
253 * @param mixed|null $value The value being validated.
254 * @param bool $is_field_missing Whether the field being validated is missing from request.
255 *
256 * @return void
257 *
258 * @throws InvalidValidationRuleException
259 *
260 * @since 1.0.0
261 */
262 protected function apply_rules($rules, $traversed_key, $value = null, $is_field_missing = \false)
263 {
264 $is_valid = \true;
265 $strict_rule_validations = [];
266 foreach ($rules as $rule) {
267 if ($rule instanceof Closure) {
268 $response = $rule($value, $traversed_key, $this->data);
269 if ($response !== \true && !\is_string($response)) {
270 throw new InvalidValidationRuleException(\sprintf('The closure must return a boolean true for valid or string as error message, "%s" given', \is_null($response) ? 'null' : (string) $response));
271 }
272 if (\is_string($response)) {
273 $this->errors[$traversed_key][] = $response;
274 continue;
275 }
276 $is_valid = $response;
277 continue;
278 }
279 $rule_instance = $this->get_rule_class($rule, $traversed_key, $value, $rules);
280 if ($rule_instance->is_check_strict_data_type()) {
281 $strict_rule_validations[$rule] = ['is_valid' => $rule_instance->is_valid(), 'message' => $this->process_error_message($rule_instance, $traversed_key), 'traversed_key' => $traversed_key];
282 continue;
283 }
284 if (!$rule_instance->is_valid()) {
285 $is_valid = \false;
286 $this->errors[$traversed_key][] = $this->process_error_message($rule_instance, $traversed_key);
287 }
288 }
289 if (!empty($strict_rule_validations)) {
290 $is_valid = $this->validate_strict_rules($strict_rule_validations);
291 }
292 if ($is_valid && !$is_field_missing) {
293 $key_segments = \explode('.', $traversed_key);
294 $this->set_validated_data($key_segments, $value);
295 }
296 }
297 /**
298 * Resolve the expected array error message for a given key.
299 *
300 * @param string $key The key.
301 *
302 * @return string
303 *
304 * @since 1.0.0
305 */
306 protected function resolve_expected_array_message(string $key)
307 {
308 if ($this->has_custom_message('expected_array', $key)) {
309 return value($this->get_custom_message('expected_array', $key), $key, null, $this->data);
310 }
311 return message('validator.expected_array', $key);
312 }
313 /**
314 * Process the error message for a given rule and key.
315 *
316 * @param Rule $rule The rule.
317 * @param string $key The key.
318 *
319 * @return string
320 *
321 * @since 1.0.0
322 */
323 protected function process_error_message(Rule $rule, string $key)
324 {
325 if (\is_null($rule_name = $this->rule_name_from($rule))) {
326 throw new InvalidValidationRuleException(\sprintf('The rule %s does not exist or is invalid', \get_class($rule)));
327 }
328 if (!$this->has_custom_message($rule_name, $key)) {
329 return $rule->get_error_message();
330 }
331 $message = $this->get_custom_message($rule_name, $key);
332 return value($message, $key, $rule->rule_value(), $this->data);
333 }
334 /**
335 * Get the rule name from a rule instance.
336 *
337 * @param Rule $rule The rule.
338 *
339 * @return string|null
340 *
341 * @since 1.0.0
342 */
343 protected function rule_name_from(Rule $rule)
344 {
345 $class_name = \get_class($rule);
346 $rule_map = \array_flip(Validation::RULE_MAP);
347 return $rule_map[$class_name] ?? null;
348 }
349 /**
350 * Check if a custom message is defined for a given key.
351 *
352 * @param string $rule_name The rule name.
353 * @param string $key The key.
354 *
355 * @return bool
356 *
357 * @since 1.0.0
358 */
359 protected function has_custom_message(string $rule_name, string $key) : bool
360 {
361 return isset($this->messages[$key][$rule_name]);
362 }
363 /**
364 * Get the custom message for a given key.
365 *
366 * @param string $rule_name The rule name.
367 * @param string $key The key.
368 *
369 * @return string|callable
370 *
371 * @since 1.0.0
372 */
373 protected function get_custom_message(string $rule_name, string $key)
374 {
375 return $this->messages[$key][$rule_name];
376 }
377 /**
378 * Validate strict rules.
379 * check is there any rule is true then return true otherwise false and set errors
380 *
381 * @param array $strict_validations The strict validations.
382 *
383 * @return bool
384 *
385 * @since 1.0.0
386 */
387 protected function validate_strict_rules(array $strict_validations)
388 {
389 if (\array_search(\true, \array_column($strict_validations, 'is_valid'), \true) !== \false) {
390 return \true;
391 }
392 foreach ($strict_validations as $item) {
393 if (!$item['is_valid'] && $item['message']) {
394 $this->errors[$item['traversed_key']][] = $item['message'];
395 }
396 }
397 return \false;
398 }
399 /**
400 * Set the validated data using a series of keys to traverse the nested array structure.
401 *
402 * @param array $keys An array of keys representing the path in the nested array structure.
403 * @param mixed $value The value to set at the specified path in the validated data array.
404 *
405 * @return void
406 *
407 * @since 1.0.0
408 */
409 protected function set_validated_data($keys, $value)
410 {
411 $ref =& $this->validated_data;
412 foreach ($keys as $key) {
413 if (!isset($ref[$key])) {
414 $ref[$key] = [];
415 }
416 $ref =& $ref[$key];
417 }
418 $ref = $value;
419 }
420 /**
421 * Get the rule class instance.
422 *
423 * @param mixed $rule The rule.
424 * @param mixed $key The key.
425 * @param mixed $value The value.
426 * @param mixed $all_applied_rules The all applied rules.
427 *
428 * @return Rule
429 *
430 * @throws InvalidValidationRuleException
431 *
432 * @since 1.0.0
433 */
434 protected function get_rule_class($rule, $key, $value, $all_applied_rules)
435 {
436 $rule_name_value_array = \explode(':', $rule, 2);
437 $rule_name = $rule_name_value_array[0];
438 $rule_value = isset($rule_name_value_array[1]) ? $rule_name_value_array[1] : null;
439 if (\is_string($rule) && isset(Validation::RULE_MAP[$rule_name_value_array[0]])) {
440 $class_name = Validation::RULE_MAP[$rule_name];
441 return new $class_name($key, $value, $rule_value, $this->data, $all_applied_rules);
442 }
443 if (\class_exists($rule_name) && \is_subclass_of($rule_name, BaseRule::class)) {
444 return new $rule_name($key, $value, $rule_value, $this->data, $all_applied_rules);
445 }
446 throw new InvalidValidationRuleException(\sprintf('The validation rule %s does not exist or is invalid', $rule_name));
447 }
448 /**
449 * Set the error message for a given key in the errors array.
450 * The key can be a dot-notated string, e.g. `user.email`.
451 *
452 * @param string $key The key to set the error
453 * @param string $error_msg The error message
454 *
455 * @return void
456 *
457 * @since 1.0.0
458 */
459 public function add_error($key, $error_msg)
460 {
461 $this->errors[$key] = $error_msg;
462 }
463 }
464