PluginProbe
The Innovative Form Builder – IvyForms / 1.5
The Innovative Form Builder – IvyForms v1.5
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 / Helpers / FormImportValidator.php

FormImportValidator.php in The Innovative Form Builder – IvyForms 1.5, at backend/src/Common/Helpers/FormImportValidator.php

398 lines 11.6 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 namespace IvyForms\Common\Helpers;
4
5 use IvyForms\Common\Exceptions\InvalidArgumentException;
6 use IvyForms\Common\Sanitizer\Sanitizer;
7 use IvyForms\Services\Translations\BackendStrings;
8
9 class FormImportValidator
10 {
11 /**
12 * Validate top-level import payload and return forms array.
13 *
14 * @param mixed $importData
15 *
16 * @return array<int, array<string, mixed>>
17 * @throws InvalidArgumentException
18 */
19 public static function validateImportPayload($importData): array
20 {
21 if (empty($importData)) {
22 throw new InvalidArgumentException(
23 BackendStrings::getExceptionStrings()['no_import_data_provided']
24 );
25 }
26
27 if (!isset($importData['forms']) || !is_array($importData['forms'])) {
28 throw new InvalidArgumentException(
29 BackendStrings::getExceptionStrings()['invalid_import_data_structure']
30 );
31 }
32
33 foreach ($importData['forms'] as $formData) {
34 self::validateSingleFormStructure($formData);
35 }
36
37 return $importData['forms'];
38 }
39
40 /**
41 * Build a map of field database ID to fieldIndex.
42 *
43 * @param array<int, array<string, mixed>> $fields
44 * @return array<int, int>
45 */
46 public static function buildFieldIdToIndexMap(array $fields): array
47 {
48 $idToIndexMap = [];
49
50 foreach ($fields as $field) {
51 if (isset($field['id']) && isset($field['fieldIndex'])) {
52 $idToIndexMap[(int)$field['id']] = (int)$field['fieldIndex'];
53 }
54 }
55
56 return $idToIndexMap;
57 }
58
59 /**
60 * Prepare form data for export from entity toArray payload.
61 *
62 * @param mixed $form
63 * @return array<string, mixed>
64 */
65 public static function prepareFormDataForExport($form): array
66 {
67 $formArray = $form->toArray();
68 unset(
69 $formArray['id'],
70 $formArray['author'],
71 $formArray['dateCreated'],
72 $formArray['dateEdited'],
73 $formArray['fields']
74 );
75
76 return $formArray;
77 }
78
79 /**
80 * Remove style settings from an exported form payload.
81 *
82 * @param array<string, mixed> $formArray
83 * @return array<string, mixed>
84 */
85 public static function stripStyleSettingsFromFormExport(array $formArray): array
86 {
87 unset($formArray['styleSettings']);
88
89 return $formArray;
90 }
91
92 /**
93 * Prepare fields data for export.
94 *
95 * @param array<int, array<string, mixed>> $fields
96 * @param array<int, int> $idToIndexMap
97 * @return array<int, array<string, mixed>>
98 */
99 public static function prepareFieldsDataForExport(array $fields, array $idToIndexMap): array
100 {
101 if (empty($fields)) {
102 return [];
103 }
104
105 $exportFields = [];
106 foreach ($fields as $fieldArray) {
107 $exportFields[] = self::prepareFieldForExport($fieldArray, $idToIndexMap);
108 }
109
110 return $exportFields;
111 }
112
113 /**
114 * Prepare a single field for export.
115 *
116 * @param array<string, mixed> $fieldArray
117 * @param array<int, int> $idToIndexMap
118 * @return array<string, mixed>
119 */
120 private static function prepareFieldForExport(array $fieldArray, array $idToIndexMap): array
121 {
122 $fieldArray = self::normalizeFieldData($fieldArray);
123 unset($fieldArray['id'], $fieldArray['formId'], $fieldArray['position'], $fieldArray['rows']);
124
125 $fieldArray['parentId'] = self::convertParentIdToFieldIndex($fieldArray, $idToIndexMap);
126 $fieldArray['fieldOptions'] = self::prepareFieldOptionsForExport($fieldArray);
127
128 return self::normalizeFieldData($fieldArray);
129 }
130
131 /**
132 * Normalize malformed top-level numeric field keys into a flat associative array.
133 *
134 * @param array<string|int, mixed> $fieldData
135 * @return array<string, mixed>
136 */
137 private static function normalizeFieldData(array $fieldData): array
138 {
139 $normalizedFieldData = [];
140
141 foreach ($fieldData as $key => $value) {
142 if (is_int($key)) {
143 if (!is_array($value)) {
144 continue;
145 }
146
147 foreach ($value as $nestedKey => $nestedValue) {
148 if (is_string($nestedKey)) {
149 $normalizedFieldData[$nestedKey] = $nestedValue;
150 }
151 }
152
153 continue;
154 }
155
156 $normalizedFieldData[$key] = $value;
157 }
158
159 return $normalizedFieldData;
160 }
161
162 /**
163 * Convert parent database ID to fieldIndex.
164 *
165 * @param array<string, mixed> $fieldArray
166 * @param array<int, int> $idToIndexMap
167 * @return int|null
168 */
169 private static function convertParentIdToFieldIndex(array $fieldArray, array $idToIndexMap): ?int
170 {
171 if (!array_key_exists('parentId', $fieldArray) || $fieldArray['parentId'] === null) {
172 return null;
173 }
174
175 return $idToIndexMap[(int)$fieldArray['parentId']] ?? null;
176 }
177
178 /**
179 * Prepare field options for export.
180 *
181 * @param array<string, mixed> $fieldArray
182 * @return array<int, array<string, mixed>>|null
183 */
184 private static function prepareFieldOptionsForExport(array $fieldArray): ?array
185 {
186 if (empty($fieldArray['fieldOptions'])) {
187 return null;
188 }
189
190 $exportFieldOptions = [];
191 foreach ($fieldArray['fieldOptions'] as $option) {
192 unset($option['id'], $option['fieldId']);
193 $exportFieldOptions[] = $option;
194 }
195
196 return $exportFieldOptions;
197 }
198
199 /**
200 * Prepare notifications data for export from entity toArray payload.
201 *
202 * @param array<int, mixed> $notifications
203 * @return array<int, array<string, mixed>>
204 */
205 public static function prepareNotificationsDataForExport(array $notifications): array
206 {
207 if (empty($notifications)) {
208 return [];
209 }
210
211 $exportNotifications = [];
212 foreach ($notifications as $notification) {
213 $notificationArray = $notification->toArray();
214 unset($notificationArray['id'], $notificationArray['formId']);
215 $exportNotifications[] = $notificationArray;
216 }
217
218 return $exportNotifications;
219 }
220
221 /**
222 * Prepare confirmation data for export from entity toArray payload.
223 *
224 * @param mixed $confirmation
225 * @return array<string, mixed>|null
226 */
227 public static function prepareConfirmationDataForExport($confirmation): ?array
228 {
229 if (!$confirmation) {
230 return null;
231 }
232
233 $confirmationArray = $confirmation->toArray();
234 unset($confirmationArray['id'], $confirmationArray['formId']);
235
236 return $confirmationArray;
237 }
238
239 /**
240 * Prepare form data payload for import.
241 *
242 * @param array<string, mixed> $formData
243 * @param string $author
244 * @param string $currentTime
245 * @param string $submitLabel
246 * @return array<string, mixed>
247 */
248 public static function prepareFormArrayForImport(
249 array $formData,
250 string $author,
251 string $currentTime,
252 string $submitLabel
253 ): array {
254 $formArray = $formData['form'] ?? [];
255 $formArray['id'] = 0;
256 $formArray['author'] = $author;
257 $formArray['dateCreated'] = $currentTime;
258 $formArray['dateEdited'] = $currentTime;
259
260 if (!isset($formArray['integrationSettings'])) {
261 $formArray['integrationSettings'] = '{}';
262 }
263
264 if (!isset($formArray['formActionButtons'])) {
265 $formArray['formActionButtons'] = [
266 'submitButtonSettings' => [
267 'label' => $submitLabel,
268 'position' => 'default'
269 ]
270 ];
271 }
272
273 if (array_key_exists('styleSettings', $formArray)) {
274 $sanitizedStyles = Sanitizer::sanitizeStyleSettings($formArray['styleSettings']);
275 if ($sanitizedStyles === null) {
276 unset($formArray['styleSettings']);
277 }
278 if ($sanitizedStyles !== null) {
279 $formArray['styleSettings'] = $sanitizedStyles;
280 }
281 }
282
283 return $formArray;
284 }
285
286 /**
287 * Normalize imported fields before persistence.
288 *
289 * @param array<int, array<string, mixed>> $fieldsData
290 * @return array<int, array<string, mixed>>
291 */
292 public static function normalizeFieldsForImport(array $fieldsData): array
293 {
294 return array_map(static fn(array $fieldData): array => self::normalizeFieldData($fieldData), $fieldsData);
295 }
296
297 /**
298 * Validate single form import structure.
299 *
300 * @param mixed $formData
301 *
302 * @return void
303 * @throws InvalidArgumentException
304 */
305 private static function validateSingleFormStructure($formData): void
306 {
307 self::assertArray(
308 $formData,
309 BackendStrings::getExceptionStrings()['invalid_import_data_structure']
310 );
311
312 self::assertRequiredArrayKey(
313 $formData,
314 'form',
315 BackendStrings::getExceptionStrings()['import_structure_form_missing'],
316 BackendStrings::getExceptionStrings()['import_structure_form_invalid']
317 );
318
319 self::assertRequiredArrayKey(
320 $formData,
321 'fields',
322 BackendStrings::getExceptionStrings()['import_structure_fields_missing'],
323 BackendStrings::getExceptionStrings()['import_structure_fields_invalid']
324 );
325
326 self::assertRequiredArrayKey(
327 $formData,
328 'notifications',
329 BackendStrings::getExceptionStrings()['import_structure_notifications_missing'],
330 BackendStrings::getExceptionStrings()['import_structure_notifications_invalid']
331 );
332
333 self::assertConfirmationStructure($formData);
334 }
335
336 /**
337 * Assert that a value is an array.
338 *
339 * @param mixed $value
340 * @param string $message
341 * @return void
342 * @throws InvalidArgumentException
343 */
344 private static function assertArray($value, string $message): void
345 {
346 if (!is_array($value)) {
347 throw new InvalidArgumentException($message);
348 }
349 }
350
351 /**
352 * Assert that a key exists and its value is an array.
353 *
354 * @param array<string, mixed> $payload
355 * @param string $key
356 * @param string $missingMessage
357 * @param string $invalidMessage
358 * @return void
359 * @throws InvalidArgumentException
360 */
361 private static function assertRequiredArrayKey(
362 array $payload,
363 string $key,
364 string $missingMessage,
365 string $invalidMessage
366 ): void {
367 if (!array_key_exists($key, $payload)) {
368 throw new InvalidArgumentException($missingMessage);
369 }
370
371 if (!is_array($payload[$key])) {
372 throw new InvalidArgumentException($invalidMessage);
373 }
374 }
375
376 /**
377 * Assert confirmation section exists and has a supported type.
378 *
379 * @param array<string, mixed> $payload
380 * @return void
381 * @throws InvalidArgumentException
382 */
383 private static function assertConfirmationStructure(array $payload): void
384 {
385 if (!array_key_exists('confirmation', $payload)) {
386 throw new InvalidArgumentException(
387 BackendStrings::getExceptionStrings()['import_structure_confirmation_missing']
388 );
389 }
390
391 if (!is_array($payload['confirmation']) && !is_object($payload['confirmation'])) {
392 throw new InvalidArgumentException(
393 BackendStrings::getExceptionStrings()['import_structure_confirmation_invalid']
394 );
395 }
396 }
397 }
398