PluginProbe
The Innovative Form Builder – IvyForms / 1.0
The Innovative Form Builder – IvyForms v1.0
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.0, at backend/src/Common/Sanitizer/Sanitizer.php

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