PluginProbe
Fluent Forms – Customizable Contact Forms, Survey, Quiz, & Conversational Form Builder / 6.2.14
Fluent Forms – Customizable Contact Forms, Survey, Quiz, & Conversational Form Builder v6.2.14
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 3.6.65 All 196 releases
← All changes | app/Helpers/Helper.php +1625 -176 3.6.656.2.14 View file →
@@ -1,20 +1,34 @@
1 1 <?php
2 2
3 3 namespace FluentForm\App\Helpers;
4 4
5 +use FluentForm\App\Models\EntryDetails;
6 +use FluentForm\App\Models\Form;
7 +use FluentForm\App\Models\FormMeta;
8 +use FluentForm\App\Models\Submission;
9 +use FluentForm\App\Models\SubmissionMeta;
10 +use FluentForm\App\Services\FormBuilder\Components\SelectCountry;
11 +use FluentForm\App\Services\FormBuilder\ShortCodeParser;
5 12 use FluentForm\Framework\Helpers\ArrayHelper;
13 +use FluentForm\App\Helpers\Traits\GlobalDefaultMessages;
14 +use FluentForm\Framework\Support\Arr;
6 15
7 16 class Helper
8 17 {
9 - static $tabIndex = 0;
18 + use GlobalDefaultMessages;
10 19
11 - static $formInstance = 0;
20 + public static $tabIndex = 0;
12 21
13 - static $loadedForms = [];
22 + public static $formInstance = 0;
14 23
15 - static $tabIndexStatus = 'na';
24 + public static $loadedForms = [];
16 25
26 + public static $tabIndexStatus = 'na';
27 +
28 + protected static $formMetaCache = [];
29 +
30 +
17 31 /**
18 32 * Sanitize form inputs recursively.
19 33 *
20 34 * @param $input
@@ -23,9 +37,9 @@
23 37 */
24 38 public static function sanitizer($input, $attribute = null, $fields = [])
25 39 {
26 40 if (is_string($input)) {
27 - if (ArrayHelper::get($fields, $attribute . '.element') === 'textarea') {
41 + if ('textarea' === ArrayHelper::get($fields, $attribute . '.element')) {
28 42 $input = sanitize_textarea_field($input);
29 43 } else {
30 44 $input = sanitize_text_field($input);
31 45 }
@@ -30,19 +44,240 @@
30 44 $input = sanitize_text_field($input);
31 45 }
32 46 } elseif (is_array($input)) {
33 47 foreach ($input as $key => &$value) {
34 - $attribute = $attribute ? $attribute . '[' . $key . ']' : $key;
48 + // Local var: mutating $attribute here would collapse every sibling
49 + // after the first onto a bare key, resolving nested inputs to the wrong element.
50 + $childAttribute = $attribute ? $attribute . '[' . $key . ']' : $key;
35 51
36 - $value = Helper::sanitizer($value, $attribute, $fields);
52 + $value = static::sanitizer($value, $childAttribute, $fields);
53 + }
54 + unset($value);
55 + }
37 56
38 - $attribute = null;
57 + return $input;
58 + }
59 +
60 + /**
61 + * Flatten a request value into a plain, printable string.
62 + *
63 + * Request values are string or array. Walks nested arrays so a crafted
64 + * param[][] cannot raise an "Array to string conversion" notice, and the
65 + * caller can escape the result in one pass instead of branching on shape.
66 + *
67 + * @param mixed $value
68 + *
69 + * @return string
70 + */
71 + public static function flattenRequestValue($value)
72 + {
73 + if (!is_array($value)) {
74 + return is_scalar($value) ? (string) $value : '';
75 + }
76 +
77 + $flat = [];
78 +
79 + array_walk_recursive($value, function ($item) use (&$flat) {
80 + if (is_scalar($item)) {
81 + $flat[] = (string) $item;
39 82 }
83 + });
84 +
85 + return implode(', ', $flat);
86 + }
87 +
88 + public static function isOptionGroup($option)
89 + {
90 + return is_array($option)
91 + && ArrayHelper::get($option, 'type') === 'group'
92 + && is_array(ArrayHelper::get($option, 'options'));
93 + }
94 +
95 + /*
96 + * Int or nothing. Persisting free text here would hand the one user class
97 + * this sanitizer exists to contain an arbitrary string in form_fields, for a
98 + * value with no PHP consumer at all — it only keys the editor's Vue list.
99 + * A non-numeric id collapses to 0, and ensureUniqueIds() regenerates it on
100 + * load. Numerics must stay numeric for the same reason: as the string "0" it
101 + * would be JS-truthy, and ensureUniqueIds() only regenerates ids that are
102 + * falsy or already seen.
103 + */
104 + public static function sanitizeOptionId($value)
105 + {
106 + return is_numeric($value) ? (int) $value : 0;
107 + }
108 +
109 + /*
110 + * sanitize_title, not sanitize_key: Pro writes inventory slugs with
111 + * sanitize_title (InventoryController), which percent-encodes non-ASCII.
112 + * sanitize_key strips '%', producing a slug that no longer exists — and a
113 + * truthy-but-missing slug resolves to quantity 0 in InventoryValidation, so
114 + * the option fails closed as permanently stocked out. sanitize_title is
115 + * idempotent over its own output and still neutralises markup.
116 + *
117 + * The is_scalar guard is ours: unlike sanitize_key, sanitize_title has no
118 + * guard of its own and raises a TypeError on PHP 8 for an array value.
119 + */
120 + public static function sanitizeOptionSlug($value)
121 + {
122 + return is_scalar($value) ? sanitize_title($value) : '';
123 + }
124 +
125 + /**
126 + * A key is unsafe when it opens a handler, or when it carries a character that
127 + * ENDS an attribute name in the HTML tokeniser -- whitespace, quote, slash,
128 + * equals or angle bracket. `esc_attr()` leaves those intact, so `x onclick`
129 + * renders as two attributes and the second one is live.
130 + *
131 + * Deny those characters rather than allow-list a charset: an allow-list also
132 + * rejects the legal-but-unusual keys real sites carry (leading underscore,
133 + * non-Latin names, framework prefixes) and silently drops working markup.
134 + *
135 + * @param string|int $key
136 + * @return bool
137 + */
138 + public static function isSafeAttributeKey($key)
139 + {
140 + $key = (string) $key;
141 +
142 + if ('' === $key || preg_match('/^on[a-z]/i', $key)) {
143 + return false;
40 144 }
41 145
42 - return $input;
146 + return !preg_match('/[\s"\'\/=<>`]|[\x00-\x1F\x7F]/', $key);
43 147 }
44 148
149 + /*
150 + * Keys the whitelist above drops but the editor and Pro Inventory need back.
151 + * They are sanitized rather than passed through, since preserving unknown
152 + * keys verbatim would defeat the whitelist for exactly the users this path
153 + * protects. Absent keys stay absent: an invented `quantity => 0` reads as
154 + * "stock out" to InventoryValidation.
155 + */
156 + protected static function optionPassthroughMap()
157 + {
158 + return [
159 + 'id' => [self::class, 'sanitizeOptionId'],
160 + 'quantity' => 'intval',
161 + 'global_inventory' => [self::class, 'sanitizeOptionSlug'],
162 + ];
163 + }
164 +
165 + public static function sanitizeAdvancedOptions($options, $depth = 0)
166 + {
167 + if (!is_array($options)) {
168 + return [];
169 + }
170 +
171 + $sanitized = [];
172 +
173 + foreach ($options as $option) {
174 + if (!is_array($option)) {
175 + continue;
176 + }
177 +
178 + if (self::isOptionGroup($option)) {
179 + $groupOptions = self::sanitizeAdvancedOptions(
180 + ArrayHelper::get($option, 'options', []),
181 + $depth + 1
182 + );
183 +
184 + if ($depth > 0) {
185 + $sanitized = array_merge($sanitized, $groupOptions);
186 + continue;
187 + }
188 +
189 + $groupLabel = ArrayHelper::get($option, 'label', '');
190 +
191 + $group = [
192 + 'type' => 'group',
193 + 'label' => wp_kses_post(is_scalar($groupLabel) ? $groupLabel : ''),
194 + 'options' => $groupOptions,
195 + ];
196 +
197 + // Groups are keyed on group.id in the editor exactly like leaf
198 + // options, and is_open is user-visible collapse state.
199 + if (array_key_exists('id', $option)) {
200 + $group['id'] = self::sanitizeOptionId($option['id']);
201 + }
202 + if (array_key_exists('is_open', $option)) {
203 + $group['is_open'] = (bool) $option['is_open'];
204 + }
205 +
206 + $sanitized[] = $group;
207 + continue;
208 + }
209 +
210 + /*
211 + * Non-scalars are flattened to '' before the WP sanitisers see them:
212 + * wp_kses_post() and sanitize_url() have no guard of their own and
213 + * raise a TypeError on PHP 8 for an array, which would be an uncaught
214 + * 500 on save for exactly the users this whitelist protects.
215 + */
216 + $scalar = function ($key) use ($option) {
217 + $value = ArrayHelper::get($option, $key, '');
218 +
219 + return is_scalar($value) ? $value : '';
220 + };
221 +
222 + $clean = [
223 + 'label' => wp_kses_post($scalar('label')),
224 + 'value' => sanitize_text_field($scalar('value')),
225 + 'image' => sanitize_url($scalar('image')),
226 + 'calc_value' => sanitize_text_field($scalar('calc_value')),
227 + 'disabled' => ArrayHelper::isTrue($option, 'disabled'),
228 + ];
229 +
230 + foreach (self::optionPassthroughMap() as $key => $sanitizer) {
231 + if (array_key_exists($key, $option)) {
232 + $clean[$key] = call_user_func($sanitizer, $option[$key]);
233 + }
234 + }
235 +
236 + $sanitized[] = $clean;
237 + }
238 +
239 + return $sanitized;
240 + }
241 +
242 + public static function flattenAdvancedOptions($options)
243 + {
244 + if (!is_array($options)) {
245 + return [];
246 + }
247 +
248 + $flattened = [];
249 +
250 + foreach ($options as $option) {
251 + if (self::isOptionGroup($option)) {
252 + $flattened = array_merge(
253 + $flattened,
254 + self::flattenAdvancedOptions(ArrayHelper::get($option, 'options', []))
255 + );
256 + continue;
257 + }
258 +
259 + if (!is_array($option)) {
260 + continue;
261 + }
262 +
263 + $flattened[] = $option;
264 + }
265 +
266 + return $flattened;
267 + }
268 +
269 + public static function advancedOptionsValueLabelMap($options)
270 + {
271 + $formatted = [];
272 +
273 + foreach (self::flattenAdvancedOptions($options) as $option) {
274 + $formatted[ArrayHelper::get($option, 'value')] = ArrayHelper::get($option, 'label');
275 + }
276 +
277 + return $formatted;
278 + }
279 +
45 280 public static function makeMenuUrl($page = 'fluent_forms_settings', $component = null)
46 281 {
47 282 $baseUrl = admin_url('admin.php?page=' . $page);
48 283
@@ -62,9 +297,8 @@
62 297 }
63 298 }
64 299
65 300 return $baseUrl;
66 -
67 301 }
68 302
69 303 public static function getHtmlElementClass($value1, $value2, $class = 'active', $default = '')
70 304 {
@@ -81,31 +315,55 @@
81 315 public static function isJson($string)
82 316 {
83 317 json_decode($string);
84 318
85 - return json_last_error() === JSON_ERROR_NONE;
319 + return JSON_ERROR_NONE === json_last_error();
86 320 }
87 321
88 -
89 322 public static function isSlackEnabled()
90 323 {
91 324 $globalModules = get_option('fluentform_global_modules_status');
92 - return $globalModules && isset($globalModules['slack']) && $globalModules['slack'] == 'yes';
325 +
326 + return $globalModules && isset($globalModules['slack']) && 'yes' == $globalModules['slack'];
93 327 }
94 328
95 329 public static function getEntryStatuses($form_id = false)
96 330 {
97 - $statuses = apply_filters('fluentform_entry_statuses_core', [
98 - 'unread' => 'Unread',
99 - 'read' => 'Read'
100 - ], $form_id);
101 - $statuses['trashed'] = 'Trashed';
331 + $statuses = [
332 + 'unread' => __('Unread', 'fluentform'),
333 + 'read' => __('Read', 'fluentform'),
334 + 'favorites' => __('Favorites', 'fluentform'),
335 + ];
336 +
337 + $statuses = apply_filters_deprecated(
338 + 'fluentform_entry_statuses_core',
339 + [
340 + $statuses,
341 + $form_id,
342 + ],
343 + FLUENTFORM_FRAMEWORK_UPGRADE,
344 + 'fluentform/entry_statuses_core',
345 + 'Use fluentform/entry_statuses_core instead of fluentform_entry_statuses_core.'
346 + );
347 +
348 + $statuses = apply_filters('fluentform/entry_statuses_core', $statuses, $form_id);
349 +
350 + $statuses['spam'] = __('Spam', 'fluentform');
351 +
352 + $statuses['trashed'] = __('Trashed', 'fluentform');
353 +
102 354 return $statuses;
103 355 }
104 356
357 + // Statuses a caller may write by hand; add-ons withhold the ones they own as workflow steps.
358 + public static function getMutableEntryStatuses($form_id = false, $submission_id = null)
359 + {
360 + return apply_filters('fluentform/entry_statuses_for_mutation', static::getEntryStatuses($form_id), $form_id, $submission_id);
361 + }
362 +
105 363 public static function getReportableInputs()
106 364 {
107 - return apply_filters('fluentform_reportable_inputs', [
365 + $data = [
108 366 'select',
109 367 'input_radio',
110 368 'input_checkbox',
111 369 'ratings',
@@ -110,141 +368,135 @@
110 368 'input_checkbox',
111 369 'ratings',
112 370 'net_promoter',
113 371 'select_country',
114 - 'net_promoter_score'
115 - ]);
372 + 'net_promoter_score',
373 + ];
374 +
375 + $data = apply_filters_deprecated(
376 + 'fluentform_reportable_inputs',
377 + [
378 + $data,
379 + ],
380 + FLUENTFORM_FRAMEWORK_UPGRADE,
381 + 'fluentform/reportable_inputs',
382 + 'Use fluentform/reportable_inputs instead of fluentform_reportable_inputs.'
383 + );
384 +
385 + return apply_filters('fluentform/reportable_inputs', $data);
116 386 }
117 387
118 388 public static function getSubFieldReportableInputs()
119 389 {
120 - return apply_filters('fluentform_subfield_reportable_inputs', [
121 - 'tabular_grid'
122 - ]);
390 + $grid = apply_filters_deprecated(
391 + 'fluentform_subfield_reportable_inputs',
392 + [
393 + ['tabular_grid'],
394 + ],
395 + FLUENTFORM_FRAMEWORK_UPGRADE,
396 + 'fluentform/subfield_reportable_inputs',
397 + 'Use fluentform/subfield_reportable_inputs instead of fluentform_subfield_reportable_inputs.'
398 + );
399 +
400 + return apply_filters('fluentform/subfield_reportable_inputs', $grid);
123 401 }
124 402
125 - public static function getFormMeta($formId, $metaKey, $default = '')
403 + public static function getFormMeta($formId, $metaKey, $default = '', $forced = false)
126 404 {
127 - $meta = wpFluent()->table('fluentform_form_meta')
128 - ->where('meta_key', $metaKey)
129 - ->where('form_id', $formId)
130 - ->first();
405 + $formattedValues = self::$formMetaCache[$formId] ?? [];
131 406
132 - if (!$meta || !$meta->value) {
133 - return $default;
134 - }
407 + if (!isset(self::$formMetaCache[$formId]) || $forced) {
408 + $formMetas = FormMeta::where('form_id', $formId)
409 + ->get();
135 410
136 - $metaValue = $meta->value;
137 - // decode the JSON data
138 - $result = json_decode($metaValue, true);
411 + $formattedValues = [];
412 + foreach ($formMetas as $formMeta) {
413 + $value = $formMeta->value;
139 414
140 - if (json_last_error() == JSON_ERROR_NONE) {
141 - return $result;
415 + $decoded = json_decode($value ?? '', true);
416 + if (is_array($decoded)) {
417 + $value = $decoded;
418 + }
419 +
420 + $formattedValues[$formMeta->meta_key] = $value;
421 + }
422 + self::$formMetaCache[$formId] = $formattedValues;
142 423 }
143 - return $metaValue;
424 +
425 + return Arr::get($formattedValues, $metaKey, $default);
144 426 }
145 427
428 +
146 429 public static function setFormMeta($formId, $metaKey, $value)
147 430 {
148 - $meta = wpFluent()->table('fluentform_form_meta')
149 - ->where('meta_key', $metaKey)
150 - ->where('form_id', $formId)
151 - ->first();
431 + if ($meta = FormMeta::persist($formId, $metaKey, $value)) {
432 + // Update the cache with the new value
433 + if (!isset(self::$formMetaCache[$formId])) {
434 + self::$formMetaCache[$formId] = [];
435 + }
436 + self::$formMetaCache[$formId][$metaKey] = $value;
152 437
153 - if (is_array($value) || is_object($value)) {
154 - $value = json_encode($value);
438 + return $meta->id;
155 439 }
440 + return null;
441 + }
156 442
157 - if (!$meta) {
158 - $insetid = wpFluent()->table('fluentform_form_meta')
159 - ->insert([
160 - 'meta_key' => $metaKey,
161 - 'form_id' => $formId,
162 - 'value' => $value
163 - ]);
164 - return $insetid;
165 - } else {
166 - wpFluent()->table('fluentform_form_meta')
167 - ->where('id', $meta->id)
168 - ->update([
169 - 'value' => $value
170 - ]);
443 + public static function deleteFormMeta($formId, $metaKey)
444 + {
445 + try {
446 + FormMeta::remove($formId, $metaKey);
447 + return true;
448 + } catch (\Exception $ex) {
449 + return null;
171 450 }
451 + }
172 452
173 - return $meta->id;
453 + /**
454 + * Resolve an entry's column => value map regardless of whether the entry is
455 + * a stdClass DB row (columns are real properties) or a WPFluent Model
456 + * (columns live in an internal attribute bag reached via __get).
457 + *
458 + * @param object|array $entry
459 + * @return array
460 + */
461 + public static function getEntryColumns($entry)
462 + {
463 + if (is_object($entry) && method_exists($entry, 'getAttributes')) {
464 + return $entry->getAttributes();
465 + }
466 +
467 + return (array) $entry;
174 468 }
175 469
176 470 public static function getSubmissionMeta($submissionId, $metaKey, $default = false)
177 471 {
178 - $meta = wpFluent()->table('fluentform_submission_meta')
179 - ->where('response_id', $submissionId)
180 - ->where('meta_key', $metaKey)
181 - ->first();
182 -
183 - if ($meta && $meta->value) {
184 - return maybe_unserialize($meta->value);
185 - }
186 -
187 - return $default;
472 + return SubmissionMeta::retrieve($metaKey, $submissionId, $default);
188 473 }
189 474
190 475 public static function setSubmissionMeta($submissionId, $metaKey, $value, $formId = false)
191 476 {
192 - $value = maybe_serialize($value);
193 -
194 - // check if submission exist
195 - $meta = wpFluent()->table('fluentform_submission_meta')
196 - ->where('response_id', $submissionId)
197 - ->where('meta_key', $metaKey)
198 - ->first();
199 -
200 - if ($meta) {
201 - wpFluent()->table('fluentform_submission_meta')
202 - ->where('id', $meta->id)
203 - ->insert([
204 - 'value' => $value,
205 - 'updated_at' => current_time('mysql')
206 - ]);
477 + if ($meta = SubmissionMeta::persist($submissionId, $metaKey, $value, $formId)) {
207 478 return $meta->id;
208 479 }
480 + return null;
481 + }
209 482
210 - if (!$formId) {
211 - $submission = wpFluent()->table('fluentform_submissions')
212 - ->find($submissionId);
213 - if ($submission) {
214 - $formId = $submission->form_id;
215 - }
483 + public static function setSubmissionMetaAsArrayPush($submissionId, $metaKey, $value, $formId = false)
484 + {
485 + if ($meta = SubmissionMeta::persistArray($submissionId, $metaKey, $value, $formId)) {
486 + return $meta->id;
216 487 }
217 -
218 - return wpFluent()->table('fluentform_submission_meta')
219 - ->insert([
220 - 'response_id' => $submissionId,
221 - 'form_id' => $formId,
222 - 'meta_key' => $metaKey,
223 - 'value' => $value,
224 - 'created_at' => current_time('mysql'),
225 - 'updated_at' => current_time('mysql')
226 - ]);
227 -
488 + return null;
228 489 }
229 490
230 491 public static function isEntryAutoDeleteEnabled($formId)
231 492 {
232 - $settings = wpFluent()->table('fluentform_form_meta')
233 - ->where('form_id', $formId)
234 - ->where('meta_key', 'formSettings')
235 - ->first();
236 -
237 - if (!$settings) {
238 - return false;
239 - }
240 -
241 - $formSettings = json_decode($settings->value, true);
242 -
243 - if ($formSettings && ArrayHelper::get($formSettings, 'delete_entry_on_submission') == 'yes') {
493 + if (
494 + 'yes' == ArrayHelper::get(static::getFormMeta($formId, 'formSettings', []), 'delete_entry_on_submission',
495 + '')
496 + ) {
244 497 return true;
245 498 }
246 -
247 499 return false;
248 500 }
249 501
250 502 public static function formExtraCssClass($form)
@@ -249,14 +501,9 @@
249 501
250 502 public static function formExtraCssClass($form)
251 503 {
252 504 if (!$form->settings) {
253 - $settings = wpFluent()->table('fluentform_form_meta')
254 - ->where('form_id', $form->id)
255 - ->where('meta_key', 'formSettings')
256 - ->first();
257 -
258 - $formSettings = json_decode($settings->value, true);
505 + $formSettings = static::getFormMeta($form->id, 'formSettings');
259 506 } else {
260 507 $formSettings = $form->settings;
261 508 }
262 509
@@ -263,10 +510,9 @@
263 510 if (!$formSettings) {
264 511 return '';
265 512 }
266 513
267 -
268 - if ($formSettings && $extraClass = ArrayHelper::get($formSettings, 'form_extra_css_class')) {
514 + if ($extraClass = ArrayHelper::get($formSettings, 'form_extra_css_class')) {
269 515 return esc_attr($extraClass);
270 516 }
271 517
272 518 return '';
@@ -273,18 +519,21 @@
273 519 }
274 520
275 521 public static function getNextTabIndex($increment = 1)
276 522 {
277 - if (self::isTabIndexEnabled()) {
523 + if (static::isTabIndexEnabled()) {
278 524 static::$tabIndex += $increment;
525 +
279 526 return static::$tabIndex;
280 527 }
528 +
281 529 return '';
282 530 }
283 531
284 532 public static function getFormInstaceClass($formId)
285 533 {
286 - static::$formInstance += 1;
534 + static::$formInstance++;
535 +
287 536 return 'ff_form_instance_' . $formId . '_' . static::$formInstance;
288 537 }
289 538
290 539 public static function resetTabIndex()
@@ -298,26 +547,37 @@
298 547 'fluent_forms',
299 548 'fluent_forms_all_entries',
300 549 'fluent_forms_transfer',
301 550 'fluent_forms_settings',
302 - 'fluent_form_add_ons',
551 + 'fluent_forms_add_ons',
303 552 'fluent_forms_docs',
304 - 'fluent_form_payment_entries',
305 - 'fluent_forms_smtp'
553 + 'fluent_forms_payment_entries',
554 + 'fluent_forms_reports',
306 555 ];
307 556
308 557 $status = true;
309 558
310 - if (!isset($_GET['page']) || !in_array($_GET['page'], $fluentPages)) {
559 + $page = wpFluentForm('request')->get('page');
560 +
561 + if (!$page || !in_array($page, $fluentPages)) {
311 562 $status = false;
312 563 }
313 564
314 - return apply_filters('fluentform_is_admin_page', $status);
565 + $status = apply_filters_deprecated(
566 + 'fluentform_is_admin_page',
567 + [
568 + $status,
569 + ],
570 + FLUENTFORM_FRAMEWORK_UPGRADE,
571 + 'fluentform/is_admin_page',
572 + 'Use fluentform/is_admin_page instead of fluentform_is_admin_page.'
573 + );
574 +
575 + return apply_filters('fluentform/is_admin_page', $status);
315 576 }
316 577
317 578 public static function getShortCodeIds($content, $tag = 'fluentform', $selector = 'id')
318 579 {
319 -
320 580 if (false === strpos($content, '[')) {
321 581 return [];
322 582 }
323 583
@@ -326,8 +586,9 @@
326 586 return [];
327 587 }
328 588
329 589 $ids = [];
590 + $attributes = [];
330 591
331 592 foreach ($matches as $shortcode) {
332 593 if (count($shortcode) >= 2 && $tag === $shortcode[2]) {
333 594 // Replace braces with empty string.
@@ -335,56 +596,150 @@
335 596
336 597 $result = shortcode_parse_atts($parsedCode);
337 598
338 599 if (!empty($result[$selector])) {
600 + if ('fluentform' == $tag && !empty($result['type']) && 'conversational' == $result['type']) {
601 + continue;
602 + }
603 +
339 604 $ids[$result[$selector]] = $result[$selector];
605 +
606 + $theme = ArrayHelper::get($result, 'theme');
607 +
608 + if ($theme) {
609 + $attributes[] = [
610 + 'formId' => $result[$selector],
611 + 'theme' => $theme,
612 + ];
613 + }
340 614 }
341 615 }
342 616 }
343 617
618 + if ($attributes) {
619 + $ids['attributes'] = $attributes;
620 + }
621 +
344 622 return $ids;
345 623 }
346 624
625 + public static function getFormsIdsFromBlocks($content)
626 + {
627 + $ids = [];
628 + $attributes = [];
629 +
630 + if (!function_exists('parse_blocks')) {
631 + return $ids;
632 + }
633 +
634 + $has_block = false !== strpos($content, '<!-- wp:fluentfom/guten-block ');
635 +
636 + if (!$has_block) {
637 + return $ids;
638 + }
639 +
640 + $parsedBlocks = parse_blocks($content);
641 + foreach ($parsedBlocks as $block) {
642 + if (!ArrayHelper::exists($block, 'blockName') || !ArrayHelper::get($block, 'attrs.formId')) {
643 + continue;
644 + }
645 +
646 + $hasBlock = strpos($block['blockName'], 'fluentfom/guten-block') === 0;
647 + if ($hasBlock) {
648 + $formId = (int) $block['attrs']['formId'];
649 +
650 + $ids[] = $formId;
651 +
652 + $theme = ArrayHelper::get($block, 'attrs.themeStyle');
653 +
654 + if ($theme) {
655 + $attributes[] = [
656 + 'formId' => $formId,
657 + 'theme' => $theme,
658 + ];
659 + }
660 + }
661 + }
662 +
663 + if ($attributes) {
664 + $ids['attributes'] = $attributes;
665 + }
666 +
667 + return $ids;
668 + }
669 +
347 670 public static function isTabIndexEnabled()
348 671 {
349 - if (static::$tabIndexStatus == 'na') {
672 + if ('na' == static::$tabIndexStatus) {
350 673 $globalSettings = get_option('_fluentform_global_form_settings');
351 - static::$tabIndexStatus = ArrayHelper::get($globalSettings, 'misc.tabIndex') == 'yes';
674 + static::$tabIndexStatus = 'yes' == ArrayHelper::get($globalSettings, 'misc.tabIndex');
352 675 }
676 +
353 677 return static::$tabIndexStatus;
354 678 }
355 679
356 - public static function isMultiStepForm($formId)
680 + public static function isMultiStepForm($formOrId)
357 681 {
358 - $form = wpFluent()->table('fluentform_forms')->find($formId);
359 - $fields = json_decode($form->form_fields, true);
682 + // Accept both form object and form ID to avoid re-querying
683 + if (is_object($formOrId)) {
684 + $form = $formOrId;
685 + } else {
686 + $form = Form::find($formOrId);
687 + }
360 688
361 - if (ArrayHelper::get($fields, 'stepsWrapper')) {
362 - return true;
689 + if (!$form) {
690 + return false;
363 691 }
364 - return false;
692 +
693 + $fieldsJson = (string) ($form->form_fields ?? '');
694 + if ('' === $fieldsJson) {
695 + return false;
696 + }
697 +
698 + $fields = json_decode($fieldsJson, true);
699 + if (!is_array($fields)) {
700 + return false;
701 + }
702 +
703 + return (bool) ArrayHelper::get($fields, 'stepsWrapper');
365 704 }
366 705
367 706 public static function hasFormElement($formId, $elementName)
368 707 {
369 - $form = wpFluent()->table('fluentform_forms')->find($formId);
708 + $form = Form::find($formId);
370 709 $fieldsJson = $form->form_fields;
371 - return strpos($fieldsJson, '"element":"' . $elementName . '"') != false;
710 +
711 + return false != strpos($fieldsJson, '"element":"' . $elementName . '"');
372 712 }
373 713
374 714 public static function isUniqueValidation($validation, $field, $formData, $fields, $form)
375 715 {
376 - if (ArrayHelper::get($field, 'raw.settings.is_unique') == 'yes') {
716 + if ('yes' == ArrayHelper::get($field, 'raw.settings.is_unique')) {
377 717 $fieldName = ArrayHelper::get($field, 'name');
378 718 if ($inputValue = ArrayHelper::get($formData, $fieldName)) {
379 - $exist = wpFluent()->table('fluentform_entry_details')
380 - ->where('form_id', $form->id)
719 + $exist = EntryDetails::where('form_id', $form->id)
381 720 ->where('field_name', $fieldName)
382 721 ->where('field_value', $inputValue)
383 - ->first();
722 + ->exists();
723 +
724 + // if form has pending payment then the value doesn't exist in EntryDetails table
725 + // further checking on Submission table if the value exists
726 + if (!$exist && $form->has_payment) {
727 + $escapedKey = wp_json_encode($fieldName);
728 + $escapedValue = wp_json_encode($inputValue);
729 + $searchPattern = trim($escapedKey, '"') . '":' . $escapedValue;
730 + $searchPattern = addcslashes($searchPattern, '%_');
731 +
732 + $exist = Submission::where('form_id', $form->id)
733 + ->where('response', 'LIKE', '%' . $searchPattern . '%')
734 + ->exists();
735 + }
736 +
384 737 if ($exist) {
738 + $typeName = ArrayHelper::get($field, 'element', 'input_text');
385 739 return [
386 - 'unique' => ArrayHelper::get($field, 'raw.settings.unique_validation_message')
740 + 'unique' => apply_filters('fluentform/validation_message_unique_' . $typeName,
741 + ArrayHelper::get($field, 'raw.settings.unique_validation_message'), $field),
387 742 ];
388 743 }
389 744 }
390 745 }
@@ -391,16 +746,29 @@
391 746
392 747 return $validation;
393 748 }
394 749
750 +
751 + public static function hasPartialEntries($formId)
752 + {
753 + static $cache = [];
754 + if (isset($cache[$formId])) {
755 + return $cache[$formId];
756 + }
757 +
758 + $cache[$formId] = 'yes' == static::getFormMeta($formId, 'form_save_state_status');
759 +
760 + return $cache[$formId];
761 + }
762 +
395 763 public static function getNumericFormatters()
396 764 {
397 - return apply_filters('fluentform_numeric_styles', array(
398 - 'none' => array(
765 + $data = [
766 + 'none' => [
399 767 'value' => '',
400 - 'label' => 'None'
401 - ),
402 - 'comma_dot_style' => array(
768 + 'label' => 'None',
769 + ],
770 + 'comma_dot_style' => [
403 771 'value' => 'comma_dot_style',
404 772 'label' => __('US Style with Decimal (EX: 123,456.00)', 'fluentform'),
405 773 'settings' => [
406 774 'decimal' => '.',
@@ -405,12 +773,12 @@
405 773 'settings' => [
406 774 'decimal' => '.',
407 775 'separator' => ',',
408 776 'precision' => 2,
409 - 'symbol' => ''
410 - ]
411 - ),
412 - 'dot_comma_style_zero' => array(
777 + 'symbol' => '',
778 + ],
779 + ],
780 + 'dot_comma_style_zero' => [
413 781 'value' => 'dot_comma_style_zero',
414 782 'label' => __('US Style without Decimal (Ex: 123,456,789)', 'fluentform'),
415 783 'settings' => [
416 784 'decimal' => '.',
@@ -415,12 +783,12 @@
415 783 'settings' => [
416 784 'decimal' => '.',
417 785 'separator' => ',',
418 786 'precision' => 0,
419 - 'symbol' => ''
420 - ]
421 - ),
422 - 'dot_comma_style' => array(
787 + 'symbol' => '',
788 + ],
789 + ],
790 + 'dot_comma_style' => [
423 791 'value' => 'dot_comma_style',
424 792 'label' => __('EU Style with Decimal (Ex: 123.456,00)', 'fluentform'),
425 793 'settings' => [
426 794 'decimal' => ',',
@@ -425,12 +793,12 @@
425 793 'settings' => [
426 794 'decimal' => ',',
427 795 'separator' => '.',
428 796 'precision' => 2,
429 - 'symbol' => ''
430 - ]
431 - ),
432 - 'comma_dot_style_zero' => array(
797 + 'symbol' => '',
798 + ],
799 + ],
800 + 'comma_dot_style_zero' => [
433 801 'value' => 'comma_dot_style_zero',
434 802 'label' => __('EU Style without Decimal (EX: 123.456.789)', 'fluentform'),
435 803 'settings' => [
436 804 'decimal' => ',',
@@ -435,22 +803,35 @@
435 803 'settings' => [
436 804 'decimal' => ',',
437 805 'separator' => '.',
438 806 'precision' => 0,
439 - 'symbol' => ''
440 - ]
441 - )
442 - ));
807 + 'symbol' => '',
808 + ],
809 + ],
810 + ];
811 +
812 + $data = apply_filters_deprecated(
813 + 'fluentform_numeric_styles',
814 + [
815 + $data,
816 + ],
817 + FLUENTFORM_FRAMEWORK_UPGRADE,
818 + 'fluentform/numeric_styles',
819 + 'Use fluentform/numeric_styles instead of fluentform_numeric_styles.'
820 + );
821 +
822 + return apply_filters('fluentform/numeric_styles', $data);
443 823 }
444 824
445 825 public static function getNumericValue($input, $formatterName)
446 826 {
447 - $formatters = self::getNumericFormatters();
827 + $formatters = static::getNumericFormatters();
448 828 if (empty($formatters[$formatterName]['settings'])) {
449 829 return $input;
450 830 }
451 831 $settings = $formatters[$formatterName]['settings'];
452 - $number = floatval(str_replace($settings['decimal'], '.', preg_replace('/[^\d' . preg_quote($settings['decimal']) . ']/', '', $input)));
832 + $number = floatval(str_replace($settings['decimal'], '.',
833 + preg_replace('/[^-?\d' . preg_quote($settings['decimal']) . ']/', '', $input)));
453 834
454 835 return number_format($number, $settings['precision'], '.', '');
455 836 }
456 837
@@ -458,13 +839,14 @@
458 839 {
459 840 if (!is_numeric($input)) {
460 841 return $input;
461 842 }
462 - $formatters = self::getNumericFormatters();
843 + $formatters = static::getNumericFormatters();
463 844 if (empty($formatters[$formatterName]['settings'])) {
464 845 return $input;
465 846 }
466 847 $settings = $formatters[$formatterName]['settings'];
848 +
467 849 return number_format($input, $settings['precision'], $settings['decimal'], $settings['separator']);
468 850 }
469 851
470 852 public static function getDuplicateFieldNames($fields)
@@ -470,12 +852,12 @@
470 852 public static function getDuplicateFieldNames($fields)
471 853 {
472 854 $fields = json_decode($fields, true);
473 855 $items = $fields['fields'];
474 - $inputNames = self::getFieldNamesStatuses($items);
856 + $inputNames = static::getFieldNamesStatuses($items);
475 857 $uniqueNames = array_unique($inputNames);
476 858
477 - if(count($inputNames) == count($uniqueNames)) {
859 + if (count($inputNames) == count($uniqueNames)) {
478 860 return [];
479 861 }
480 862
481 863 return array_diff_assoc($inputNames, $uniqueNames);
@@ -480,26 +862,1093 @@
480 862
481 863 return array_diff_assoc($inputNames, $uniqueNames);
482 864 }
483 865
866 + public static function getRankingFieldsWithDuplicateOptionValues($fields)
867 + {
868 + if (is_string($fields)) {
869 + $fields = json_decode($fields, true);
870 + }
871 +
872 + if (!is_array($fields)) {
873 + return [];
874 + }
875 +
876 + $items = ArrayHelper::get($fields, 'fields', []);
877 +
878 + if (!is_array($items)) {
879 + return [];
880 + }
881 +
882 + return static::collectRankingFieldsWithDuplicateOptionValues($items);
883 + }
884 +
484 885 protected static function getFieldNamesStatuses($fields)
485 886 {
486 887 $names = [];
487 888
488 889 foreach ($fields as $field) {
489 - if(ArrayHelper::get($field, 'element') == 'container') {
890 + if ('container' == ArrayHelper::get($field, 'element')) {
490 891 $columns = ArrayHelper::get($field, 'columns', []);
491 892 foreach ($columns as $column) {
492 - $columnInputs = self::getFieldNamesStatuses(ArrayHelper::get($column, 'fields', []));
893 + $columnInputs = static::getFieldNamesStatuses(ArrayHelper::get($column, 'fields', []));
493 894 $names = array_merge($names, $columnInputs);
494 895 }
896 + } elseif ($name = ArrayHelper::get($field, 'attributes.name')) {
897 + $names[] = $name;
898 + }
899 + }
900 +
901 + return $names;
902 + }
903 +
904 + protected static function collectRankingFieldsWithDuplicateOptionValues($fields)
905 + {
906 + $duplicates = [];
907 +
908 + foreach ($fields as $field) {
909 + if ('container' === ArrayHelper::get($field, 'element')) {
910 + $columns = ArrayHelper::get($field, 'columns', []);
911 + foreach ($columns as $column) {
912 + $columnFields = ArrayHelper::get($column, 'fields', []);
913 + $duplicates = array_merge(
914 + $duplicates,
915 + static::collectRankingFieldsWithDuplicateOptionValues($columnFields)
916 + );
917 + }
918 + continue;
919 + }
920 +
921 + if (!empty($field['fields']) && is_array($field['fields'])) {
922 + $duplicates = array_merge(
923 + $duplicates,
924 + static::collectRankingFieldsWithDuplicateOptionValues($field['fields'])
925 + );
926 + continue;
927 + }
928 +
929 + if ('input_ranking' !== ArrayHelper::get($field, 'element')) {
930 + continue;
931 + }
932 +
933 + $formattedOptions = ArrayHelper::get($field, 'settings.advanced_options', []);
934 + if (!$formattedOptions) {
935 + $formattedOptions = [];
936 + foreach (ArrayHelper::get($field, 'options', []) as $value => $label) {
937 + $formattedOptions[] = [
938 + 'label' => $label,
939 + 'value' => $value,
940 + ];
941 + }
942 + }
943 +
944 + $optionValues = array_values(array_filter(array_map(
945 + 'sanitize_text_field',
946 + array_column(static::flattenAdvancedOptions($formattedOptions), 'value')
947 + ), function ($value) {
948 + return '' !== $value;
949 + }));
950 +
951 + if (count($optionValues) !== count(array_unique($optionValues))) {
952 + $fieldLabel = ArrayHelper::get($field, 'settings.admin_field_label');
953 + if (!$fieldLabel) {
954 + $fieldLabel = ArrayHelper::get($field, 'settings.label');
955 + }
956 + if (!$fieldLabel) {
957 + $fieldLabel = ArrayHelper::get($field, 'attributes.name');
958 + }
959 + $duplicates[] = $fieldLabel ? $fieldLabel : __('Ranking Field', 'fluentform');
960 + }
961 + }
962 +
963 + return $duplicates;
964 + }
965 +
966 + public static function isConversionForm($formId)
967 + {
968 + static $cache = [];
969 + if (isset($cache[$formId])) {
970 + return $cache[$formId];
971 + }
972 +
973 + $cache[$formId] = 'yes' == static::getFormMeta($formId, 'is_conversion_form');
974 +
975 + return $cache[$formId];
976 + }
977 +
978 + public static function getPreviewUrl($formId, $type = '')
979 + {
980 + if ('conversational' == $type) {
981 + return static::getConversionUrl($formId);
982 + } elseif ('classic' == $type) {
983 + return site_url('?fluent_forms_pages=1&design_mode=1&preview_id=' . $formId) . '#ff_preview';
984 + } elseif (static::isConversionForm($formId)) {
985 + return static::getConversionUrl($formId);
986 + }
987 +
988 + return site_url('?fluent_forms_pages=1&design_mode=1&preview_id=' . $formId) . '#ff_preview';
989 + }
990 +
991 + public static function getFormAdminPermalink($route, $form)
992 + {
993 + $baseUrl = admin_url('admin.php?page=fluent_forms');
994 +
995 + return $baseUrl . '&route=' . $route . '&form_id=' . $form->id;
996 + }
997 +
998 + public static function getFormSettingsUrl($form)
999 + {
1000 + $baseUrl = admin_url('admin.php?page=fluent_forms');
1001 +
1002 + return $baseUrl . '&form_id=' . $form->id . '&route=settings&sub_route=form_settings#basic_settings';
1003 + }
1004 +
1005 + private static function getConversionUrl($formId)
1006 + {
1007 + $meta = static::getFormMeta($formId, 'ffc_form_settings_meta', []);
1008 + $key = ArrayHelper::get($meta, 'share_key', '');
1009 +
1010 + $slug = apply_filters_deprecated(
1011 + 'fluentform_conversational_url_slug',
1012 + [
1013 + 'fluent-form',
1014 + ],
1015 + FLUENTFORM_FRAMEWORK_UPGRADE,
1016 + 'fluentform/conversational_url_slug',
1017 + 'Use fluentform/conversational_url_slug instead of fluentform_conversational_url_slug.'
1018 + );
1019 +
1020 + $paramKey = apply_filters('fluentform/conversational_url_slug', $slug);
1021 +
1022 + if ('form' == $paramKey) {
1023 + $paramKey = 'fluent-form';
1024 + }
1025 + if ($key) {
1026 + return static::getFrontendFacingUrl('?' . $paramKey . '=' . $formId . '&form=' . $key);
1027 + }
1028 + return static::getFrontendFacingUrl('?' . $paramKey . '=' . $formId);
1029 + }
1030 +
1031 + public static function fileUploadLocations()
1032 + {
1033 + $locations = [
1034 + [
1035 + 'value' => 'default',
1036 + 'label' => __('Fluent Forms Default', 'fluentform'),
1037 + ],
1038 + [
1039 + 'value' => 'wp_media',
1040 + 'label' => __('Media Library', 'fluentform'),
1041 + ],
1042 + ];
1043 +
1044 + $locations = apply_filters_deprecated(
1045 + 'fluentform_file_upload_options',
1046 + [
1047 + $locations,
1048 + ],
1049 + FLUENTFORM_FRAMEWORK_UPGRADE,
1050 + 'fluentform/file_upload_options',
1051 + 'Use fluentform/file_upload_options instead of fluentform_file_upload_options'
1052 + );
1053 +
1054 + return apply_filters('fluentform/file_upload_options', $locations);
1055 + }
1056 +
1057 + public static function unreadCount($formId)
1058 + {
1059 + return Submission::where('status', 'unread')
1060 + ->where('form_id', $formId)
1061 + ->count();
1062 + }
1063 +
1064 + public static function getForms()
1065 + {
1066 + $ff_list = Form::select(['id', 'title'])->orderBy('id', 'DESC')->get();
1067 + $forms = [];
1068 +
1069 + if (count($ff_list) > 0) {
1070 + $forms[0] = esc_html__('Select a Fluent Forms', 'fluentform');
1071 + foreach ($ff_list as $form) {
1072 + $forms[$form->id] = esc_html($form->title) . ' (' . $form->id . ')';
1073 + }
1074 + } else {
1075 + $forms[0] = esc_html__('Create a Form First', 'fluentform');
1076 + }
1077 +
1078 + return $forms;
1079 + }
1080 +
1081 + public static function replaceBrTag($content, $with = '')
1082 + {
1083 + if (is_array($content)) {
1084 + foreach ($content as $key => $value) {
1085 + $content[$key] = static::replaceBrTag($value, $with);
1086 + }
1087 + } elseif (static::hasBrTag($content)) {
1088 + $content = str_replace('<br />', $with, $content);
1089 + }
1090 +
1091 + return $content;
1092 + }
1093 +
1094 + public static function hasBrTag($content)
1095 + {
1096 + return is_string($content) && false !== strpos($content, '<br />');
1097 + }
1098 +
1099 + public static function sanitizeForCSV($content)
1100 + {
1101 + $formulas = ['=', '-', '+', '@', "\t", "\r", "\n"];
1102 +
1103 + $formulas = apply_filters('fluentform/csv_sanitize_formulas', $formulas);
1104 +
1105 + if (Str::startsWith($content, $formulas)) {
1106 + $content = "'" . $content;
1107 + }
1108 +
1109 + return $content;
1110 + }
1111 +
1112 + public static function sanitizeOrderValue($orderType = '')
1113 + {
1114 + $orderBys = ['ASC', 'DESC'];
1115 +
1116 + $orderType = trim(strtoupper($orderType));
1117 +
1118 + return in_array($orderType, $orderBys) ? $orderType : 'DESC';
1119 + }
1120 +
1121 + public static function getForm($id)
1122 + {
1123 + return Form::where('id', $id)->first();
1124 + }
1125 +
1126 + public static function shouldHidePassword($formId)
1127 + {
1128 + $isTruncate = apply_filters_deprecated(
1129 + 'fluentform_truncate_password_values',
1130 + [
1131 + true,
1132 + $formId,
1133 + ],
1134 + FLUENTFORM_FRAMEWORK_UPGRADE,
1135 + 'fluentform/truncate_password_values',
1136 + 'Use fluentform/truncate_password_values instead of fluentform_truncate_password_values.'
1137 + );
1138 +
1139 + return apply_filters('fluentform/truncate_password_values', $isTruncate, $formId) &&
1140 + (
1141 + (defined('FLUENTFORM_RENDERING_ENTRIES') && FLUENTFORM_RENDERING_ENTRIES) ||
1142 + (defined('FLUENTFORM_RENDERING_ENTRY') && FLUENTFORM_RENDERING_ENTRY) ||
1143 + (defined('FLUENTFORM_EXPORTING_ENTRIES') && FLUENTFORM_EXPORTING_ENTRIES)
1144 + );
1145 + }
1146 +
1147 + // make tabular-grid value markdown format
1148 + public static function getTabularGridFormatValue(
1149 + $girdData,
1150 + $field,
1151 + $rowJoiner = '<br />',
1152 + $colJoiner = ', ',
1153 + $type = ''
1154 + ) {
1155 + if (!$girdData || !$field) {
1156 + return '';
1157 + }
1158 + $girdRows = ArrayHelper::get($field, 'raw.settings.grid_rows', []);
1159 + $girdRows = fluentFormSanitizer($girdRows);
1160 + $girdCols = ArrayHelper::get($field, 'raw.settings.grid_columns', []);
1161 + $girdCols = fluentFormSanitizer($girdCols);
1162 +
1163 + $value = '';
1164 + $lastRow = key(array_slice($girdData, -1, 1, true));
1165 + foreach ($girdData as $row => $column) {
1166 + $_row = $row;
1167 + if ($girdRows && isset($girdRows[$row])) {
1168 + $row = $girdRows[$row];
1169 + }
1170 + if ('markdown' === $type) {
1171 + $value .= '- *' . $row . '* : ';
495 1172 } else {
496 - if($name = ArrayHelper::get($field, 'attributes.name')) {
497 - $names[] = $name;
1173 + $value .= $row . ': ';
1174 + }
1175 + if (is_array($column)) {
1176 + foreach ($column as $index => $item) {
1177 + $_colJoiner = $colJoiner;
1178 + if ($girdCols && isset($girdCols[$item])) {
1179 + $item = $girdCols[$item];
1180 + }
1181 + if ((count($column) - 1) == $index) {
1182 + $_colJoiner = '';
1183 + }
1184 + $value .= $item . $_colJoiner;
498 1185 }
1186 + } else {
1187 + if ($girdCols && isset($girdCols[$column])) {
1188 + $column = $girdCols[$column];
1189 + }
1190 + $value .= $column;
499 1191 }
1192 + if ($_row != $lastRow) {
1193 + $value .= $rowJoiner;
1194 + }
500 1195 }
501 1196
502 - return $names;
1197 + return $value;
1198 + }
503 1199
1200 + public static function getInputNameFromShortCode($value)
1201 + {
1202 + preg_match('/{+(.*?)}/', $value, $matches);
1203 + if ($matches && false !== strpos($matches[1], 'inputs.')) {
1204 + return substr($matches[1], strlen('inputs.'));
1205 + }
1206 +
1207 + return '';
1208 + }
1209 +
1210 + public static function getRestInfo()
1211 + {
1212 + $config = wpFluentForm('config');
1213 +
1214 + $namespace = $config->get('app.rest_namespace');
1215 + $version = $config->get('app.rest_version');
1216 + $restUrl = rest_url($namespace . '/' . $version);
1217 + $restUrl = rtrim($restUrl, '/\\');
1218 +
1219 + return [
1220 + 'base_url' => esc_url_raw(rest_url()),
1221 + 'url' => $restUrl,
1222 + 'nonce' => wp_create_nonce('wp_rest'),
1223 + 'namespace' => $namespace,
1224 + 'version' => $version,
1225 + ];
1226 + }
1227 +
1228 + public static function getLogInitiator($action, $type = 'log')
1229 + {
1230 + if ('log' === $type) {
1231 + $title = ucwords(implode(' ', preg_split('/(?=[A-Z])/', $action)));
1232 + } else {
1233 + $title = ucwords(
1234 + str_replace(
1235 + ['fluentform/integration_notify_', 'fluentform_', '_notification_feed', '_'],
1236 + ['', '', '', ' '],
1237 + $action
1238 + )
1239 + );
1240 + }
1241 +
1242 + return $title;
1243 + }
1244 +
1245 + public static function getIpinfo()
1246 + {
1247 + return ArrayHelper::get(get_option('_fluentform_global_form_settings'), 'misc.geo_provider_token');
1248 + }
1249 +
1250 + public static function isAutoloadCaptchaEnabled()
1251 + {
1252 + return ArrayHelper::get(get_option('_fluentform_global_form_settings'), 'misc.autoload_captcha');
1253 + }
1254 +
1255 + public static function isAutosaveEnabled()
1256 + {
1257 + $autosaveEnabled = ArrayHelper::get(get_option('_fluentform_global_form_settings'), 'misc.autosave_enabled', 'no');
1258 + return 'yes' === $autosaveEnabled;
1259 + }
1260 +
1261 + public static function maybeDecryptUrl($url)
1262 + {
1263 + $uploadDir = str_replace('/', '\/', FLUENTFORM_UPLOAD_DIR . '/temp');
1264 + $pattern = "/(?<={$uploadDir}\/).*$/";
1265 + preg_match($pattern, $url, $match);
1266 + if (!empty($match)) {
1267 + $url = str_replace($match[0], Protector::decrypt($match[0]), $url);
1268 + }
1269 + return $url;
1270 + }
1271 +
1272 + public static function arrayFilterRecursive($arrayItems)
1273 + {
1274 + foreach ($arrayItems as $key => $item) {
1275 + is_array($item) && $arrayItems[$key] = self::arrayFilterRecursive($item);
1276 + if (empty($arrayItems[$key])) {
1277 + unset($arrayItems[$key]);
1278 + }
1279 + }
1280 + return $arrayItems;
1281 + }
1282 +
1283 + public static function isBlockEditor()
1284 + {
1285 + // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Checking REST API context
1286 + return defined('REST_REQUEST') && REST_REQUEST && !empty($_REQUEST['context']) && 'edit' === $_REQUEST['context'];
1287 + }
1288 +
1289 + public static function resolveValidationRulesGlobalOption(&$field)
1290 + {
1291 + if (isset($field['fields']) && is_array($field['fields'])) {
1292 + foreach ($field['fields'] as &$subField) {
1293 + static::resolveValidationRulesGlobalOption($subField);
1294 + }
1295 + } elseif (ArrayHelper::get($field, 'settings.validation_rules')) {
1296 + foreach ($field['settings']['validation_rules'] as $key => &$rule) {
1297 + if (!isset($rule['global'])) {
1298 + $rule['global'] = false;
1299 + }
1300 + $rule['global_message'] = static::getGlobalDefaultMessage($key);
1301 + }
1302 + }
1303 + }
1304 +
1305 + /**
1306 + * Validate form input value against database values
1307 + *
1308 + * @param $field array Form Field
1309 + * @param $formData array From Data
1310 + * @param $form object From
1311 + * @param $fieldName string optional
1312 + * @param $inputValue mixed optional
1313 + *
1314 + * @return string
1315 + * Return Error message on fail. Otherwise, return empty string
1316 + */
1317 + public static function validateInput($field, $formData, $form, $fieldName = '', $inputValue = [])
1318 + {
1319 + $error = '';
1320 + if (!$fieldName) {
1321 + $fieldName = ArrayHelper::get($field, 'name');
1322 + }
1323 + if (!$fieldName) {
1324 + return $error;
1325 + }
1326 + if (!$inputValue) {
1327 + $inputValue = ArrayHelper::get($formData, $fieldName);
1328 + }
1329 + if ($inputValue) {
1330 + $rawField = ArrayHelper::get($field, 'raw');
1331 + if (!$rawField) {
1332 + $rawField = $field;
1333 + }
1334 + $fieldType = ArrayHelper::get($rawField, 'element');
1335 + $rawField = apply_filters('fluentform/rendering_field_data_' . $fieldType, $rawField, $form);
1336 + $options = [];
1337 + $otherPrefix = '';
1338 + if ('net_promoter_score' === $fieldType) {
1339 + $options = array_flip(ArrayHelper::get($rawField, 'options', []));
1340 + } elseif ('ratings' == $fieldType) {
1341 + $options = array_keys(ArrayHelper::get($rawField, 'options', []));
1342 + } elseif ('gdpr_agreement' == $fieldType) {
1343 + $options = ['on'];
1344 + } elseif ('terms_and_condition' == $fieldType) {
1345 + $options = ['on', 'off'];
1346 + } elseif (in_array($fieldType, ['input_radio', 'select', 'input_checkbox', 'input_ranking'])) {
1347 + if (ArrayHelper::isTrue($rawField, 'attributes.multiple')) {
1348 + $fieldType = 'multi_select';
1349 + }
1350 + $formattedOptions = ArrayHelper::get($rawField, 'settings.advanced_options', []);
1351 + if (!$formattedOptions) {
1352 + $formattedOptions = [];
1353 + foreach (ArrayHelper::get($rawField, 'options', []) as $value => $label) {
1354 + $formattedOptions[] = [
1355 + 'label' => $label,
1356 + 'value' => $value,
1357 + ];
1358 + }
1359 + // @todo : Update all reference in form templates
1360 + }
1361 +
1362 + $options = array_column(self::flattenAdvancedOptions($formattedOptions), 'value');
1363 +
1364 + // Add field-specific __ff_other__ to options if "Other" option is enabled
1365 + if (in_array($fieldType, ['input_checkbox', 'input_radio']) &&
1366 + ArrayHelper::get($rawField, 'settings.enable_other_option') === 'yes') {
1367 + $fieldName = sanitize_key(str_replace(['[', ']'], '', ArrayHelper::get($rawField, 'attributes.name', '')));
1368 + $options[] = '__ff_other_' . $fieldName . '__';
1369 + $otherPrefix = static::getOtherOptionValuePrefix($rawField);
1370 + }
1371 + } elseif ('dynamic_field' == $fieldType) {
1372 + $dynamicFetchValue = 'yes' == ArrayHelper::get($rawField, 'settings.dynamic_fetch');
1373 + if ($dynamicFetchValue) {
1374 + $rawField = apply_filters('fluentform/dynamic_field_re_fetch_result_and_resolve_value', $rawField);
1375 + }
1376 + $dfElementType = ArrayHelper::get($rawField, 'attributes.type');
1377 + if (in_array($dfElementType, ['radio', 'select', 'checkbox'])) {
1378 + $fieldType = 'dynamic_field_options';
1379 + $options = array_column(
1380 + ArrayHelper::get($rawField, 'settings.advanced_options', []),
1381 + 'value'
1382 + );
1383 + }
1384 + }
1385 +
1386 + if ($options) {
1387 + $options = array_map('sanitize_text_field', $options);
1388 + }
1389 +
1390 + $isValid = true;
1391 + switch ($fieldType) {
1392 + case 'input_ranking':
1393 + $skipValidationInputsWithOptions = apply_filters('fluentform/skip_validation_inputs_with_options', false, $fieldType, $form, $formData);
1394 + if ($skipValidationInputsWithOptions) {
1395 + break;
1396 + }
1397 +
1398 + if (!is_array($inputValue)) {
1399 + $isValid = false;
1400 + break;
1401 + }
1402 +
1403 + $filteredValues = array_values(array_filter(array_map('sanitize_text_field', $inputValue), function ($value) {
1404 + return '' !== $value;
1405 + }));
1406 +
1407 + $normalizedOptions = array_values(array_filter(array_map('sanitize_text_field', $options), function ($value) {
1408 + return '' !== $value;
1409 + }));
1410 +
1411 + sort($filteredValues);
1412 + sort($normalizedOptions);
1413 +
1414 + $isValid = count($inputValue) === count($options)
1415 + && count($filteredValues) === count(array_unique($filteredValues))
1416 + && $filteredValues === $normalizedOptions;
1417 + break;
1418 + case 'input_radio':
1419 + case 'select':
1420 + case 'net_promoter_score':
1421 + case 'ratings':
1422 + case 'gdpr_agreement':
1423 + case 'terms_and_condition':
1424 + case 'input_checkbox':
1425 + case 'multi_select':
1426 + case 'dynamic_field_options':
1427 + $skipValidationInputsWithOptions = apply_filters('fluentform/skip_validation_inputs_with_options', false, $fieldType, $form, $formData);
1428 + if ($skipValidationInputsWithOptions) {
1429 + break;
1430 + }
1431 + if (is_array($inputValue)) {
1432 + // Skip "Other" values — raw, localized or legacy English prefix
1433 + $filteredValues = array_filter($inputValue, function ($value) use ($otherPrefix) {
1434 + return !preg_match('/^__ff_other_.*__$/', $value) &&
1435 + !preg_match('/^Other:\s/', $value) &&
1436 + !($otherPrefix && 0 === strpos($value, $otherPrefix));
1437 + });
1438 + $isValid = array_diff($filteredValues, $options);
1439 + $isValid = empty($isValid);
1440 + } elseif (preg_match('/^__ff_other_.*__$/', $inputValue) ||
1441 + preg_match('/^Other:\s/', $inputValue) ||
1442 + ($otherPrefix && 0 === strpos($inputValue, $otherPrefix))) {
1443 + // Accept "Other" values — raw, localized or legacy English prefix
1444 + $isValid = true;
1445 + } else {
1446 + $isValid = in_array($inputValue, $options);
1447 + }
1448 + break;
1449 + case 'input_number':
1450 + if (is_array($inputValue)) {
1451 + $hasNonNumricValue = in_array(false, array_map('is_numeric', $inputValue));
1452 + if ($hasNonNumricValue) {
1453 + $isValid = false;
1454 + }
1455 + } else {
1456 + $isValid = is_numeric($inputValue);
1457 + }
1458 + break;
1459 + case 'select_country':
1460 + $fieldData = ArrayHelper::get($field, 'raw');
1461 + $data = (new SelectCountry())->loadCountries($fieldData);
1462 + $validCountries = ArrayHelper::get($fieldData, 'settings.country_list.priority_based', []);
1463 + $validCountries = array_merge($validCountries, array_keys((array) ArrayHelper::get($data, 'options', [])));
1464 + $isValid = in_array($inputValue, $validCountries);
1465 + break;
1466 + case 'repeater_field':
1467 + case 'repeater_container':
1468 + foreach (ArrayHelper::get($rawField, 'fields', []) as $index => $repeaterField) {
1469 + $repeaterFieldValue = array_filter(array_column($inputValue, $index));
1470 + if ($repeaterFieldValue && $error = static::validateInput($repeaterField, $formData, $form,
1471 + $fieldName, $repeaterFieldValue)) {
1472 + $isValid = false;
1473 + break;
1474 + }
1475 + }
1476 + break;
1477 + case 'tabular_grid':
1478 + $rows = array_keys(ArrayHelper::get($rawField, 'settings.grid_rows', []));
1479 + $rows = array_map(function ($row) {
1480 + return trim(sanitize_text_field($row));
1481 + }, $rows);
1482 +
1483 + $submittedRows = array_keys(ArrayHelper::get($formData, $fieldName, []));
1484 + $submittedRows = array_map('trim', $submittedRows);
1485 +
1486 + $rowDiff = array_diff($submittedRows, $rows);
1487 +
1488 + $isValid = empty($rowDiff);
1489 + if ($isValid) {
1490 + $columns = array_keys(ArrayHelper::get($rawField, 'settings.grid_columns', []));
1491 + $columns = array_map(function ($column) {
1492 + return trim(sanitize_text_field($column));
1493 + }, $columns);
1494 + $submittedCols = ArrayHelper::flatten(ArrayHelper::get($formData, $fieldName, []));
1495 + $submittedCols = array_map('trim', $submittedCols);
1496 + $colDiff = array_diff($submittedCols, $columns);
1497 + $isValid = empty($colDiff);
1498 + }
1499 + break;
1500 + default:
1501 + break;
1502 + }
1503 + if (!$isValid) {
1504 + $error = __('The given data was invalid', 'fluentform');
1505 + }
1506 + }
1507 + return $error;
1508 + }
1509 +
1510 + /**
1511 + * Enforce how many options a field allows the user to pick.
1512 + *
1513 + * A field with no selections is left alone, so a floor never turns an
1514 + * optional field into a required one — that is what `required` is for.
1515 + *
1516 + * @param array $rawField
1517 + * @param mixed $inputValue
1518 + * @return array rule name => message, empty when within the limits
1519 + */
1520 + public static function validateSelectionLimits($rawField, $inputValue)
1521 + {
1522 + // Distinct choices, not array entries. The same option repeated is one
1523 + // answer: counting entries lets a crafted post satisfy a floor of two by
1524 + // sending one option twice, and lets padding manufacture a ceiling breach
1525 + // the visitor never made. No UI can produce a duplicate, so this only
1526 + // ever arrives crafted. A filled-in "Other" is a single element and still
1527 + // counts once.
1528 + $selected = is_array($inputValue) ? count(array_unique(array_filter($inputValue, function ($value) {
1529 + return '' !== $value && null !== $value;
1530 + }))) : (('' === $inputValue || null === $inputValue) ? 0 : 1);
1531 +
1532 + if (!$selected) {
1533 + return [];
1534 + }
1535 +
1536 + $rules = ArrayHelper::get($rawField, 'settings.validation_rules', []);
1537 +
1538 + $limits = [
1539 + 'min_selection' => ArrayHelper::get($rules, 'min_selection.value'),
1540 + 'max_selection' => static::resolveMaxSelection($rawField),
1541 + ];
1542 +
1543 + $errors = [];
1544 +
1545 + foreach ($limits as $rule => $limit) {
1546 + // '' is how "no limit" ships, so it must never mean a limit of zero.
1547 + if ('' === $limit || null === $limit || !is_numeric($limit)) {
1548 + continue;
1549 + }
1550 +
1551 + $limit = (int) $limit;
1552 +
1553 + if ($limit < 1) {
1554 + continue;
1555 + }
1556 +
1557 + $breached = 'min_selection' === $rule ? $selected < $limit : $selected > $limit;
1558 +
1559 + if ($breached) {
1560 + $errors[$rule] = static::getSelectionLimitMessage($rules, $rule);
1561 + }
1562 + }
1563 +
1564 + return $errors;
1565 + }
1566 +
1567 + /**
1568 + * The effective ceiling for a field, preferring the rule over the legacy
1569 + * `settings.max_selection`.
1570 + *
1571 + * The rule takes ownership as soon as its KEY exists, empty value included.
1572 + * Falling back on an empty value instead would make the limit unremovable:
1573 + * nothing writes to the legacy setting any more, so clearing the box in the
1574 + * editor would silently drop back to whatever was frozen there.
1575 + *
1576 + * @param array $field
1577 + * @return mixed
1578 + */
1579 + public static function resolveMaxSelection($field)
1580 + {
1581 + $rules = ArrayHelper::get($field, 'settings.validation_rules', []);
1582 +
1583 + if (is_array($rules) && array_key_exists('max_selection', $rules)) {
1584 + return ArrayHelper::get($rules, 'max_selection.value');
1585 + }
1586 +
1587 + return ArrayHelper::get($field, 'settings.max_selection');
1588 + }
1589 +
1590 + /**
1591 + * The field's own wording for a breached limit, or the site-wide default.
1592 + *
1593 + * @param array $rules
1594 + * @param string $rule
1595 + * @return string
1596 + */
1597 + public static function getSelectionLimitMessage($rules, $rule)
1598 + {
1599 + // `global_message` is a copy taken when the field was last saved, so it
1600 + // goes stale the moment Global Settings change — resolve the live value
1601 + // the way every other rule does.
1602 + $message = ArrayHelper::isTrue($rules, $rule . '.global')
1603 + ? static::getGlobalDefaultMessage($rule)
1604 + : ArrayHelper::get($rules, $rule . '.message');
1605 +
1606 + if (!$message) {
1607 + $message = static::getGlobalDefaultMessage($rule);
1608 + }
1609 +
1610 + return apply_filters('fluentform/selection_limit_message', $message, $rule, $rules);
1611 + }
1612 +
1613 + /**
1614 + * Prefix used to store a checkable field's "Other" option value,
1615 + * built from the field's own (translated) label. Pass $form to run
1616 + * the field through the rendering filter (translation plugins) first.
1617 + *
1618 + * @param array $rawField
1619 + * @param object|null $form
1620 + * @return string
1621 + */
1622 + public static function getOtherOptionValuePrefix($rawField, $form = null)
1623 + {
1624 + $fieldType = ArrayHelper::get($rawField, 'element');
1625 + if ($form && $fieldType) {
1626 + $rawField = apply_filters('fluentform/rendering_field_data_' . $fieldType, $rawField, $form);
1627 + }
1628 +
1629 + $label = trim((string) ArrayHelper::get($rawField, 'settings.other_option_label'));
1630 +
1631 + if ('' === $label) {
1632 + $label = __('Other', 'fluentform');
1633 + }
1634 +
1635 + // Avoid "::" when the label already ends with a colon
1636 + return ':' === substr($label, -1) ? $label . ' ' : $label . ': ';
1637 + }
1638 +
1639 + public static function getWhiteListedFields($formId)
1640 + {
1641 + $whiteListedFields = [
1642 + '__fluent_form_embded_post_id',
1643 + '_fluentform_' . $formId . '_fluentformnonce',
1644 + '_wp_http_referer',
1645 + 'g-recaptcha-response',
1646 + 'h-captcha-response',
1647 + 'cf-turnstile-response',
1648 + '__stripe_payment_method_id',
1649 + '__ff_all_applied_coupons',
1650 + '__entry_intermediate_hash',
1651 + '__square_payment_method_id',
1652 + '__square_verify_buyer_id',
1653 + 'ct_bot_detector_event_token',
1654 + 'ff_ct_form_load_time',
1655 + ];
1656 +
1657 + return apply_filters('fluentform/white_listed_fields', $whiteListedFields, $formId);
1658 + }
1659 +
1660 + /**
1661 + * Shortcode parse on validation message
1662 + *
1663 + * @param string $message
1664 + * @param object $form
1665 + * @param string $fieldName
1666 + * @return string
1667 + */
1668 + public static function shortCodeParseOnValidationMessage($message, $form, $fieldName)
1669 + {
1670 + // Return early if form is null to prevent errors
1671 + if (null === $form) {
1672 + return $message;
1673 + }
1674 +
1675 + // For validation message there is no entry & form data
1676 + // Add 'current_field' name as data array to resolve {labels.current_field} shortcode if it has
1677 + return ShortCodeParser::parse(
1678 + $message,
1679 + (object) ['response' => '', 'form_id' => $form->id],
1680 + ['current_field' => $fieldName],
1681 + $form
1682 + );
1683 + }
1684 +
1685 + public static function getAjaxUrl()
1686 + {
1687 + return apply_filters('fluentform/ajax_url', admin_url('admin-ajax.php'));
1688 + }
1689 +
1690 + public static function getDefaultDateTimeFormatForMoment()
1691 + {
1692 + $phpFormat = get_option('date_format') . ' ' . get_option('time_format');
1693 +
1694 + $replacements = [
1695 + 'A' => 'A', // for the sake of escaping below
1696 + 'a' => 'a', // for the sake of escaping below
1697 + 'B' => '', // Swatch internet time (.beats), no equivalent
1698 + 'c' => 'YYYY-MM-DD[T]HH:mm:ssZ', // ISO 8601
1699 + 'D' => 'ddd',
1700 + 'd' => 'DD',
1701 + 'e' => 'zz', // deprecated since version 1.6.0 of moment.js
1702 + 'F' => 'MMMM',
1703 + 'G' => 'H',
1704 + 'g' => 'h',
1705 + 'H' => 'HH',
1706 + 'h' => 'hh',
1707 + 'I' => '', // Daylight Saving Time? => moment().isDST();
1708 + 'i' => 'mm',
1709 + 'j' => 'D',
1710 + 'L' => '', // Leap year? => moment().isLeapYear();
1711 + 'l' => 'dddd',
1712 + 'M' => 'MMM',
1713 + 'm' => 'MM',
1714 + 'N' => 'E',
1715 + 'n' => 'M',
1716 + 'O' => 'ZZ',
1717 + 'o' => 'YYYY',
1718 + 'P' => 'Z',
1719 + 'r' => 'ddd, DD MMM YYYY HH:mm:ss ZZ', // RFC 2822
1720 + 'S' => 'o',
1721 + 's' => 'ss',
1722 + 'T' => 'z', // deprecated since version 1.6.0 of moment.js
1723 + 't' => '', // days in the month => moment().daysInMonth();
1724 + 'U' => 'X',
1725 + 'u' => 'SSSSSS', // microseconds
1726 + 'v' => 'SSS', // milliseconds (from PHP 7.0.0)
1727 + 'W' => 'W', // for the sake of escaping below
1728 + 'w' => 'e',
1729 + 'Y' => 'YYYY',
1730 + 'y' => 'YY',
1731 + 'Z' => '', // time zone offset in minutes => moment().zone();
1732 + 'z' => 'DDD',
1733 + ];
1734 +
1735 + // Converts escaped characters.
1736 + foreach ($replacements as $from => $to) {
1737 + $replacements['\\' . $from] = '[' . $from . ']';
1738 + }
1739 +
1740 + $format = strtr($phpFormat, $replacements);
1741 +
1742 + return apply_filters('fluentform/moment_date_time_format', $format);
1743 + }
1744 +
1745 + public static function isDefaultWPDateEnabled()
1746 + {
1747 + $globalSettings = get_option('_fluentform_global_form_settings');
1748 + return 'wp_default' === ArrayHelper::get($globalSettings, 'misc.default_admin_date_time');
1749 + }
1750 +
1751 + public static function isPaymentCompatible()
1752 + {
1753 + if (!self::hasPro()) {
1754 + return true;
1755 + } else {
1756 + return version_compare(FLUENTFORMPRO_VERSION, FLUENTFORM_MINIMUM_PRO_VERSION, '>=');
1757 + }
1758 + }
1759 +
1760 + /**
1761 + * Determine pro payment script is compatible or not
1762 + * Script is compatible if pro version meets the minimum required version
1763 + *
1764 + * @return bool
1765 + */
1766 + public static function isProPaymentScriptCompatible()
1767 + {
1768 + if (self::hasPro()) {
1769 + return version_compare(FLUENTFORMPRO_VERSION, FLUENTFORM_MINIMUM_PRO_VERSION, '>=');
1770 + }
1771 + return false;
1772 + }
1773 +
1774 + public static function hasPro()
1775 + {
1776 + return defined('FLUENTFORMPRO');
1777 + }
1778 +
1779 + public static function utmUrl($baseUrl, $utmContent = '', $utmCampaign = 'upgrade_pro')
1780 + {
1781 + $params = [
1782 + 'utm_source' => 'fluent-forms',
1783 + 'utm_medium' => self::hasPro() ? 'pro_plugin' : 'free_plugin',
1784 + 'utm_campaign' => $utmCampaign,
1785 + 'utm_term' => FLUENTFORM_VERSION,
1786 + 'theme_style' => fluentform_get_active_theme_slug(),
1787 + ];
1788 +
1789 + if ($utmContent) {
1790 + $params['utm_content'] = $utmContent;
1791 + }
1792 +
1793 + return add_query_arg($params, $baseUrl);
1794 + }
1795 +
1796 + public static function getLandingPageEnabledForms()
1797 + {
1798 + if (class_exists(\FluentFormPro\classes\SharePage\SharePage::class)) {
1799 + if (method_exists(\FluentFormPro\classes\SharePage\SharePage::class, 'getLandingPageFormIds')) {
1800 + $sharePage = new \FluentFormPro\classes\SharePage\SharePage();
1801 + return $sharePage->getLandingPageFormIds();
1802 + }
1803 + }
1804 + return [];
1805 + }
1806 +
1807 + public static function sanitizeArrayKeysAndValues($values)
1808 + {
1809 + if (is_array($values)) {
1810 + $sanitized = [];
1811 + foreach ($values as $key => $value) {
1812 + $trimmedKey = sanitize_text_field(trim($key));
1813 + $trimmedValue = sanitize_text_field(trim($value));
1814 + $sanitized[$trimmedKey] = $trimmedValue;
1815 + }
1816 + return $sanitized;
1817 + }
1818 + return sanitize_text_field(trim($values));
1819 + }
1820 + public static function getFrontendFacingUrl($args = '')
1821 + {
1822 + return home_url($args);
1823 + }
1824 +
1825 + public static function getCountryCodeFromHeaders($forRestriction = false)
1826 + {
1827 + // SECURITY (FINDING-26): CDN country headers are client-spoofable. Trust them for analytics
1828 + // storage (spoof is cosmetic) but not for restriction enforcement (spoof = bypass). Filterable.
1829 + $trustHeaders = apply_filters('fluentform/trust_geo_headers', !$forRestriction);
1830 + if (!$trustHeaders) {
1831 + return null;
1832 + }
1833 +
1834 + $headers = [
1835 + // Cloudflare (most common)
1836 + 'HTTP_CF_IPCOUNTRY',
1837 + 'CF-IPCountry',
1838 +
1839 + // AWS CloudFront (widely used)
1840 + 'HTTP_CLOUDFRONT_VIEWER_COUNTRY',
1841 + 'CloudFront-Viewer-Country',
1842 +
1843 + // Common standard headers
1844 + 'HTTP_X_COUNTRY_CODE',
1845 + 'X-Country-Code',
1846 + 'HTTP_X_FORWARDED_COUNTRY',
1847 + 'X-Forwarded-Country',
1848 +
1849 + // GeoIP (used by many systems)
1850 + 'HTTP_GEOIP_COUNTRY_CODE',
1851 + 'GEOIP_COUNTRY_CODE',
1852 + 'HTTP_X_GEOIP_COUNTRY',
1853 + 'X-GeoIP-Country',
1854 +
1855 + // General purpose country headers
1856 + 'HTTP_X_COUNTRY',
1857 + 'X-Country',
1858 + 'HTTP_X_COUNTRY_ISO',
1859 + 'X-Country-ISO',
1860 + ];
1861 +
1862 + foreach ($headers as $header) {
1863 + // Try directly from $_SERVER
1864 + if (isset($_SERVER[$header])) {
1865 + // phpcs:ignore WordPress.Security.ValidatedSanitizedInput -- Country code from CDN/proxy header, validated below
1866 + $code = trim(sanitize_text_field(wp_unslash($_SERVER[$header])));
1867 + } elseif (strpos($header, 'HTTP_') !== 0) {
1868 + // Try with HTTP_ prefix if not already present
1869 + $httpHeader = 'HTTP_' . str_replace('-', '_', strtoupper($header));
1870 + if (isset($_SERVER[$httpHeader])) {
1871 + // phpcs:ignore WordPress.Security.ValidatedSanitizedInput -- Country code from CDN/proxy header, validated below
1872 + $code = trim(sanitize_text_field(wp_unslash($_SERVER[$httpHeader])));
1873 + } else {
1874 + continue;
1875 + }
1876 + } else {
1877 + continue;
1878 + }
1879 +
1880 + // Basic validation - should be 2-letter country code
1881 + if (!empty($code) && is_string($code) && 2 === strlen($code) && ctype_alpha($code) && 'XX' !== $code) {
1882 + return strtoupper($code);
1883 + }
1884 + }
1885 +
1886 + return null;
1887 + }
1888 +
1889 + /**
1890 + * Fixes PHP Object Injection Vulnerability
1891 + *
1892 + * @param $data
1893 + * @return mixed
1894 + */
1895 + public static function safeUnserialize($data)
1896 + {
1897 + if (is_serialized($data)) { // Don't attempt to unserialize data that wasn't serialized going in.
1898 + return @unserialize(trim($data), ['allowed_classes' => false]);
1899 + }
1900 + return $data;
1901 + }
1902 +
1903 + /**
1904 + * If elementor editor is open
1905 + *
1906 + * @return bool
1907 + */
1908 + public static function isElementorEditor()
1909 + {
1910 + return defined('ELEMENTOR_VERSION') &&
1911 + class_exists('\Elementor\Plugin') &&
1912 + isset(\Elementor\Plugin::$instance) &&
1913 + \Elementor\Plugin::$instance->editor->is_edit_mode();
1914 + }
1915 +
1916 + /**
1917 + * Check if we're in block editor context (Site Editor, Template Editor, or Post/Page Editor)
1918 + * Covers all Gutenberg block editor contexts including mobile/tablet preview iframes
1919 + *
1920 + * @return bool
1921 + */
1922 + public static function isSiteEditor()
1923 + {
1924 + if (!is_admin() || !function_exists('get_current_screen')) {
1925 + return false;
1926 + }
1927 +
1928 + $screen = get_current_screen();
1929 +
1930 + // Check for Site Editor and Template Editor contexts
1931 + if ($screen && in_array($screen->base, ['site-editor', 'edit-site', 'appearance_page_gutenberg-edit-site'], true )) {
1932 + return true;
1933 + }
1934 +
1935 + // Check for Post/Page block editor contexts
1936 + if ($screen && in_array($screen->base, ['post', 'page'], true) && $screen->is_block_editor()) {
1937 + return true;
1938 + }
1939 +
1940 + // Check for custom post types with block editor
1941 + if ($screen && $screen->is_block_editor()) {
1942 + return true;
1943 + }
1944 +
1945 + // Fallback checks for various block editor contexts
1946 + // phpcs:ignore WordPress.Security.ValidatedSanitizedInput -- REQUEST_URI used for string comparison only
1947 + $request_uri = isset($_SERVER['REQUEST_URI']) ? sanitize_text_field(wp_unslash($_SERVER['REQUEST_URI'])) : '';
1948 + // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Checking block editor context
1949 + return isset( $_GET['_wp-find-template'] ) ||
1950 + strpos( $request_uri, 'site-editor.php' ) !== false ||
1951 + // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Checking REST API context
1952 + (defined('REST_REQUEST') && REST_REQUEST && !empty($_REQUEST['context']) && 'edit' === $_REQUEST['context']);
504 1953 }
505 1954 }