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 +130 -10 6.2.13 → 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
@@ -47,9 +49,9 @@
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.
55 57 *
@@ -106,9 +108,9 @@
106 108 throw new Exception('The title field is required.');
107 109 }
108 110 }
109 111
110 - private function sanitizeFields($formFields)
112 + private function sanitizeFields($formFields, $existingFormFields)
111 113 {
112 114 if (fluentformCanUnfilteredHTML()) {
113 115 return $formFields;
114 116 }
@@ -113,10 +115,18 @@
113 115 return $formFields;
114 116 }
115 117
116 118 $fieldsArray = json_decode($formFields, true);
119 + $existingFieldsArray = json_decode($existingFormFields, true);
120 + $existingFields = Arr::get($existingFieldsArray, 'fields', []);
117 121
118 122 if (isset($fieldsArray['submitButton'])) {
123 + if (!empty($fieldsArray['submitButton']['attributes'])) {
124 + $fieldsArray['submitButton']['attributes'] = $this->dropEventHandlerAttributeKeys(
125 + $fieldsArray['submitButton']['attributes']
126 + );
127 + }
128 +
119 129 $fieldsArray['submitButton']['settings']['button_ui']['text'] = fluentform_sanitize_html(
120 130 $fieldsArray['submitButton']['settings']['button_ui']['text']
121 131 );
122 132
@@ -126,8 +136,9 @@
126 136 );
127 137 }
128 138 }
129 139 $fieldsArray['fields'] = $this->sanitizeFieldMaps($fieldsArray['fields']);
140 + $fieldsArray['fields'] = DateConfigPolicy::preserveStored($fieldsArray['fields'], $existingFields);
130 141 $fieldsArray['fields'] = $this->sanitizeCustomSubmit($fieldsArray['fields']);
131 142 if ($stepsWrapper = Arr::get($fieldsArray, 'stepsWrapper')) {
132 143 $fieldsArray['stepsWrapper'] = $this->sanitizeStepsWrapper($stepsWrapper);
133 144 }
@@ -141,13 +152,14 @@
141 152 return $fields;
142 153 }
143 154
144 155 $attributesMap = [
145 - 'name' => 'sanitize_key',
146 - 'value' => 'sanitize_textarea_field',
147 - 'id' => 'sanitize_key',
148 - 'class' => 'sanitize_text_field',
149 - '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'],
150 162 ];
151 163
152 164 $attributesKeys = array_keys($attributesMap);
153 165
@@ -170,9 +182,13 @@
170 182 'html_codes' => 'fluentform_sanitize_html',
171 183 'description' => 'fluentform_sanitize_html',
172 184 'grid_columns' => [Helper::class, 'sanitizeArrayKeysAndValues'],
173 185 'grid_rows' => [Helper::class, 'sanitizeArrayKeysAndValues'],
174 - 'date_config' => 'fluentform_sanitize_json_object',
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'],
175 191 'enable_crop' => 'sanitize_text_field',
176 192 'crop_mode' => 'sanitize_text_field',
177 193 'crop_ratio' => 'sanitize_text_field',
178 194 'crop_width' => 'absint',
@@ -177,8 +193,12 @@
177 193 'crop_ratio' => 'sanitize_text_field',
178 194 'crop_width' => 'absint',
179 195 'crop_height' => 'absint',
180 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',
181 201 ];
182 202
183 203 $settingsKeys = array_keys($settingsMap);
184 204
@@ -191,8 +211,13 @@
191 211
192 212 foreach ($fields as $fieldIndex => &$field) {
193 213 $element = Arr::get($field, 'element');
194 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 +
195 220 if ('container' == $element) {
196 221 $columns = $field['columns'];
197 222 foreach ($columns as $columnIndex => $column) {
198 223 $fields[$fieldIndex]['columns'][$columnIndex]['fields'] = $this->sanitizeFieldMaps($column['fields']);
@@ -247,9 +272,9 @@
247 272 }
248 273 }
249 274 }
250 275
251 - if (!empty($field['attributes'])) {
276 + if (!empty($field['attributes']) && is_array($field['attributes'])) {
252 277 $attributes = array_filter(Arr::only($field['attributes'], $attributesKeys));
253 278
254 279 foreach ($attributes as $key => $value) {
255 280 $fields[$fieldIndex]['attributes'][$key] = call_user_func($attributesMap[$key], $value);
@@ -256,9 +281,9 @@
256 281 }
257 282 }
258 283
259 284 if (!empty($field['settings'])) {
260 - $settings = array_filter(Arr::only($field['settings'], array_values($settingsKeys)));
285 + $settings = Arr::only($field['settings'], array_values($settingsKeys));
261 286 foreach ($settings as $key => $value) {
262 287 $fields[$fieldIndex]['settings'][$key] = call_user_func($settingsMap[$key], $value);
263 288 }
264 289 }
@@ -292,8 +317,80 @@
292 317
293 318 return $fields;
294 319 }
295 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 +
296 393 private function updatePrimaryEmail($form)
297 394 {
298 395 $emailInputs = FormFieldsParser::getElement($form, ['input_email'], ['element', 'attributes']);
299 396
@@ -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' => [
@@ -403,8 +519,12 @@
403 519 }
404 520
405 521 if ('step_start' === $element && isset($field['fields'])) {
406 522 $field['fields'] = $this->sanitizeStepsWrapper($field['fields']);
523 + }
524 +
525 + if (!empty($field['attributes'])) {
526 + $field['attributes'] = $this->dropEventHandlerAttributeKeys($field['attributes']);
407 527 }
408 528
409 529 $stepWrapper[$fieldIndex] = $field;
410 530 }