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

403 lines 12.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
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($data['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' => sanitize_email($data['sender'] ?? ''),
56 'receiver' => sanitize_email($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 ];
105 }, $fields);
106 }
107
108 /**
109 * Sanitize entry data
110 *
111 * @param mixed[] $data
112 *
113 * @return mixed[]
114 */
115 public static function sanitizeEntryData(array $data): array
116 {
117 return [
118 'id' => (int)($data['id'] ?? 0),
119 'formId' => (int)($data['formId'] ?? 0),
120 'userId' => (int)($data['userId'] ?? 0),
121 'status' => sanitize_text_field($data['status'] ?? ''),
122 'ipAddress' => sanitize_text_field($data['ipAddress'] ?? ''),
123 'userAgent' => sanitize_text_field($data['userAgent'] ?? ''),
124 'sourceURL' => sanitize_text_field($data['sourceURL'] ?? ''),
125 'dateCreated' => sanitize_text_field($data['dateCreated'] ?? ''),
126 'dateEdited' => sanitize_text_field($data['dateEdited'] ?? ''),
127 'starred' => (bool)($data['starred'] ?? false),
128 ];
129 }
130
131 /**
132 * Sanitize ID
133 *
134 * @param int $id
135 *
136 * @return int
137 *
138 * @throws InvalidArgumentException
139 */
140 public static function sanitizeId(int $id): int
141 {
142 if ($id <= 0) {
143 throw new InvalidArgumentException(
144 BackendStrings::getExceptionStrings()['invalid_id']
145 );
146 }
147
148 return $id;
149 }
150
151 /**
152 * Sanitize multiple IDs
153 *
154 * @param int[] $ids
155 *
156 * @return int[]
157 *
158 */
159 public static function sanitizeIds(array $ids): array
160 {
161 return array_map(function ($id) {
162 return (int)$id;
163 }, $ids);
164 }
165
166 /**
167 * Sanitize search parameters
168 *
169 * @param array<string, mixed> $params
170 *
171 * @return array<string, mixed>
172 * @throws InvalidArgumentException
173 */
174 public static function sanitizeSearchParams(array $params): array
175 {
176 $filters = [
177 'starred' => array_key_exists('starred', $params['filters'] ?? [])
178 ? (int)filter_var($params['filters']['starred'], FILTER_VALIDATE_BOOLEAN)
179 : null,
180 'published' => array_key_exists('published', $params['filters'] ?? [])
181 ? (int)filter_var($params['filters']['published'], FILTER_VALIDATE_BOOLEAN)
182 : null,
183 'status' => sanitize_text_field($params['filters']['read'] ?? ''),
184 ];
185 // Add formId to filters if present and valid
186 if (
187 isset($params['filters']['formId']) &&
188 is_numeric($params['filters']['formId']) &&
189 (int)$params['filters']['formId'] > 0
190 ) {
191 $filters['formId'] = self::sanitizeId((int)$params['filters']['formId']);
192 }
193 return [
194 'page' => max((int)($params['page'] ?? 1), 1),
195 'perPage' => max((int)($params['perPage'] ?? 10), 1),
196 'search' => sanitize_text_field($params['search'] ?? ''),
197 'orderBy' => sanitize_text_field($params['orderBy'] ?? 'id'),
198 'order' => strtolower(sanitize_text_field($params['order'] ?? 'asc')) === 'desc' ? 'desc' : 'asc',
199 'filters' => $filters,
200 'dateRange' => [
201 sanitize_text_field($params['dateRange'][0] ?? ''),
202 sanitize_text_field($params['dateRange'][1] ?? ''),
203 ],
204 'shouldGetCount' => (bool)($params['shouldGetCount'] ?? false),
205 'searchFieldValue' => filter_var($params['searchFieldValue'] ?? false, FILTER_VALIDATE_BOOLEAN),
206 ];
207 }
208
209 /**
210 * Sanitize form submission data based on field types and IDs
211 *
212 * @param mixed[] $data Form submission data
213 * @param mixed[] $formFields Form fields configuration
214 *
215 * @return mixed[] Sanitized submission data
216 */
217 public static function sanitizeFormSubmissionData(array $data, array $formFields): array
218 {
219 $sanitizedData = [];
220 list($fieldIdMap, $typeIndexMap) = self::buildFieldMaps($formFields);
221
222 foreach ($data['values'] as $key => $value) {
223 if (self::isInternalField($key)) {
224 $sanitizedData[$key] = $value;
225 continue;
226 }
227 if (isset($fieldIdMap[$key])) {
228 $sanitizedData[$key] = self::sanitizeByFieldType($fieldIdMap[$key], $value);
229 continue;
230 }
231 if (isset($typeIndexMap[$key])) {
232 $fieldId = $typeIndexMap[$key]['id'];
233 $fieldType = $typeIndexMap[$key]['type'];
234 $sanitizedData[$fieldId] = self::sanitizeByFieldType($fieldType, $value);
235 }
236 }
237
238 // Add form ID to sanitized data if present
239 if (isset($data['formId'])) {
240 $sanitizedData['formId'] = (int)$data['formId'];
241 }
242
243 return $sanitizedData;
244 }
245
246 /**
247 * Build field ID and type-index maps from form fields
248 *
249 * @param mixed[] $formFields Form fields configuration
250 * @return mixed[]
251 */
252 private static function buildFieldMaps(array $formFields): array
253 {
254 $fieldIdMap = [];
255 $typeIndexMap = [];
256 foreach ($formFields as $field) {
257 $fieldId = $field->getId() ?? null;
258 $fieldType = $field->getType() ?? null;
259 $fieldIndex = $field->getIndex() ?? null;
260 if ($fieldId !== null) {
261 $fieldIdMap[$fieldId] = $fieldType;
262 if ($fieldType !== null && $fieldIndex !== null) {
263 $typeIndexMap[$fieldType . '_' . $fieldIndex] = [
264 'id' => $fieldId,
265 'type' => $fieldType
266 ];
267 }
268 }
269 }
270 return [$fieldIdMap, $typeIndexMap];
271 }
272
273 /**
274 * Check if a key is an internal field
275 *
276 * @param string $key
277 * @return bool
278 */
279 private static function isInternalField(string $key): bool
280 {
281 return in_array($key, ['nonce', 'formId']);
282 }
283
284 /**
285 * Sanitize a value based on its field type
286 *
287 * @param string $fieldType The type of field
288 * @param mixed $value The value to sanitize
289 *
290 * @return mixed Sanitized value
291 */
292 private static function sanitizeByFieldType(string $fieldType, $value)
293 {
294 switch ($fieldType) {
295 case 'email':
296 return sanitize_email($value);
297
298 case 'number':
299 return is_numeric($value) ? intval($value) : 0;
300
301 case 'phone':
302 // Remove everything except digits, +, and spaces
303 return preg_replace('/[^\d\s+]/', '', $value);
304
305 case 'paragraph':
306 return sanitize_textarea_field($value);
307
308 case 'website':
309 return sanitize_url($value);
310
311 case 'text':
312 default:
313 // Default to text field sanitization
314 return self::sanitizeText($value);
315 }
316 }
317
318 /**
319 * Verify nonce with custom error handling
320 *
321 * @param string|null $nonce
322 *
323 * @throws ForbiddenException
324 */
325 public static function verifyNonce(?string $nonce): void
326 {
327 $nonce = sanitize_text_field($nonce);
328
329 if (!$nonce || !wp_verify_nonce($nonce, 'wp_rest')) {
330 throw new ForbiddenException(
331 BackendStrings::getExceptionStrings()['invalid_nonce']
332 );
333 }
334 }
335 /**
336 * Verify submission nonce with custom error handling
337 *
338 * @param string|null $nonce
339 * @param int $formId
340 *
341 * @throws ForbiddenException
342 */
343 public static function verifySubmissionNonce(?string $nonce, int $formId): void
344 {
345 $nonce = sanitize_text_field($nonce);
346
347 // Verify the form-specific nonce
348 if (!wp_verify_nonce($nonce, 'ivyformsFrontSubmissionNonce_' . $formId)) {
349 throw new ForbiddenException(
350 BackendStrings::getExceptionStrings()['invalid_nonce']
351 );
352 }
353 }
354
355 /**
356 * Sanitize text
357 *
358 * @param string|null $text
359 *
360 * @return string
361 */
362 public static function sanitizeText(?string $text): ?string
363 {
364 if (is_null($text)) {
365 return '';
366 }
367
368 return wp_kses_post(sanitize_text_field($text));
369 }
370
371 /**
372 * Sanitize settings fields
373 *
374 * @param mixed $value
375 *
376 * @return mixed Sanitized value
377 */
378 public static function sanitizeSettingsFields($value)
379 {
380 if (is_array($value)) {
381 return array_map([self::class, 'sanitizeSettingsFields'], $value);
382 }
383
384 if (is_object($value)) {
385 return (object)array_map([self::class, 'sanitizeSettingsFields'], (array)$value);
386 }
387
388 if (is_string($value)) {
389 return self::sanitizeText($value);
390 }
391
392 if (is_int($value)) {
393 return intval($value);
394 }
395
396 if (is_bool($value)) {
397 return filter_var($value, FILTER_VALIDATE_BOOLEAN);
398 }
399
400 return '';
401 }
402 }
403