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

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