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/Transfer/TransferService.php +149 -7 6.2.13 → 6.2.15 View file →
@@ -3,8 +3,9 @@
3 3 namespace FluentForm\App\Services\Transfer;
4 4
5 5 defined('ABSPATH') or die;
6 6
7 +use FluentForm\App\Services\FormBuilder\AutocompleteTokens;
7 8 use Exception;
8 9 use FluentForm\App\Helpers\Helper;
9 10 use FluentForm\App\Models\Form;
10 11 use FluentForm\App\Models\FormMeta;
@@ -13,8 +14,9 @@
13 14 use FluentForm\App\Modules\Acl\Acl;
14 15 use FluentForm\App\Modules\Form\FormDataParser;
15 16 use FluentForm\App\Modules\Form\FormFieldsParser;
16 17 use FluentForm\App\Services\FormBuilder\ShortCodeParser;
18 +use FluentForm\App\Services\FormBuilder\DateConfigPolicy;
17 19 use FluentForm\Framework\Foundation\App;
18 20 use FluentForm\Framework\Http\Request\File;
19 21 use FluentForm\Framework\Support\Arr;
20 22
@@ -46,14 +48,24 @@
46 48
47 49 private static function sanitizeJsonNode(&$node)
48 50 {
49 51 if (is_array($node)) {
50 - foreach ($node as &$value) {
52 + foreach ($node as $key => &$value) {
53 + if ('attributes' === $key) {
54 + $value = self::dropEventHandlerAttributeKeys($value);
55 + $value = self::sanitizeFieldAttributes($value);
56 + }
57 + $value = self::sanitizeAttributeControlSetting($key, $value);
51 58 self::sanitizeJsonNode($value);
52 59 }
53 60 unset($value);
54 61 } elseif (is_object($node)) {
55 62 foreach (get_object_vars($node) as $key => $value) {
63 + if ('attributes' === $key) {
64 + $value = self::dropEventHandlerAttributeKeys($value);
65 + $value = self::sanitizeFieldAttributes($value);
66 + }
67 + $value = self::sanitizeAttributeControlSetting($key, $value);
56 68 self::sanitizeJsonNode($value);
57 69 $node->{$key} = $value;
58 70 }
59 71 } elseif (is_string($node)) {
@@ -60,8 +72,110 @@
60 72 $node = wp_kses_post($node);
61 73 }
62 74 }
63 75
76 + // Scoped to a field's own attributes: sanitizeJsonNode() walks the whole meta
77 + // tree, so matching on key name alone would rewrite any unrelated property
78 + // that happens to be called autocomplete.
79 + private static function sanitizeFieldAttributes($attributes)
80 + {
81 + if (is_object($attributes) && property_exists($attributes, 'autocomplete')) {
82 + $attributes->autocomplete = AutocompleteTokens::sanitize($attributes->autocomplete);
83 + } elseif (is_array($attributes) && array_key_exists('autocomplete', $attributes)) {
84 + $attributes['autocomplete'] = AutocompleteTokens::sanitize($attributes['autocomplete']);
85 + }
86 +
87 + return $attributes;
88 + }
89 +
90 + private static function sanitizeAttributeControlSetting($key, $value)
91 + {
92 + if ('max_repeat_field' === $key) {
93 + return is_scalar($value) && '' !== trim((string) $value) ? absint($value) : '';
94 + }
95 +
96 + if ('display_mode' === $key) {
97 + $mode = is_scalar($value) ? sanitize_key((string) $value) : '';
98 + return in_array($mode, ['accordion', 'tabs'], true) ? $mode : 'accordion';
99 + }
100 +
101 + if ('display_type' === $key) {
102 + return is_scalar($value) ? sanitize_html_class((string) $value) : '';
103 + }
104 +
105 + if ('subscription_options' === $key && is_array($value)) {
106 + foreach ($value as &$option) {
107 + if (!is_array($option)) {
108 + continue;
109 + }
110 +
111 + foreach (['name', 'user_input_label'] as $labelKey) {
112 + if (!array_key_exists($labelKey, $option)) {
113 + continue;
114 + }
115 +
116 + $label = $option[$labelKey];
117 + $option[$labelKey] = is_scalar($label) ? fluentform_sanitize_html((string) $label) : '';
118 + }
119 + }
120 + unset($option);
121 +
122 + return $value;
123 + }
124 +
125 + if ('pricing_options' !== $key || !is_array($value)) {
126 + return $value;
127 + }
128 +
129 + foreach ($value as &$option) {
130 + if (!is_array($option)) {
131 + continue;
132 + }
133 +
134 + if (array_key_exists('label', $option)) {
135 + $label = $option['label'];
136 + $option['label'] = is_scalar($label) ? fluentform_sanitize_html((string) $label) : '';
137 + }
138 +
139 + if (array_key_exists('image', $option)) {
140 + $image = $option['image'];
141 + $option['image'] = is_scalar($image) ? esc_url_raw((string) $image) : '';
142 + }
143 + }
144 + unset($option);
145 +
146 + return $value;
147 + }
148 +
149 + /**
150 + * kses cleans string values only, so an `onfocus` KEY survives an import untouched.
151 + * Shares the Helper rule so the two write paths cannot drift apart.
152 + */
153 + private static function dropEventHandlerAttributeKeys($attributes)
154 + {
155 + if (!is_array($attributes) && !is_object($attributes)) {
156 + return $attributes;
157 + }
158 +
159 + $keys = is_object($attributes)
160 + ? array_keys(get_object_vars($attributes))
161 + : array_keys($attributes);
162 +
163 + foreach ($keys as $key) {
164 + if (Helper::isSafeAttributeKey($key)) {
165 + continue;
166 + }
167 +
168 + if (is_object($attributes)) {
169 + unset($attributes->{$key});
170 + } else {
171 + unset($attributes[$key]);
172 + }
173 + }
174 +
175 + return $attributes;
176 + }
177 +
64 178 public static function exportForms($formIds)
65 179 {
66 180 $result = Form::with(['formMeta'])
67 181 ->whereIn('id', $formIds)
@@ -88,10 +202,10 @@
88 202 die();
89 203 }
90 204
91 205 /**
92 - * Build the notice shown when imported custom JS/CSS was skipped because the
93 - * importer lacks unfiltered_html. Returns an empty string when nothing was skipped.
206 + * Build the notice shown when imported custom JS/CSS or executable date
207 + * configuration was skipped because the importer lacks unfiltered_html. Returns an empty string when nothing was skipped.
94 208 *
95 209 * @param int $skippedForms
96 210 * @param int $totalForms
97 211 * @return string
@@ -102,14 +216,14 @@
102 216 return '';
103 217 }
104 218
105 219 if ($totalForms < 2) {
106 - return __('Custom JS and CSS were not imported because your account cannot add custom code. Ask an administrator to add it.', 'fluentform');
220 + return __('Custom JS, CSS and advanced date configuration were not imported because your account cannot add custom code. Ask an administrator to add it.', 'fluentform');
107 221 }
108 222
109 223 return sprintf(
110 224 /* translators: 1: number of forms whose custom code was skipped, 2: total number of imported forms */
111 - __('Custom JS and CSS were not imported for %1$d of %2$d forms because your account cannot add custom code. Ask an administrator to add it.', 'fluentform'),
225 + __('Custom JS, CSS and advanced date configuration were not imported for %1$d of %2$d forms because your account cannot add custom code. Ask an administrator to add it.', 'fluentform'),
112 226 $skippedForms,
113 227 $totalForms
114 228 );
115 229 }
@@ -141,12 +255,17 @@
141 255 // stored them verbatim, so an importer without unfiltered_html could plant
142 256 // stored XSS (e.g. a field label of <img onerror=...>). Apply the same
143 257 // recursive HTML sanitizer used for imported meta values unless the importer
144 258 // may author raw HTML.
259 + $droppedDateConfigs = 0;
145 260 if (!fluentformCanUnfilteredHTML()) {
146 261 $decodedFields = json_decode($formFields, true);
147 262 if (is_array($decodedFields)) {
148 - static::sanitizeJsonNode($decodedFields);
263 + self::sanitizeJsonNode($decodedFields);
264 + $decodedFields['fields'] = DateConfigPolicy::dropExecutableConfigs(
265 + Arr::get($decodedFields, 'fields', []),
266 + $droppedDateConfigs
267 + );
149 268 $formFields = wp_json_encode($decodedFields) ?: $formFields;
150 269 }
151 270 }
152 271
@@ -173,9 +292,9 @@
173 292 'title' => $form['title'],
174 293 'edit_url' => admin_url('admin.php?page=fluent_forms&route=editor&form_id=' . $formId),
175 294 ];
176 295
177 - $skippedCustomCode = false;
296 + $skippedCustomCode = $droppedDateConfigs > 0;
178 297
179 298 if (isset($formItem['metas'])) {
180 299 foreach ($formItem['metas'] as $metaData) {
181 300 $metaKey = sanitize_text_field(Arr::get($metaData, 'meta_key'));
@@ -258,8 +377,9 @@
258 377 $type = sanitize_key(Arr::get($args, 'format', 'csv'));
259 378 if (!in_array($type, ['csv', 'ods', 'xlsx', 'json'])) {
260 379 exit('Invalid requested format');
261 380 }
381 + self::markDownloadStarted(Arr::get($args, 'download_token'));
262 382 if ('json' == $type) {
263 383 self::exportAsJSON($form, $args);
264 384 }
265 385 if (!defined('FLUENTFORM_DOING_CSV_EXPORT')) {
@@ -572,8 +692,30 @@
572 692 $sanitizedTitle = 'export';
573 693 }
574 694
575 695 return $sanitizedTitle . '-' . date('Y-m-d');
696 + }
697 +
698 + /**
699 + * Set a short-lived cookie the admin page polls to know the download has started.
700 + *
701 + * @param string|null $token
702 + * @return void
703 + */
704 + private static function markDownloadStarted($token)
705 + {
706 + $token = substr(sanitize_key((string) $token), 0, 32);
707 +
708 + if (!$token || headers_sent()) {
709 + return;
710 + }
711 +
712 + setcookie('ff_export_' . $token, '1', [
713 + 'expires' => time() + 60,
714 + 'path' => '/',
715 + 'secure' => is_ssl(),
716 + 'samesite' => 'Lax',
717 + ]);
576 718 }
577 719
578 720 private static function sendDownloadHeaders($contentType, $fileName)
579 721 {