PluginProbe
Fluent Forms – Customizable Contact Forms, Survey, Quiz, & Conversational Form Builder / 6.2.13
Fluent Forms – Customizable Contact Forms, Survey, Quiz, & Conversational Form Builder v6.2.13
6.2.15 6.2.14 6.2.13 6.2.12 6.2.10 6.2.11 6.2.9 6.2.8 6.2.7 6.2.6 6.2.5 6.2.4 6.2.3 6.2.2 3.6.22 3.6.31 3.6.40 3.6.41 3.6.42 3.6.50 3.6.51 3.6.60 3.6.61 3.6.62 3.6.64 All 197 releases
fluentform / app / Services / FluentConversational / Classes / Converter / Converter.php

Converter.php in Fluent Forms – Customizable Contact Forms, Survey, Quiz, & Conversational Form Builder 6.2.13, at app/Services/FluentConversational/Classes/Converter/Converter.php

1,544 lines 73.3 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\Services\FluentConversational\Classes\Converter;
4
5 defined('ABSPATH') or die;
6
7 use FluentForm\App\Helpers\Helper;
8 use FluentForm\App\Modules\Payments\PaymentHelper;
9 use FluentForm\App\Modules\Component\Component;
10 use FluentForm\App\Services\FormBuilder\RatingIcon;
11 use FluentForm\App\Services\FormBuilder\Components\DateTime;
12 use FluentForm\App\Modules\Form\FormFieldsParser;
13 use FluentForm\Framework\Helpers\ArrayHelper;
14
15 class Converter
16 {
17 /**
18 * Get Component instance
19 */
20 private static function getComponent()
21 {
22 static $component = null;
23 if ($component === null) {
24 $component = new Component(wpFluentForm());
25 }
26 return $component;
27 }
28
29 public static function convert($form)
30 {
31 // Return early if form is null or invalid
32 if (!$form || !is_object($form)) {
33 return $form;
34 }
35
36 $fields = $form->fields['fields'];
37
38 $form->submit_button = $form->fields['submitButton'];
39
40 $form->reCaptcha = false;
41 $form->hCaptcha = false;
42 $form->turnstile = false;
43 $form->hasCalculation = false;
44
45 $questions = [];
46
47 $imagePreloads = [];
48
49 $allowedFields = static::fieldTypes();
50
51 $hasSaveAndResume = static::hasSaveAndResume($form);
52 $saveAndResumeData = [];
53
54 if ($hasSaveAndResume) {
55 $saveAndResumeData = static::getSaveAndResumeData($form);
56 $form->stepCompleted = intval(ArrayHelper::get($saveAndResumeData, 'step_completed', 0));
57 }
58
59 foreach ($fields as $field) {
60 $field = apply_filters_deprecated('fluentform_rendering_field_data_' . $field['element'],
61 [
62 $field,
63 $form
64 ],
65 FLUENTFORM_FRAMEWORK_UPGRADE,
66 'fluentform/rendering_field_data_' . $field['element'],
67 'Use fluentform/rendering_field_data_' . $field['element'] . ' instead of fluentform_rendering_field_data_' . $field['element']
68 );
69
70 $field = apply_filters('fluentform/rendering_field_data_' . $field['element'], $field, $form);
71
72 $validationsRules = self::resolveValidationsRules($field, $form);
73
74 $question = static::buildBaseQuestion($field, $validationsRules, $form);
75
76 if (!$hasSaveAndResume && $answer = self::setDefaultValue(ArrayHelper::get($field, 'attributes.value'), $field, $form)) {
77 $question['answer'] = $answer;
78 }
79
80 if ($hasSaveAndResume && $saveAndResumeData) {
81 $response = ArrayHelper::get($saveAndResumeData, 'response');
82 $questionId = ArrayHelper::get($question, 'id');
83
84 // Special handling for section breaks and custom HTML
85 if (ArrayHelper::get($field, 'element') == 'section_break' || ArrayHelper::get($field, 'element') == 'custom_html') {
86 $question['answered'] = true;
87 $question['answer'] = 'section_viewed';
88 } else {
89 $value = $questionId ? ArrayHelper::get($response, $questionId) : null;
90 if (!empty($value)) {
91 if (in_array(ArrayHelper::get($field, 'element'), ['input_file', 'input_image'])) {
92 $files = ArrayHelper::get($response, $questionId);
93 foreach ($files as $file) {
94 if (is_array($file)) {
95 $question['answer'][] = ArrayHelper::get($file, 'data_src');
96 } else {
97 // Already a plain URL string (legacy or non-encrypted)
98 $question['answer'][] = $file;
99 }
100 }
101 } elseif (ArrayHelper::get($field, 'element') == 'signature') {
102 // Signature is stored as a URL string
103 $question['answer'] = $value;
104 } elseif (
105 ArrayHelper::get($field, 'element') == 'rangeslider' ||
106 ArrayHelper::get($field, 'element') == 'subscription_payment_component' ||
107 ArrayHelper::get($field, 'element') == 'net_promoter_score'
108 ) {
109 $question['answer'] = +$value;
110 } else {
111 $question['answer'] = $value;
112 }
113 }
114 }
115 }
116
117 if ('default' != ArrayHelper::get($question, 'style_pref.layout')) {
118 $media = ArrayHelper::get($question, 'style_pref.media');
119 if ($media) {
120 $imagePreloads[] = $media;
121 }
122 }
123 if ('address' === $field['element']) {
124 if ($order = ArrayHelper::get($field, 'settings.field_order')) {
125 $order = array_values(array_column($order, 'value'));
126 $fields = ArrayHelper::get($field, 'fields');
127 $field['fields'] = array_merge(array_flip($order), $fields);
128 }
129
130 $provider = ArrayHelper::get($field, 'settings.autocomplete_provider');
131 $isLegacyProvider = !ArrayHelper::has($field, 'settings.autocomplete_provider');
132 $googleAutoComplete = 'yes' === ArrayHelper::get($field, 'settings.enable_g_autocomplete');
133 if (defined('FLUENTFORMPRO') && ($provider === 'google' || ($isLegacyProvider && $googleAutoComplete))) {
134 $question['ff_map_autocomplete'] = true;
135 $question['ff_with_g_map'] = 'yes' == ArrayHelper::get($field, 'settings.enable_g_map');
136 $question['ff_with_auto_locate'] = ArrayHelper::get($field, 'settings.enable_auto_locate', false);
137 $question['GmapApiKey'] = apply_filters('fluentform/conversational_form_address_gmap_api_key', '');
138 }
139
140 // Handle HTML5 geolocation
141 if ($provider === 'html5') {
142 $question['ff_html5_geolocate'] = true;
143 $question['ff_html5_locate'] = ArrayHelper::get($field, 'settings.enable_auto_locate', 'on_click');
144 }
145
146 foreach ($field['fields'] as $item) {
147 if ($item['settings']['visible']) {
148 $itemName = ArrayHelper::get($item, 'attributes.name');
149 if ($parentName = ArrayHelper::get($question, 'name')) {
150 $itemName = $parentName . '.' . $itemName;
151 }
152 $validationsRules = self::resolveValidationsRules($item, $form, $itemName);
153 $itemQuestion = [
154 'title' => self::getComponent()->replaceEditorSmartCodes(ArrayHelper::get($item, 'settings.label'), $form),
155 'container_class' => ArrayHelper::get($item, 'settings.container_class'),
156 'required' => ArrayHelper::get($validationsRules, 'required.value'),
157 'requiredMsg' => ArrayHelper::get($validationsRules, 'required.message'),
158 'errorMessage' => ArrayHelper::get($validationsRules, 'required.message'),
159 'validationRules' => $validationsRules,
160 'conditional_logics' => self::parseConditionalLogic($item),
161 ];
162 if ('select_country' === $item['element']) {
163 $countryComponent = new \FluentForm\App\Services\FormBuilder\Components\SelectCountry();
164 $item = $countryComponent->loadCountries($item);
165 $activeList = ArrayHelper::get($item, 'settings.country_list.active_list');
166 if ('priority_based' == $activeList) {
167 $selectCountries = ArrayHelper::get($item, 'settings.country_list.priority_based', []);
168 $priorityCountries = $countryComponent->getSelectedCountries($selectCountries);
169 $item['options'] = array_merge($priorityCountries, $item['options']);
170 }
171
172 if ('visible_list' === $activeList && $googleAutoComplete) {
173 $restrictionCountries = (array) ArrayHelper::get($item, 'attributes.value', []);
174 $restrictionCountries = array_unique(
175 array_merge(
176 $restrictionCountries,
177 ArrayHelper::get($item, 'settings.country_list.visible_list', [])
178 )
179 );
180 $question['autocomplete_restrictions'] = array_filter($restrictionCountries);
181 }
182
183 $options = [];
184 $countries = $item['options'];
185 foreach ($countries as $key => $value) {
186 $options[] = [
187 'label' => html_entity_decode($value, ENT_QUOTES, 'UTF-8'),
188 'value' => $key,
189 ];
190 }
191 $item['type'] = 'FlowFormTextType';
192 $item['options'] = $options;
193 }
194 if ($itemQuestion['required']) {
195 $question['requiredFields'][] = [
196 "name" => ArrayHelper::get($item, 'attributes.name', ''),
197 'requiredMessage' => ArrayHelper::get($itemQuestion, 'requiredMsg')
198 ];
199 $question['required'] = true;
200 }
201
202 if (!$hasSaveAndResume) {
203 $item['attributes']['value'] = self::setDefaultValue(ArrayHelper::get($item, 'attributes.value', ''), $item, $form);;
204 }
205 $question['fields'][] = wp_parse_args($itemQuestion, $item);
206 }
207 }
208 } elseif ('input_name' === $field['element']) {
209
210 foreach ($field['fields'] as $item) {
211 if ($item['settings']['visible']) {
212 $itemName = ArrayHelper::get($item, 'attributes.name');
213 if ($parentName = ArrayHelper::get($question, 'name')) {
214 $itemName = $parentName . '.' . $itemName;
215 }
216 $validationsRules = self::resolveValidationsRules($item, $form, $itemName);
217 $itemQuestion = [
218 'title' => self::getComponent()->replaceEditorSmartCodes(ArrayHelper::get($item, 'settings.label'), $form),
219 'container_class' => ArrayHelper::get($item, 'settings.container_class'),
220 'required' => ArrayHelper::get($validationsRules, 'required.value'),
221 'requiredMsg' => ArrayHelper::get($validationsRules, 'required.message'),
222 'errorMessage' => ArrayHelper::get($validationsRules, 'required.message'),
223 'validationRules' => $validationsRules,
224 'conditional_logics' => self::parseConditionalLogic($item),
225 ];
226 if ($itemQuestion['required']) {
227 $question['requiredFields'][] = [
228 "name" => ArrayHelper::get($item, 'attributes.name', ''),
229 'requiredMessage' => ArrayHelper::get($itemQuestion, 'requiredMsg', '')
230 ];
231 $question['required'] = true;
232 }
233
234 $item['attributes']['value'] = self::getComponent()->replaceEditorSmartCodes(ArrayHelper::get($item, 'attributes.value'), $form);
235
236 $question['fields'][] = wp_parse_args($itemQuestion, $item);
237 }
238 }
239 } elseif ('input_text' === $field['element']) {
240 $mask = ArrayHelper::get($field, 'settings.temp_mask');
241
242 $mask = 'custom' === $mask ? ArrayHelper::get($field, 'attributes.data-mask') : $mask;
243
244 if ($mask) {
245 $question['mask'] = $mask;
246 }
247 } elseif ('welcome_screen' === $field['element']) {
248 $question['settings'] = ArrayHelper::get($field, 'settings', []);
249 $question['subtitle'] = self::getComponent()->replaceEditorSmartCodes(ArrayHelper::get($field, 'settings.description'), $form);
250 $question['required'] = false;
251 // $question['css'] = (new \FluentConversational\Form)->getSubmitBttnStyle($field);
252 } elseif ('select' === $field['element']) {
253 $question['options'] = self::getAdvancedOptions($field, $form);
254 $question['placeholder'] = self::getComponent()->replaceEditorSmartCodes(ArrayHelper::get($field, 'settings.placeholder', null), $form);
255 $question['searchable'] = ArrayHelper::get($field, 'settings.enable_select_2');
256 $isMultiple = ArrayHelper::get($field, 'attributes.multiple', false);
257
258 if ($isMultiple) {
259 $question['multiple'] = true;
260 $question['placeholder'] = self::getComponent()->replaceEditorSmartCodes(ArrayHelper::get($field, 'attributes.placeholder', false), $form);
261 $maxSelection = Helper::resolveMaxSelection($field);
262 $question['max_selection'] = $maxSelection ? intval($maxSelection) : 0;
263
264 // Resolved here so a form on the legacy setting — which has no
265 // rule to read a message from — still has wording.
266 $question['max_selection_message'] = Helper::getSelectionLimitMessage(
267 ArrayHelper::get($field, 'settings.validation_rules', []),
268 'max_selection'
269 );
270 }
271 } elseif ('select_country' === $field['element']) {
272 $countryComponent = new \FluentForm\App\Services\FormBuilder\Components\SelectCountry();
273 $field = $countryComponent->loadCountries($field);
274 $activeList = ArrayHelper::get($field, 'settings.country_list.active_list');
275 if ('priority_based' == $activeList) {
276 $selectCountries = ArrayHelper::get($field, 'settings.country_list.priority_based', []);
277 $priorityCountries = $countryComponent->getSelectedCountries($selectCountries);
278 // @todo : add opt group in conversation js
279 $question['has_opt_grp'] = true;
280 $primaryListLabel = ArrayHelper::get($field, 'settings.primary_label');
281 $otherListLabel = ArrayHelper::get($field, 'settings.other_label');
282 $field['options'] = array_merge($priorityCountries, $field['options']);
283 }
284
285 $options = [];
286 $countries = $field['options'];
287 foreach ($countries as $key => $value) {
288 $options[] = [
289 'label' => html_entity_decode($value, ENT_QUOTES, 'UTF-8'),
290 'value' => $key,
291 ];
292 }
293 $question['options'] = $options;
294 $question['placeholder'] = self::getComponent()->replaceEditorSmartCodes(ArrayHelper::get($field, 'attributes.placeholder', null), $form);
295 $question['searchable'] = ArrayHelper::get($field, 'settings.enable_select_2');
296 } elseif ('dynamic_field' === $field['element']) {
297 $dynamicFetchValue = 'yes' == ArrayHelper::get($field, 'settings.dynamic_fetch');
298 if ($dynamicFetchValue) {
299 $field = apply_filters('fluentform/dynamic_field_re_fetch_result_and_resolve_value', $field);
300 $question['answer'] = self::getComponent()->replaceEditorSmartCodes(ArrayHelper::get($field, 'attributes.value'), $form);
301 }
302 $type = ArrayHelper::get($field, 'settings.field_type', 'select');
303 if (in_array($type, ['checkbox', 'radio'])) {
304 $question['type'] = 'FlowFormMultipleChoiceType';
305 $question['options'] = self::getAdvancedOptions($field, $form);
306 if ('checkbox' == $type) {
307 $question['multiple'] = true;
308 }
309 } elseif (in_array($type, ['select', 'multi_select'])) {
310 $question['type'] = 'FlowFormDropdownType';
311 $question['options'] = self::getAdvancedOptions($field, $form);
312 $question['searchable'] = ArrayHelper::get($field, 'settings.enable_select_2');
313 $question['multiple'] = ArrayHelper::isTrue($field, 'attributes.multiple');
314 } else {
315 continue;
316 }
317 $question['nextStepOnAnswer'] = true;
318 } elseif ('input_checkbox' === $field['element']) {
319 $question['options'] = self::getAdvancedOptions($field, $form);
320 $question['multiple'] = true;
321 $question = static::hasPictureMode($field, $question);
322 $question = static::maybeAddOtherOption($field, $question);
323 } elseif ('input_radio' === $field['element']) {
324 $question['options'] = self::getAdvancedOptions($field, $form);
325 $question['nextStepOnAnswer'] = true;
326 $question = static::hasPictureMode($field, $question);
327 $question = static::maybeAddOtherOption($field, $question);
328 } elseif ('input_ranking' === $field['element']) {
329 $question['options'] = self::getAdvancedOptions($field, $form);
330 $question['type'] = 'FlowFormRankingType';
331 $question['multiple'] = true;
332 $question['allowImages'] = (bool) ArrayHelper::get($field, 'settings.enable_image_input');
333 $question['rankingDisplayType'] = ArrayHelper::get($field, 'settings.ranking_display_type', 'list');
334 $question['rankingGridColumns'] = ArrayHelper::get($field, 'settings.ranking_grid_columns', '3');
335 $question['showResetIcon'] = ArrayHelper::get($field, 'settings.show_reset_icon', 'no') === 'yes';
336 $question['showPositionSerial'] = ArrayHelper::get($field, 'settings.show_position_serial', 'yes');
337 $question['accentColor'] = ArrayHelper::get($field, 'settings.accent_color', '');
338 } elseif ('custom_html' === $field['element']) {
339 $question['content'] = self::getComponent()->replaceEditorSmartCodes(ArrayHelper::get($field, 'settings.html_codes', ''), $form);
340 } elseif ('section_break' === $field['element']) {
341 $question['content'] = self::getComponent()->replaceEditorSmartCodes(ArrayHelper::get($field, 'settings.description', ''), $form);
342 $question['contentAlign'] = ArrayHelper::get($field, 'settings.align', '');
343 } elseif ('phone' === $field['element']) {
344 if (defined('FLUENTFORMPRO')) {
345 $question['phone_settings'] = self::getPhoneFieldSettings($field, $form);
346
347 if ($question['phone_settings']['enabled']) {
348 $cssSource = FLUENTFORMPRO_DIR_URL . 'public/libs/intl-tel-input/css/intlTelInput.min.css';
349 if (is_rtl()) {
350 $cssSource = FLUENTFORMPRO_DIR_URL . 'public/libs/intl-tel-input/css/intlTelInput-rtl.min.css';
351 }
352 wp_enqueue_style('intlTelInput', $cssSource, [], '25.5.2');
353 if (version_compare( FLUENTFORMPRO_VERSION, '6.1.0', '>' ) ) {
354 wp_enqueue_script('intlTelInputWithUtils', FLUENTFORMPRO_DIR_URL . 'public/libs/intl-tel-input/js/intlTelInputWithUtils.min.js', [], '25.5.2', true);
355 } else {
356 wp_enqueue_script('intlTelInputUtils', FLUENTFORMPRO_DIR_URL . 'public/libs/intl-tel-input/js/utils.js', [], '18.1.1', true);
357 wp_enqueue_script('intlTelInput', FLUENTFORMPRO_DIR_URL . 'public/libs/intl-tel-input/js/intlTelInput.min.js', [], '18.1.1', true);
358 }
359 }
360 }
361 } elseif ('input_number' === $field['element']) {
362 $question['min'] = ArrayHelper::get($field, 'settings.validation_rules.min.value');
363 $question['max'] = ArrayHelper::get($field, 'settings.validation_rules.max.value');
364 $question['min'] = is_numeric($question['min']) ? $question['min'] : null;
365 $question['max'] = is_numeric($question['max']) ? $question['max'] : null;
366 $question['is_calculable'] = true;
367
368 if (!$form->hasCalculation) {
369 $form->hasCalculation = static::hasFormula($question);
370 }
371
372 do_action_deprecated(
373 'ff_rendering_calculation_form',
374 [
375 $form,
376 $field
377 ],
378 FLUENTFORM_FRAMEWORK_UPGRADE,
379 'fluentform/rendering_calculation_form',
380 'Use fluentform/rendering_calculation_form instead of ff_rendering_calculation_form'
381 );
382
383 do_action('fluentform/rendering_calculation_form', $form, $field);
384 } elseif (in_array($field['element'], ['terms_and_condition', 'gdpr_agreement'])) {
385 $question['options'] = [
386 [
387 'label' => self::getComponent()->replaceEditorSmartCodes(ArrayHelper::get($field, 'settings.tc_agree_text', 'I accept'), $form),
388 'value' => 'on',
389 ],
390 ];
391
392 if ('terms_and_condition' === $field['element']) {
393 $hideDisagreeOption = ArrayHelper::isTrue($field, 'settings.hide_disagree');
394
395 if (!$hideDisagreeOption) {
396 $question['options'][] = [
397 'label' => self::getComponent()->replaceEditorSmartCodes(ArrayHelper::get($field, 'settings.tc_dis_agree_text', 'I don\'t accept'), $form),
398 'value' => 'off',
399 ];
400 }
401 }
402
403 $question['nextStepOnAnswer'] = true;
404 $question['title'] = self::getComponent()->replaceEditorSmartCodes(ArrayHelper::get($field, 'settings.tnc_html'), $form);
405 if ('gdpr_agreement' === $field['element']) {
406 $question['required'] = true;
407 }
408 } elseif ('ratings' === $field['element']) {
409 $iconSettings = RatingIcon::resolveSettings($field);
410 $question['show_text'] = ArrayHelper::get($field, 'settings.show_text');
411 $question['rateOptions'] = ArrayHelper::get($field, 'options', []);
412 $question['iconSource'] = $iconSettings['icon_source'];
413 $question['iconType'] = $iconSettings['icon_type'];
414 $question['customIconSvg'] = $iconSettings['custom_icon_svg'];
415 $question['inactiveColor'] = $iconSettings['inactive_color'];
416 $question['activeColor'] = $iconSettings['active_color'];
417 $question['iconSvg'] = RatingIcon::getResolvedIconSvg($field, [
418 'class' => 'ff-rating-icon-svg',
419 ]);
420 $question['nextStepOnAnswer'] = true;
421 } elseif ('input_date' === $field['element']) {
422 $app = wpFluentForm();
423 $dateField = new DateTime();
424
425 wp_enqueue_style('flatpickr', fluentFormMix('libs/flatpickr/flatpickr.min.css'), [], FLUENTFORM_VERSION);
426 wp_enqueue_script('flatpickr', fluentFormMix('libs/flatpickr/flatpickr.min.js'), [], FLUENTFORM_VERSION, true);
427
428 $question['dateConfig'] = json_decode($dateField->getDateFormatConfigJSON($field['settings'], $form));
429 $question['dateCustomConfig'] = $dateField->getCustomConfig($field['settings']);
430 } elseif (in_array($field['element'], ['input_image', 'input_file'])) {
431 $question['multiple'] = true;
432 $question['btn_text'] = ArrayHelper::get($field, 'settings.btn_text', __('Choose File', 'fluentform'));
433
434 $maxFileCount = ArrayHelper::get($field, 'settings.validation_rules.max_file_count');
435 $maxFileSize = ArrayHelper::get($field, 'settings.validation_rules.max_file_size');
436
437 if ('input_image' === $field['element']) {
438 $allowedFieldTypes = ArrayHelper::get($field, 'settings.validation_rules.allowed_image_types.value');
439
440 $question['validationRules']['allowed_file_types'] = $question['validationRules']['allowed_image_types'];
441 } else {
442 $allowedFieldTypes = ArrayHelper::get($field, 'settings.validation_rules.allowed_file_types.value');
443 }
444
445 if ($maxFileCount) {
446 $question['max'] = $maxFileCount['value'];
447 }
448
449 if ($maxFileSize) {
450 $question['maxSize'] = $maxFileSize['value'];
451 }
452
453 if ($allowedFieldTypes) {
454 $question['accept'] = implode('|', $allowedFieldTypes);
455 }
456 } elseif ('signature' === $field['element']) {
457 // Signature field settings
458 $question['signature_settings'] = [
459 'background_color' => ArrayHelper::get($field, 'settings.sign_background_color', '#ffffff'),
460 'border_color' => ArrayHelper::get($field, 'settings.sign_border_color', '#FFEB3B'),
461 'pen_color' => ArrayHelper::get($field, 'settings.sign_pen_color', '#333'),
462 'pen_size' => ArrayHelper::get($field, 'settings.sign_pen_size', 2),
463 'pad_height' => ArrayHelper::get($field, 'settings.sign_pad_height', 200),
464 'instruction' => self::getComponent()->replaceEditorSmartCodes(ArrayHelper::get($field, 'settings.sign_instruction', __('Sign Here', 'fluentform')), $form),
465 ];
466 $question['multiple'] = false;
467
468 // Enqueue signature scripts and styles
469 if (defined('FLUENTFORM_SIGNATURE')) {
470 wp_enqueue_style(
471 'fluentform-signature',
472 FLUENTFORM_SIGNATURE_URL . 'public/css/fluentform-signature.css',
473 [],
474 FLUENTFORM_SIGNATURE_VERSION
475 );
476
477 wp_enqueue_script(
478 'signature_pad',
479 FLUENTFORM_SIGNATURE_URL . 'public/js/signature_pad.js',
480 ['jquery'],
481 '2.3.2',
482 true
483 );
484
485 wp_enqueue_script(
486 'fluentform-signature',
487 FLUENTFORM_SIGNATURE_URL . 'public/js/fluentform-signature.js',
488 ['jquery', 'signature_pad'],
489 FLUENTFORM_SIGNATURE_VERSION,
490 true
491 );
492 }
493 } elseif ('tabular_grid' === $field['element']) {
494 $question['grid_columns'] = $field['settings']['grid_columns'];
495 $question['grid_rows'] = $field['settings']['grid_rows'];
496 $question['selected_grids'] = $field['settings']['selected_grids'];
497 $question['multiple'] = 'checkbox' === $field['settings']['tabular_field_type'];
498
499 if ($field['settings']['selected_grids']) {
500 $rowValues = array_keys($question['grid_rows']);
501 $colValues = array_keys($question['grid_columns']);
502
503 foreach ($field['settings']['selected_grids'] as $selected) {
504 if (in_array($selected, $rowValues)) {
505 $question['answer'][$selected] = $colValues;
506 } else {
507 foreach ($rowValues as $rowValue) {
508 if ($question['multiple']) {
509 $question['answer'][$rowValue][] = $selected;
510 } else {
511 $question['answer'][$rowValue] = $selected;
512 }
513 }
514 }
515 }
516 }
517
518 $question['requiredPerRow'] = ArrayHelper::get($field, 'settings.validation_rules.required.per_row');
519 } elseif ('rangeslider' === $field['element']) {
520 $question['min'] = intval($field['attributes']['min']);
521 $question['max'] = intval($field['attributes']['max']);
522
523 if ($step = ArrayHelper::get($field, 'settings.number_step')) {
524 $question['step'] = intval($step);
525 } else {
526 $question['step'] = 1;
527 }
528
529 $enabledQtyMapping = 'yes' === ArrayHelper::get($field, 'settings.enable_target_product');
530 if ($enabledQtyMapping && $targetProductName = ArrayHelper::get($field, 'settings.target_product')) {
531 $question['targetProduct'] = $targetProductName;
532 }
533
534 $question['is_calculable'] = true;
535 $question['type'] = 'FlowFormRangesliderType';
536 } elseif ('save_progress_button' === $field['element']) {
537 // Add the save progress button data to the previous question
538 if ($questions && count($questions) > 0) {
539 $lastQuestion = &$questions[count($questions) - 1];
540 $question['id'] = ArrayHelper::get($field, 'attributes.name');
541 $question['parent_id'] = $lastQuestion['id'];
542 $question['title'] = self::getComponent()->replaceEditorSmartCodes(ArrayHelper::get($field, 'editor_options.title'), $form);
543 $question['settings'] = ArrayHelper::get($field, 'settings');
544 $question['counter'] = count($questions) - 1;
545 $lastQuestion['has_save_and_resume_button'] = true;
546 $lastQuestion['save_and_resume_button'] = $question;
547 $form->hasSaveAndResumeButton = true;
548 }
549 // Skip Save Progress Button as a separate question
550 continue;
551 } elseif ('multi_payment_component' === $field['element']) {
552 $type = $field['attributes']['type'];
553
554 if ('single' == $type) {
555 $question['priceLabel'] = self::getComponent()->replaceEditorSmartCodes($field['settings']['price_label'], $form);
556 } else {
557 $question['nextStepOnAnswer'] = true;
558
559 if ('radio' == $type || 'checkbox' == $type) {
560 $question['paymentType'] = 'MultipleChoiceType';
561 $question['type'] = 'FlowFormMultipleChoiceType';
562 $question = static::hasPictureMode($field, $question);
563
564 if ('checkbox' == $type) {
565 $question['multiple'] = true;
566 }
567 } else {
568 $question['paymentType'] = 'DropdownType';
569 $question['type'] = 'FlowFormDropdownType';
570 }
571
572 $pricingOptions = ArrayHelper::get($field, 'settings.pricing_options');
573 foreach ($pricingOptions as &$option) {
574 $option['label'] = self::getComponent()->replaceEditorSmartCodes($option['label'], $form);
575 }
576 $question['options'] = $pricingOptions;
577 }
578
579 $question['is_payment_field'] = true;
580 $question['is_calculable'] = true;
581 } elseif ('subscription_payment_component' === $field['element']) {
582 $question['is_payment_field'] = true;
583 $question['is_subscription_field'] = true;
584
585 if (!$hasSaveAndResume) {
586 $question['answer'] = null;
587 }
588
589 $type = $field['attributes']['type'];
590 $question['subscriptionFieldType'] = $type;
591 $currency = PaymentHelper::getFormCurrency($form->id);
592
593 // Filter out expired plans that are set to hide
594 $subscriptionOptions = $field['settings']['subscription_options'];
595 $visibleOptions = array_filter($subscriptionOptions, function ($opt) {
596 return !PaymentHelper::isPlanExpiredAndHidden($opt);
597 });
598
599 if (empty($visibleOptions)) {
600 continue;
601 }
602
603 foreach ($subscriptionOptions as $index => &$option) {
604 if (PaymentHelper::isPlanExpiredAndHidden($option)) {
605 continue;
606 }
607
608 $hasCustomPayment = false;
609
610 if (array_key_exists('user_input', $option) && 'yes' == $option['user_input']) {
611 $hasCustomPayment = true;
612 $option['subscription_amount'] = 0;
613
614 if (array_key_exists('user_input_default_value', $option)) {
615 $option['subscription_amount'] = $option['user_input_default_value'];
616 }
617 }
618
619 $paymentSummaryText = PaymentHelper::getPaymentSummaryText($option, $form->id, $currency, false);
620
621 $planValue = 'single' == $type ? $option['subscription_amount'] : $index;
622
623 $field['plans'][] = [
624 'label' => self::getComponent()->replaceEditorSmartCodes($option['name'], $form),
625 'value' => $planValue,
626 'sub' => wp_strip_all_tags($paymentSummaryText),
627 'subscription_amount' => $planValue,
628 ];
629
630 $option['sub'] = wp_strip_all_tags($paymentSummaryText);
631
632 if ('yes' == $option['is_default'] && !$hasSaveAndResume) {
633 $question['answer'] = $index;
634 }
635
636 if ($hasCustomPayment) {
637 $option['customInput'] = $question['name'] . '_custom_' . $index;
638
639 if ($hasSaveAndResume && $saveAndResumeData) {
640 if ($customPayment = ArrayHelper::get($saveAndResumeData, 'response.' . $option['customInput'])) {
641 $question['customPayment'] = $customPayment;
642 }
643 } else {
644 $question['customPayment'] = $option['subscription_amount'];
645 }
646 }
647 }
648
649 $filteredPlans = array_values(array_filter($subscriptionOptions, function ($opt) {
650 return !PaymentHelper::isPlanExpiredAndHidden($opt);
651 }));
652 $question['plans'] = $filteredPlans;
653
654 // Re-map default answer to the new re-indexed position
655 foreach ($filteredPlans as $newIndex => $plan) {
656 if ('yes' == $plan['is_default'] && !$hasSaveAndResume) {
657 $question['answer'] = $newIndex;
658 break;
659 }
660 }
661
662 if ('single' != $type) {
663 $question['options'] = $field['plans'];
664 $question['subscriptionFieldType'] = 'radio' == $field['settings']['selection_type'] ? 'FlowFormMultipleChoiceType' : 'FlowFormDropdownType';
665 $question['nextStepOnAnswer'] = true;
666 }
667 } elseif ('custom_payment_component' === $field['element']) {
668 $question['type'] = 'FlowFormNumberType';
669 $question['min'] = ArrayHelper::get($field, 'settings.validation_rules.min.value');
670 $question['max'] = ArrayHelper::get($field, 'settings.validation_rules.max.value');
671 $question['min'] = is_numeric($question['min']) ? $question['min'] : null;
672 $question['max'] = is_numeric($question['max']) ? $question['max'] : null;
673
674 $question['is_payment_field'] = true;
675 $question['is_calculable'] = true;
676
677 if (!$form->hasCalculation) {
678 $form->hasCalculation = static::hasFormula($question);
679 }
680
681 do_action_deprecated(
682 'ff_rendering_calculation_form',
683 [
684 $form,
685 $field
686 ],
687 FLUENTFORM_FRAMEWORK_UPGRADE,
688 'fluentform/rendering_calculation_form',
689 'Use fluentform/rendering_calculation_form instead of ff_rendering_calculation_form'
690 );
691
692 do_action('fluentform/rendering_calculation_form', $form, $field);
693 } elseif ('item_quantity_component' === $field['element']) {
694 $question['type'] = $allowedFields['input_number'];
695 $question['targetProduct'] = $field['settings']['target_product'];
696
697 $question['min'] = ArrayHelper::get($field, 'settings.validation_rules.min.value');
698 $question['max'] = ArrayHelper::get($field, 'settings.validation_rules.max.value');
699 $question['min'] = is_numeric($question['min']) ? $question['min'] : null;
700 $question['max'] = is_numeric($question['max']) ? $question['max'] : null;
701 $question['step'] = 1;
702 $question['stepErrorMsg'] = __('Please enter a valid value. The two nearest valid values are {lower_value} and {upper_value}', 'fluentform');
703 } elseif ('payment_method' === $field['element']) {
704 $question['nextStepOnAnswer'] = true;
705 $question['options'] = [];
706 $question['paymentMethods'] = [];
707
708 foreach ($field['settings']['payment_methods'] as $methodName => $paymentMethod) {
709 if ('yes' === $paymentMethod['enabled']) {
710 $question['options'][] = [
711 'label' => self::getComponent()->replaceEditorSmartCodes($paymentMethod['settings']['option_label']['value'], $form),
712 'value' => $paymentMethod['method_value'],
713 ];
714
715 $question['paymentMethods'][$methodName] = $paymentMethod;
716
717 do_action_deprecated(
718 'fluentform_rendering_payment_method_' . $methodName,
719 [
720 $paymentMethod,
721 $field,
722 $form
723 ],
724 FLUENTFORM_FRAMEWORK_UPGRADE,
725 'fluentform/rendering_payment_method_' . $methodName,
726 'Use fluentform/rendering_payment_method_' . $methodName . ' instead of fluentform_rendering_payment_method_' . $methodName
727 );
728
729 do_action(
730 'fluentform/rendering_payment_method_' . $methodName,
731 $paymentMethod,
732 $field,
733 $form
734 );
735 }
736 }
737 } elseif ('payment_summary_component' === $field['element']) {
738 $question['title'] = self::getComponent()->replaceEditorSmartCodes(__('Payment Summary', 'fluentform'), $form);
739 $question['emptyText'] = $field['settings']['cart_empty_text'];
740 } elseif ('net_promoter_score' === $field['element']) {
741 if (!ArrayHelper::exists($question, 'answer')) {
742 $question['answer'] = (int) $field['attributes']['value'];
743 }
744
745 $question['start_text'] = ArrayHelper::get($field, 'settings.start_text');
746 $question['end_text'] = ArrayHelper::get($field, 'settings.end_text');
747 $question['is_calculable'] = true;
748 $question['nextStepOnAnswer'] = true;
749 $question['type'] = 'FlowFormNetPromoterScoreType';
750 } elseif ('recaptcha' === $field['element']) {
751 $reCaptchaConfig = get_option('_fluentform_reCaptcha_details');
752 $siteKey = ArrayHelper::get($reCaptchaConfig, 'siteKey');
753
754 if (! $siteKey) {
755 continue;
756 }
757
758 $question['siteKey'] = $siteKey;
759 $question['answer'] = '';
760
761 $apiVersion = ArrayHelper::get($reCaptchaConfig, 'api_version', 'v2_visible');
762 $apiVersion = 'v3_invisible' == $apiVersion ? 3 : 2;
763 $api = 'https://www.google.com/recaptcha/api.js';
764
765 $form->reCaptcha = [
766 'version' => $apiVersion,
767 'siteKey' => $siteKey,
768 ];
769
770 if (3 === $apiVersion) {
771 $api .= '?render=' . $siteKey;
772 }
773
774 wp_enqueue_script(
775 'google-recaptcha',
776 $api,
777 [],
778 FLUENTFORM_VERSION,
779 true
780 );
781
782 if (3 === $apiVersion) {
783 $shouldRenderBadge = ArrayHelper::get($field, 'settings.render_recaptcha_v3_badge', false);
784
785 if (!$shouldRenderBadge) {
786 // Add CSS to hide reCAPTCHA badge
787 add_action('fluentform/conversational_frame_footer', function() {
788 echo
789 "<style>
790 .grecaptcha-badge {
791 visibility: hidden;
792 }
793 </style>";
794 });
795 }
796
797 continue;
798 }
799 } elseif (('hcaptcha' === $field['element'])) {
800 $hCaptchaConfig = get_option('_fluentform_hCaptcha_details');
801 $siteKey = ArrayHelper::get($hCaptchaConfig, 'siteKey');
802
803 if (! $siteKey) {
804 continue;
805 }
806
807 $question['siteKey'] = $siteKey;
808
809 $api = 'https://js.hcaptcha.com/1/api.js';
810
811 $form->hCaptcha = [
812 'siteKey' => $siteKey,
813 ];
814
815 wp_enqueue_script(
816 'hcaptcha',
817 $api,
818 [],
819 FLUENTFORM_VERSION,
820 true
821 );
822 } elseif ('turnstile' === $field['element']) {
823 $turnstileConfig = get_option('_fluentform_turnstile_details');
824 $siteKey = ArrayHelper::get($turnstileConfig, 'siteKey');
825 $appearance = ArrayHelper::get($turnstileConfig, 'appearance', 'always');
826 $theme = ArrayHelper::get($turnstileConfig, 'theme', 'auto');
827
828 if (! $siteKey) {
829 continue;
830 }
831
832 $question['siteKey'] = $siteKey;
833 $question['appearance'] = $appearance;
834 $question['theme'] = $theme;
835
836 $form->turnstile = [
837 'siteKey' => $siteKey,
838 'appearance' => $appearance,
839 ];
840
841 wp_enqueue_script(
842 'turnstile_conv',
843 'https://challenges.cloudflare.com/turnstile/v0/api.js?render=explicit', // phpcs:ignore PluginCheck.CodeAnalysis.EnqueuedResourceOffloading.OffloadedContent -- Cloudflare Turnstile requires loading from their CDN for CAPTCHA functionality
844 [],
845 FLUENTFORM_VERSION,
846 false
847 );
848
849 // for WP Rocket compatibility
850 wp_script_add_data('turnstile_conv', 'data-cfasync', 'false');
851
852 if ('interaction-only' === $appearance) {
853 continue;
854 }
855 } elseif ('payment_coupon' === $field['element']) {
856 if ($hasSaveAndResume && $saveAndResumeData) {
857 if ($coupons = ArrayHelper::get($saveAndResumeData, 'response.__ff_all_applied_coupons')) {
858 $question['answer'] = json_decode($coupons);
859 }
860 }
861 }
862
863 do_action('fluentform/conversational_question', $question, $field, $form);
864
865 if ($question['type']) {
866 $questions[] = $question;
867 }
868 if ('custom_submit_button' === $field['element']) {
869 $form->submit_button = $field;
870 }
871 }
872 $form->questions = $questions;
873
874 $form->image_preloads = $imagePreloads;
875
876 return $form;
877 }
878
879 public static function convertExistingForm($form)
880 {
881 $formFields = json_decode($form->form_fields, true);
882 $fields = static::flattenLayoutContainers(ArrayHelper::get($formFields, 'fields', []));
883 $formattedFields = [];
884
885 if (is_array($fields) && ! empty($fields)) {
886 foreach ($fields as $field) {
887 $element = ArrayHelper::get($field, 'element');
888 $allowedFields = array_keys(static::fieldTypes());
889 if (!in_array($element, $allowedFields)) {
890 continue;
891 }
892
893 if (! ArrayHelper::exists($field, 'style_pref')) {
894 $field['style_pref'] = [
895 'layout' => 'default',
896 'media' => fluentFormGetRandomPhoto(),
897 'brightness' => 0,
898 'alt_text' => '',
899 'media_x_position' => 50,
900 'media_y_position' => 50,
901 ];
902 }
903 $formattedFields[] = $field;
904 }
905 }
906
907 $formFields['fields'] = $formattedFields;
908
909 return json_encode($formFields);
910 }
911
912 /**
913 * Lift fields out of layout containers so they survive conversion.
914 *
915 * A conversational form is a flat sequence of questions, so `container` has
916 * no equivalent and is not in fieldTypes(). Dropping it wholesale used to
917 * take every input nested inside it as well — silent, irreversible data loss
918 * on a one-click action, and column layouts are common.
919 *
920 * Only `columns` is unwrapped. Composite fields (input_name, address) keep
921 * their sub-inputs under `fields` and have their own conversational type, so
922 * they must stay whole. Nested containers are handled recursively.
923 *
924 * @param array $fields
925 * @return array
926 */
927 protected static function flattenLayoutContainers($fields)
928 {
929 if (! is_array($fields)) {
930 return [];
931 }
932
933 $flattened = [];
934
935 foreach ($fields as $field) {
936 if (! is_array($field)) {
937 continue;
938 }
939
940 $columns = ArrayHelper::get($field, 'columns');
941
942 if ('container' !== ArrayHelper::get($field, 'element') || ! is_array($columns)) {
943 $flattened[] = $field;
944
945 continue;
946 }
947
948 foreach ($columns as $column) {
949 $columnFields = static::flattenLayoutContainers(
950 ArrayHelper::get($column, 'fields', [])
951 );
952
953 foreach ($columnFields as $columnField) {
954 $flattened[] = $columnField;
955 }
956 }
957 }
958
959 return $flattened;
960 }
961
962 private static function buildBaseQuestion($field, $validationsRules, $form)
963 {
964 return [
965 'id' => ArrayHelper::get($field, 'attributes.name'),
966 'name' => ArrayHelper::get($field, 'attributes.name'),
967 'title' => self::getComponent()->replaceEditorSmartCodes(ArrayHelper::get($field, 'settings.label'), $form),
968 'type' => ArrayHelper::get(static::fieldTypes(), $field['element']),
969 'ff_input_type' => ArrayHelper::get($field, 'element'),
970 'container_class' => ArrayHelper::get($field, 'settings.container_class'),
971 'placeholder' => self::getComponent()->replaceEditorSmartCodes(ArrayHelper::get($field, 'attributes.placeholder'), $form),
972 'maxLength' => ArrayHelper::get($field, 'attributes.maxlength'),
973 'required' => ArrayHelper::get($validationsRules, 'required.value'),
974 'requiredMsg' => ArrayHelper::get($validationsRules, 'required.message'),
975 'errorMessage' => ArrayHelper::get($validationsRules, 'required.message'),
976 'validationRules' => $validationsRules,
977 'tagline' => self::getComponent()->replaceEditorSmartCodes(ArrayHelper::get($field, 'settings.help_message'), $form),
978 'style_pref' => ArrayHelper::get($field, 'style_pref', [
979 'layout' => 'default',
980 'media' => '',
981 'brightness' => 0,
982 'alt_text' => '',
983 'media_x_position' => 50,
984 'media_y_position' => 50,
985 ]),
986 'conditional_logics' => self::parseConditionalLogic($field),
987 'calculation_settings' => ArrayHelper::get($field, 'settings.calculation_settings'),
988 'is_calculable' => ArrayHelper::get($field, 'settings.calc_value_status', false),
989 'has_save_and_resume' => static::hasSaveAndResume($form)
990 ];
991 }
992
993 public static function fieldTypes()
994 {
995 $fieldTypes = [
996 'input_url' => 'FlowFormUrlType',
997 'input_date' => 'FlowFormDateType',
998 'input_text' => 'FlowFormTextType',
999 'input_email' => 'FlowFormEmailType',
1000 'input_hidden' => 'FlowFormHiddenType',
1001 'input_number' => 'FlowFormNumberType',
1002 'select' => 'FlowFormDropdownType',
1003 'select_country' => 'FlowFormDropdownType',
1004 'textarea' => 'FlowFormLongTextType',
1005 'input_password' => 'FlowFormPasswordType',
1006 'custom_html' => 'FlowFormSectionBreakType',
1007 'section_break' => 'FlowFormSectionBreakType',
1008 'welcome_screen' => 'FlowFormWelcomeScreenType',
1009 'input_checkbox' => 'FlowFormMultipleChoiceType',
1010 'input_radio' => 'FlowFormMultipleChoiceType',
1011 'terms_and_condition' => 'FlowFormTermsAndConditionType',
1012 'gdpr_agreement' => 'FlowFormTermsAndConditionType',
1013 'MultiplePictureChoice' => 'FlowFormMultiplePictureChoiceType',
1014 'recaptcha' => 'FlowFormReCaptchaType',
1015 'hcaptcha' => 'FlowFormHCaptchaType',
1016 'turnstile' => 'FlowFormTurnstileType',
1017 'address' => 'FlowFormAddressType',
1018 'input_name' => 'FlowFormNameType',
1019 'ffc_custom' => 'FlowFormCustomType',
1020 'payment_method' => 'FlowFormPaymentMethodType',
1021 'multi_payment_component' => 'FlowFormPaymentType',
1022 'custom_payment_component' => 'FlowFormPaymentType',
1023 'item_quantity_component' => 'FlowFormPaymentType',
1024 'payment_summary_component' => 'FlowFormPaymentSummaryType',
1025 'subscription_payment_component' => 'FlowFormSubscriptionType',
1026 ];
1027
1028 if (defined('FLUENTFORM_SIGNATURE')) {
1029 $fieldTypes['signature'] = 'FlowFormSignatureType';
1030 }
1031
1032 if (Helper::hasPro()) {
1033 $fieldTypes['phone'] = 'FlowFormPhoneType';
1034 $fieldTypes['input_image'] = 'FlowFormFileType';
1035 $fieldTypes['input_file'] = 'FlowFormFileType';
1036 $fieldTypes['ratings'] = 'FlowFormRateType';
1037 $fieldTypes['tabular_grid'] = 'FlowFormMatrixType';
1038 $fieldTypes['payment_coupon'] = 'FlowFormCouponType';
1039 $fieldTypes['quiz_score'] = 'FlowFormHiddenType';
1040 $fieldTypes['rangeslider'] = 'FlowFormRangesliderType';
1041 $fieldTypes['save_progress_button'] = 'FlowFormSaveAndResumeType';
1042 $fieldTypes['dynamic_field'] = 'FlowFormDynamicFieldType';
1043 $fieldTypes['net_promoter_score'] = 'FlowFormNetPromoterScoreType';
1044 $fieldTypes['input_ranking'] = 'FlowFormRankingType';
1045 }
1046
1047 return apply_filters('fluentform/conversational_field_types', $fieldTypes);
1048 }
1049
1050 public static function hasPictureMode($field, $question)
1051 {
1052 $pictureMode = ArrayHelper::get($field, 'settings.enable_image_input');
1053
1054 if ($pictureMode) {
1055 $question['type'] = static::fieldTypes()['MultiplePictureChoice'];
1056 }
1057
1058 return $question;
1059 }
1060
1061 public static function hex2rgb($color, $opacity = 0.3)
1062 {
1063 if (! $color) {
1064 return;
1065 }
1066 $rgbValues = [$r, $g, $b] = array_map(
1067 function ($c) {
1068 return hexdec(str_pad($c, 2, $c));
1069 },
1070 str_split(ltrim($color, '#'), strlen($color) > 4 ? 2 : 1)
1071 );
1072 $rgbValues[3] = $opacity;
1073 $formattedValues = implode(',', $rgbValues);
1074
1075 return "rgb({$formattedValues})";
1076 }
1077
1078 public static function getPhoneFieldSettings($data, $form)
1079 {
1080 $geoLocate = 'yes' == ArrayHelper::get($data, 'settings.auto_select_country');
1081
1082 // todo:: remove the 'with_extended_validation' check in future.
1083 $enabled = ArrayHelper::get($data, 'settings.validation_rules.valid_phone_number.value');
1084 if (! $enabled) {
1085 $enabled = 'with_extended_validation' == ArrayHelper::get($data, 'settings.int_tel_number');
1086 }
1087
1088 $itlOptions = [
1089 'separateDialCode' => false,
1090 'nationalMode' => true,
1091 'autoPlaceholder' => 'aggressive',
1092 'formatOnDisplay' => true,
1093 'validationNumberTypes' => [
1094 'MOBILE',
1095 'FIXED_LINE_OR_MOBILE',
1096 'FIXED_LINE',
1097 'TOLL_FREE',
1098 ]
1099 ];
1100
1101 if ($geoLocate) {
1102 $itlOptions['initialCountry'] = 'auto';
1103 } else {
1104 $itlOptions['initialCountry'] = ArrayHelper::get($data, 'settings.default_country', '');
1105 }
1106
1107 $activeList = ArrayHelper::get($data, 'settings.phone_country_list.active_list');
1108 if ('priority_based' == $activeList) {
1109 $selectCountries = ArrayHelper::get($data, 'settings.phone_country_list.priority_based', []);
1110 $priorityCountries = self::getSelectedCountries($selectCountries);
1111 $key = version_compare(FLUENTFORMPRO_VERSION, '6.1.0', '>') ? 'countryOrder' : 'preferredCountries';
1112 $itlOptions[$key] = array_keys($priorityCountries);
1113 } elseif ('visible_list' == $activeList) {
1114 $onlyCountries = ArrayHelper::get($data, 'settings.phone_country_list.visible_list', []);
1115 $itlOptions['onlyCountries'] = $onlyCountries;
1116 } elseif ('hidden_list' == $activeList) {
1117 $countries = self::loadCountries($data);
1118 $itlOptions['onlyCountries'] = array_keys($countries);
1119 }
1120
1121 $itlOptions = apply_filters_deprecated(
1122 'fluentform_itl_options',
1123 [
1124 $itlOptions,
1125 $data,
1126 $form
1127 ],
1128 FLUENTFORM_FRAMEWORK_UPGRADE,
1129 'fluentform/itl_options',
1130 'Use fluentform/itl_options instead of fluentform_itl_options'
1131 );
1132
1133 $itlOptions = apply_filters('fluentform/itl_options', $itlOptions, $data, $form);
1134 $itlOptions = json_encode($itlOptions);
1135
1136 $settings = get_option('_fluentform_global_form_settings');
1137 $token = ArrayHelper::get($settings, 'misc.geo_provider_token');
1138
1139 $url = 'https://ipinfo.io';
1140 if ($token) {
1141 $url = 'https://ipinfo.io/?token=' . $token;
1142 }
1143
1144 $url = apply_filters_deprecated(
1145 'fluentform_ip_provider',
1146 [
1147 $url
1148 ],
1149 FLUENTFORM_FRAMEWORK_UPGRADE,
1150 'fluentform/ip_provider',
1151 'Use fluentform/ip_provider instead of fluentform_ip_provider'
1152 );
1153
1154 $ipProviderUrl = apply_filters('fluentform/ip_provider', $url);
1155
1156 return [
1157 'enabled' => $enabled,
1158 'itlOptions' => $itlOptions,
1159 'ipLookupUrl' => ($geoLocate && $ipProviderUrl) ? $ipProviderUrl : false,
1160 ];
1161 }
1162
1163 /**
1164 * Load country list from file
1165 *
1166 * @param array $data
1167 *
1168 * @return array
1169 */
1170 public static function loadCountries($data)
1171 {
1172 $data['options'] = [];
1173 $activeList = ArrayHelper::get($data, 'settings.phone_country_list.active_list');
1174 $countries = getFluentFormCountryList();
1175 $filteredCountries = [];
1176 if ('visible_list' == $activeList) {
1177 $selectCountries = ArrayHelper::get($data, 'settings.phone_country_list.' . $activeList, []);
1178 foreach ($selectCountries as $value) {
1179 $filteredCountries[$value] = $countries[$value];
1180 }
1181 } elseif ('hidden_list' == $activeList || 'priority_based' == $activeList) {
1182 $filteredCountries = $countries;
1183 $selectCountries = ArrayHelper::get($data, 'settings.phone_country_list.' . $activeList, []);
1184 foreach ($selectCountries as $value) {
1185 unset($filteredCountries[$value]);
1186 }
1187 } else {
1188 $filteredCountries = $countries;
1189 }
1190
1191 return $filteredCountries;
1192 }
1193
1194 public static function getSelectedCountries($keys = [])
1195 {
1196 $options = [];
1197 $countries = getFluentFormCountryList();
1198 foreach ($keys as $value) {
1199 $options[$value] = $countries[$value];
1200 }
1201
1202 return $options;
1203 }
1204
1205 public static function setDefaultValue($value, $field, $form)
1206 {
1207 if ($dynamicValue = ArrayHelper::get($field, 'settings.dynamic_default_value')) {
1208 $dynamicVal = self::getComponent()->replaceEditorSmartCodes($dynamicValue, $form);
1209
1210 $element = $field['element'];
1211
1212 if ($dynamicVal && 'input_checkbox' == $element) {
1213 $defaultValues = explode(',', $dynamicVal);
1214
1215 return array_map('trim', $defaultValues);
1216 }
1217
1218 if ($dynamicVal) {
1219 return $dynamicVal;
1220 }
1221 }
1222
1223 if (! $value) {
1224 return $value;
1225 }
1226
1227 if (is_string($value)) {
1228 return self::getComponent()->replaceEditorSmartCodes($value, $form);
1229 }
1230
1231 return $value;
1232 }
1233
1234 private static function parseConditionalLogic($field)
1235 {
1236 $logics = ArrayHelper::get($field, 'settings.conditional_logics', []);
1237
1238 if (! $logics || ! $logics['status']) {
1239 return [];
1240 }
1241
1242 $type = ArrayHelper::get($logics, 'type', '');
1243 if ($type === 'group') {
1244 return self::parseGroupConditionalLogic($logics);
1245 } else {
1246 return self::parseSimpleConditionalLogic($logics);
1247 }
1248 }
1249
1250 private static function parseSimpleConditionalLogic($logics)
1251 {
1252 $validConditions = self::parseConditions(ArrayHelper::get($logics, 'conditions'));
1253
1254 if (!$validConditions) {
1255 return [];
1256 }
1257
1258 $logics['conditions'] = $validConditions;
1259 return $logics;
1260 }
1261
1262 private static function parseGroupConditionalLogic($logics)
1263 {
1264 $validGroups = [];
1265 $conditionGroups = ArrayHelper::get($logics, 'condition_groups', []);
1266
1267 foreach ($conditionGroups as $group) {
1268 $validConditions = self::parseConditions(ArrayHelper::get($group, 'rules'));
1269
1270 if ($validConditions) {
1271 $validGroups[] = [
1272 'rules' => $validConditions,
1273 ];
1274 }
1275 }
1276
1277 if (!$validGroups) {
1278 return [];
1279 }
1280
1281 $logics['condition_groups'] = $validGroups;
1282 return $logics;
1283 }
1284
1285 private static function parseConditions($conditions)
1286 {
1287 if (!$conditions) {
1288 return [];
1289 }
1290 return array_filter($conditions, function($condition) {
1291 return !empty($condition['field']) && !empty($condition['operator']);
1292 });
1293 }
1294
1295 private static function getAdvancedOptions($field, $form)
1296 {
1297 $options = ArrayHelper::get($field, 'settings.advanced_options', []);
1298
1299 foreach ($options as &$option) {
1300 if (\FluentForm\App\Helpers\Helper::isOptionGroup($option)) {
1301 foreach ($option['options'] as &$groupOption) {
1302 $groupOption['label'] = self::getComponent()->replaceEditorSmartCodes($groupOption['label'], $form);
1303 }
1304 unset($groupOption);
1305 $option['label'] = self::getComponent()->replaceEditorSmartCodes($option['label'], $form);
1306 continue;
1307 }
1308
1309 $option['label'] = self::getComponent()->replaceEditorSmartCodes($option['label'], $form);
1310 }
1311 unset($option);
1312
1313 if ($options && 'yes' == ArrayHelper::get($field, 'settings.randomize_options')) {
1314 shuffle($options);
1315 }
1316
1317 return $options;
1318 }
1319
1320 private static function maybeAddOtherOption($field, $question)
1321 {
1322 // Add "Other" option if enabled
1323 $enableOtherOption = ArrayHelper::get($field, 'settings.enable_other_option') === 'yes';
1324 $attributeType = ArrayHelper::get($field, 'attributes.type');
1325 if ($enableOtherOption && in_array($attributeType, ['checkbox', 'radio']) && Helper::hasPro()) {
1326 $otherLabel = ArrayHelper::get($field, 'settings.other_option_label', __('Other', 'fluentform'));
1327 $placeholder = ArrayHelper::get($field, 'settings.other_option_placeholder', __('Please specify', 'fluentform'));
1328 $fieldName = sanitize_text_field(str_replace(['[', ']'], '', ArrayHelper::get($field, 'attributes.name', '')));
1329 $otherValue = '__ff_other_' . $fieldName . '__';
1330 $otherInputName = $fieldName . '__ff_other_input__';
1331 $question['allowOther'] = true;
1332 $question['otherValue'] = $otherValue;
1333 $question['otherInputLabel'] = $otherLabel;
1334 $question['otherInputPlaceholder'] = $placeholder;
1335 $question['otherInputName'] = $otherInputName;
1336 $question['otherInputValue'] = '';
1337 }
1338 return $question;
1339 }
1340
1341 private static function hasFormula($question)
1342 {
1343 return (bool) (
1344 ArrayHelper::get($question, 'calculation_settings.status')
1345 && ArrayHelper::get($question, 'calculation_settings.formula')
1346 );
1347 }
1348
1349 private static function hasSaveAndResume($form)
1350 {
1351 if (!defined('FLUENTFORMPRO')){
1352 return false;
1353 }
1354
1355 if (!version_compare(FLUENTFORMPRO_VERSION,'5.1.13' ,'>=')){
1356 return false;
1357 }
1358
1359 $hasSaveProgressButton = FormFieldsParser::hasElement($form, 'save_progress_button');
1360 $perStepSave = ArrayHelper::get($form->settings, 'conv_form_per_step_save');
1361
1362 if ($perStepSave || $hasSaveProgressButton) {
1363 $saveAndResume = false;
1364 $hash = '';
1365 $form->save_state = false;
1366 $userId = (int)get_current_user_id();
1367 $viaLink = false;
1368
1369 // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Public form state parameter for save & resume functionality, verified by hash comparison in database
1370 $key = isset($_GET['fluent_state']) ? sanitize_text_field(wp_unslash($_GET['fluent_state'])) : false;
1371
1372 if ($key) {
1373 $hash = base64_decode($key);
1374 $form->save_state = true;
1375 $viaLink = true;
1376 } else {
1377 $cookieName = 'fluentform_step_form_hash_' . $form->id;
1378 $hash = isset($_COOKIE[$cookieName])
1379 ? sanitize_text_field(wp_unslash($_COOKIE[$cookieName]))
1380 : wp_generate_uuid4();
1381 }
1382
1383 \FluentFormPro\classes\DraftSubmissionsManager::migrate();
1384
1385 $draftForm = null;
1386
1387 // Logged-in users: prefer their own latest draft for this form so
1388 // cross-device restore works and a stale cookie can't pin them to
1389 // an older row. Link-share path is explicit and skips this.
1390 if (!$viaLink && $userId) {
1391 $draftForm = wpFluent()->table('fluentform_draft_submissions')
1392 ->where('user_id', $userId)
1393 ->where('form_id', $form->id)
1394 ->orderBy('updated_at', 'desc')
1395 ->first();
1396 }
1397
1398 if (!$draftForm) {
1399 $candidate = \FluentFormPro\classes\DraftSubmissionsManager::get($hash);
1400 // Privacy guard: a draft owned by a real user (user_id > 0)
1401 // may only be loaded by that same user. Link-share path is
1402 // explicit sharing and is exempt.
1403 if ($candidate && !$viaLink) {
1404 $entryUserId = (int)$candidate->user_id;
1405 if ($entryUserId > 0 && $entryUserId !== $userId) {
1406 $candidate = null;
1407 }
1408 }
1409 if ($candidate) {
1410 $draftForm = $candidate;
1411 }
1412 }
1413
1414 if ($draftForm) {
1415 $saveAndResume = true;
1416 }
1417
1418 return $saveAndResume;
1419 }
1420
1421 return false;
1422 }
1423
1424
1425 /**
1426 * @param array $field
1427 * @param object $form
1428 * @param string $fieldName
1429 * @return array
1430 */
1431 private static function resolveValidationsRules($field, $form, $fieldName = '')
1432 {
1433 $validationsRules = ArrayHelper::get($field, 'settings.validation_rules', []);
1434 if ($validationsRules) {
1435 if (!$fieldName) {
1436 $fieldName = ArrayHelper::get($field, 'attributes.name');
1437 }
1438 foreach ($validationsRules as $ruleName => $rule) {
1439 if (ArrayHelper::exists($rule, 'message')) {
1440 if (ArrayHelper::isTrue($rule, 'global')) {
1441 $rule['message'] = apply_filters('fluentform/get_global_message_' . $ruleName, $rule['message']);;
1442 }
1443 // Shortcode parse on validation message
1444 $rule['message'] = Helper::shortCodeParseOnValidationMessage($rule['message'], $form, $fieldName);
1445 $validationsRules[$ruleName]['message'] = apply_filters('fluentform/validation_message_' . $ruleName, $rule['message'], $field);
1446 }
1447 }
1448 }
1449 return $validationsRules;
1450 }
1451
1452 private static function getSaveAndResumeData($form)
1453 {
1454 $draftForm = null;
1455 $data = [];
1456 $formId = $form->id;
1457 $userId = (int)get_current_user_id();
1458 $viaLink = false;
1459 // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Public form state parameter for save & resume functionality, verified by hash comparison in database
1460 $key = isset($_GET['fluent_state']) ? sanitize_text_field(wp_unslash($_GET['fluent_state'])) : false;
1461 if ($key) {
1462 $hash = base64_decode($key);
1463 $viaLink = true;
1464 } else {
1465 $cookieName = 'fluentform_step_form_hash_' . $formId;
1466 $hash = isset($_COOKIE[$cookieName])
1467 ? sanitize_text_field(wp_unslash($_COOKIE[$cookieName]))
1468 : wp_generate_uuid4();
1469 }
1470
1471 // Logged-in users: prefer their own latest draft (cross-device sync).
1472 // Link-share path stays explicit and skips this.
1473 if (!$viaLink && $userId) {
1474 $draftForm = wpFluent()->table('fluentform_draft_submissions')
1475 ->where('user_id', $userId)
1476 ->where('form_id', $formId)
1477 ->orderBy('updated_at', 'desc')
1478 ->first();
1479 }
1480
1481 if (!$draftForm && $hash) {
1482 $candidate = \FluentFormPro\classes\DraftSubmissionsManager::get($hash, $formId);
1483 // Privacy guard: a draft owned by a real user (user_id > 0) may
1484 // only be loaded by that same user. Link-share path is exempt.
1485 if ($candidate && !$viaLink) {
1486 $entryUserId = (int)$candidate->user_id;
1487 if ($entryUserId > 0 && $entryUserId !== $userId) {
1488 $candidate = null;
1489 }
1490 }
1491 if ($candidate) {
1492 $draftForm = $candidate;
1493 }
1494 }
1495
1496 if (!$draftForm) {
1497 return $data;
1498 }
1499
1500 if ($draftForm) {
1501 $data['step_completed'] = (int)$draftForm->step_completed;
1502 $data['response'] = json_decode($draftForm->response, true);
1503
1504 $fileFieldTypes = ['input_file', 'input_image'];
1505 if (defined('FLUENTFORM_SIGNATURE')) {
1506 $fileFieldTypes[] = 'signature';
1507 }
1508 $fields = FormFieldsParser::getInputsByElementTypes($form, $fileFieldTypes);
1509 foreach ($fields as $name => $field) {
1510 if ($urls = ArrayHelper::get($data['response'], $name)) {
1511 if (!is_array($urls)) {
1512 // Single value (e.g. signature stored as a string)
1513 $urls = [$urls];
1514 }
1515 foreach ($urls as $index => $url) {
1516 if (is_array($url)) {
1517 // Already wrapped (e.g. from a previous load) — keep as-is
1518 continue;
1519 }
1520 if (!is_string($url) || $url === '[object Object]') {
1521 // Corrupted data — skip this entry
1522 unset($data['response'][$name][$index]);
1523 continue;
1524 }
1525 $data['response'][$name][$index] = [
1526 "data_src" => $url,
1527 "url" => \FluentForm\App\Helpers\Helper::maybeDecryptUrl($url)
1528 ];
1529 }
1530 // Re-index in case entries were removed
1531 $data['response'][$name] = array_values($data['response'][$name]);
1532 }
1533 }
1534 unset(
1535 $data['response']['_wp_http_referer'],
1536 $data['response']['__fluent_form_embded_post_id'],
1537 $data['response']['_fluentform_' . $draftForm->form_id . '_fluentformnonce']
1538 );
1539 }
1540
1541 return $data;
1542 }
1543 }
1544