PluginProbe
Fluent Forms – Customizable Contact Forms, Survey, Quiz, & Conversational Form Builder / 6.2.14
Fluent Forms – Customizable Contact Forms, Survey, Quiz, & Conversational Form Builder v6.2.14
6.2.14 6.2.13 6.2.12 6.2.10 6.2.11 6.2.9 6.2.8 6.2.7 6.2.6 6.2.5 6.2.4 6.2.3 6.2.2 3.6.22 3.6.31 3.6.40 3.6.41 3.6.42 3.6.50 3.6.51 3.6.60 3.6.61 3.6.62 3.6.64 3.6.65 All 196 releases
fluentform / app / Services / Form / Updater.php

Updater.php in Fluent Forms – Customizable Contact Forms, Survey, Quiz, & Conversational Form Builder 6.2.14, at app/Services/Form/Updater.php

560 lines 21.3 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 namespace FluentForm\App\Services\Form;
4
5 use Exception;
6 use FluentForm\App\Helpers\Helper;
7 use FluentForm\App\Models\Form;
8 use FluentForm\App\Models\FormMeta;
9 use FluentForm\App\Services\FormBuilder\DateConfigPolicy;
10 use FluentForm\App\Services\FormBuilder\RatingIcon;
11 use FluentForm\Framework\Support\Arr;
12 use FluentForm\App\Modules\Form\FormFieldsParser;
13
14 class Updater
15 {
16 public function update($attributes = [])
17 {
18 $formId = (int) Arr::get($attributes, 'form_id');
19 $formFields = Arr::get($attributes, 'formFields');
20 $status = sanitize_text_field(Arr::get($attributes, 'status', 'published'));
21 $title = sanitize_text_field(Arr::get($attributes, 'title'));
22
23 $this->validate([
24 'title' => $title,
25 'formFields' => $formFields,
26 ]);
27
28 try {
29 $form = Form::findOrFail($formId);
30 } catch (Exception $e) {
31 throw new \Exception("The form couldn't be found.");
32 }
33
34 $data = [
35 'title' => $title,
36 'status' => $status,
37 'updated_at' => current_time('mysql'),
38 ];
39
40 if ($formFields) {
41 $formFields = apply_filters_deprecated(
42 'fluentform_form_fields_update',
43 [
44 $formFields,
45 $formId,
46 ],
47 FLUENTFORM_FRAMEWORK_UPGRADE,
48 'fluentform/form_fields_update',
49 'Use fluentform/form_fields_update instead of fluentform_form_fields_update.'
50 );
51 $formFields = apply_filters('fluentform/form_fields_update', $formFields, $formId);
52 $formFields = $this->sanitizeFields($formFields, $form->form_fields);
53 $data['form_fields'] = $formFields;
54 /**
55 * Fires before a Form is updated.
56 *
57 * @since 5.2.1
58 */
59 do_action('fluentform/before_updating_form', $form, $data);
60
61 $form->fill($data);
62
63 if (FormFieldsParser::hasPaymentFields($form)) {
64 $data['has_payment'] = 1;
65 } elseif ($form->has_payment) {
66 $data['has_payment'] = 0;
67 }
68
69 $this->updatePrimaryEmail($form);
70
71 }
72
73 $form->fill($data)->save();
74
75 return $form;
76 }
77
78 private function validate($attributes)
79 {
80 if ($attributes['formFields']) {
81 $duplicates = Helper::getDuplicateFieldNames($attributes['formFields']);
82
83 if ($duplicates) {
84 $duplicateString = implode(', ', $duplicates);
85
86 throw new Exception(
87 sprintf('Name attribute %s has duplicate value.', esc_html($duplicateString))
88 );
89 }
90
91 $duplicateRankingFields = Helper::getRankingFieldsWithDuplicateOptionValues($attributes['formFields']);
92
93 if ($duplicateRankingFields) {
94 $duplicateRankingFields = implode(', ', array_unique($duplicateRankingFields));
95
96 throw new Exception(
97 sprintf(
98 // translators: %s is the ranking field name(s) with duplicate option values.
99 esc_html__('Ranking field %s has duplicate option values. Please make each option value unique.', 'fluentform'),
100 esc_html($duplicateRankingFields)
101 )
102 );
103 }
104 }
105
106 if (!$attributes['title']) {
107 throw new Exception('The title field is required.');
108 }
109 }
110
111 private function sanitizeFields($formFields, $existingFormFields)
112 {
113 if (fluentformCanUnfilteredHTML()) {
114 return $formFields;
115 }
116
117 $fieldsArray = json_decode($formFields, true);
118 $existingFieldsArray = json_decode($existingFormFields, true);
119 $existingFields = Arr::get($existingFieldsArray, 'fields', []);
120
121 if (isset($fieldsArray['submitButton'])) {
122 if (!empty($fieldsArray['submitButton']['attributes'])) {
123 $fieldsArray['submitButton']['attributes'] = $this->dropEventHandlerAttributeKeys(
124 $fieldsArray['submitButton']['attributes']
125 );
126 }
127
128 $fieldsArray['submitButton']['settings']['button_ui']['text'] = fluentform_sanitize_html(
129 $fieldsArray['submitButton']['settings']['button_ui']['text']
130 );
131
132 if (!empty($fieldsArray['submitButton']['settings']['button_ui']['img_url'])) {
133 $fieldsArray['submitButton']['settings']['button_ui']['img_url'] = sanitize_url(
134 $fieldsArray['submitButton']['settings']['button_ui']['img_url']
135 );
136 }
137 }
138 $fieldsArray['fields'] = $this->sanitizeFieldMaps($fieldsArray['fields']);
139 $fieldsArray['fields'] = DateConfigPolicy::preserveStored($fieldsArray['fields'], $existingFields);
140 $fieldsArray['fields'] = $this->sanitizeCustomSubmit($fieldsArray['fields']);
141 if ($stepsWrapper = Arr::get($fieldsArray, 'stepsWrapper')) {
142 $fieldsArray['stepsWrapper'] = $this->sanitizeStepsWrapper($stepsWrapper);
143 }
144
145 return json_encode($fieldsArray);
146 }
147
148 private function sanitizeFieldMaps($fields)
149 {
150 if (!is_array($fields)) {
151 return $fields;
152 }
153
154 $attributesMap = [
155 'name' => 'sanitize_key',
156 'value' => 'sanitize_textarea_field',
157 'id' => 'sanitize_key',
158 'class' => 'sanitize_text_field',
159 'placeholder' => 'sanitize_text_field',
160 ];
161
162 $attributesKeys = array_keys($attributesMap);
163
164 $settingsMap = [
165 'container_class' => 'sanitize_text_field',
166 'label' => 'fluentform_sanitize_html',
167 'tnc_html' => 'fluentform_sanitize_html',
168 'label_placement' => 'sanitize_text_field',
169 'help_message' => 'wp_kses_post',
170 'admin_field_label' => 'sanitize_text_field',
171 'prefix_label' => 'sanitize_text_field',
172 'suffix_label' => 'sanitize_text_field',
173 'icon_source' => 'sanitize_key',
174 'icon_type' => 'sanitize_key',
175 'custom_icon_svg' => [RatingIcon::class, 'sanitizeCustomSvg'],
176 'inactive_color' => [RatingIcon::class, 'sanitizeColor'],
177 'active_color' => [RatingIcon::class, 'sanitizeColor'],
178 'unique_validation_message' => 'sanitize_text_field',
179 'advanced_options' => 'fluentform_options_sanitize',
180 'html_codes' => 'fluentform_sanitize_html',
181 'description' => 'fluentform_sanitize_html',
182 'grid_columns' => [Helper::class, 'sanitizeArrayKeysAndValues'],
183 'grid_rows' => [Helper::class, 'sanitizeArrayKeysAndValues'],
184 'max_repeat_field' => [$this, 'sanitizeRepeatLimit'],
185 'display_mode' => [$this, 'sanitizeDisplayMode'],
186 'display_type' => [$this, 'sanitizeClassSetting'],
187 'pricing_options' => [$this, 'sanitizePricingOptionImages'],
188 'subscription_options' => [$this, 'sanitizeSubscriptionOptions'],
189 'enable_crop' => 'sanitize_text_field',
190 'crop_mode' => 'sanitize_text_field',
191 'crop_ratio' => 'sanitize_text_field',
192 'crop_width' => 'absint',
193 'crop_height' => 'absint',
194 'enforce_image_dimensions' => 'sanitize_text_field',
195 ];
196
197 $settingsKeys = array_keys($settingsMap);
198
199 $stylePrefMap = [
200 'layout' => 'sanitize_key',
201 'media' => 'sanitize_url',
202 'alt_text' => 'sanitize_text_field',
203 ];
204 $stylePrefKeys = array_keys($stylePrefMap);
205
206 foreach ($fields as $fieldIndex => &$field) {
207 $element = Arr::get($field, 'element');
208
209 // Must stay above the element branching: containers return early yet still render their attributes.
210 if (!empty($field['attributes'])) {
211 $fields[$fieldIndex]['attributes'] = $this->dropEventHandlerAttributeKeys($field['attributes']);
212 }
213
214 if ('container' == $element) {
215 $columns = $field['columns'];
216 foreach ($columns as $columnIndex => $column) {
217 $fields[$fieldIndex]['columns'][$columnIndex]['fields'] = $this->sanitizeFieldMaps($column['fields']);
218 }
219 continue;
220 }
221
222 if ('welcome_screen' == $element) {
223 if ($value = Arr::get($field, 'settings.button_ui.text')) {
224 $field['settings']['button_ui']['text'] = fluentform_sanitize_html($value);
225 }
226 }
227
228 if ('form_step' == $element) {
229 foreach (['next_btn', 'prev_btn'] as $buttonKey) {
230 $buttonSettings = Arr::get($field, 'settings.' . $buttonKey);
231 if (!is_array($buttonSettings)) {
232 continue;
233 }
234
235 if (isset($buttonSettings['type'])) {
236 $field['settings'][$buttonKey]['type'] = sanitize_text_field($buttonSettings['type']);
237 }
238
239 if (isset($buttonSettings['text'])) {
240 $field['settings'][$buttonKey]['text'] = fluentform_sanitize_html($buttonSettings['text']);
241 }
242
243 if (isset($buttonSettings['img_url'])) {
244 $field['settings'][$buttonKey]['img_url'] = esc_url_raw($buttonSettings['img_url']);
245 }
246
247 if (isset($buttonSettings['img_alt'])) {
248 $field['settings'][$buttonKey]['img_alt'] = sanitize_text_field($buttonSettings['img_alt']);
249 }
250 }
251 }
252
253 if ('save_progress_button' == $element) {
254 $buttonUi = Arr::get($field, 'settings.button_ui');
255 if (is_array($buttonUi)) {
256 if (isset($buttonUi['type'])) {
257 $field['settings']['button_ui']['type'] = sanitize_text_field($buttonUi['type']);
258 }
259
260 if (isset($buttonUi['text'])) {
261 $field['settings']['button_ui']['text'] = fluentform_sanitize_html($buttonUi['text']);
262 }
263
264 if (isset($buttonUi['img_url'])) {
265 $field['settings']['button_ui']['img_url'] = esc_url_raw($buttonUi['img_url']);
266 }
267 }
268 }
269
270 if (!empty($field['attributes']) && is_array($field['attributes'])) {
271 $attributes = array_filter(Arr::only($field['attributes'], $attributesKeys));
272
273 foreach ($attributes as $key => $value) {
274 $fields[$fieldIndex]['attributes'][$key] = call_user_func($attributesMap[$key], $value);
275 }
276 }
277
278 if (!empty($field['settings'])) {
279 $settings = Arr::only($field['settings'], array_values($settingsKeys));
280 foreach ($settings as $key => $value) {
281 $fields[$fieldIndex]['settings'][$key] = call_user_func($settingsMap[$key], $value);
282 }
283 }
284 /*
285 * Handle Name or address fields
286 */
287 if (!empty($field['fields'])) {
288 $fields[$fieldIndex]['fields'] = $this->sanitizeFieldMaps($field['fields']);
289 continue;
290 }
291
292 if (!empty($field['style_pref'])) {
293 $settings = array_filter(Arr::only($field['style_pref'], $stylePrefKeys));
294
295 foreach ($settings as $key => $value) {
296 $fields[$fieldIndex]['style_pref'][$key] = call_user_func($stylePrefMap[$key], $value);
297 }
298 }
299
300 $validationRules = Arr::get($field, 'settings.validation_rules');
301 if (!empty($validationRules)) {
302 foreach ($validationRules as $key => $rule) {
303 if (isset($rule['message'])) {
304 $message = $rule['message'];
305 $field['settings']['validation_rules'][$key]['message'] = wp_kses_post($message);
306 continue;
307 }
308 }
309 }
310 }
311
312 return $fields;
313 }
314
315 private function sanitizeRepeatLimit($value)
316 {
317 if (!is_scalar($value) || '' === trim((string) $value)) {
318 return '';
319 }
320
321 return absint($value);
322 }
323
324 private function sanitizeDisplayMode($value)
325 {
326 $value = is_scalar($value) ? sanitize_key((string) $value) : '';
327
328 return in_array($value, ['accordion', 'tabs'], true) ? $value : 'accordion';
329 }
330
331 private function sanitizeClassSetting($value)
332 {
333 return is_scalar($value) ? sanitize_html_class((string) $value) : '';
334 }
335
336 private function sanitizePricingOptionImages($options)
337 {
338 if (!is_array($options)) {
339 return [];
340 }
341
342 foreach ($options as &$option) {
343 if (!is_array($option)) {
344 continue;
345 }
346
347 if (array_key_exists('label', $option)) {
348 $label = $option['label'];
349 $option['label'] = is_scalar($label) ? fluentform_sanitize_html((string) $label) : '';
350 }
351
352 if (array_key_exists('image', $option)) {
353 $image = $option['image'];
354 $option['image'] = is_scalar($image) ? esc_url_raw((string) $image) : '';
355 }
356 }
357 unset($option);
358
359 return $options;
360 }
361
362 private function sanitizeSubscriptionOptions($options)
363 {
364 if (!is_array($options)) {
365 return [];
366 }
367
368 foreach ($options as &$option) {
369 if (!is_array($option)) {
370 continue;
371 }
372
373 foreach (['name', 'user_input_label'] as $labelKey) {
374 if (!array_key_exists($labelKey, $option)) {
375 continue;
376 }
377
378 $label = $option[$labelKey];
379 $option[$labelKey] = is_scalar($label) ? fluentform_sanitize_html((string) $label) : '';
380 }
381 }
382 unset($option);
383
384 return $options;
385 }
386
387 private function updatePrimaryEmail($form)
388 {
389 $emailInputs = FormFieldsParser::getElement($form, ['input_email'], ['element', 'attributes']);
390
391 if ($emailInputs) {
392 $emailInput = array_shift($emailInputs);
393 $emailInputName = Arr::get($emailInput, 'attributes.name');
394 } else {
395 $emailInputName = '';
396 }
397
398 FormMeta::persist($form->id, '_primary_email_field', $emailInputName);
399 }
400
401 private function sanitizeCustomSubmit($fields)
402 {
403 $customSubmitSanitizationMap = [
404 'hover_styles' => [
405 'backgroundColor' => [$this, 'sanitizeRgbColor'],
406 'borderColor' => [$this, 'sanitizeRgbColor'],
407 'color' => [$this, 'sanitizeRgbColor'],
408 'borderRadius' => 'sanitize_text_field',
409 'minWidth' => [$this, 'sanitizeMinWidth'],
410 ],
411 'normal_styles' => [
412 'backgroundColor' => [$this, 'sanitizeRgbColor'],
413 'borderColor' => [$this, 'sanitizeRgbColor'],
414 'color' => [$this, 'sanitizeRgbColor'],
415 'borderRadius' => 'sanitize_text_field',
416 'minWidth' => [$this, 'sanitizeMinWidth'],
417 ],
418 'button_ui' => [
419 'type' => 'sanitize_text_field',
420 'text' => 'fluentform_sanitize_html',
421 'img_url' => 'esc_url_raw',
422 ],
423 ];
424 foreach ($fields as $fieldIndex => $field) {
425 $element = Arr::get($field, 'element');
426
427 if ('custom_submit_button' == $element) {
428 $styleAttr = ['hover_styles', 'normal_styles', 'button_ui'];
429 foreach ($styleAttr as $attr) {
430 if ($styleConfigs = Arr::get($field, 'settings.' . $attr)) {
431 foreach ($styleConfigs as $key => $value) {
432 if (isset($customSubmitSanitizationMap[$attr][$key])) {
433 $sanitizeFunction = $customSubmitSanitizationMap[$attr][$key];
434 $fields[$fieldIndex]['settings'][$attr][$key] = $sanitizeFunction($value);
435 }
436 }
437 }
438 }
439 } elseif ('container' == $element) {
440 $columns = $field['columns'];
441 foreach ($columns as $columnIndex => $column) {
442 $fields[$fieldIndex]['columns'][$columnIndex]['fields'] = $this->sanitizeCustomSubmit($column['fields']);
443 }
444 return $fields;
445 }
446 }
447 return $fields;
448 }
449
450 /**
451 * An `on*` key renders as a live event handler, and every attribute map here is
452 * sanitized by value against keys it already knows — never by key.
453 */
454 private function dropEventHandlerAttributeKeys($attributes)
455 {
456 if (!is_array($attributes)) {
457 return $attributes;
458 }
459
460 foreach (array_keys($attributes) as $attributeKey) {
461 if (!Helper::isSafeAttributeKey($attributeKey)) {
462 unset($attributes[$attributeKey]);
463 }
464 }
465
466 return $attributes;
467 }
468
469 private function sanitizeStepsWrapper($stepWrapper)
470 {
471 $stepsSanitizationMap = [
472 'prev_btn' => [
473 'type' => 'sanitize_text_field',
474 'text' => 'fluentform_sanitize_html',
475 'img_url' => 'esc_url_raw',
476 ],
477 ];
478
479 foreach ($stepWrapper as $fieldIndex => $field) {
480 $element = Arr::get($field, 'element');
481
482 if ('step_start' === $element || 'step_end' === $element) {
483 if (!empty($field['settings']['step_titles']) && is_array($field['settings']['step_titles'])) {
484 foreach ($field['settings']['step_titles'] as $index => $title) {
485 $field['settings']['step_titles'][$index] = fluentform_sanitize_html($title);
486 }
487 }
488
489 if (isset($field['settings']['tabs_show_progress_bar'])) {
490 $field['settings']['tabs_show_progress_bar'] = sanitize_text_field($field['settings']['tabs_show_progress_bar']) === 'yes' ? 'yes' : 'no';
491 }
492
493 if (isset($field['settings']['progress_layout'])) {
494 $progressLayout = sanitize_text_field($field['settings']['progress_layout']);
495 $field['settings']['progress_layout'] = in_array($progressLayout, ['top', 'left'], true) ? $progressLayout : 'top';
496 }
497
498 if (!empty($field['settings']['prev_btn']) && is_array($field['settings']['prev_btn'])) {
499 foreach ($field['settings']['prev_btn'] as $key => $value) {
500 if (isset($stepsSanitizationMap['prev_btn'][$key])) {
501 $sanitizeFunction = $stepsSanitizationMap['prev_btn'][$key];
502 $field['settings']['prev_btn'][$key] = $sanitizeFunction($value);
503 }
504 }
505 }
506
507 if (!empty($field['attributes']['class'])) {
508 $field['attributes']['class'] = sanitize_text_field($field['attributes']['class']);
509 }
510 if (!empty($field['attributes']['id'])) {
511 $field['attributes']['id'] = sanitize_text_field($field['attributes']['id']);
512 }
513 }
514
515 if ('step_start' === $element && isset($field['fields'])) {
516 $field['fields'] = $this->sanitizeStepsWrapper($field['fields']);
517 }
518
519 if (!empty($field['attributes'])) {
520 $field['attributes'] = $this->dropEventHandlerAttributeKeys($field['attributes']);
521 }
522
523 $stepWrapper[$fieldIndex] = $field;
524 }
525
526 return $stepWrapper;
527 }
528
529 public function sanitizeMinWidth($value)
530 {
531 if (is_string($value) && preg_match('/^\d+%$/', $value)) {
532 return $value;
533 }
534 return '';
535 }
536
537 public function sanitizeRgbColor($value)
538 {
539 if (!is_string($value)) {
540 return '';
541 }
542
543 // rgb() takes three 0-255 channels; rgba() takes those plus an alpha.
544 // A single combined pattern cannot express that, and treating the last
545 // component as an alpha in both cases discarded every rgb() whose blue
546 // channel was not 0 or 1 - white included.
547 $channel = '\s*(?:25[0-5]|2[0-4][0-9]|1[0-9]{2}|[0-9]{1,2})\s*';
548 $alpha = '\s*(?:0|1|0?\.[0-9]+|1\.0+)\s*';
549
550 $rgb = '/^rgb\(' . $channel . ',' . $channel . ',' . $channel . '\)$/';
551 $rgba = '/^rgba\(' . $channel . ',' . $channel . ',' . $channel . ',' . $alpha . '\)$/';
552
553 if (preg_match($rgb, $value) || preg_match($rgba, $value)) {
554 return $value;
555 }
556
557 return '';
558 }
559 }
560