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