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

508 lines 17.6 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 /**
4 * @copyright © TMS-Plugins. All rights reserved.
5 * @licence See COPYING.md for license details.
6 */
7
8 namespace IvyForms\Common\Sanitizer;
9
10 // phpcs:disable PSR1.Files.SideEffects
11 if (!defined('ABSPATH')) {
12 exit; // Exit if accessed directly
13 }
14
15 use IvyForms\Common\Exceptions\ForbiddenException;
16 use IvyForms\Common\Exceptions\InvalidArgumentException;
17 use IvyForms\Services\Field\FieldType;
18 use IvyForms\Services\Translations\BackendStrings;
19
20 class Sanitizer
21 {
22 /**
23 * Sanitize all form data
24 *
25 * @param mixed[] $data
26 *
27 * @return mixed[]
28 */
29 public static function sanitizeFormData(array $data): array
30 {
31 $fields = $data['fields'] ?? [];
32 return [
33 'id' => (int)($data['id'] ?? 0),
34 'name' => sanitize_text_field($data['name'] ?? ''),
35 'description' => sanitize_text_field($data['description'] ?? ''),
36 'showTitle' => (bool)($data['showTitle'] ?? false),
37 'published' => (bool)($data['published'] ?? false),
38 'showDescription' => (bool)($data['showDescription'] ?? false),
39 'storeEntries' => (bool)($data['storeEntries'] ?? false),
40 'fields' => self::sanitizeFields($fields)
41 ];
42 }
43 /**
44 * Sanitize all notification data
45 *
46 * @param mixed[] $data
47 *
48 * @return mixed[]
49 */
50 public static function sanitizeNotificationData(array $data): array
51 {
52 return [
53 'id' => (int)($data['id'] ?? 0),
54 'name' => sanitize_text_field($data['name'] ?? ''),
55 'sender' => self::sanitizeEmailOrPlaceholder($data['sender'] ?? ''),
56 'receiver' => self::sanitizeEmailOrPlaceholder($data['receiver'] ?? ''),
57 'enabled' => (bool)($data['enabled'] ?? true),
58 'subject' => sanitize_text_field($data['subject'] ?? ''),
59 'message' => sanitize_textarea_field($data['message'] ?? ''),
60 'smartLogic' => (bool)($data['smartLogic'] ?? false),
61 'formId' => (int)($data['formId'] ?? 0),
62 ];
63 }
64 /**
65 * Sanitize all confirmation data
66 *
67 * @param mixed[] $data
68 *
69 * @return mixed[]
70 */
71 public static function sanitizeConfirmationData(array $data): array
72 {
73 return [
74 'id' => (int)($data['id'] ?? 0),
75 'formId' => (int)($data['formId'] ?? 0),
76 'type' => sanitize_text_field($data['type'] ?? ''),
77 'enabled' => (bool)($data['enabled'] ?? true),
78 'showForm' => (bool)($data['showForm'] ?? false),
79 'message' => sanitize_textarea_field($data['message'] ?? ''),
80 'url' => sanitize_url(isset($data['url']) ? urldecode($data['url']) : ''),
81 'page' => sanitize_text_field($data['page'] ?? ''),
82 ];
83 }
84 /**
85 * Sanitize form fields
86 *
87 * @param mixed[] $fields
88 *
89 * @return mixed[]
90 */
91 private static function sanitizeFields(array $fields): array
92 {
93 return array_map(function ($field) {
94 $fieldType = FieldType::sanitize($field['type'] ?? '');
95 return [
96 'id' => (int)($field['id'] ?? 0),
97 'fieldIndex' => (int)($field['fieldIndex'] ?? 0),
98 'type' => sanitize_text_field($fieldType),
99 'label' => sanitize_text_field($field['label'] ?? ''),
100 'required' => (bool)($field['required'] ?? false),
101 'defaultValue' => sanitize_text_field($field['defaultValue'] ?? ''),
102 'placeholder' => sanitize_text_field($field['placeholder'] ?? ''),
103 'position' => (int)($field['position'] ?? 0),
104 'parentId' => isset($field['parentId']) ? max(0, (int)$field['parentId']) : null,
105 'hideLabel' => (bool)($field['hideLabel'] ?? false),
106 'readOnly' => (bool)($field['readOnly'] ?? false),
107 'description' => sanitize_text_field($field['description'] ?? ''),
108 'requiredMessage' => sanitize_text_field($field['requiredMessage'] ?? ''),
109 'cssClasses' => sanitize_text_field($field['cssClasses'] ?? ''),
110 'fieldOptions' => self::sanitizeFieldOptions($field['fieldOptions'] ?? []),
111 'shuffleOptions' => (bool)($field['shuffleOptions'] ?? false),
112 'showValues' => (bool)($field['showValues'] ?? false),
113 'enableSearch' => (bool)($field['enableSearch'] ?? false),
114 'rows' => (int)($field['rows'] ?? 0),
115 'confirmFieldEnabled' => (bool)($field['confirmFieldEnabled'] ?? false),
116 'confirmFieldLabel' => sanitize_text_field($field['confirmFieldLabel'] ?? ''),
117 'confirmFieldPlaceholder' => sanitize_text_field($field['confirmFieldPlaceholder'] ?? ''),
118 'confirmFieldHideLabel' => (bool)($field['confirmFieldHideLabel'] ?? false),
119 'phoneFormat' => self::sanitizePhoneFormat($field['phoneFormat'] ?? ''),
120 'phoneAutoDetect' => (bool)($field['phoneAutoDetect'] ?? false),
121 'minValue' => (float)($field['minValue'] ?? 0.0),
122 'maxValue' => (float)($field['maxValue'] ?? 0.0),
123 'step' => (float)($field['step'] ?? 1.0),
124 'numberFormat' => sanitize_text_field($field['numberFormat'] ?? ''),
125 'visible' => (bool)($field['visible'] ?? true),
126 'limitMaxLength' => (bool)($field['limitMaxLength'] ?? false),
127 'maxLength' => (int)($field['maxLength'] ?? 255),
128 'customValidationMessage' => sanitize_text_field($field['customValidationMessage'] ?? ''),
129 'labelPosition' => sanitize_text_field($field['labelPosition'] ?? 'default'),
130 'noDuplicates' => (bool)($field['noDuplicates'] ?? false),
131 ];
132 }, $fields);
133 }
134
135 /**
136 * Sanitize field options
137 *
138 * @param mixed[] $options
139 *
140 * @return mixed[]
141 */
142 private static function sanitizeFieldOptions(array $options): array
143 {
144 return array_map(function ($option) {
145 return [
146 'id' => (int)($option['id'] ?? 0),
147 'fieldId' => (int)($option['fieldId'] ?? 0),
148 'label' => sanitize_text_field($option['label'] ?? ''),
149 'value' => sanitize_text_field($option['value'] ?? ''),
150 'isDefault' => (bool)($option['isDefault'] ?? false),
151 'position' => (int)($option['position'] ?? 1),
152 ];
153 }, $options);
154 }
155
156 private static function sanitizePhoneFormat(string $format): string
157 {
158 $allowed = ['international','national','e164'];
159 $formatLower = strtolower($format);
160 return in_array($formatLower, $allowed, true) ? $formatLower : '';
161 }
162
163 /**
164 * Sanitize entry data
165 *
166 * @param mixed[] $data
167 *
168 * @return mixed[]
169 */
170 public static function sanitizeEntryData(array $data): array
171 {
172 return [
173 'id' => (int)($data['id'] ?? 0),
174 'formId' => (int)($data['formId'] ?? 0),
175 'userId' => (int)($data['userId'] ?? 0),
176 'status' => sanitize_text_field($data['status'] ?? ''),
177 'ipAddress' => sanitize_text_field($data['ipAddress'] ?? ''),
178 'userAgent' => sanitize_text_field($data['userAgent'] ?? ''),
179 'sourceURL' => sanitize_text_field($data['sourceURL'] ?? ''),
180 'dateCreated' => sanitize_text_field($data['dateCreated'] ?? ''),
181 'dateEdited' => sanitize_text_field($data['dateEdited'] ?? ''),
182 'starred' => (bool)($data['starred'] ?? false),
183 ];
184 }
185
186 /**
187 * Sanitize ID
188 *
189 * @param int $id
190 *
191 * @return int
192 *
193 * @throws InvalidArgumentException
194 */
195 public static function sanitizeId(int $id): int
196 {
197 if ($id <= 0) {
198 throw new InvalidArgumentException(
199 BackendStrings::getExceptionStrings()['invalid_id']
200 );
201 }
202
203 return $id;
204 }
205
206 /**
207 * Sanitize multiple IDs
208 *
209 * @param int[] $ids
210 *
211 * @return int[]
212 *
213 */
214 public static function sanitizeIds(array $ids): array
215 {
216 return array_map(function ($id) {
217 return (int)$id;
218 }, $ids);
219 }
220
221 /**
222 * Sanitize search parameters
223 *
224 * @param array<string, mixed> $params
225 *
226 * @return array<string, mixed>
227 * @throws InvalidArgumentException
228 */
229 public static function sanitizeSearchParams(array $params): array
230 {
231 $filters = [
232 'starred' => array_key_exists('starred', $params['filters'] ?? [])
233 ? (int)filter_var($params['filters']['starred'], FILTER_VALIDATE_BOOLEAN)
234 : null,
235 'published' => array_key_exists('published', $params['filters'] ?? [])
236 ? (int)filter_var($params['filters']['published'], FILTER_VALIDATE_BOOLEAN)
237 : null,
238 'status' => sanitize_text_field($params['filters']['read'] ?? ''),
239 ];
240 // Add formId to filters if present and valid
241 if (
242 isset($params['filters']['formId']) &&
243 is_numeric($params['filters']['formId']) &&
244 (int)$params['filters']['formId'] > 0
245 ) {
246 $filters['formId'] = self::sanitizeId((int)$params['filters']['formId']);
247 }
248 return [
249 'page' => max((int)($params['page'] ?? 1), 1),
250 'perPage' => max((int)($params['perPage'] ?? 10), 1),
251 'search' => sanitize_text_field($params['search'] ?? ''),
252 'orderBy' => sanitize_text_field($params['orderBy'] ?? 'id'),
253 'order' => strtolower(sanitize_text_field($params['order'] ?? 'asc')) === 'desc' ? 'desc' : 'asc',
254 'filters' => $filters,
255 'dateRange' => [
256 sanitize_text_field($params['dateRange'][0] ?? ''),
257 sanitize_text_field($params['dateRange'][1] ?? ''),
258 ],
259 'shouldGetCount' => (bool)($params['shouldGetCount'] ?? false),
260 'searchFieldValue' => filter_var($params['searchFieldValue'] ?? false, FILTER_VALIDATE_BOOLEAN),
261 ];
262 }
263
264 /**
265 * Sanitize form submission data based on field types and IDs
266 *
267 * @param mixed[] $data Form submission data
268 * @param mixed[] $formFields Form fields configuration
269 *
270 * @return mixed[] Sanitized submission data
271 */
272 public static function sanitizeFormSubmissionData(array $data, array $formFields): array
273 {
274 $sanitizedData = [];
275 list($fieldIdMap, $typeIndexMap) = self::buildFieldMaps($formFields);
276
277 foreach ($data['values'] as $key => $value) {
278 if (self::isInternalField($key)) {
279 $sanitizedData[$key] = $value;
280 continue;
281 }
282 if (isset($fieldIdMap[$key])) {
283 $sanitizedData[$key] = self::sanitizeByFieldType($fieldIdMap[$key], $value);
284 continue;
285 }
286 if (isset($typeIndexMap[$key])) {
287 $fieldId = $typeIndexMap[$key]['id'];
288 $fieldType = $typeIndexMap[$key]['type'];
289 $sanitizedData[$fieldId] = self::sanitizeByFieldType($fieldType, $value);
290 }
291 }
292
293 // Add form ID to sanitized data if present
294 if (isset($data['formId'])) {
295 $sanitizedData['formId'] = (int)$data['formId'];
296 }
297
298 // Add post ID and referer if present (for placeholders)
299 if (isset($data['postId'])) {
300 $sanitizedData['postId'] = (int)($data['postId']);
301 }
302 if (isset($data['referer'])) {
303 $sanitizedData['referer'] = sanitize_text_field($data['referer']);
304 }
305
306 return $sanitizedData;
307 }
308
309 /**
310 * Build field ID and type-index maps from form fields
311 *
312 * @param mixed[] $formFields Form fields configuration
313 * @return mixed[]
314 */
315 private static function buildFieldMaps(array $formFields): array
316 {
317 $fieldIdMap = [];
318 $typeIndexMap = [];
319 foreach ($formFields as $field) {
320 $fieldId = $field->getId() ?? null;
321 $fieldType = $field->getType() ?? null;
322 $fieldIndex = $field->getIndex() ?? null;
323 if ($fieldId !== null) {
324 $fieldIdMap[$fieldId] = $fieldType;
325 if ($fieldType !== null && $fieldIndex !== null) {
326 $typeIndexMap[$fieldType . '_' . $fieldIndex] = [
327 'id' => $fieldId,
328 'type' => $fieldType
329 ];
330 }
331 }
332 }
333 return [$fieldIdMap, $typeIndexMap];
334 }
335
336 /**
337 * Check if a key is an internal field
338 *
339 * @param string $key
340 * @return bool
341 */
342 private static function isInternalField(string $key): bool
343 {
344 return in_array($key, ['nonce', 'formId']);
345 }
346
347 /**
348 * Sanitize a value based on its field type
349 *
350 * @param string $fieldType The type of field
351 * @param mixed $value The value to sanitize
352 *
353 * @return mixed Sanitized value
354 */
355 private static function sanitizeByFieldType(string $fieldType, $value)
356 {
357 // Recursively sanitize arrays/objects (parent-child fields)
358 if (is_array($value)) {
359 return array_map(function ($v) use ($fieldType) {
360 return self::sanitizeByFieldType($fieldType, $v);
361 }, $value);
362 }
363 if (is_object($value)) {
364 $sanitized = array_map(function ($v) use ($fieldType) {
365 return self::sanitizeByFieldType($fieldType, $v);
366 }, get_object_vars($value));
367 return (object)$sanitized;
368 }
369 switch ($fieldType) {
370 case 'email':
371 return sanitize_email($value);
372
373 case 'number':
374 // Preserve decimals
375 return is_numeric($value) ? (float)$value : 0.0;
376
377 case 'phone':
378 // Remove everything except digits, +, and spaces
379 return preg_replace('/[^\d\s+]/', '', $value);
380
381 case 'paragraph':
382 return sanitize_textarea_field($value);
383
384 case 'website':
385 return sanitize_url($value);
386
387 case 'text':
388 default:
389 // Default to text field sanitization
390 return self::sanitizeText($value);
391 }
392 }
393
394 /**
395 * Verify nonce with custom error handling
396 *
397 * @param string|null $nonce
398 *
399 * @throws ForbiddenException
400 */
401 public static function verifyNonce(?string $nonce): void
402 {
403 $nonce = sanitize_text_field($nonce);
404
405 if (!$nonce || !wp_verify_nonce($nonce, 'wp_rest')) {
406 throw new ForbiddenException(
407 BackendStrings::getExceptionStrings()['invalid_nonce']
408 );
409 }
410 }
411 /**
412 * Verify submission nonce with custom error handling
413 *
414 * @param string|null $nonce
415 * @param int $formId
416 *
417 * @throws ForbiddenException
418 */
419 public static function verifySubmissionNonce(?string $nonce, int $formId): void
420 {
421 $nonce = sanitize_text_field($nonce);
422
423 // Verify the form-specific nonce
424 if (!wp_verify_nonce($nonce, 'ivyformsFrontSubmissionNonce_' . $formId)) {
425 throw new ForbiddenException(
426 BackendStrings::getExceptionStrings()['invalid_nonce']
427 );
428 }
429 }
430
431 /**
432 * Sanitize text
433 *
434 * @param string|null $text
435 *
436 * @return string
437 */
438 public static function sanitizeText(?string $text): ?string
439 {
440 if (is_null($text)) {
441 return '';
442 }
443
444 return wp_kses_post(sanitize_text_field($text));
445 }
446
447 /**
448 * Sanitize settings fields
449 *
450 * @param mixed $value
451 *
452 * @return mixed Sanitized value
453 */
454 public static function sanitizeSettingsFields($value)
455 {
456 if (is_array($value)) {
457 return array_map([self::class, 'sanitizeSettingsFields'], $value);
458 }
459
460 if (is_object($value)) {
461 return (object)array_map([self::class, 'sanitizeSettingsFields'], (array)$value);
462 }
463
464 if (is_string($value)) {
465 return self::sanitizeText($value);
466 }
467
468 if (is_int($value)) {
469 return intval($value);
470 }
471
472 if (is_bool($value)) {
473 return filter_var($value, FILTER_VALIDATE_BOOLEAN);
474 }
475
476 return '';
477 }
478
479 /**
480 * Sanitize email or placeholder
481 *
482 * @param string $value
483 *
484 * @return string
485 */
486 public static function sanitizeEmailOrPlaceholder($value): string
487 {
488 $value = trim($value);
489 // Allow placeholders
490 if (preg_match('/^{{[^}]+}}$/', $value)) {
491 return $value;
492 }
493 // Otherwise, sanitize as email
494 return sanitize_email($value);
495 }
496
497 /**
498 * Validate email address.
499 *
500 * @param string $email
501 * @return bool
502 */
503 public static function isValidEmail(string $email): bool
504 {
505 return filter_var($email, FILTER_VALIDATE_EMAIL) !== false;
506 }
507 }
508