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

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