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

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

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