PluginProbe ʕ •ᴥ•ʔ
Superb Addons: Blocks, Patterns, Pre-built Pages, Sliders, Popups, Free Forms, Animations & More / 4.0.0
Superb Addons: Blocks, Patterns, Pre-built Pages, Sliders, Popups, Free Forms, Animations & More v4.0.0
4.0.7 4.0.6 4.0.5 4.0.4 4.0.3 4.0.2 4.0.1 4.0.0 trunk 1.0.0 2.0.0 2.0.1 2.0.2 2.0.3 3.0 3.0.1 3.0.2 3.0.5 3.0.6 3.0.7 3.0.8 3.0.9 3.1.0 3.1.2 3.1.3 3.2.0 3.2.1 3.2.2 3.2.4 3.2.5 3.2.7 3.2.8 3.2.9 3.3.0 3.3.1 3.3.2 3.4.0 3.4.1 3.4.2 3.4.5 3.4.6 3.5.0 3.5.1 3.5.2 3.5.3 3.5.4 3.5.6 3.5.7 3.5.8 3.5.9 3.6.0 3.6.1 3.6.2 3.7.0 3.7.1
superb-blocks / src / gutenberg / form / class-form-field-validator.php
superb-blocks / src / gutenberg / form Last commit date
class-form-access-control.php 2 months ago class-form-captcha-handler.php 2 months ago class-form-controller.php 2 months ago class-form-email-config-check.php 2 months ago class-form-email-handler.php 2 months ago class-form-encryption.php 2 months ago class-form-exporter.php 2 months ago class-form-field-validator.php 2 months ago class-form-file-handler.php 2 months ago class-form-google-auth.php 2 months ago class-form-integration-handler.php 2 months ago class-form-math-parser.php 2 months ago class-form-permissions.php 2 months ago class-form-registry.php 2 months ago class-form-settings.php 2 months ago class-form-submission-cpt.php 2 months ago class-form-submission-handler.php 2 months ago
class-form-field-validator.php
640 lines
1 <?php
2
3 namespace SuperbAddons\Gutenberg\Form;
4
5 defined('ABSPATH') || exit();
6
7 class FormFieldValidator
8 {
9 /**
10 * Validate submitted fields against server-side form config.
11 *
12 * @param array $submitted_fields field_id => value from submission
13 * @param array $form_fields_config Array of field attribute arrays from form config
14 * @return array ('fields' => cleaned fields, 'errors' => field_id => error message)
15 */
16 public static function Validate($submitted_fields, $form_fields_config)
17 {
18 // Build lookup: fieldId => config
19 $config_lookup = array();
20 foreach ($form_fields_config as $field_def) {
21 if (isset($field_def['fieldId']) && $field_def['fieldId'] !== '') {
22 $config_lookup[$field_def['fieldId']] = $field_def;
23 }
24 }
25
26 $cleaned = array();
27 $errors = array();
28
29 // Only accept known field IDs
30 foreach ($submitted_fields as $key => $value) {
31 if (isset($config_lookup[$key])) {
32 $cleaned[$key] = $value;
33 }
34 }
35
36 // Validate each configured field
37 foreach ($config_lookup as $field_id => $field_config) {
38 $field_type = isset($field_config['fieldType']) ? $field_config['fieldType'] : 'text';
39
40 // Hidden fields: override with server-side default, skip validation
41 if ($field_type === 'hidden') {
42 $cleaned[$field_id] = isset($field_config['defaultValue']) ? $field_config['defaultValue'] : '';
43 continue;
44 }
45
46 // File fields: validate via FormFileHandler (separate from text validation)
47 if ($field_type === 'file') {
48 $file_error = FormFileHandler::ValidateFiles($field_config);
49 if ($file_error !== '') {
50 $errors[$field_id] = $file_error;
51 }
52 // Remove file field from text fields — files are handled separately
53 unset($cleaned[$field_id]);
54 continue;
55 }
56
57 $value = isset($cleaned[$field_id]) ? $cleaned[$field_id] : '';
58 $required = !empty($field_config['required']);
59
60 // Conditional logic: if field has active rules and was not submitted, skip required check
61 if ($required && $value === '' && self::HasConditionalLogic($field_config)) {
62 continue;
63 }
64
65 $error = self::ValidateField($value, $field_config);
66
67 /**
68 * Filter the validation error for a field.
69 * Allows premium plugin to add custom validation for new field types.
70 *
71 * @param string $error Error message, empty if valid.
72 * @param string $value Submitted field value.
73 * @param array $field_config Field configuration from server-side config.
74 */
75 $error = apply_filters('superbaddons_form_validate_field', $error, $value, $field_config);
76
77 if ($error !== '') {
78 $errors[$field_id] = $error;
79 }
80 }
81
82 return array(
83 'fields' => $cleaned,
84 'errors' => $errors,
85 );
86 }
87
88 /**
89 * Validate a single field value against its config.
90 *
91 * @param string $value Submitted value
92 * @param array $config Field config array
93 * @return string Error message, empty if valid
94 */
95 private static function ValidateField($value, $config)
96 {
97 $field_type = isset($config['fieldType']) ? $config['fieldType'] : 'text';
98 $required = !empty($config['required']);
99
100 // Required check
101 if ($required && ($value === '' || $value === null)) {
102 return __('This field is required.', 'superb-blocks');
103 }
104
105 // Skip further validation if empty and not required
106 if ($value === '' || $value === null) {
107 return '';
108 }
109
110 // Dispatch by field type
111 switch ($field_type) {
112 case 'text':
113 case 'phone':
114 return self::ValidateTextLike($value, $config);
115
116 case 'email':
117 return self::ValidateEmail($value, $config);
118
119 case 'url':
120 return self::ValidateUrl($value, $config);
121
122 case 'textarea':
123 return self::ValidateTextarea($value, $config);
124
125 case 'number':
126 return self::ValidateNumber($value, $config);
127
128 case 'select':
129 case 'radio':
130 return self::ValidateSingleOption($value, $config);
131
132 case 'checkbox':
133 return self::ValidateCheckbox($value, $config);
134
135 case 'date':
136 return self::ValidateDate($value, $config);
137
138 case 'time':
139 return self::ValidateTime($value, $config);
140
141 case 'rating':
142 return self::ValidateRating($value, $config);
143
144 case 'calculated':
145 // Calculated fields are validated by server-side recalculation (see FormController)
146 return '';
147
148 case 'slider':
149 return self::ValidateSlider($value, $config);
150
151 case 'signature':
152 return self::ValidateSignature($value);
153
154 case 'colorpicker':
155 return self::ValidateColor($value);
156
157 default:
158 return '';
159 }
160 }
161
162 /**
163 * Validate text-like fields (text, phone): length + pattern.
164 */
165 private static function ValidateTextLike($value, $config)
166 {
167 $error = self::CheckLength($value, $config);
168 if ($error !== '') {
169 return $error;
170 }
171
172 return self::CheckPattern($value, $config);
173 }
174
175 /**
176 * Validate email: length + pattern + is_email().
177 */
178 private static function ValidateEmail($value, $config)
179 {
180 $error = self::CheckLength($value, $config);
181 if ($error !== '') {
182 return $error;
183 }
184
185 $error = self::CheckPattern($value, $config);
186 if ($error !== '') {
187 return $error;
188 }
189
190 if (!is_email($value)) {
191 return __('Please enter a valid email address.', 'superb-blocks');
192 }
193
194 return '';
195 }
196
197 /**
198 * Validate URL: length + pattern + esc_url_raw check.
199 */
200 private static function ValidateUrl($value, $config)
201 {
202 $error = self::CheckLength($value, $config);
203 if ($error !== '') {
204 return $error;
205 }
206
207 $error = self::CheckPattern($value, $config);
208 if ($error !== '') {
209 return $error;
210 }
211
212 if (esc_url_raw($value) === '') {
213 return __('Please enter a valid URL.', 'superb-blocks');
214 }
215
216 return '';
217 }
218
219 /**
220 * Validate textarea: length only (no pattern per field type spec).
221 */
222 private static function ValidateTextarea($value, $config)
223 {
224 return self::CheckLength($value, $config);
225 }
226
227 /**
228 * Validate number: is_numeric + min/max value.
229 */
230 private static function ValidateNumber($value, $config)
231 {
232 if (!is_numeric($value)) {
233 return __('Please enter a valid number.', 'superb-blocks');
234 }
235
236 $num = floatval($value);
237
238 $min = isset($config['minValue']) ? $config['minValue'] : null;
239 $max = isset($config['maxValue']) ? $config['maxValue'] : null;
240
241 if ($min !== null && $min !== '' && $num < floatval($min)) {
242 /* translators: %s: minimum allowed numeric value */
243 return sprintf(__('Minimum value is %s.', 'superb-blocks'), $min);
244 }
245
246 if ($max !== null && $max !== '' && $num > floatval($max)) {
247 /* translators: %s: maximum allowed numeric value */
248 return sprintf(__('Maximum value is %s.', 'superb-blocks'), $max);
249 }
250
251 return '';
252 }
253
254 /**
255 * Validate select/radio: value must be in configured options.
256 */
257 private static function ValidateSingleOption($value, $config)
258 {
259 $options = isset($config['options']) && is_array($config['options']) ? $config['options'] : array();
260 $allowed = array();
261 foreach ($options as $opt) {
262 if (isset($opt['value'])) {
263 $allowed[] = $opt['value'];
264 }
265 }
266
267 if (!in_array($value, $allowed, true)) {
268 return __('Invalid selection.', 'superb-blocks');
269 }
270
271 return '';
272 }
273
274 /**
275 * Validate checkbox: comma-separated values, each must be in configured options.
276 */
277 private static function ValidateCheckbox($value, $config)
278 {
279 $options = isset($config['options']) && is_array($config['options']) ? $config['options'] : array();
280 $allowed = array();
281 foreach ($options as $opt) {
282 if (isset($opt['value'])) {
283 $allowed[] = $opt['value'];
284 }
285 }
286
287 // Client sends checkbox values as "Value A, Value B"
288 $selected = array_map('trim', explode(',', $value));
289 foreach ($selected as $sel) {
290 if ($sel !== '' && !in_array($sel, $allowed, true)) {
291 return __('Invalid selection.', 'superb-blocks');
292 }
293 }
294
295 return '';
296 }
297
298 /**
299 * Validate date: format + constraints (future/past/range) + excludeDays.
300 */
301 private static function ValidateDate($value, $config)
302 {
303 // Validate format YYYY-MM-DD
304 if (!preg_match('/^\d{4}-\d{2}-\d{2}$/', $value)) {
305 return __('Invalid date format.', 'superb-blocks');
306 }
307
308 $parts = explode('-', $value);
309 if (!checkdate(intval($parts[1]), intval($parts[2]), intval($parts[0]))) {
310 return __('Invalid date.', 'superb-blocks');
311 }
312
313 $ds = isset($config['dateSettings']) && is_array($config['dateSettings']) ? $config['dateSettings'] : array();
314 $constraint = isset($ds['dateConstraint']) ? $ds['dateConstraint'] : 'none';
315 if ($constraint === 'none') {
316 return '';
317 }
318
319 $custom_msg = isset($ds['dateTimeMessage']) && $ds['dateTimeMessage'] !== ''
320 ? $ds['dateTimeMessage']
321 : '';
322
323 // Resolve min/max dates using site timezone (matches client behavior)
324 $today = current_time('Y-m-d');
325 $min_date = null;
326 $max_date = null;
327
328 if ($constraint === 'future') {
329 $min_date = $today;
330 } elseif ($constraint === 'past') {
331 $max_date = $today;
332 } elseif ($constraint === 'range') {
333 $resolved = self::ResolveDateRange($ds, $today);
334 $min_date = $resolved['min'];
335 $max_date = $resolved['max'];
336 }
337
338 if ($min_date !== null && $value < $min_date) {
339 if ($custom_msg !== '') {
340 return $custom_msg;
341 }
342 /* translators: %s: earliest allowed date in Y-m-d format */
343 return sprintf(__('Please select a date on or after %s.', 'superb-blocks'), $min_date);
344 }
345
346 if ($max_date !== null && $value > $max_date) {
347 if ($custom_msg !== '') {
348 return $custom_msg;
349 }
350 /* translators: %s: latest allowed date in Y-m-d format */
351 return sprintf(__('Please select a date on or before %s.', 'superb-blocks'), $max_date);
352 }
353
354 // Exclude days
355 $exclude_days = isset($ds['excludeDays']) && is_array($ds['excludeDays']) ? $ds['excludeDays'] : array();
356 if (!empty($exclude_days)) {
357 $timestamp = strtotime($value . ' UTC');
358 if ($timestamp !== false) {
359 $day_of_week = intval(gmdate('w', $timestamp)); // 0 = Sunday
360 if (in_array($day_of_week, array_map('intval', $exclude_days), true)) {
361 if ($custom_msg !== '') {
362 return $custom_msg;
363 }
364 return __('The selected day of the week is not allowed.', 'superb-blocks');
365 }
366 }
367 }
368
369 return '';
370 }
371
372 /**
373 * Resolve min/max date for range constraint.
374 * Mirrors JS initDateConstraints() in form.js.
375 *
376 * @param array $config Field config
377 * @param string $today Today's date in Y-m-d
378 * @return array ('min' => string|null, 'max' => string|null)
379 */
380 private static function ResolveDateRange($config, $today)
381 {
382 $min_date_mode = isset($config['minDateMode']) ? $config['minDateMode'] : 'none';
383 $max_date_mode = isset($config['maxDateMode']) ? $config['maxDateMode'] : 'none';
384 $min_date = null;
385 $max_date = null;
386
387 // First pass: resolve independent dates (specific and relative)
388 if ($min_date_mode === 'specific') {
389 $min_date = isset($config['minDate']) && $config['minDate'] !== '' ? $config['minDate'] : null;
390 } elseif ($min_date_mode === 'relative') {
391 $offset = isset($config['minDateOffset']) ? intval($config['minDateOffset']) : 0;
392 $min_date = self::OffsetDate($today, $offset);
393 }
394
395 if ($max_date_mode === 'specific') {
396 $max_date = isset($config['maxDate']) && $config['maxDate'] !== '' ? $config['maxDate'] : null;
397 } elseif ($max_date_mode === 'relative') {
398 $offset = isset($config['maxDateOffset']) ? intval($config['maxDateOffset']) : 0;
399 $max_date = self::OffsetDate($today, $offset);
400 }
401
402 // Second pass: resolve offsets that depend on the other date
403 if ($max_date_mode === 'offset') {
404 $offset = isset($config['maxDateOffset']) ? intval($config['maxDateOffset']) : 0;
405 $base = $min_date !== null ? $min_date : $today;
406 $max_date = self::OffsetDate($base, $offset);
407 }
408
409 if ($min_date_mode === 'offset') {
410 $offset = isset($config['minDateOffset']) ? intval($config['minDateOffset']) : 0;
411 $base = $max_date !== null ? $max_date : $today;
412 $min_date = self::OffsetDate($base, -$offset);
413 }
414
415 return array('min' => $min_date, 'max' => $max_date);
416 }
417
418 /**
419 * Add days offset to a date string.
420 *
421 * @param string $date Y-m-d format
422 * @param int $days Number of days (can be negative)
423 * @return string Y-m-d
424 */
425 private static function OffsetDate($date, $days)
426 {
427 $sign = $days >= 0 ? '+' : '';
428 $ts = strtotime($date . ' UTC ' . $sign . $days . ' days');
429 return $ts !== false ? gmdate('Y-m-d', $ts) : $date;
430 }
431
432 /**
433 * Validate time: format + min/max constraints.
434 */
435 private static function ValidateTime($value, $config)
436 {
437 // Validate format HH:MM
438 if (!preg_match('/^\d{2}:\d{2}$/', $value)) {
439 return __('Invalid time format.', 'superb-blocks');
440 }
441
442 $ts = isset($config['timeSettings']) && is_array($config['timeSettings']) ? $config['timeSettings'] : array();
443
444 $custom_msg = isset($ts['dateTimeMessage']) && $ts['dateTimeMessage'] !== ''
445 ? $ts['dateTimeMessage']
446 : '';
447
448 $min_time = isset($ts['minTime']) && $ts['minTime'] !== '' ? $ts['minTime'] : null;
449 $max_time = isset($ts['maxTime']) && $ts['maxTime'] !== '' ? $ts['maxTime'] : null;
450
451 // HH:MM string comparison is lexicographically correct
452 if ($min_time !== null && $value < $min_time) {
453 if ($custom_msg !== '') {
454 return $custom_msg;
455 }
456 /* translators: %s: earliest allowed time in HH:MM format */
457 return sprintf(__('Please select a time at or after %s.', 'superb-blocks'), $min_time);
458 }
459
460 if ($max_time !== null && $value > $max_time) {
461 if ($custom_msg !== '') {
462 return $custom_msg;
463 }
464 /* translators: %s: latest allowed time in HH:MM format */
465 return sprintf(__('Please select a time at or before %s.', 'superb-blocks'), $max_time);
466 }
467
468 return '';
469 }
470
471 /**
472 * Validate signature: must be a valid PNG data URL within size limit.
473 */
474 private static function ValidateSignature($value)
475 {
476 // Max 500KB
477 if (strlen($value) > 500000) {
478 return __('Signature data is too large.', 'superb-blocks');
479 }
480
481 // Must start with PNG data URL prefix
482 $prefix = 'data:image/png;base64,';
483 if (strpos($value, $prefix) !== 0) {
484 return __('Invalid signature format.', 'superb-blocks');
485 }
486
487 // Validate base64 portion
488 $base64 = substr($value, strlen($prefix));
489 if (!preg_match('/^[A-Za-z0-9+\/=]+$/', $base64)) {
490 return __('Invalid signature data.', 'superb-blocks');
491 }
492
493 return '';
494 }
495
496 /**
497 * Validate slider: single numeric value within bounds.
498 */
499 private static function ValidateSlider($value, $config)
500 {
501 $ss = isset($config['sliderSettings']) && is_array($config['sliderSettings']) ? $config['sliderSettings'] : array();
502 $min = isset($ss['min']) ? floatval($ss['min']) : 0;
503 $max = isset($ss['max']) ? floatval($ss['max']) : 100;
504
505 if (!is_numeric($value)) {
506 return __('Please enter a valid number.', 'superb-blocks');
507 }
508
509 $num = floatval($value);
510 if ($num < $min || $num > $max) {
511 /* translators: 1: minimum allowed value, 2: maximum allowed value */
512 return sprintf(__('Value must be between %1$s and %2$s.', 'superb-blocks'), $min, $max);
513 }
514
515 return '';
516 }
517
518 /**
519 * Validate color: must be a valid hex color.
520 */
521 private static function ValidateColor($value)
522 {
523 if (!preg_match('/^#[0-9a-fA-F]{6}$/', $value)) {
524 return __('Please enter a valid hex color.', 'superb-blocks');
525 }
526
527 return '';
528 }
529
530 /**
531 * Validate rating: must be an integer between 0 and maxRating.
532 */
533 private static function ValidateRating($value, $config)
534 {
535 if (!is_numeric($value)) {
536 return __('Invalid rating.', 'superb-blocks');
537 }
538
539 $num = intval($value);
540 if ($num != floatval($value)) {
541 // Not an integer
542 return __('Invalid rating.', 'superb-blocks');
543 }
544
545 $rs = isset($config['ratingSettings']) && is_array($config['ratingSettings']) ? $config['ratingSettings'] : array();
546 $max_rating = isset($rs['maxRating']) ? intval($rs['maxRating']) : 5;
547
548 if ($num < 0 || $num > $max_rating) {
549 return __('Invalid rating.', 'superb-blocks');
550 }
551
552 return '';
553 }
554
555 // --- Helper methods ---
556
557 /**
558 * Check min/max length constraints.
559 */
560 private static function CheckLength($value, $config)
561 {
562 $len = function_exists('mb_strlen') ? mb_strlen($value, 'UTF-8') : strlen($value);
563
564 $min = isset($config['minLength']) ? $config['minLength'] : null;
565 $max = isset($config['maxLength']) ? $config['maxLength'] : null;
566
567 if ($min !== null && $min !== '' && $len < intval($min)) {
568 /* translators: %d: minimum required character count */
569 return sprintf(__('Minimum length is %d characters.', 'superb-blocks'), $min);
570 }
571
572 if ($max !== null && $max !== '' && $len > intval($max)) {
573 /* translators: %d: maximum allowed character count */
574 return sprintf(__('Maximum length is %d characters.', 'superb-blocks'), $max);
575 }
576
577 return '';
578 }
579
580 /**
581 * Check regex pattern constraint.
582 */
583 private static function CheckPattern($value, $config)
584 {
585 $pattern = isset($config['pattern']) && $config['pattern'] !== '' ? $config['pattern'] : null;
586 if ($pattern === null) {
587 return '';
588 }
589
590 // Wrap pattern in delimiters for PCRE, using '/' with escaping
591 $regex = '/' . str_replace('/', '\\/', $pattern) . '/';
592
593 // Guard against ReDoS from user-supplied patterns by tightening PCRE's backtrack limit around this single preg_match call. ini_set is intentional: there is no WP-API equivalent for runtime PCRE limits, the override is reverted immediately after, and the limit is per-process rather than persistent.
594 $old_limit = ini_get('pcre.backtrack_limit');
595 // phpcs:ignore Squiz.PHP.DiscouragedFunctions.Discouraged
596 ini_set('pcre.backtrack_limit', 10000);
597 $result = @preg_match($regex, $value);
598 // phpcs:ignore Squiz.PHP.DiscouragedFunctions.Discouraged
599 ini_set('pcre.backtrack_limit', $old_limit);
600
601 // If regex is invalid ($result === false) or doesn't match ($result === 0)
602 if ($result === 0) {
603 $msg = isset($config['patternMessage']) && $config['patternMessage'] !== ''
604 ? $config['patternMessage']
605 : __('Invalid format.', 'superb-blocks');
606 return $msg;
607 }
608
609 // Invalid regex or match: pass
610 return '';
611 }
612
613 /**
614 * Check if a field has active conditional logic rules.
615 */
616 private static function HasConditionalLogic($config)
617 {
618 if (!isset($config['conditionalLogic']) || !is_array($config['conditionalLogic'])) {
619 return false;
620 }
621
622 $logic = $config['conditionalLogic'];
623 if (!isset($logic['ruleGroups']) || !is_array($logic['ruleGroups'])) {
624 return false;
625 }
626
627 foreach ($logic['ruleGroups'] as $group) {
628 if (isset($group['conditions']) && is_array($group['conditions'])) {
629 foreach ($group['conditions'] as $cond) {
630 if (!empty($cond['field'])) {
631 return true;
632 }
633 }
634 }
635 }
636
637 return false;
638 }
639 }
640