PluginProbe
Fluent Forms – Customizable Contact Forms, Survey, Quiz, & Conversational Form Builder / 6.2.15
Fluent Forms – Customizable Contact Forms, Survey, Quiz, & Conversational Form Builder v6.2.15
6.2.15 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 All 197 releases
fluentform / app / Services / Form / Updater.php

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

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