PluginProbe
The Innovative Form Builder – IvyForms / 1.1
The Innovative Form Builder – IvyForms v1.1
1.5.1 1.5 1.4.1 1.4 trunk 0.1.2 0.2 0.2.1 0.3 0.3.1 0.4 0.5 0.6 0.6.1 0.6.1-backup 0.6.1.1 0.7 0.8 0.8.1 0.8.2 0.9 0.9.1 1.0 1.1 1.1.1 All 27 releases
ivyforms / backend / src / Common / Sanitizer / Sanitizer.php

Sanitizer.php in The Innovative Form Builder – IvyForms 1.1, at backend/src/Common/Sanitizer/Sanitizer.php

1,791 lines 70.9 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 /**
4 * @copyright © Melograno Venture Studio. All rights reserved.
5 * @licence See COPYING.md for license details.
6 */
7
8 namespace IvyForms\Common\Sanitizer;
9
10 use IvyForms\Common\Exceptions\ForbiddenException;
11 use IvyForms\Common\Exceptions\InvalidArgumentException;
12 use IvyForms\Services\Field\FieldType;
13 use IvyForms\Services\Form\Style\StyleConstants;
14 use IvyForms\Services\Translations\BackendStrings;
15
16 class Sanitizer
17 {
18 /**
19 * Sanitize HTML editor content with support for inline styles (colors, formatting)
20 *
21 * @param string $content
22 *
23 * @return string
24 */
25 public static function sanitizeEditorContent(string $content): string
26 {
27 // Extract all style attributes before wp_kses processes them
28 $style_placeholders = [];
29 $placeholder_index = 0;
30
31 // Extract data: URLs in img src attributes (preserve them from wp_kses stripping)
32 $data_url_placeholders = [];
33 $data_url_index = 0;
34
35 // Extract data: image URLs and replace with safe placeholders in data-img-src attribute
36 $content = preg_replace_callback(
37 '/src\s*=\s*["\']?(data:image\/[^"\'>\s]+)["\']?/i',
38 function ($matches) use (&$data_url_placeholders, &$data_url_index) {
39 $placeholder = 'IMGDATA' . $data_url_index;
40 $data_url_placeholders[$placeholder] = $matches[1];
41 $data_url_index++;
42 // Use data-img-src instead of src so wp_kses doesn't strip it
43 return 'data-img-src="' . $placeholder . '"';
44 },
45 $content
46 );
47
48 $content = preg_replace_callback(
49 '/(<[^>]+)\s+style="([^"]*)"/i',
50 function ($matches) use (&$style_placeholders, &$placeholder_index) {
51 $placeholder = '___STYLE_PLACEHOLDER_' . $placeholder_index . '___';
52 // Sanitize the CSS value for security
53 $style_placeholders[$placeholder] = CssSanitizer::sanitizeCssValue(
54 $matches[2]
55 );
56 $placeholder_index++;
57 $html = ' data-style-placeholder="' . $placeholder . '"';
58 return $matches[1] . $html;
59 },
60 $content
61 );
62
63 // Define allowed HTML tags and attributes for the editor
64 $allowed_tags = wp_kses_allowed_html('post');
65
66 // Add necessary attributes to tags
67 $tags_with_style = [
68 'span',
69 'p',
70 'div',
71 'h1',
72 'h2',
73 'h3',
74 'h4',
75 'h5',
76 'h6',
77 'li',
78 'ul',
79 'ol',
80 'blockquote',
81 'strong',
82 'em',
83 'u',
84 'a'
85 ];
86
87 foreach ($tags_with_style as $tag) {
88 if (!isset($allowed_tags[$tag])) {
89 $allowed_tags[$tag] = [];
90 }
91 if (!is_array($allowed_tags[$tag])) {
92 $allowed_tags[$tag] = [];
93 }
94 $allowed_tags[$tag]['data-style-placeholder'] = true;
95 $allowed_tags[$tag]['class'] = true;
96 }
97
98 // Ensure 'a' tag has all necessary attributes
99 if (!isset($allowed_tags['a'])) {
100 $allowed_tags['a'] = [];
101 }
102 $allowed_tags['a']['href'] = true;
103 $allowed_tags['a']['target'] = true;
104 $allowed_tags['a']['rel'] = true;
105 $allowed_tags['a']['class'] = true;
106 $allowed_tags['a']['data-style-placeholder'] = true;
107
108 // Ensure 'img' tag is in allowed tags with data: URLs support
109 if (!isset($allowed_tags['img'])) {
110 $allowed_tags['img'] = [];
111 }
112 $allowed_tags['img']['src'] = true;
113 $allowed_tags['img']['alt'] = true;
114 $allowed_tags['img']['class'] = true;
115 $allowed_tags['img']['style'] = true;
116 $allowed_tags['img']['width'] = true;
117 $allowed_tags['img']['height'] = true;
118 $allowed_tags['img']['data-img-src'] = true; // Allow placeholder attribute
119
120 // Sanitize HTML structure (but style content is preserved in placeholders)
121 $result = wp_kses($content, $allowed_tags);
122
123 // Restore data: URLs from data-img-src back to src attribute
124 $result = preg_replace_callback(
125 '/<img[^>]*data-img-src=["\']?(IMGDATA\d+)["\']?[^>]*>/i',
126 function ($matches) use ($data_url_placeholders) {
127 // Get the full img tag
128 $imgTag = $matches[0];
129 // Find the placeholder
130 $placeholderMatch = [];
131 if (preg_match('/data-img-src=["\']?(IMGDATA\d+)["\']?/i', $imgTag, $placeholderMatch)) {
132 $placeholder = $placeholderMatch[1];
133 if (isset($data_url_placeholders[$placeholder])) {
134 // Remove data-img-src
135 $imgTag = preg_replace('/\s*data-img-src=["\']?IMGDATA\d+["\']?/i', '', $imgTag);
136 // Only add src if it does not already exist
137 if (!preg_match('/\ssrc=["\']?[^"\'>]+["\']?/i', $imgTag)) {
138 // Add src attribute before the closing >
139 $imgTag = str_replace('>', ' src="' . $data_url_placeholders[$placeholder] . '">', $imgTag);
140 }
141 return $imgTag;
142 }
143 }
144 return $matches[0];
145 },
146 $result
147 );
148 // Restore style attributes from placeholders
149 $result = preg_replace_callback(
150 '/data-style-placeholder="(___STYLE_PLACEHOLDER_\d+___)"/i',
151 function ($matches) use ($style_placeholders) {
152 $placeholder = $matches[1];
153 if (isset($style_placeholders[$placeholder])) {
154 $sanitized_style = $style_placeholders[$placeholder];
155 // Only add style attribute if there's actual content
156 if (!empty($sanitized_style)) {
157 return 'style="' . $sanitized_style . '"';
158 }
159 }
160 return '';
161 },
162 $result
163 );
164 return $result;
165 }
166
167 /**
168 * Sanitize agreement text allowing basic HTML tags (links, formatting)
169 *
170 * @param string $content The agreement text to sanitize
171 *
172 * @return string The sanitized agreement text
173 */
174 public static function sanitizeAgreementText(string $content): string
175 {
176 // Define allowed HTML tags for agreement text
177 $allowed_tags = [
178 'a' => [
179 'href' => true,
180 'target' => true,
181 'rel' => true,
182 'title' => true,
183 ],
184 'strong' => [],
185 'b' => [],
186 'em' => [],
187 'i' => [],
188 'u' => [],
189 'br' => [],
190 'p' => [],
191 'span' => [
192 'class' => true,
193 ],
194 ];
195
196 // Sanitize with allowed tags
197 $sanitized = wp_kses($content, $allowed_tags);
198
199 // Ensure external links have rel="noopener noreferrer" for security
200 $sanitized = preg_replace_callback(
201 '/<a\s+([^>]*href=["\']https?:\/\/[^"\']+["\'][^>]*)>/i',
202 function ($matches) {
203 $attrs = $matches[1];
204 // Add rel if not present
205 if (stripos($attrs, 'rel=') === false) {
206 return '<a ' . $attrs . ' rel="noopener noreferrer">';
207 }
208 return '<a ' . $attrs . '>';
209 },
210 $sanitized
211 );
212
213 return $sanitized;
214 }
215
216 /**
217 * Sanitize form action buttons settings
218 *
219 * @param mixed[] $settings
220 *
221 * @return mixed[]
222 */
223 public static function sanitizeFormActionButtons(array $settings): array
224 {
225 $submitButtonSettings = $settings['submitButtonSettings'] ?? [];
226 $validPositions = ['default', 'left', 'center', 'right'];
227 $position = $submitButtonSettings['position'] ?? 'default';
228
229 // Validate position
230 if (!in_array($position, $validPositions, true)) {
231 $position = 'default';
232 }
233
234 $sanitized = [
235 'submitButtonSettings' => [
236 'label' => sanitize_text_field(
237 $submitButtonSettings['label'] ?? BackendStrings::getCommonStrings()['submit']
238 ),
239 'position' => $position
240 ]
241 ];
242
243 // Allow extensions (e.g. Pro) to merge pageNavigationSettings and other keys.
244 // Always call apply_filters; do not gate on has_filter (extensions register at plugins_loaded).
245 return apply_filters('ivyforms/sanitize/form_action_buttons', $sanitized, $settings);
246 }
247
248 /**
249 * Sanitize all form data
250 *
251 * @param mixed[] $data
252 *
253 * @return mixed[]
254 * @throws InvalidArgumentException
255 */
256 public static function sanitizeFormData(array $data): array
257 {
258 $fields = $data['fields'] ?? [];
259
260 $sanitized = [
261 'id' => (int)($data['id'] ?? 0),
262 'name' => sanitize_text_field($data['name'] ?? ''),
263 'description' => sanitize_text_field($data['description'] ?? ''),
264 'showTitle' => (bool)($data['showTitle'] ?? false),
265 'published' => (bool)($data['published'] ?? false),
266 'showDescription' => (bool)($data['showDescription'] ?? false),
267 'storeEntries' => (bool)($data['storeEntries'] ?? false),
268 'fields' => self::sanitizeFields($fields),
269 'integrationSettings' => self::sanitizeIntegrationSettings(
270 $data['integrationSettings'] ?? []
271 ),
272 'styleSettings' => self::sanitizeStyleSettings($data['styleSettings'] ?? null),
273 'formActionButtons' => self::sanitizeFormActionButtons(
274 $data['formActionButtons'] ?? []
275 )
276 ];
277
278 /**
279 * Allows Pro plugin to add additional form data properties (pages, progressIndicator, etc.)
280 *
281 * @since 0.1.0
282 *
283 * @param array<string, mixed> $sanitized The sanitized form data
284 * @param array<string, mixed> $data The raw form data
285 * @return array<string, mixed> The modified sanitized data
286 */
287 return apply_filters('ivyforms/sanitize/form_data', $sanitized, $data);
288 }
289 /**
290 * Sanitize all notification data
291 *
292 * @param mixed[] $data
293 *
294 * @return mixed[]
295 */
296 public static function sanitizeNotificationData(array $data): array
297 {
298 return [
299 'id' => (int)($data['id'] ?? 0),
300 'name' => sanitize_text_field($data['name'] ?? ''),
301 'sender' => self::sanitizeEmailOrPlaceholder($data['sender'] ?? ''),
302 'replyTo' => self::sanitizeEmailOrPlaceholder($data['replyTo'] ?? ''),
303 'receiver' => self::sanitizeEmailOrPlaceholder($data['receiver'] ?? ''),
304 'enabled' => (bool)($data['enabled'] ?? true),
305 'subject' => sanitize_text_field($data['subject'] ?? ''),
306 'message' => self::sanitizeEditorContent($data['message'] ?? ''),
307 'showEmptyFields' => (bool)($data['showEmptyFields'] ?? false),
308 'smartLogic' => self::sanitizeNotificationSmartLogic($data['smartLogic'] ?? false),
309 'formId' => (int)($data['formId'] ?? 0),
310 ];
311 }
312
313 /**
314 * Sanitize notification smart logic payload.
315 *
316 * @param mixed $smartLogic
317 * @return array<string, mixed>
318 */
319 public static function sanitizeNotificationSmartLogic($smartLogic): array
320 {
321 if (!is_array($smartLogic)) {
322 return [
323 'enabled' => (bool)$smartLogic,
324 'match' => 'any',
325 'rules' => [],
326 ];
327 }
328
329 $match = sanitize_text_field((string)($smartLogic['match'] ?? 'any'));
330 if (!in_array($match, ['any', 'all'], true)) {
331 $match = 'any';
332 }
333
334 $rules = [];
335 $rawRules = $smartLogic['rules'] ?? [];
336 if (is_array($rawRules)) {
337 foreach ($rawRules as $rule) {
338 if (!is_array($rule)) {
339 continue;
340 }
341
342 $operator = sanitize_text_field((string)($rule['operator'] ?? 'equals'));
343 if (!in_array($operator, ['equals', 'not_equals'], true)) {
344 $operator = 'equals';
345 }
346
347 $field = isset($rule['field']) ? sanitize_text_field((string)$rule['field']) : null;
348
349 $rules[] = [
350 'id' => isset($rule['id']) ? (int)$rule['id'] : 0,
351 'field' => $field === '' ? null : $field,
352 'operator' => $operator,
353 'value' => sanitize_text_field((string)($rule['value'] ?? '')),
354 ];
355 }
356 }
357
358 return [
359 'enabled' => (bool)($smartLogic['enabled'] ?? false),
360 'match' => $match,
361 'rules' => $rules,
362 ];
363 }
364 /**
365 * Sanitize all confirmation data
366 *
367 * @param mixed[] $data
368 *
369 * @return mixed[]
370 */
371 public static function sanitizeConfirmationData(array $data): array
372 {
373 return [
374 'id' => (int)($data['id'] ?? 0),
375 'formId' => (int)($data['formId'] ?? 0),
376 'type' => sanitize_text_field($data['type'] ?? ''),
377 'enabled' => (bool)($data['enabled'] ?? true),
378 'showForm' => (bool)($data['showForm'] ?? false),
379 'message' => self::sanitizeEditorContent($data['message'] ?? ''),
380 'url' => sanitize_url(
381 isset($data['url']) ? urldecode($data['url']) : ''
382 ),
383 'page' => sanitize_text_field($data['page'] ?? ''),
384 ];
385 }
386
387 /**
388 * Sanitize form fields
389 *
390 * @param mixed[] $fields
391 *
392 * @return mixed[]
393 * @throws InvalidArgumentException
394 */
395 private static function sanitizeFields(array $fields): array
396 {
397 return array_map(function ($field) {
398 // Validate field type
399 $fieldType = sanitize_text_field($field['type'] ?? '');
400 if (!FieldType::isValid($field['type'])) {
401 throw new InvalidArgumentException(
402 BackendStrings::getExceptionStrings()['invalid_field_type']
403 );
404 }
405
406 $sanitizedField = [
407 'id' => (int)($field['id'] ?? 0),
408 'fieldIndex' => (int)($field['fieldIndex'] ?? 0),
409 'type' => $fieldType,
410 'label' => sanitize_text_field($field['label'] ?? ''),
411 'required' => (bool)($field['required'] ?? false),
412 'defaultValue' => sanitize_text_field($field['defaultValue'] ?? ''),
413 'placeholder' => sanitize_text_field($field['placeholder'] ?? ''),
414 'position' => (int)($field['position'] ?? 0),
415 'rowIndex' => max(0, (int)($field['rowIndex'] ?? 0)),
416 'columnIndex' => max(0, min(4, (int)($field['columnIndex'] ?? 0))),
417 'width' => max(20, min(100, (int)($field['width'] ?? 100))),
418 'parentId' => isset($field['parentId']) ? max(0, (int)$field['parentId']) : null,
419 'hideLabel' => (bool)($field['hideLabel'] ?? false),
420 'readOnly' => (bool)($field['readOnly'] ?? false),
421 'description' => sanitize_text_field($field['description'] ?? ''),
422 'requiredMessage' => sanitize_text_field($field['requiredMessage'] ?? ''),
423 'cssClasses' => self::sanitizeCssClasses($field['cssClasses'] ?? ''),
424 'fieldOptions' => self::sanitizeFieldOptions($field['fieldOptions'] ?? []),
425 'shuffleOptions' => (bool)($field['shuffleOptions'] ?? false),
426 'showValues' => (bool)($field['showValues'] ?? false),
427 'enableSearch' => (bool)($field['enableSearch'] ?? false),
428 'rows' => (int)($field['rows'] ?? 0),
429 'confirmFieldEnabled' => (bool)($field['confirmFieldEnabled'] ?? false),
430 'confirmFieldLabel' => sanitize_text_field($field['confirmFieldLabel'] ?? ''),
431 'confirmFieldPlaceholder' => sanitize_text_field($field['confirmFieldPlaceholder'] ?? ''),
432 'confirmFieldHideLabel' => (bool)($field['confirmFieldHideLabel'] ?? false),
433 'phoneFormat' => self::sanitizePhoneFormat($field['phoneFormat'] ?? ''),
434 'phoneAutoDetect' => (bool)($field['phoneAutoDetect'] ?? false),
435 'minValue' => self::sanitizeNumericValue($field['minValue'] ?? null),
436 'maxValue' => self::sanitizeNumericValue($field['maxValue'] ?? null),
437 'step' => (float)($field['step'] ?? 1.0),
438 'showGrid' => (bool)($field['showGrid'] ?? false),
439 'numberFormat' => sanitize_text_field($field['numberFormat'] ?? ''),
440 'visible' => (bool)($field['visible'] ?? true),
441 'limitMaxLength' => (bool)($field['limitMaxLength'] ?? false),
442 'limitRange' => (bool)($field['limitRange'] ?? false),
443 'maxLength' => (int)($field['maxLength'] ?? 255),
444 'customValidationMessage' => sanitize_text_field($field['customValidationMessage'] ?? ''),
445 'labelPosition' => sanitize_text_field($field['labelPosition'] ?? 'default'),
446 'noDuplicates' => (bool)($field['noDuplicates'] ?? false),
447 'inputPrefix' => sanitize_text_field($field['inputPrefix'] ?? ''),
448 'inputSuffix' => sanitize_text_field($field['inputSuffix'] ?? ''),
449 'timeFieldType' => sanitize_text_field($field['timeFieldType'] ?? ''),
450 'timeFormat' => sanitize_text_field($field['timeFormat'] ?? ''),
451 'dateFieldType' => sanitize_text_field($field['dateFieldType'] ?? ''),
452 'dateFormat' => sanitize_text_field($field['dateFormat'] ?? ''),
453 'minDateValue' => sanitize_text_field($field['minDateValue'] ?? ''),
454 'maxDateValue' => sanitize_text_field($field['maxDateValue'] ?? ''),
455 'showRatingText' => (bool)($field['showRatingText'] ?? false),
456 'ratingIcon' => sanitize_text_field($field['ratingIcon'] ?? 'star'),
457 'htmlContent' => self::sanitizeEditorContent($field['htmlContent'] ?? ''),
458 'agreementText' => self::sanitizeAgreementText($field['agreementText'] ?? ''),
459 'limitDateRange' => (bool)($field['limitDateRange'] ?? false),
460 'minTimeValue' => sanitize_text_field($field['minTimeValue'] ?? ''),
461 'maxTimeValue' => sanitize_text_field($field['maxTimeValue'] ?? ''),
462 'limitTimeRange' => (bool)($field['limitTimeRange'] ?? false),
463 'pageId' => ($pageId = sanitize_text_field($field['pageId'] ?? ''))
464 !== '' ? $pageId : null,
465 ];
466
467 /**
468 * This filter allows to sanitize form fields properties
469 * for fields that are not part of the core plugin.
470 *
471 * @since 0.1.0
472 *
473 * @param mixed[] $sanitizedField The sanitized field data so far.
474 * @param mixed[] $field The original field data before sanitization.
475 * @return mixed[] The modified sanitized field data.
476 */
477 return apply_filters('ivyforms/sanitizer/field_properties', $sanitizedField, $field);
478 }, $fields);
479 }
480
481 /**
482 * Sanitize field options
483 *
484 * @param mixed[] $options
485 *
486 * @return mixed[]
487 */
488 private static function sanitizeFieldOptions(array $options): array
489 {
490 return array_map(function ($option) {
491 return [
492 'id' => (int)($option['id'] ?? 0),
493 'fieldId' => (int)($option['fieldId'] ?? 0),
494 'label' => sanitize_text_field($option['label'] ?? ''),
495 'value' => sanitize_text_field($option['value'] ?? ''),
496 'isDefault' => (bool)($option['isDefault'] ?? false),
497 'position' => (int)($option['position'] ?? 1),
498 ];
499 }, $options);
500 }
501
502 private static function sanitizePhoneFormat(string $format): string
503 {
504 $allowed = ['international','national','e164'];
505 $formatLower = strtolower($format);
506 return in_array($formatLower, $allowed, true) ? $formatLower : '';
507 }
508
509 /**
510 * Sanitize entry data
511 *
512 * @param mixed[] $data
513 *
514 * @return mixed[]
515 */
516 public static function sanitizeEntryData(array $data): array
517 {
518 return [
519 'id' => (int)($data['id'] ?? 0),
520 'formId' => (int)($data['formId'] ?? 0),
521 'userId' => (int)($data['userId'] ?? 0),
522 'status' => sanitize_text_field($data['status'] ?? ''),
523 'ipAddress' => sanitize_text_field($data['ipAddress'] ?? ''),
524 'userAgent' => sanitize_text_field($data['userAgent'] ?? ''),
525 'sourceURL' => sanitize_text_field($data['sourceURL'] ?? ''),
526 'dateCreated' => sanitize_text_field($data['dateCreated'] ?? ''),
527 'dateEdited' => sanitize_text_field($data['dateEdited'] ?? ''),
528 'starred' => (bool)($data['starred'] ?? false),
529 ];
530 }
531
532 /**
533 * Sanitize ID
534 *
535 * @param int $id
536 *
537 * @return int
538 *
539 * @throws InvalidArgumentException
540 */
541 public static function sanitizeId(int $id): int
542 {
543 if ($id <= 0) {
544 throw new InvalidArgumentException(
545 BackendStrings::getExceptionStrings()['invalid_id']
546 );
547 }
548
549 return $id;
550 }
551
552 /**
553 * Sanitize multiple IDs
554 *
555 * @param int[] $ids
556 *
557 * @return int[]
558 *
559 */
560 public static function sanitizeIds(array $ids): array
561 {
562 return array_map(function ($id) {
563 return (int)$id;
564 }, $ids);
565 }
566
567 /**
568 * Sanitize search parameters
569 *
570 * @param array<string, mixed> $params
571 *
572 * @return array<string, mixed>
573 * @throws InvalidArgumentException
574 */
575 public static function sanitizeSearchParams(array $params): array
576 {
577 $filters = [
578 'starred' => array_key_exists('starred', $params['filters'] ?? [])
579 ? (int)filter_var($params['filters']['starred'], FILTER_VALIDATE_BOOLEAN)
580 : null,
581 'published' => array_key_exists('published', $params['filters'] ?? [])
582 ? (int)filter_var($params['filters']['published'], FILTER_VALIDATE_BOOLEAN)
583 : null,
584 'status' => sanitize_text_field($params['filters']['read'] ?? ''),
585 ];
586 // Add formId to filters if present and valid
587 if (
588 isset($params['filters']['formId']) &&
589 is_numeric($params['filters']['formId']) &&
590 (int)$params['filters']['formId'] > 0
591 ) {
592 $filters['formId'] = self::sanitizeId((int)$params['filters']['formId']);
593 }
594 return [
595 'page' => max((int)($params['page'] ?? 1), 1),
596 'perPage' => max((int)($params['perPage'] ?? 10), 1),
597 'search' => sanitize_text_field($params['search'] ?? ''),
598 'orderBy' => sanitize_text_field($params['orderBy'] ?? 'id'),
599 'order' => strtolower(sanitize_text_field($params['order'] ?? 'asc')) === 'desc' ? 'desc' : 'asc',
600 'filters' => $filters,
601 'dateRange' => [
602 sanitize_text_field($params['dateRange'][0] ?? ''),
603 sanitize_text_field($params['dateRange'][1] ?? ''),
604 ],
605 'shouldGetCount' => (bool)($params['shouldGetCount'] ?? false),
606 'searchFieldValue' => filter_var($params['searchFieldValue'] ?? false, FILTER_VALIDATE_BOOLEAN),
607 ];
608 }
609
610 /**
611 * Sanitize form submission data based on field types and IDs
612 *
613 * @param mixed[] $data Form submission data
614 * @param mixed[] $formFields Form fields configuration
615 *
616 * @return mixed[] Sanitized submission data
617 */
618 public static function sanitizeFormSubmissionData(array $data, array $formFields): array
619 {
620 $sanitizedData = [];
621 list($fieldIdMap, $typeIndexMap) = self::buildFieldMaps($formFields);
622
623 foreach ($data['values'] as $key => $value) {
624 if (self::isInternalField($key)) {
625 $sanitizedData[$key] = $value;
626 continue;
627 }
628 if (isset($fieldIdMap[$key])) {
629 $sanitizedData[$key] = self::sanitizeByFieldType($fieldIdMap[$key], $value);
630 continue;
631 }
632 if (isset($typeIndexMap[$key])) {
633 $fieldId = $typeIndexMap[$key]['id'];
634 $fieldType = $typeIndexMap[$key]['type'];
635 $sanitizedData[$fieldId] = self::sanitizeByFieldType($fieldType, $value);
636 }
637 }
638
639 // Add form ID to sanitized data if present
640 if (isset($data['formId'])) {
641 $sanitizedData['formId'] = (int)$data['formId'];
642 }
643
644 // Add post ID and referer if present (for placeholders)
645 if (isset($data['postId'])) {
646 $sanitizedData['postId'] = (int)($data['postId']);
647 }
648 if (isset($data['referer'])) {
649 $sanitizedData['referer'] = sanitize_text_field($data['referer']);
650 }
651
652 return $sanitizedData;
653 }
654
655 /**
656 * Build field ID and type-index maps from form fields
657 *
658 * @param mixed[] $formFields Form fields configuration
659 * @return mixed[]
660 */
661 private static function buildFieldMaps(array $formFields): array
662 {
663 $fieldIdMap = [];
664 $typeIndexMap = [];
665 foreach ($formFields as $field) {
666 $fieldId = $field->getId() ?? null;
667 $fieldType = $field->getType() ?? null;
668 $fieldIndex = $field->getIndex() ?? null;
669 if ($fieldId !== null) {
670 $fieldIdMap[$fieldId] = $fieldType;
671 if ($fieldType !== null && $fieldIndex !== null) {
672 $typeIndexMap[$fieldType . '_' . $fieldIndex] = [
673 'id' => $fieldId,
674 'type' => $fieldType
675 ];
676 }
677 }
678 }
679 return [$fieldIdMap, $typeIndexMap];
680 }
681
682 /**
683 * Check if a key is an internal field
684 *
685 * @param string $key
686 * @return bool
687 */
688 private static function isInternalField(string $key): bool
689 {
690 return in_array($key, ['nonce', 'formId']);
691 }
692
693 /**
694 * Sanitize a value based on its field type
695 *
696 * @param string $fieldType The type of field
697 * @param mixed $value The value to sanitize
698 *
699 * @return mixed Sanitized value
700 */
701 private static function sanitizeByFieldType(string $fieldType, $value)
702 {
703 // Recursively sanitize arrays/objects (parent-child fields)
704 if (is_array($value)) {
705 return array_map(function ($v) use ($fieldType) {
706 return self::sanitizeByFieldType($fieldType, $v);
707 }, $value);
708 }
709 if (is_object($value)) {
710 $sanitized = array_map(function ($v) use ($fieldType) {
711 return self::sanitizeByFieldType($fieldType, $v);
712 }, get_object_vars($value));
713 return (object)$sanitized;
714 }
715 switch ($fieldType) {
716 case 'email':
717 return sanitize_email($value);
718
719 case 'number':
720 return is_numeric($value) ? (float)$value : null;
721
722 case 'slider':
723 return is_numeric($value) ? (float)$value : null;
724
725 case 'phone':
726 // Remove everything except digits, +, and spaces
727 return preg_replace('/[^\d\s+]/', '', $value);
728
729 case 'paragraph':
730 return sanitize_textarea_field($value);
731
732 case 'website':
733 return sanitize_url($value);
734
735 case 'time':
736 $sanitizedTime = preg_replace('/[^0-9: ]/i', '', (string)$value);
737 return sanitize_text_field($sanitizedTime);
738
739 case 'date':
740 return sanitize_text_field($value);
741
742 case 'gdpr':
743 // GDPR checkbox: sanitize as boolean-like value
744 return (bool)$value ? 'Yes' : 'No';
745
746 case 'text':
747 default:
748 // Default to text field sanitization
749 return self::sanitizeText($value);
750 }
751 }
752
753 /**
754 * Verify nonce with custom error handling
755 *
756 * @param string|null $nonce
757 *
758 * @throws ForbiddenException
759 */
760 public static function verifyNonce(?string $nonce): void
761 {
762 $nonce = sanitize_text_field($nonce);
763
764 if (!$nonce || !wp_verify_nonce($nonce, 'wp_rest')) {
765 throw new ForbiddenException(
766 BackendStrings::getExceptionStrings()['invalid_nonce']
767 );
768 }
769 }
770 /**
771 * Verify submission nonce with custom error handling
772 *
773 * @param string|null $nonce
774 * @param int $formId
775 *
776 * @throws ForbiddenException
777 */
778 public static function verifySubmissionNonce(?string $nonce, int $formId): void
779 {
780 $nonce = sanitize_text_field($nonce);
781
782 // Verify the form-specific nonce
783 if (!wp_verify_nonce($nonce, 'ivyformsFrontSubmissionNonce_' . $formId)) {
784 throw new ForbiddenException(
785 BackendStrings::getExceptionStrings()['invalid_nonce']
786 );
787 }
788 }
789
790 /**
791 * Sanitize text
792 *
793 * @param string|null $text
794 *
795 * @return string
796 */
797 public static function sanitizeText(?string $text): ?string
798 {
799 if (is_null($text)) {
800 return '';
801 }
802
803 return wp_kses_post(sanitize_text_field($text));
804 }
805
806 /**
807 * Sanitize settings fields
808 *
809 * @param mixed $value
810 *
811 * @return mixed Sanitized value
812 */
813 public static function sanitizeSettingsFields($value)
814 {
815 if (is_array($value)) {
816 return array_map([self::class, 'sanitizeSettingsFields'], $value);
817 }
818
819 if (is_object($value)) {
820 return (object)array_map([self::class, 'sanitizeSettingsFields'], (array)$value);
821 }
822
823 if (is_string($value)) {
824 return self::sanitizeText($value);
825 }
826
827 if (is_int($value)) {
828 return intval($value);
829 }
830
831 if (is_bool($value)) {
832 return filter_var($value, FILTER_VALIDATE_BOOLEAN);
833 }
834
835 return '';
836 }
837
838 /**
839 * Sanitize email or placeholder
840 *
841 * @param string $value
842 *
843 * @return string
844 */
845 public static function sanitizeEmailOrPlaceholder($value): string
846 {
847 $value = trim($value);
848 // Allow placeholders
849 if (preg_match('/^{{[^}]+}}$/', $value)) {
850 return $value;
851 }
852 // Otherwise, sanitize as email
853 return sanitize_email($value);
854 }
855
856 /**
857 * Sanitize numeric values for min/max fields
858 * Convert empty/unselected to null, but allow 0 as a valid value
859 *
860 * @param mixed $value
861 * @return float|null
862 */
863 private static function sanitizeNumericValue($value): ?float
864 {
865 // If value is null or empty string, return null (not selected)
866 if ($value === null || $value === '') {
867 return null;
868 }
869
870 // If value is numeric (including 0), convert to float
871 if (is_numeric($value)) {
872 return (float)$value;
873 }
874
875 // If not numeric, return null
876 return null;
877 }
878
879 /**
880 * Validate email address.
881 *
882 * @param string $email
883 * @return bool
884 */
885 public static function isValidEmail(string $email): bool
886 {
887 return filter_var($email, FILTER_VALIDATE_EMAIL) !== false;
888 }
889
890 /**
891 * Normalize field value to string (comma-separated if array)
892 * @param mixed $value
893 * @return string
894 */
895 public static function normalizeFieldValue($value): string
896 {
897 if (is_array($value)) {
898 return implode(', ', $value);
899 }
900 return (string)$value;
901 }
902
903 /**
904 * Sanitize integration settings
905 * Recursively handles nested structures and various data types.
906 *
907 * @param mixed $integrationSettings
908 *
909 * @return array<string, array<string, mixed>>
910 */
911 public static function sanitizeIntegrationSettings($integrationSettings): array
912 {
913 // Handle null or non-array inputs
914 if (!is_array($integrationSettings)) {
915 return [];
916 }
917
918 $cleanedSettings = [];
919
920 foreach ($integrationSettings as $integration => $settings) {
921 // Skip if not a valid integration name or settings structure
922 if (!is_string($integration) || !is_array($settings)) {
923 continue;
924 }
925
926 $cleanedSettings[sanitize_text_field($integration)] = self::sanitizeIntegrationSettingsRecursive($settings);
927 }
928
929 return $cleanedSettings;
930 }
931
932 /**
933 * Recursively sanitize integration settings values
934 *
935 * @param mixed $value
936 * @return mixed
937 */
938 public static function sanitizeIntegrationSettingsRecursive($value)
939 {
940 // Handle arrays recursively
941 if (is_array($value)) {
942 $sanitized = [];
943 foreach ($value as $key => $val) {
944 $sanitizedKey = is_string($key) ? sanitize_text_field($key) : $key;
945 $sanitized[$sanitizedKey] = self::sanitizeIntegrationSettingsRecursive($val);
946 }
947 return $sanitized;
948 }
949
950 // Handle different scalar types
951 if (is_bool($value)) {
952 return $value;
953 }
954 if (is_int($value)) {
955 return $value;
956 }
957 if (is_float($value)) {
958 return $value;
959 }
960 if (is_string($value)) {
961 return sanitize_text_field($value);
962 }
963
964 return '';
965 }
966
967 /**
968 * Sanitize style settings for form styling
969 *
970 * @param mixed $styleSettings
971 * @return array<string, mixed>|null
972 */
973 public static function sanitizeStyleSettings($styleSettings): ?array
974 {
975 // Handle null or non-array inputs
976 if (!is_array($styleSettings) || !isset($styleSettings['stylesEnabled'])) {
977 return null;
978 }
979
980 $sanitized = [];
981
982 // Sanitize style mode flags
983 $sanitized['stylesEnabled'] = (bool)$styleSettings['stylesEnabled'];
984 $sanitized['selectedTheme'] = sanitize_text_field($styleSettings['selectedTheme'] ?? 'ivy-default');
985 $sanitized['styleMode'] = in_array($styleSettings['styleMode'] ?? 'quick', ['quick', 'advanced'])
986 ? $styleSettings['styleMode']
987 : 'quick';
988
989 // Sanitize quick mode settings
990 $sanitized['quickMode'] = self::sanitizeQuickStyleSettings($styleSettings['quickMode']);
991
992 // Sanitize advanced mode settings
993 $sanitized['advancedMode'] = self::sanitizeAdvancedStyleSettings($styleSettings['advancedMode']);
994
995 // Sanitize custom CSS class
996 $sanitized['customCssClass'] = CssSanitizer::sanitizeCssClass(
997 $styleSettings['customCssClass'] ?? ''
998 );
999 $sanitized['additionalCssClasses'] = CssSanitizer::sanitizeCssClassList(
1000 $styleSettings['additionalCssClasses'] ?? ''
1001 );
1002
1003 return $sanitized;
1004 }
1005
1006 /**
1007 * Sanitize quick style settings with basic color and unit validation
1008 *
1009 * @param mixed $quickModeSettings
1010 * @return array<string, mixed>
1011 */
1012 private static function sanitizeQuickStyleSettings($quickModeSettings): array
1013 {
1014 if (isset($quickModeSettings) && is_array($quickModeSettings)) {
1015 return [
1016 'formBackground' => self::sanitizeColor($quickModeSettings['formBackground'] ?? ''),
1017 'formBorderColor' => self::sanitizeColor($quickModeSettings['formBorderColor'] ?? ''),
1018 'formBorderWidth' => CssSanitizer::sanitizeCssUnit($quickModeSettings['formBorderWidth'] ?? ''),
1019 'verticalSpacing' => CssSanitizer::sanitizeCssUnit(
1020 $quickModeSettings['verticalSpacing'] ?? ''
1021 ),
1022 'horizontalSpacing' => CssSanitizer::sanitizeCssUnit(
1023 $quickModeSettings['horizontalSpacing'] ?? ''
1024 ),
1025 'formCorner' => self::sanitizeCornerSetting($quickModeSettings['formCorner'] ?? ''),
1026 'formTitleText' => self::sanitizeColor($quickModeSettings['formTitleText'] ?? ''),
1027 'fieldBackground' => self::sanitizeColor($quickModeSettings['fieldBackground'] ?? ''),
1028 'fieldText' => self::sanitizeColor(
1029 $quickModeSettings['fieldText'] ?? ''
1030 ),
1031 'fieldBorderColor' => self::sanitizeColor(
1032 $quickModeSettings['fieldBorderColor'] ?? ''
1033 ),
1034 'fieldCorner' => self::sanitizeCornerSetting(
1035 $quickModeSettings['fieldCorner'] ?? ''
1036 ),
1037 'fieldBorderWidth' => CssSanitizer::sanitizeCssUnit(
1038 $quickModeSettings['fieldBorderWidth'] ?? ''
1039 ),
1040 'fieldPaddingLeftRight' => CssSanitizer::sanitizeCssUnit(
1041 $quickModeSettings['fieldPaddingLeftRight'] ?? ''
1042 ),
1043 'fieldPaddingTopBottom' => CssSanitizer::sanitizeCssUnit(
1044 $quickModeSettings['fieldPaddingTopBottom'] ?? ''
1045 ),
1046 'labelFontColor' => self::sanitizeColor(
1047 $quickModeSettings['labelFontColor'] ?? ''
1048 ),
1049 'buttonBackground' => self::sanitizeColor(
1050 $quickModeSettings['buttonBackground'] ?? ''
1051 ),
1052 'buttonBorderColor' => self::sanitizeColor(
1053 $quickModeSettings['buttonBorderColor'] ?? ''
1054 ),
1055 'buttonBorderWidth' => CssSanitizer::sanitizeCssUnit(
1056 $quickModeSettings['buttonBorderWidth'] ?? ''
1057 ),
1058 'buttonPaddingLeftRight' => CssSanitizer::sanitizeCssUnit(
1059 $quickModeSettings['buttonPaddingLeftRight'] ?? ''
1060 ),
1061 'buttonPaddingTopBottom' => CssSanitizer::sanitizeCssUnit(
1062 $quickModeSettings['buttonPaddingTopBottom'] ?? ''
1063 ),
1064 'buttonWidth' => CssSanitizer::sanitizeCssUnitOrAuto(
1065 $quickModeSettings['buttonWidth'] ?? ''
1066 ),
1067 'buttonHeight' => CssSanitizer::sanitizeCssUnitOrAuto(
1068 $quickModeSettings['buttonHeight'] ?? ''
1069 ),
1070 'buttonText' => self::sanitizeColor(
1071 $quickModeSettings['buttonText'] ?? ''
1072 ),
1073 ];
1074 }
1075 return [];
1076 }
1077
1078 /**
1079 * Sanitize advanced style settings with basic color and unit validation
1080 *
1081 * @param mixed $advancedModeSettings
1082 * @return array<string, mixed>
1083 */
1084 private static function sanitizeAdvancedStyleSettings($advancedModeSettings): array
1085 {
1086 $advanced = [];
1087
1088 if (isset($advancedModeSettings) && is_array($advancedModeSettings)) {
1089 if (isset($advancedModeSettings['general'])) {
1090 $advanced['general'] = [
1091 'formBackground' => self::sanitizeColor(
1092 $advancedModeSettings['general']['formBackground'] ?? ''
1093 ),
1094 'formBorderColor' => self::sanitizeColor(
1095 $advancedModeSettings['general']['formBorderColor'] ?? ''
1096 ),
1097 'formBorderWidth' => CssSanitizer::sanitizeCssUnit(
1098 $advancedModeSettings['general']['formBorderWidth'] ?? ''
1099 ),
1100 'formBorderStyle' => self::sanitizeBorderStyle(
1101 $advancedModeSettings['general']['formBorderStyle'] ?? 'solid'
1102 ),
1103 'formPaddingLeftRight' => CssSanitizer::sanitizeCssUnit(
1104 $advancedModeSettings['general']['formPaddingLeftRight'] ?? ''
1105 ),
1106 'formPaddingTopBottom' => CssSanitizer::sanitizeCssUnit(
1107 $advancedModeSettings['general']['formPaddingTopBottom'] ?? ''
1108 ),
1109 'verticalSpacing' => CssSanitizer::sanitizeCssUnit(
1110 $advancedModeSettings['general']['verticalSpacing'] ?? ''
1111 ),
1112 'horizontalSpacing' => CssSanitizer::sanitizeCssUnit(
1113 $advancedModeSettings['general']['horizontalSpacing'] ?? ''
1114 ),
1115 'alignment' => self::sanitizeAlignment(
1116 $advancedModeSettings['general']['alignment'] ?? 'left'
1117 ),
1118 'formCorner' => self::sanitizeCornerSetting(
1119 $advancedModeSettings['general']['formCorner'] ?? ''
1120 ),
1121 ];
1122 }
1123
1124 if (isset($advancedModeSettings['formTitle'])) {
1125 $advanced['formTitle'] = [
1126 'fontColor' => self::sanitizeColor(
1127 $advancedModeSettings['formTitle']['fontColor'] ?? ''
1128 ),
1129 'fontWeight' => self::sanitizeFontWeight(
1130 $advancedModeSettings['formTitle']['fontWeight'] ?? ''
1131 ),
1132 'fontStyle' => self::sanitizeFontStyle(
1133 $advancedModeSettings['formTitle']['fontStyle'] ?? 'normal'
1134 ),
1135 'fontSize' => CssSanitizer::sanitizeCssUnit(
1136 $advancedModeSettings['formTitle']['fontSize'] ?? ''
1137 ),
1138 'paddingLeftRight' => CssSanitizer::sanitizeCssUnit(
1139 $advancedModeSettings['formTitle']['paddingLeftRight'] ?? ''
1140 ),
1141 'paddingTopBottom' => CssSanitizer::sanitizeCssUnit(
1142 $advancedModeSettings['formTitle']['paddingTopBottom'] ?? ''
1143 ),
1144 ];
1145 }
1146
1147 if (isset($advancedModeSettings['formDescription'])) {
1148 $advanced['formDescription'] = [
1149 'fontColor' => self::sanitizeColor(
1150 $advancedModeSettings['formDescription']['fontColor'] ?? ''
1151 ),
1152 'fontWeight' => self::sanitizeFontWeight(
1153 $advancedModeSettings['formDescription']['fontWeight'] ?? ''
1154 ),
1155 'fontStyle' => self::sanitizeFontStyle(
1156 $advancedModeSettings['formDescription']['fontStyle'] ?? 'normal'
1157 ),
1158 'fontSize' => CssSanitizer::sanitizeCssUnit(
1159 $advancedModeSettings['formDescription']['fontSize'] ?? ''
1160 ),
1161 'paddingLeftRight' => CssSanitizer::sanitizeCssUnit(
1162 $advancedModeSettings['formDescription']['paddingLeftRight'] ?? ''
1163 ),
1164 'paddingTopBottom' => CssSanitizer::sanitizeCssUnit(
1165 $advancedModeSettings['formDescription']['paddingTopBottom'] ?? ''
1166 ),
1167 ];
1168 }
1169
1170 if (isset($advancedModeSettings['fieldLabels'])) {
1171 $labelFontColor = $advancedModeSettings['fieldLabels']['label']['fontColor']
1172 ?? $advancedModeSettings['fieldLabels']['fontColor']
1173 ?? '';
1174 $requiredIndicatorColor =
1175 $advancedModeSettings['fieldLabels']['requiredIndicator']['fontColor']
1176 ?? $advancedModeSettings['fieldLabels']['requiredIndicatorFontColor']
1177 ?? '';
1178 $advanced['fieldLabels'] = [
1179 'label' => [
1180 'fontColor' => self::sanitizeColor($labelFontColor),
1181 'fontWeight' => self::sanitizeFontWeight(
1182 $advancedModeSettings['fieldLabels']['label']['fontWeight'] ?? ''
1183 ),
1184 'fontStyle' => self::sanitizeFontStyle(
1185 $advancedModeSettings['fieldLabels']['label']['fontStyle'] ?? 'normal'
1186 ),
1187 'fontSize' => CssSanitizer::sanitizeCssUnit(
1188 $advancedModeSettings['fieldLabels']['label']['fontSize'] ?? ''
1189 ),
1190 'paddingLeftRight' => CssSanitizer::sanitizeCssUnit(
1191 $advancedModeSettings['fieldLabels']['label']['paddingLeftRight'] ?? ''
1192 ),
1193 'paddingTopBottom' => CssSanitizer::sanitizeCssUnit(
1194 $advancedModeSettings['fieldLabels']['label']['paddingTopBottom'] ?? ''
1195 ),
1196 'alignment' => self::sanitizeAlignment(
1197 $advancedModeSettings['fieldLabels']['label']['alignment'] ?? 'left'
1198 ),
1199 ],
1200 'requiredIndicator' => [
1201 'fontColor' => self::sanitizeColor($requiredIndicatorColor),
1202 ],
1203 ];
1204 }
1205
1206 if (isset($advancedModeSettings['fieldPlaceholder'])) {
1207 $advanced['fieldPlaceholder'] = [
1208 'fontColor' => self::sanitizeColor(
1209 $advancedModeSettings['fieldPlaceholder']['fontColor'] ?? ''
1210 ),
1211 'fontWeight' => self::sanitizeFontWeight(
1212 $advancedModeSettings['fieldPlaceholder']['fontWeight'] ?? ''
1213 ),
1214 'fontStyle' => self::sanitizeFontStyle(
1215 $advancedModeSettings['fieldPlaceholder']['fontStyle'] ?? 'normal'
1216 ),
1217 'fontSize' => CssSanitizer::sanitizeCssUnit(
1218 $advancedModeSettings['fieldPlaceholder']['fontSize'] ?? ''
1219 ),
1220 'paddingLeftRight' => CssSanitizer::sanitizeCssUnit(
1221 $advancedModeSettings['fieldPlaceholder']['paddingLeftRight'] ?? ''
1222 ),
1223 'alignment' => self::sanitizeAlignment(
1224 $advancedModeSettings['fieldPlaceholder']['alignment'] ?? 'left'
1225 ),
1226 ];
1227 }
1228
1229 if (isset($advancedModeSettings['fieldStates'])) {
1230 $advanced['fieldStates'] = [];
1231 if (isset($advancedModeSettings['fieldStates']['default'])) {
1232 $advanced['fieldStates']['default'] = [
1233 'background' => self::sanitizeColor(
1234 $advancedModeSettings['fieldStates']['default']['background']
1235 ?? $advancedModeSettings['fieldStates']['default']['backgroundColor']
1236 ?? ''
1237 ),
1238 'textColor' => self::sanitizeColor(
1239 $advancedModeSettings['fieldStates']['default']['textColor'] ?? ''
1240 ),
1241 'fontWeight' => self::sanitizeFontWeight(
1242 $advancedModeSettings['fieldStates']['default']['fontWeight'] ?? ''
1243 ),
1244 'fontStyle' => self::sanitizeFontStyle(
1245 $advancedModeSettings['fieldStates']['default']['fontStyle'] ?? 'normal'
1246 ),
1247 'fontSize' => CssSanitizer::sanitizeCssUnit(
1248 $advancedModeSettings['fieldStates']['default']['fontSize'] ?? ''
1249 ),
1250 'descriptionColor' => self::sanitizeColor(
1251 $advancedModeSettings['fieldStates']['default']['descriptionColor'] ?? ''
1252 ),
1253 'borderColor' => self::sanitizeColor(
1254 $advancedModeSettings['fieldStates']['default']['borderColor'] ?? ''
1255 ),
1256 'borderStyle' => self::sanitizeBorderStyle(
1257 $advancedModeSettings['fieldStates']['default']['borderStyle'] ?? 'solid'
1258 ),
1259 'corner' => self::sanitizeCornerSetting(
1260 $advancedModeSettings['fieldStates']['default']['corner'] ?? ''
1261 ),
1262 'borderWidth' => CssSanitizer::sanitizeCssUnit(
1263 $advancedModeSettings['fieldStates']['default']['borderWidth'] ?? ''
1264 ),
1265 'paddingLeftRight' => CssSanitizer::sanitizeCssUnit(
1266 $advancedModeSettings['fieldStates']['default']['paddingLeftRight'] ?? ''
1267 ),
1268 'paddingTopBottom' => CssSanitizer::sanitizeCssUnit(
1269 $advancedModeSettings['fieldStates']['default']['paddingTopBottom'] ?? ''
1270 ),
1271 ];
1272 }
1273 if (isset($advancedModeSettings['fieldStates']['active'])) {
1274 $advanced['fieldStates']['active'] = [
1275 'background' => self::sanitizeColor(
1276 $advancedModeSettings['fieldStates']['active']['background']
1277 ?? $advancedModeSettings['fieldStates']['active']['backgroundColor']
1278 ?? ''
1279 ),
1280 'textColor' => self::sanitizeColor(
1281 $advancedModeSettings['fieldStates']['active']['textColor'] ?? ''
1282 ),
1283 'descriptionColor' => self::sanitizeColor(
1284 $advancedModeSettings['fieldStates']['active']['descriptionColor'] ?? ''
1285 ),
1286 'borderColor' => self::sanitizeColor(
1287 $advancedModeSettings['fieldStates']['active']['borderColor'] ?? ''
1288 ),
1289 'borderStyle' => self::sanitizeBorderStyle(
1290 $advancedModeSettings['fieldStates']['active']['borderStyle'] ?? 'solid'
1291 ),
1292 ];
1293 }
1294 if (isset($advancedModeSettings['fieldStates']['readOnly'])) {
1295 $advanced['fieldStates']['readOnly'] = [
1296 'background' => self::sanitizeColor(
1297 $advancedModeSettings['fieldStates']['readOnly']['background']
1298 ?? $advancedModeSettings['fieldStates']['readOnly']['backgroundColor']
1299 ?? ''
1300 ),
1301 'textColor' => self::sanitizeColor(
1302 $advancedModeSettings['fieldStates']['readOnly']['textColor'] ?? ''
1303 ),
1304 'descriptionColor' => self::sanitizeColor(
1305 $advancedModeSettings['fieldStates']['readOnly']['descriptionColor'] ?? ''
1306 ),
1307 'borderColor' => self::sanitizeColor(
1308 $advancedModeSettings['fieldStates']['readOnly']['borderColor'] ?? ''
1309 ),
1310 'borderStyle' => self::sanitizeBorderStyle(
1311 $advancedModeSettings['fieldStates']['readOnly']['borderStyle'] ?? 'solid'
1312 ),
1313 ];
1314 }
1315 if (isset($advancedModeSettings['fieldStates']['error'])) {
1316 $advanced['fieldStates']['error'] = [
1317 'background' => self::sanitizeColor(
1318 $advancedModeSettings['fieldStates']['error']['background']
1319 ?? $advancedModeSettings['fieldStates']['error']['backgroundColor']
1320 ?? ''
1321 ),
1322 'textColor' => self::sanitizeColor(
1323 $advancedModeSettings['fieldStates']['error']['textColor'] ?? ''
1324 ),
1325 'descriptionColor' => self::sanitizeColor(
1326 $advancedModeSettings['fieldStates']['error']['descriptionColor'] ?? ''
1327 ),
1328 'borderColor' => self::sanitizeColor(
1329 $advancedModeSettings['fieldStates']['error']['borderColor'] ?? ''
1330 ),
1331 'borderStyle' => self::sanitizeBorderStyle(
1332 $advancedModeSettings['fieldStates']['error']['borderStyle'] ?? 'solid'
1333 ),
1334 ];
1335 }
1336 }
1337
1338 if (isset($advancedModeSettings['checkboxRadio'])) {
1339 $advanced['checkboxRadio'] = [
1340 'fontColor' => self::sanitizeColor(
1341 $advancedModeSettings['checkboxRadio']['fontColor'] ?? ''
1342 ),
1343 'fontWeight' => self::sanitizeFontWeight(
1344 $advancedModeSettings['checkboxRadio']['fontWeight'] ?? ''
1345 ),
1346 'fontStyle' => self::sanitizeFontStyle(
1347 $advancedModeSettings['checkboxRadio']['fontStyle'] ?? 'normal'
1348 ),
1349 'fontSize' => CssSanitizer::sanitizeCssUnit(
1350 $advancedModeSettings['checkboxRadio']['fontSize'] ?? ''
1351 ),
1352 'selectionColor' => self::sanitizeColor(
1353 $advancedModeSettings['checkboxRadio']['selectionColor'] ?? ''
1354 ),
1355 'borderColor' => self::sanitizeColor(
1356 $advancedModeSettings['checkboxRadio']['borderColor'] ?? ''
1357 ),
1358 ];
1359 }
1360
1361 if (isset($advancedModeSettings['buttonStates'])) {
1362 $advanced['buttonStates'] = [
1363 'normal' => [
1364 'background' => self::sanitizeColor(
1365 $advancedModeSettings['buttonStates']['normal']['background']
1366 ?? $advancedModeSettings['buttonStates']['normal']['backgroundColor']
1367 ?? ''
1368 ),
1369 'borderColor' => self::sanitizeColor(
1370 $advancedModeSettings['buttonStates']['normal']['borderColor'] ?? ''
1371 ),
1372 'textColor' => self::sanitizeColor(
1373 $advancedModeSettings['buttonStates']['normal']['textColor']
1374 ?? $advancedModeSettings['buttonStates']['normal']['fontColor']
1375 ?? ''
1376 ),
1377 'fontWeight' => self::sanitizeFontWeight(
1378 $advancedModeSettings['buttonStates']['normal']['fontWeight'] ?? ''
1379 ),
1380 'fontStyle' => self::sanitizeFontStyle(
1381 $advancedModeSettings['buttonStates']['normal']['fontStyle'] ?? 'normal'
1382 ),
1383 'fontSize' => CssSanitizer::sanitizeCssUnit(
1384 $advancedModeSettings['buttonStates']['normal']['fontSize'] ?? ''
1385 ),
1386 'borderWidth' => CssSanitizer::sanitizeCssUnit(
1387 $advancedModeSettings['buttonStates']['normal']['borderWidth'] ?? ''
1388 ),
1389 'corner' => self::sanitizeCornerSetting(
1390 $advancedModeSettings['buttonStates']['normal']['corner'] ?? ''
1391 ),
1392 'paddingLeftRight' => CssSanitizer::sanitizeCssUnit(
1393 $advancedModeSettings['buttonStates']['normal']['paddingLeftRight'] ?? ''
1394 ),
1395 'paddingTopBottom' => CssSanitizer::sanitizeCssUnit(
1396 $advancedModeSettings['buttonStates']['normal']['paddingTopBottom'] ?? ''
1397 ),
1398 'width' => CssSanitizer::sanitizeCssUnitOrAuto(
1399 $advancedModeSettings['buttonStates']['normal']['width'] ?? ''
1400 ),
1401 'height' => CssSanitizer::sanitizeCssUnitOrAuto(
1402 $advancedModeSettings['buttonStates']['normal']['height'] ?? ''
1403 ),
1404 ],
1405 'hover' => [
1406 'background' => self::sanitizeColor(
1407 $advancedModeSettings['buttonStates']['hover']['background']
1408 ?? $advancedModeSettings['buttonStates']['hover']['backgroundColor']
1409 ?? ''
1410 ),
1411 'borderColor' => self::sanitizeColor(
1412 $advancedModeSettings['buttonStates']['hover']['borderColor'] ?? ''
1413 ),
1414 'textColor' => self::sanitizeColor(
1415 $advancedModeSettings['buttonStates']['hover']['textColor']
1416 ?? $advancedModeSettings['buttonStates']['hover']['fontColor']
1417 ?? ''
1418 ),
1419 'corner' => self::sanitizeCornerSetting(
1420 $advancedModeSettings['buttonStates']['hover']['corner']
1421 ?? $advancedModeSettings['buttonStates']['normal']['corner']
1422 ?? ''
1423 ),
1424 ],
1425 ];
1426 }
1427
1428 if (
1429 isset($advancedModeSettings['confirmationMessage'])
1430 || isset($advancedModeSettings['confirmation'])
1431 ) {
1432 $confirmationMessage = $advancedModeSettings['confirmationMessage']
1433 ?? $advancedModeSettings['confirmation'];
1434 $advanced['confirmationMessage'] = [
1435 'borderColor' => self::sanitizeColor(
1436 $confirmationMessage['borderColor'] ?? ''
1437 ),
1438 'borderStyle' => self::sanitizeBorderStyle(
1439 $confirmationMessage['borderStyle'] ?? 'solid'
1440 ),
1441 'backgroundColor' => self::sanitizeColor(
1442 $confirmationMessage['backgroundColor'] ?? ''
1443 ),
1444 'textColor' => self::sanitizeColor(
1445 $confirmationMessage['textColor'] ?? ''
1446 ),
1447 'borderWidth' => CssSanitizer::sanitizeCssUnit(
1448 $confirmationMessage['borderWidth'] ?? ''
1449 ),
1450 'corner' => self::sanitizeCornerSetting(
1451 $confirmationMessage['corner'] ?? ''
1452 ),
1453 ];
1454 }
1455
1456 if (is_array($advancedModeSettings['rating'] ?? null)) {
1457 $advanced['rating'] = [
1458 'iconSize' => CssSanitizer::sanitizeCssUnit(
1459 $advancedModeSettings['rating']['iconSize'] ?? ''
1460 ),
1461 'iconColor' => self::sanitizeColor(
1462 $advancedModeSettings['rating']['iconColor'] ?? ''
1463 ),
1464 ];
1465 }
1466
1467 if (is_array($advancedModeSettings['dateTimePicker'] ?? null)) {
1468 $advanced['dateTimePicker'] = [
1469 'calendarBackground' => self::sanitizeColor(
1470 $advancedModeSettings['dateTimePicker']['calendarBackground'] ?? ''
1471 ),
1472 'numberTextColor' => self::sanitizeColor(
1473 $advancedModeSettings['dateTimePicker']['numberTextColor'] ?? ''
1474 ),
1475 'selectionColor' => self::sanitizeColor(
1476 $advancedModeSettings['dateTimePicker']['selectionColor'] ?? ''
1477 ),
1478 ];
1479 }
1480
1481 if (is_array($advancedModeSettings['select'] ?? null)) {
1482 $advanced['select'] = [
1483 'tagColor' => self::sanitizeColor(
1484 $advancedModeSettings['select']['tagColor'] ?? ''
1485 ),
1486 'tagTextColor' => self::sanitizeColor(
1487 $advancedModeSettings['select']['tagTextColor'] ?? ''
1488 ),
1489 'optionHoverColor' => self::sanitizeColor(
1490 $advancedModeSettings['select']['optionHoverColor'] ?? ''
1491 ),
1492 'selectionTextColor' => self::sanitizeColor(
1493 $advancedModeSettings['select']['selectionTextColor'] ?? ''
1494 ),
1495 'selectionBackgroundColor' => self::sanitizeColor(
1496 $advancedModeSettings['select']['selectionBackgroundColor'] ?? ''
1497 ),
1498 ];
1499 }
1500 }
1501
1502 return $advanced;
1503 }
1504
1505 /**
1506 * Sanitize linked or per-corner border radius settings.
1507 *
1508 * @param mixed $cornerSetting
1509 * @return array<string, mixed>|string
1510 */
1511 private static function sanitizeCornerSetting($cornerSetting)
1512 {
1513 if (is_array($cornerSetting)) {
1514 return [
1515 'isLinked' => isset($cornerSetting['isLinked']) ? (bool)$cornerSetting['isLinked'] : true,
1516 'value' => CssSanitizer::sanitizeCssUnit($cornerSetting['value'] ?? ''),
1517 'topLeft' => CssSanitizer::sanitizeCssUnit($cornerSetting['topLeft'] ?? ''),
1518 'topRight' => CssSanitizer::sanitizeCssUnit($cornerSetting['topRight'] ?? ''),
1519 'bottomRight' => CssSanitizer::sanitizeCssUnit($cornerSetting['bottomRight'] ?? ''),
1520 'bottomLeft' => CssSanitizer::sanitizeCssUnit($cornerSetting['bottomLeft'] ?? ''),
1521 ];
1522 }
1523
1524 if (is_string($cornerSetting)) {
1525 return CssSanitizer::sanitizeCssUnit($cornerSetting);
1526 }
1527
1528 return '';
1529 }
1530
1531 /**
1532 * Sanitize CSS border style value.
1533 *
1534 * @param string $value
1535 * @return string
1536 */
1537 private static function sanitizeBorderStyle(string $value): string
1538 {
1539 $normalizedValue = strtolower(trim($value));
1540 return in_array($normalizedValue, StyleConstants::ALLOWED_BORDER_STYLES, true) ? $normalizedValue : 'solid';
1541 }
1542
1543 /**
1544 * Sanitize font weight value.
1545 *
1546 * @param string $value
1547 * @return string
1548 */
1549 private static function sanitizeFontWeight(string $value): string
1550 {
1551 $normalizedValue = trim($value);
1552 return in_array($normalizedValue, StyleConstants::ALLOWED_FONT_WEIGHTS, true) ? $normalizedValue : '';
1553 }
1554
1555 private static function sanitizeFontStyle(string $value): string
1556 {
1557 $normalizedValue = strtolower(trim($value));
1558 return in_array($normalizedValue, StyleConstants::ALLOWED_FONT_STYLES, true) ? $normalizedValue : 'normal';
1559 }
1560
1561 private static function sanitizeAlignment(string $value): string
1562 {
1563 $trimmed = trim($value);
1564 return in_array($trimmed, StyleConstants::ALLOWED_ALIGNMENTS, true) ? $trimmed : 'left';
1565 }
1566
1567 /**
1568 * Sanitize color value (hex, rgb, rgba)
1569 *
1570 * @param string $color
1571 * @return string
1572 */
1573 private static function sanitizeColor(string $color): string
1574 {
1575 // Allow hex colors, rgb(), rgba(), color names
1576 $color = trim($color);
1577
1578 // Check if it's a hex color
1579 if (preg_match('/^#[a-f0-9]{6}$/i', $color) || preg_match('/^#[a-f0-9]{3}$/i', $color)) {
1580 return $color;
1581 }
1582
1583 // Check if it's rgb() or rgba()
1584 if (preg_match('/^rgba?\([0-9,.\s]+\)$/i', $color)) {
1585 return sanitize_text_field($color);
1586 }
1587
1588 // Default safe color
1589 return sanitize_text_field($color);
1590 }
1591
1592 /**
1593 * Get allowed HTML tags and attributes for wp_kses sanitization.
1594 * Includes support for signature images with data: URIs.
1595 *
1596 * @return array<string, array<string, bool>>
1597 */
1598 public static function getAllowedHtmlTags(): array
1599 {
1600 return [
1601 'img' => [
1602 'src' => true,
1603 'alt' => true,
1604 'style' => true,
1605 'class' => true,
1606 'width' => true,
1607 'height' => true,
1608 ],
1609 'a' => [
1610 'href' => true,
1611 'title' => true,
1612 'target' => true,
1613 'rel' => true,
1614 ],
1615 'p' => ['class' => true, 'style' => true],
1616 'br' => [],
1617 'strong' => [],
1618 'b' => [],
1619 'em' => [],
1620 'i' => [],
1621 'u' => [],
1622 'span' => ['class' => true, 'style' => true],
1623 'div' => ['class' => true, 'style' => true],
1624 'ul' => ['class' => true],
1625 'ol' => ['class' => true],
1626 'li' => ['class' => true],
1627 'h1' => ['class' => true, 'style' => true],
1628 'h2' => ['class' => true, 'style' => true],
1629 'h3' => ['class' => true, 'style' => true],
1630 'h4' => ['class' => true, 'style' => true],
1631 'h5' => ['class' => true, 'style' => true],
1632 'h6' => ['class' => true, 'style' => true],
1633 'blockquote' => ['class' => true, 'style' => true],
1634 ];
1635 }
1636
1637 /**
1638 * Get allowed protocols for wp_kses sanitization.
1639 * Includes 'data' protocol for signature images.
1640 *
1641 * @return array<int, string>
1642 */
1643 public static function getAllowedProtocols(): array
1644 {
1645 return ['http', 'https', 'mailto', 'data'];
1646 }
1647
1648 /**
1649 * Sanitize HTML content using wp_kses with support for signature images.
1650 *
1651 * @param string $content
1652 * @return string
1653 */
1654 public static function sanitizeHtmlContent(string $content): string
1655 {
1656 return wp_kses($content, self::getAllowedHtmlTags(), self::getAllowedProtocols());
1657 }
1658
1659 /**
1660 * Sanitize bulk update request data
1661 *
1662 * @param array<string, mixed> $data
1663 * @return array<string, mixed>
1664 */
1665 public static function sanitizeBulkUpdateData(array $data): array
1666 {
1667 $sanitized = [];
1668
1669 // Sanitize column name (alphanumeric and underscore only)
1670 if (isset($data['column'])) {
1671 $sanitized['column'] = preg_replace('/[^a-zA-Z0-9_]/', '', $data['column']);
1672 }
1673
1674 // Sanitize value (depends on column)
1675 if (isset($data['value'])) {
1676 $column = $sanitized['column'] ?? '';
1677 if ($column !== '') {
1678 if (in_array($column, ['starred', 'status'], true)) {
1679 $sanitized['value'] = self::sanitizeEntryColumnValue($column, $data['value']);
1680 } else {
1681 $sanitized['value'] = self::sanitizeFormColumnValue($column, $data['value']);
1682 }
1683 } else {
1684 $sanitized['value'] = sanitize_text_field($data['value']);
1685 }
1686 }
1687
1688 // Sanitize IDs array
1689 if (isset($data['ids']) && is_array($data['ids'])) {
1690 $sanitized['ids'] = array_map('intval', $data['ids']);
1691 $sanitized['ids'] = array_filter($sanitized['ids'], fn($id) => $id > 0);
1692 }
1693
1694 return $sanitized;
1695 }
1696
1697 /**
1698 * Sanitize starred value (convert to boolean integer)
1699 *
1700 * @param mixed $value
1701 * @return int
1702 */
1703 public static function sanitizeStarredValue($value): int
1704 {
1705 return $value ? 1 : 0;
1706 }
1707
1708 /**
1709 * Sanitize entry status value
1710 *
1711 * @param string $status
1712 * @return string
1713 */
1714 public static function sanitizeEntryStatus(string $status): string
1715 {
1716 $allowedStatuses = ['read', 'unread'];
1717 $status = sanitize_text_field($status);
1718 return in_array($status, $allowedStatuses, true) ? $status : 'unread';
1719 }
1720
1721 /**
1722 * Sanitize form column value based on column type
1723 *
1724 * @param string $column
1725 * @param mixed $value
1726 * @return mixed
1727 */
1728 public static function sanitizeFormColumnValue(string $column, $value)
1729 {
1730 // Use FormRepository constants
1731 if ($column === 'starred') {
1732 return self::sanitizeStarredValue($value);
1733 }
1734 return sanitize_text_field($value);
1735 }
1736
1737 /**
1738 * Sanitize entry column value based on column type
1739 *
1740 * @param string $column
1741 * @param mixed $value
1742 * @return mixed
1743 */
1744 public static function sanitizeEntryColumnValue(string $column, $value)
1745 {
1746 // Use EntryRepository constants
1747 if ($column === 'starred') {
1748 return self::sanitizeStarredValue($value);
1749 }
1750 if ($column === 'status') {
1751 return self::sanitizeEntryStatus($value);
1752 }
1753 return sanitize_text_field($value);
1754 }
1755
1756 /**
1757 * Sanitize a space-separated string of CSS classes
1758 *
1759 * Uses sanitize_html_class for each individual class name,
1760 * which is more appropriate than sanitize_text_field for CSS classes.
1761 *
1762 * @param string $classString Space-separated CSS class names
1763 * @return string Sanitized space-separated CSS class names
1764 */
1765 public static function sanitizeCssClasses(string $classString): string
1766 {
1767 $classes = explode(' ', $classString);
1768 $sanitized = array_map('sanitize_html_class', $classes);
1769 $sanitized = array_filter($sanitized);
1770
1771 return implode(' ', $sanitized);
1772 }
1773
1774 /**
1775 * Sanitize a single field
1776 *
1777 * @param mixed[] $field
1778 * @return mixed[]
1779 */
1780 public static function sanitizeField(array $field): array
1781 {
1782 return [
1783 'id' => (int)($field['id'] ?? 0),
1784 'fieldIndex' => (int)($field['fieldIndex'] ?? 0),
1785 'type' => sanitize_text_field($field['type'] ?? ''),
1786 'label' => sanitize_text_field($field['label'] ?? ''),
1787 'placeholder' => sanitize_text_field($field['placeholder'] ?? ''),
1788 ];
1789 }
1790 }
1791