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

WsFormImportConverter.php in The Innovative Form Builder – IvyForms 1.5.1, at backend/src/Common/Helpers/Import/WsFormImportConverter.php

331 lines 10.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\Import;
4
5 /**
6 * Convert a WS Form JSON export into the IvyForms import payload structure.
7 *
8 * Unsupported WS Form field types and platform-specific settings are skipped
9 * gracefully so the import never fails because of them.
10 */
11 class WsFormImportConverter
12 {
13 /**
14 * Map WS Form field types to IvyForms field types. Types absent from this
15 * map (submit, signature, captcha, etc.) are skipped during import.
16 *
17 * @var array<string, string>
18 */
19 private const TYPE_MAP = [
20 'text' => 'text',
21 'email' => 'email',
22 'tel' => 'phone',
23 'url' => 'website',
24 'number' => 'number',
25 'textarea' => 'textarea',
26 'select' => 'select',
27 'radio' => 'radio',
28 'checkbox' => 'checkbox',
29 'date' => 'date',
30 'datetime' => 'date',
31 'time' => 'time',
32 'range' => 'slider',
33 'rating' => 'rating',
34 'file' => 'file-upload',
35 'html' => 'html',
36 'message' => 'html',
37 ];
38
39 /**
40 * Field types that carry selectable options.
41 *
42 * @var array<int, string>
43 */
44 private const OPTION_TYPES = ['select', 'radio', 'checkbox', 'multi-select'];
45
46 /**
47 * Convert a raw WS Form export array into an IvyForms import payload.
48 *
49 * @param array<string, mixed> $rawData
50 * @return array<string, mixed>
51 */
52 public static function convert(array $rawData): array
53 {
54 $name = isset($rawData['label']) ? sanitize_text_field((string) $rawData['label']) : '';
55 $published = isset($rawData['status']) && $rawData['status'] === 'publish';
56
57 return [
58 'forms' => [
59 [
60 'form' => [
61 'name' => $name,
62 'published' => $published,
63 'showTitle' => true,
64 'showDescription' => false,
65 'storeEntries' => true,
66 ],
67 'fields' => self::extractFields($rawData),
68 'notifications' => [],
69 'confirmation' => self::extractConfirmation($rawData),
70 ],
71 ],
72 ];
73 }
74
75 /**
76 * Walk the groups/sections/fields tree and build IvyForms fields.
77 *
78 * @param array<string, mixed> $rawData
79 * @return array<int, array<string, mixed>>
80 */
81 private static function extractFields(array $rawData): array
82 {
83 $context = ['id' => 1, 'fieldIndex' => 1, 'position' => 1, 'rowIndex' => 0];
84 $wsFields = self::flattenFields($rawData);
85 $total = count($wsFields);
86 $fields = [];
87
88 for ($index = 0; $index < $total; $index++) {
89 // WS Form splits names across text fields; merge them back into a
90 // single IvyForms name field when the parts can be identified.
91 $nameRun = WsFormNameFieldExtractor::collectRun($wsFields, $index);
92 if ($nameRun !== null) {
93 WsFormNameFieldExtractor::append($nameRun, $fields, $context);
94 $index += count($nameRun) - 1;
95 continue;
96 }
97
98 self::appendField($wsFields[$index], $fields, $context);
99 }
100
101 return $fields;
102 }
103
104 /**
105 * Flatten the groups/sections/fields tree into an ordered list of fields.
106 *
107 * @param array<string, mixed> $rawData
108 * @return array<int, array<string, mixed>>
109 */
110 private static function flattenFields(array $rawData): array
111 {
112 $wsFields = [];
113
114 foreach (($rawData['groups'] ?? []) as $group) {
115 if (!is_array($group)) {
116 continue;
117 }
118 foreach (($group['sections'] ?? []) as $section) {
119 if (!is_array($section)) {
120 continue;
121 }
122 foreach (($section['fields'] ?? []) as $wsField) {
123 if (is_array($wsField)) {
124 $wsFields[] = $wsField;
125 }
126 }
127 }
128 }
129
130 return $wsFields;
131 }
132
133 /**
134 * Map a single WS Form field and append it to the running fields list.
135 *
136 * @param array<string, mixed> $wsField
137 * @param array<int, array<string, mixed>> $fields
138 * @param array<string, int> $context
139 * @return void
140 */
141 private static function appendField(array $wsField, array &$fields, array &$context): void
142 {
143 $wsType = isset($wsField['type']) ? (string) $wsField['type'] : '';
144 $ivyType = self::TYPE_MAP[$wsType] ?? null;
145 if ($ivyType === null) {
146 return;
147 }
148
149 $meta = is_array($wsField['meta'] ?? null) ? $wsField['meta'] : [];
150 $ivyType = self::resolveType($wsType, $ivyType, $meta);
151 $htmlContent = self::resolveHtmlContent($ivyType, $meta, $wsType);
152 if ($ivyType === 'html' && $htmlContent === null) {
153 return;
154 }
155
156 $field = self::buildFieldBase($wsField, $meta, $ivyType, $context);
157 if ($htmlContent !== null) {
158 $field['htmlContent'] = $htmlContent;
159 }
160
161 if (WsFormGdprFieldMapper::matches($wsType, $meta)) {
162 WsFormGdprFieldMapper::apply($field, $meta);
163 $fields[] = $field;
164 return;
165 }
166
167 self::attachOptions($field, $meta, $wsType, $ivyType);
168 $fields[] = $field;
169 }
170
171 /**
172 * Build the common IvyForms field properties from a WS Form field.
173 *
174 * @param array<string, mixed> $wsField
175 * @param array<string, mixed> $meta
176 * @param string $ivyType
177 * @param array<string, int> $context
178 * @return array<string, mixed>
179 */
180 private static function buildFieldBase(
181 array $wsField,
182 array $meta,
183 string $ivyType,
184 array &$context
185 ): array {
186 return [
187 'id' => $context['id']++,
188 'fieldIndex' => $context['fieldIndex']++,
189 'type' => $ivyType,
190 'label' => sanitize_text_field((string) ($wsField['label'] ?? '')),
191 'placeholder' => sanitize_text_field((string) ($meta['placeholder'] ?? '')),
192 'description' => sanitize_text_field((string) ($meta['help'] ?? '')),
193 'required' => (($meta['required'] ?? '') === 'on'),
194 'requiredMessage' => sanitize_text_field((string) ($meta['invalid_feedback'] ?? '')),
195 'defaultValue' => sanitize_text_field((string) ($meta['default_value'] ?? '')),
196 'hideLabel' => isset($meta['label_render']) && $meta['label_render'] !== 'on',
197 'parentId' => null,
198 'position' => $context['position']++,
199 'rowIndex' => $context['rowIndex']++,
200 'columnIndex' => 0,
201 'width' => 100,
202 ];
203 }
204
205 /**
206 * Attach selectable options when the mapped type supports them.
207 *
208 * @param array<string, mixed> $field
209 * @param array<string, mixed> $meta
210 * @param string $wsType
211 * @param string $ivyType
212 * @return void
213 */
214 private static function attachOptions(array &$field, array $meta, string $wsType, string $ivyType): void
215 {
216 if (!in_array($ivyType, self::OPTION_TYPES, true)) {
217 return;
218 }
219
220 $options = WsFormOptionsExtractor::extract($meta, $wsType);
221 if (!empty($options)) {
222 $field['fieldOptions'] = $options;
223 }
224 }
225
226 /**
227 * Resolve IvyForms type refinements (e.g. multi-select).
228 *
229 * @param string $wsType
230 * @param string $ivyType
231 * @param array<string, mixed> $meta
232 * @return string
233 */
234 private static function resolveType(string $wsType, string $ivyType, array $meta): string
235 {
236 if ($wsType === 'select' && ($meta['multiple'] ?? '') === 'on') {
237 return 'multi-select';
238 }
239
240 return $ivyType;
241 }
242
243 /**
244 * Resolve HTML content for html-mapped fields, or null when empty/skipped.
245 *
246 * @param string $ivyType
247 * @param array<string, mixed> $meta
248 * @param string $wsType
249 * @return string|null
250 */
251 private static function resolveHtmlContent(string $ivyType, array $meta, string $wsType): ?string
252 {
253 if ($ivyType !== 'html') {
254 return null;
255 }
256
257 $htmlContent = self::extractHtmlContent($meta, $wsType);
258
259 return $htmlContent !== '' ? $htmlContent : null;
260 }
261
262 /**
263 * Extract sanitized HTML content from WS Form field meta.
264 *
265 * HTML fields store content in `html_editor_content`; message fields use
266 * `text_editor`. Empty HTML fields are skipped (nothing useful to import).
267 *
268 * @param array<string, mixed> $meta
269 * @param string $wsType
270 * @return string
271 */
272 private static function extractHtmlContent(array $meta, string $wsType): string
273 {
274 $candidates = $wsType === 'message'
275 ? ['text_editor', 'html_editor_content', 'html_content']
276 : ['html_editor_content', 'html_content', 'text_editor'];
277
278 foreach ($candidates as $key) {
279 if (!empty($meta[$key]) && is_string($meta[$key])) {
280 return wp_kses_post($meta[$key]);
281 }
282 }
283
284 return '';
285 }
286
287 /**
288 * Build the IvyForms confirmation from a WS Form "Show Message" action.
289 *
290 * @param array<string, mixed> $rawData
291 * @return array<string, mixed>
292 */
293 private static function extractConfirmation(array $rawData): array
294 {
295 $actionGrid = $rawData['meta']['action'] ?? null;
296 if (!is_array($actionGrid)) {
297 return [];
298 }
299
300 foreach (($actionGrid['groups'] ?? []) as $actionGroup) {
301 foreach (($actionGroup['rows'] ?? []) as $row) {
302 $rowData = is_array($row['data'] ?? null) ? $row['data'] : [];
303 if (!isset($rowData[1])) {
304 continue;
305 }
306
307 $config = json_decode((string) $rowData[1], true);
308 if (!is_array($config) || ($config['id'] ?? '') !== 'message') {
309 continue;
310 }
311
312 $message = (string) ($config['meta']['action_message_message'] ?? '');
313 if ($message === '') {
314 continue;
315 }
316
317 return [
318 'type' => 'successMessage',
319 'enabled' => true,
320 'showForm' => false,
321 'message' => wp_kses_post($message),
322 'url' => '',
323 'page' => '',
324 ];
325 }
326 }
327
328 return [];
329 }
330 }
331