PluginProbe
Fluent Forms – Customizable Contact Forms, Survey, Quiz, & Conversational Form Builder / 6.2.12
Fluent Forms – Customizable Contact Forms, Survey, Quiz, & Conversational Form Builder v6.2.12
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
fluentform / app / Helpers / Helper.php

Helper.php in Fluent Forms – Customizable Contact Forms, Survey, Quiz, & Conversational Form Builder 6.2.12, at app/Helpers/Helper.php

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