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

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

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