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
← All changes | app/Services/Form/Updater.php +206 -36 6.2.2 → 6.2.15 View file →
@@ -2,11 +2,14 @@
2 2
3 3 namespace FluentForm\App\Services\Form;
4 4
5 5 use Exception;
6 +use FluentForm\App\Helpers\Helper;
6 7 use FluentForm\App\Models\Form;
7 -use FluentForm\App\Helpers\Helper;
8 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;
9 12 use FluentForm\Framework\Support\Arr;
10 13 use FluentForm\App\Modules\Form\FormFieldsParser;
11 14
12 15 class Updater
@@ -12,13 +15,13 @@
12 15 class Updater
13 16 {
14 17 public function update($attributes = [])
15 18 {
16 - $formId = (int)Arr::get($attributes, 'form_id');
19 + $formId = (int) Arr::get($attributes, 'form_id');
17 20 $formFields = Arr::get($attributes, 'formFields');
18 21 $status = sanitize_text_field(Arr::get($attributes, 'status', 'published'));
19 22 $title = sanitize_text_field(Arr::get($attributes, 'title'));
20 -
23 +
21 24 $this->validate([
22 25 'title' => $title,
23 26 'formFields' => $formFields,
24 27 ]);
@@ -39,9 +42,9 @@
39 42 $formFields = apply_filters_deprecated(
40 43 'fluentform_form_fields_update',
41 44 [
42 45 $formFields,
43 - $formId
46 + $formId,
44 47 ],
45 48 FLUENTFORM_FRAMEWORK_UPGRADE,
46 49 'fluentform/form_fields_update',
47 50 'Use fluentform/form_fields_update instead of fluentform_form_fields_update.'
@@ -46,16 +49,17 @@
46 49 'fluentform/form_fields_update',
47 50 'Use fluentform/form_fields_update instead of fluentform_form_fields_update.'
48 51 );
49 52 $formFields = apply_filters('fluentform/form_fields_update', $formFields, $formId);
50 - $formFields = $this->sanitizeFields($formFields);
53 + $formFields = $this->sanitizeFields($formFields, $form->form_fields);
51 54 $data['form_fields'] = $formFields;
52 55 /**
53 56 * Fires before a Form is updated.
57 + *
54 58 * @since 5.2.1
55 59 */
56 60 do_action('fluentform/before_updating_form', $form, $data);
57 -
61 +
58 62 $form->fill($data);
59 63
60 64 if (FormFieldsParser::hasPaymentFields($form)) {
61 65 $data['has_payment'] = 1;
@@ -63,9 +67,9 @@
63 67 $data['has_payment'] = 0;
64 68 }
65 69
66 70 $this->updatePrimaryEmail($form);
67 -
71 +
68 72 }
69 73
70 74 $form->fill($data)->save();
71 75
@@ -83,8 +87,22 @@
83 87 throw new Exception(
84 88 sprintf('Name attribute %s has duplicate value.', esc_html($duplicateString))
85 89 );
86 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 + }
87 105 }
88 106
89 107 if (!$attributes['title']) {
90 108 throw new Exception('The title field is required.');
@@ -90,17 +108,25 @@
90 108 throw new Exception('The title field is required.');
91 109 }
92 110 }
93 111
94 - private function sanitizeFields($formFields)
112 + private function sanitizeFields($formFields, $existingFormFields)
95 113 {
96 114 if (fluentformCanUnfilteredHTML()) {
97 115 return $formFields;
98 116 }
99 -
117 +
100 118 $fieldsArray = json_decode($formFields, true);
119 + $existingFieldsArray = json_decode($existingFormFields, true);
120 + $existingFields = Arr::get($existingFieldsArray, 'fields', []);
101 121
102 122 if (isset($fieldsArray['submitButton'])) {
123 + if (!empty($fieldsArray['submitButton']['attributes'])) {
124 + $fieldsArray['submitButton']['attributes'] = $this->dropEventHandlerAttributeKeys(
125 + $fieldsArray['submitButton']['attributes']
126 + );
127 + }
128 +
103 129 $fieldsArray['submitButton']['settings']['button_ui']['text'] = fluentform_sanitize_html(
104 130 $fieldsArray['submitButton']['settings']['button_ui']['text']
105 131 );
106 132
@@ -110,8 +136,9 @@
110 136 );
111 137 }
112 138 }
113 139 $fieldsArray['fields'] = $this->sanitizeFieldMaps($fieldsArray['fields']);
140 + $fieldsArray['fields'] = DateConfigPolicy::preserveStored($fieldsArray['fields'], $existingFields);
114 141 $fieldsArray['fields'] = $this->sanitizeCustomSubmit($fieldsArray['fields']);
115 142 if ($stepsWrapper = Arr::get($fieldsArray, 'stepsWrapper')) {
116 143 $fieldsArray['stepsWrapper'] = $this->sanitizeStepsWrapper($stepsWrapper);
117 144 }
@@ -125,17 +152,18 @@
125 152 return $fields;
126 153 }
127 154
128 155 $attributesMap = [
129 - 'name' => 'sanitize_key',
130 - 'value' => 'sanitize_textarea_field',
131 - 'id' => 'sanitize_key',
132 - 'class' => 'sanitize_text_field',
133 - 'placeholder' => 'sanitize_text_field',
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'],
134 162 ];
135 163
136 164 $attributesKeys = array_keys($attributesMap);
137 -
165 +
138 166 $settingsMap = [
139 167 'container_class' => 'sanitize_text_field',
140 168 'label' => 'fluentform_sanitize_html',
141 169 'tnc_html' => 'fluentform_sanitize_html',
@@ -143,8 +171,13 @@
143 171 'help_message' => 'wp_kses_post',
144 172 'admin_field_label' => 'sanitize_text_field',
145 173 'prefix_label' => 'sanitize_text_field',
146 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'],
147 180 'unique_validation_message' => 'sanitize_text_field',
148 181 'advanced_options' => 'fluentform_options_sanitize',
149 182 'html_codes' => 'fluentform_sanitize_html',
150 183 'description' => 'fluentform_sanitize_html',
@@ -149,10 +182,24 @@
149 182 'html_codes' => 'fluentform_sanitize_html',
150 183 'description' => 'fluentform_sanitize_html',
151 184 'grid_columns' => [Helper::class, 'sanitizeArrayKeysAndValues'],
152 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',
153 201 ];
154 -
155 202
156 203 $settingsKeys = array_keys($settingsMap);
157 204
158 205 $stylePrefMap = [
@@ -160,12 +207,17 @@
160 207 'media' => 'sanitize_url',
161 208 'alt_text' => 'sanitize_text_field',
162 209 ];
163 210 $stylePrefKeys = array_keys($stylePrefMap);
164 -
211 +
165 212 foreach ($fields as $fieldIndex => &$field) {
166 213 $element = Arr::get($field, 'element');
167 -
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 +
168 220 if ('container' == $element) {
169 221 $columns = $field['columns'];
170 222 foreach ($columns as $columnIndex => $column) {
171 223 $fields[$fieldIndex]['columns'][$columnIndex]['fields'] = $this->sanitizeFieldMaps($column['fields']);
@@ -219,11 +271,10 @@
219 271 $field['settings']['button_ui']['img_url'] = esc_url_raw($buttonUi['img_url']);
220 272 }
221 273 }
222 274 }
223 -
224 275
225 - if (!empty($field['attributes'])) {
276 + if (!empty($field['attributes']) && is_array($field['attributes'])) {
226 277 $attributes = array_filter(Arr::only($field['attributes'], $attributesKeys));
227 278
228 279 foreach ($attributes as $key => $value) {
229 280 $fields[$fieldIndex]['attributes'][$key] = call_user_func($attributesMap[$key], $value);
@@ -230,9 +281,9 @@
230 281 }
231 282 }
232 283
233 284 if (!empty($field['settings'])) {
234 - $settings = array_filter(Arr::only($field['settings'], array_values($settingsKeys)));
285 + $settings = Arr::only($field['settings'], array_values($settingsKeys));
235 286 foreach ($settings as $key => $value) {
236 287 $fields[$fieldIndex]['settings'][$key] = call_user_func($settingsMap[$key], $value);
237 288 }
238 289 }
@@ -242,9 +293,9 @@
242 293 if (!empty($field['fields'])) {
243 294 $fields[$fieldIndex]['fields'] = $this->sanitizeFieldMaps($field['fields']);
244 295 continue;
245 296 }
246 -
297 +
247 298 if (!empty($field['style_pref'])) {
248 299 $settings = array_filter(Arr::only($field['style_pref'], $stylePrefKeys));
249 300
250 301 foreach ($settings as $key => $value) {
@@ -250,9 +301,9 @@
250 301 foreach ($settings as $key => $value) {
251 302 $fields[$fieldIndex]['style_pref'][$key] = call_user_func($stylePrefMap[$key], $value);
252 303 }
253 304 }
254 -
305 +
255 306 $validationRules = Arr::get($field, 'settings.validation_rules');
256 307 if (!empty($validationRules)) {
257 308 foreach ($validationRules as $key => $rule) {
258 309 if (isset($rule['message'])) {
@@ -262,12 +313,84 @@
262 313 }
263 314 }
264 315 }
265 316 }
266 -
317 +
267 318 return $fields;
268 319 }
269 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 +
270 393 private function updatePrimaryEmail($form)
271 394 {
272 395 $emailInputs = FormFieldsParser::getElement($form, ['input_email'], ['element', 'attributes']);
273 396
@@ -279,9 +402,9 @@
279 402 }
280 403
281 404 FormMeta::persist($form->id, '_primary_email_field', $emailInputName);
282 405 }
283 -
406 +
284 407 private function sanitizeCustomSubmit($fields)
285 408 {
286 409 $customSubmitSanitizationMap = [
287 410 'hover_styles' => [
@@ -288,9 +411,9 @@
288 411 'backgroundColor' => [$this, 'sanitizeRgbColor'],
289 412 'borderColor' => [$this, 'sanitizeRgbColor'],
290 413 'color' => [$this, 'sanitizeRgbColor'],
291 414 'borderRadius' => 'sanitize_text_field',
292 - 'minWidth' => [$this, 'sanitizeMinWidth']
415 + 'minWidth' => [$this, 'sanitizeMinWidth'],
293 416 ],
294 417 'normal_styles' => [
295 418 'backgroundColor' => [$this, 'sanitizeRgbColor'],
296 419 'borderColor' => [$this, 'sanitizeRgbColor'],
@@ -295,9 +418,9 @@
295 418 'backgroundColor' => [$this, 'sanitizeRgbColor'],
296 419 'borderColor' => [$this, 'sanitizeRgbColor'],
297 420 'color' => [$this, 'sanitizeRgbColor'],
298 421 'borderRadius' => 'sanitize_text_field',
299 - 'minWidth' => [$this, 'sanitizeMinWidth']
422 + 'minWidth' => [$this, 'sanitizeMinWidth'],
300 423 ],
301 424 'button_ui' => [
302 425 'type' => 'sanitize_text_field',
303 426 'text' => 'fluentform_sanitize_html',
@@ -305,9 +428,9 @@
305 428 ],
306 429 ];
307 430 foreach ($fields as $fieldIndex => $field) {
308 431 $element = Arr::get($field, 'element');
309 -
432 +
310 433 if ('custom_submit_button' == $element) {
311 434 $styleAttr = ['hover_styles', 'normal_styles', 'button_ui'];
312 435 foreach ($styleAttr as $attr) {
313 436 if ($styleConfigs = Arr::get($field, 'settings.' . $attr)) {
@@ -318,10 +441,9 @@
318 441 }
319 442 }
320 443 }
321 444 }
322 - }
323 - elseif ('container' == $element) {
445 + } elseif ('container' == $element) {
324 446 $columns = $field['columns'];
325 447 foreach ($columns as $columnIndex => $column) {
326 448 $fields[$fieldIndex]['columns'][$columnIndex]['fields'] = $this->sanitizeCustomSubmit($column['fields']);
327 449 }
@@ -330,8 +452,27 @@
330 452 }
331 453 return $fields;
332 454 }
333 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 +
334 475 private function sanitizeStepsWrapper($stepWrapper)
335 476 {
336 477 $stepsSanitizationMap = [
337 478 'prev_btn' => [
@@ -343,9 +484,9 @@
343 484
344 485 foreach ($stepWrapper as $fieldIndex => $field) {
345 486 $element = Arr::get($field, 'element');
346 487
347 - if ($element === 'step_start' || $element === 'step_end') {
488 + if ('step_start' === $element || 'step_end' === $element) {
348 489 if (!empty($field['settings']['step_titles']) && is_array($field['settings']['step_titles'])) {
349 490 foreach ($field['settings']['step_titles'] as $index => $title) {
350 491 $field['settings']['step_titles'][$index] = fluentform_sanitize_html($title);
351 492 }
@@ -350,8 +491,17 @@
350 491 $field['settings']['step_titles'][$index] = fluentform_sanitize_html($title);
351 492 }
352 493 }
353 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 +
354 504 if (!empty($field['settings']['prev_btn']) && is_array($field['settings']['prev_btn'])) {
355 505 foreach ($field['settings']['prev_btn'] as $key => $value) {
356 506 if (isset($stepsSanitizationMap['prev_btn'][$key])) {
357 507 $sanitizeFunction = $stepsSanitizationMap['prev_btn'][$key];
@@ -358,9 +508,9 @@
358 508 $field['settings']['prev_btn'][$key] = $sanitizeFunction($value);
359 509 }
360 510 }
361 511 }
362 -
512 +
363 513 if (!empty($field['attributes']['class'])) {
364 514 $field['attributes']['class'] = sanitize_text_field($field['attributes']['class']);
365 515 }
366 516 if (!empty($field['attributes']['id'])) {
@@ -367,12 +517,16 @@
367 517 $field['attributes']['id'] = sanitize_text_field($field['attributes']['id']);
368 518 }
369 519 }
370 520
371 - if ($element === 'step_start' && isset($field['fields'])) {
521 + if ('step_start' === $element && isset($field['fields'])) {
372 522 $field['fields'] = $this->sanitizeStepsWrapper($field['fields']);
373 523 }
374 524
525 + if (!empty($field['attributes'])) {
526 + $field['attributes'] = $this->dropEventHandlerAttributeKeys($field['attributes']);
527 + }
528 +
375 529 $stepWrapper[$fieldIndex] = $field;
376 530 }
377 531
378 532 return $stepWrapper;
@@ -384,12 +538,28 @@
384 538 return $value;
385 539 }
386 540 return '';
387 541 }
388 -
389 - public function sanitizeRgbColor($value) {
390 - if (preg_match('/^rgba?\((\d{1,3}\s*,\s*){2,3}(0|1|0?\.\d+)\)$/', $value)) {
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)) {
391 560 return $value;
392 561 }
562 +
393 563 return '';
394 564 }
395 565 }