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

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

1,487 lines 71.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 $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 = $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 private static function buildBaseQuestion($field, $validationsRules, $form)
906 {
907 return [
908 'id' => ArrayHelper::get($field, 'attributes.name'),
909 'name' => ArrayHelper::get($field, 'attributes.name'),
910 'title' => self::getComponent()->replaceEditorSmartCodes(ArrayHelper::get($field, 'settings.label'), $form),
911 'type' => ArrayHelper::get(static::fieldTypes(), $field['element']),
912 'ff_input_type' => ArrayHelper::get($field, 'element'),
913 'container_class' => ArrayHelper::get($field, 'settings.container_class'),
914 'placeholder' => self::getComponent()->replaceEditorSmartCodes(ArrayHelper::get($field, 'attributes.placeholder'), $form),
915 'maxLength' => ArrayHelper::get($field, 'attributes.maxlength'),
916 'required' => ArrayHelper::get($validationsRules, 'required.value'),
917 'requiredMsg' => ArrayHelper::get($validationsRules, 'required.message'),
918 'errorMessage' => ArrayHelper::get($validationsRules, 'required.message'),
919 'validationRules' => $validationsRules,
920 'tagline' => self::getComponent()->replaceEditorSmartCodes(ArrayHelper::get($field, 'settings.help_message'), $form),
921 'style_pref' => ArrayHelper::get($field, 'style_pref', [
922 'layout' => 'default',
923 'media' => '',
924 'brightness' => 0,
925 'alt_text' => '',
926 'media_x_position' => 50,
927 'media_y_position' => 50,
928 ]),
929 'conditional_logics' => self::parseConditionalLogic($field),
930 'calculation_settings' => ArrayHelper::get($field, 'settings.calculation_settings'),
931 'is_calculable' => ArrayHelper::get($field, 'settings.calc_value_status', false),
932 'has_save_and_resume' => static::hasSaveAndResume($form)
933 ];
934 }
935
936 public static function fieldTypes()
937 {
938 $fieldTypes = [
939 'input_url' => 'FlowFormUrlType',
940 'input_date' => 'FlowFormDateType',
941 'input_text' => 'FlowFormTextType',
942 'input_email' => 'FlowFormEmailType',
943 'input_hidden' => 'FlowFormHiddenType',
944 'input_number' => 'FlowFormNumberType',
945 'select' => 'FlowFormDropdownType',
946 'select_country' => 'FlowFormDropdownType',
947 'textarea' => 'FlowFormLongTextType',
948 'input_password' => 'FlowFormPasswordType',
949 'custom_html' => 'FlowFormSectionBreakType',
950 'section_break' => 'FlowFormSectionBreakType',
951 'welcome_screen' => 'FlowFormWelcomeScreenType',
952 'input_checkbox' => 'FlowFormMultipleChoiceType',
953 'input_radio' => 'FlowFormMultipleChoiceType',
954 'terms_and_condition' => 'FlowFormTermsAndConditionType',
955 'gdpr_agreement' => 'FlowFormTermsAndConditionType',
956 'MultiplePictureChoice' => 'FlowFormMultiplePictureChoiceType',
957 'recaptcha' => 'FlowFormReCaptchaType',
958 'hcaptcha' => 'FlowFormHCaptchaType',
959 'turnstile' => 'FlowFormTurnstileType',
960 'address' => 'FlowFormAddressType',
961 'input_name' => 'FlowFormNameType',
962 'ffc_custom' => 'FlowFormCustomType',
963 'payment_method' => 'FlowFormPaymentMethodType',
964 'multi_payment_component' => 'FlowFormPaymentType',
965 'custom_payment_component' => 'FlowFormPaymentType',
966 'item_quantity_component' => 'FlowFormPaymentType',
967 'payment_summary_component' => 'FlowFormPaymentSummaryType',
968 'subscription_payment_component' => 'FlowFormSubscriptionType',
969 ];
970
971 if (defined('FLUENTFORM_SIGNATURE')) {
972 $fieldTypes['signature'] = 'FlowFormSignatureType';
973 }
974
975 if (Helper::hasPro()) {
976 $fieldTypes['phone'] = 'FlowFormPhoneType';
977 $fieldTypes['input_image'] = 'FlowFormFileType';
978 $fieldTypes['input_file'] = 'FlowFormFileType';
979 $fieldTypes['ratings'] = 'FlowFormRateType';
980 $fieldTypes['tabular_grid'] = 'FlowFormMatrixType';
981 $fieldTypes['payment_coupon'] = 'FlowFormCouponType';
982 $fieldTypes['quiz_score'] = 'FlowFormHiddenType';
983 $fieldTypes['rangeslider'] = 'FlowFormRangesliderType';
984 $fieldTypes['save_progress_button'] = 'FlowFormSaveAndResumeType';
985 $fieldTypes['dynamic_field'] = 'FlowFormDynamicFieldType';
986 $fieldTypes['net_promoter_score'] = 'FlowFormNetPromoterScoreType';
987 $fieldTypes['input_ranking'] = 'FlowFormRankingType';
988 }
989
990 return apply_filters('fluentform/conversational_field_types', $fieldTypes);
991 }
992
993 public static function hasPictureMode($field, $question)
994 {
995 $pictureMode = ArrayHelper::get($field, 'settings.enable_image_input');
996
997 if ($pictureMode) {
998 $question['type'] = static::fieldTypes()['MultiplePictureChoice'];
999 }
1000
1001 return $question;
1002 }
1003
1004 public static function hex2rgb($color, $opacity = 0.3)
1005 {
1006 if (! $color) {
1007 return;
1008 }
1009 $rgbValues = [$r, $g, $b] = array_map(
1010 function ($c) {
1011 return hexdec(str_pad($c, 2, $c));
1012 },
1013 str_split(ltrim($color, '#'), strlen($color) > 4 ? 2 : 1)
1014 );
1015 $rgbValues[3] = $opacity;
1016 $formattedValues = implode(',', $rgbValues);
1017
1018 return "rgb({$formattedValues})";
1019 }
1020
1021 public static function getPhoneFieldSettings($data, $form)
1022 {
1023 $geoLocate = 'yes' == ArrayHelper::get($data, 'settings.auto_select_country');
1024
1025 // todo:: remove the 'with_extended_validation' check in future.
1026 $enabled = ArrayHelper::get($data, 'settings.validation_rules.valid_phone_number.value');
1027 if (! $enabled) {
1028 $enabled = 'with_extended_validation' == ArrayHelper::get($data, 'settings.int_tel_number');
1029 }
1030
1031 $itlOptions = [
1032 'separateDialCode' => false,
1033 'nationalMode' => true,
1034 'autoPlaceholder' => 'aggressive',
1035 'formatOnDisplay' => true,
1036 'validationNumberTypes' => [
1037 'MOBILE',
1038 'FIXED_LINE_OR_MOBILE',
1039 'FIXED_LINE',
1040 'TOLL_FREE',
1041 ]
1042 ];
1043
1044 if ($geoLocate) {
1045 $itlOptions['initialCountry'] = 'auto';
1046 } else {
1047 $itlOptions['initialCountry'] = ArrayHelper::get($data, 'settings.default_country', '');
1048 }
1049
1050 $activeList = ArrayHelper::get($data, 'settings.phone_country_list.active_list');
1051 if ('priority_based' == $activeList) {
1052 $selectCountries = ArrayHelper::get($data, 'settings.phone_country_list.priority_based', []);
1053 $priorityCountries = self::getSelectedCountries($selectCountries);
1054 $key = version_compare(FLUENTFORMPRO_VERSION, '6.1.0', '>') ? 'countryOrder' : 'preferredCountries';
1055 $itlOptions[$key] = array_keys($priorityCountries);
1056 } elseif ('visible_list' == $activeList) {
1057 $onlyCountries = ArrayHelper::get($data, 'settings.phone_country_list.visible_list', []);
1058 $itlOptions['onlyCountries'] = $onlyCountries;
1059 } elseif ('hidden_list' == $activeList) {
1060 $countries = self::loadCountries($data);
1061 $itlOptions['onlyCountries'] = array_keys($countries);
1062 }
1063
1064 $itlOptions = apply_filters_deprecated(
1065 'fluentform_itl_options',
1066 [
1067 $itlOptions,
1068 $data,
1069 $form
1070 ],
1071 FLUENTFORM_FRAMEWORK_UPGRADE,
1072 'fluentform/itl_options',
1073 'Use fluentform/itl_options instead of fluentform_itl_options'
1074 );
1075
1076 $itlOptions = apply_filters('fluentform/itl_options', $itlOptions, $data, $form);
1077 $itlOptions = json_encode($itlOptions);
1078
1079 $settings = get_option('_fluentform_global_form_settings');
1080 $token = ArrayHelper::get($settings, 'misc.geo_provider_token');
1081
1082 $url = 'https://ipinfo.io';
1083 if ($token) {
1084 $url = 'https://ipinfo.io/?token=' . $token;
1085 }
1086
1087 $url = apply_filters_deprecated(
1088 'fluentform_ip_provider',
1089 [
1090 $url
1091 ],
1092 FLUENTFORM_FRAMEWORK_UPGRADE,
1093 'fluentform/ip_provider',
1094 'Use fluentform/ip_provider instead of fluentform_ip_provider'
1095 );
1096
1097 $ipProviderUrl = apply_filters('fluentform/ip_provider', $url);
1098
1099 return [
1100 'enabled' => $enabled,
1101 'itlOptions' => $itlOptions,
1102 'ipLookupUrl' => ($geoLocate && $ipProviderUrl) ? $ipProviderUrl : false,
1103 ];
1104 }
1105
1106 /**
1107 * Load country list from file
1108 *
1109 * @param array $data
1110 *
1111 * @return array
1112 */
1113 public static function loadCountries($data)
1114 {
1115 $data['options'] = [];
1116 $activeList = ArrayHelper::get($data, 'settings.phone_country_list.active_list');
1117 $countries = getFluentFormCountryList();
1118 $filteredCountries = [];
1119 if ('visible_list' == $activeList) {
1120 $selectCountries = ArrayHelper::get($data, 'settings.phone_country_list.' . $activeList, []);
1121 foreach ($selectCountries as $value) {
1122 $filteredCountries[$value] = $countries[$value];
1123 }
1124 } elseif ('hidden_list' == $activeList || 'priority_based' == $activeList) {
1125 $filteredCountries = $countries;
1126 $selectCountries = ArrayHelper::get($data, 'settings.phone_country_list.' . $activeList, []);
1127 foreach ($selectCountries as $value) {
1128 unset($filteredCountries[$value]);
1129 }
1130 } else {
1131 $filteredCountries = $countries;
1132 }
1133
1134 return $filteredCountries;
1135 }
1136
1137 public static function getSelectedCountries($keys = [])
1138 {
1139 $options = [];
1140 $countries = getFluentFormCountryList();
1141 foreach ($keys as $value) {
1142 $options[$value] = $countries[$value];
1143 }
1144
1145 return $options;
1146 }
1147
1148 public static function setDefaultValue($value, $field, $form)
1149 {
1150 if ($dynamicValue = ArrayHelper::get($field, 'settings.dynamic_default_value')) {
1151 $dynamicVal = self::getComponent()->replaceEditorSmartCodes($dynamicValue, $form);
1152
1153 $element = $field['element'];
1154
1155 if ($dynamicVal && 'input_checkbox' == $element) {
1156 $defaultValues = explode(',', $dynamicVal);
1157
1158 return array_map('trim', $defaultValues);
1159 }
1160
1161 if ($dynamicVal) {
1162 return $dynamicVal;
1163 }
1164 }
1165
1166 if (! $value) {
1167 return $value;
1168 }
1169
1170 if (is_string($value)) {
1171 return self::getComponent()->replaceEditorSmartCodes($value, $form);
1172 }
1173
1174 return $value;
1175 }
1176
1177 private static function parseConditionalLogic($field)
1178 {
1179 $logics = ArrayHelper::get($field, 'settings.conditional_logics', []);
1180
1181 if (! $logics || ! $logics['status']) {
1182 return [];
1183 }
1184
1185 $type = ArrayHelper::get($logics, 'type', '');
1186 if ($type === 'group') {
1187 return self::parseGroupConditionalLogic($logics);
1188 } else {
1189 return self::parseSimpleConditionalLogic($logics);
1190 }
1191 }
1192
1193 private static function parseSimpleConditionalLogic($logics)
1194 {
1195 $validConditions = self::parseConditions(ArrayHelper::get($logics, 'conditions'));
1196
1197 if (!$validConditions) {
1198 return [];
1199 }
1200
1201 $logics['conditions'] = $validConditions;
1202 return $logics;
1203 }
1204
1205 private static function parseGroupConditionalLogic($logics)
1206 {
1207 $validGroups = [];
1208 $conditionGroups = ArrayHelper::get($logics, 'condition_groups', []);
1209
1210 foreach ($conditionGroups as $group) {
1211 $validConditions = self::parseConditions(ArrayHelper::get($group, 'rules'));
1212
1213 if ($validConditions) {
1214 $validGroups[] = [
1215 'rules' => $validConditions,
1216 ];
1217 }
1218 }
1219
1220 if (!$validGroups) {
1221 return [];
1222 }
1223
1224 $logics['condition_groups'] = $validGroups;
1225 return $logics;
1226 }
1227
1228 private static function parseConditions($conditions)
1229 {
1230 if (!$conditions) {
1231 return [];
1232 }
1233 return array_filter($conditions, function($condition) {
1234 return !empty($condition['field']) && !empty($condition['operator']);
1235 });
1236 }
1237
1238 private static function getAdvancedOptions($field, $form)
1239 {
1240 $options = ArrayHelper::get($field, 'settings.advanced_options', []);
1241
1242 foreach ($options as &$option) {
1243 if (\FluentForm\App\Helpers\Helper::isOptionGroup($option)) {
1244 foreach ($option['options'] as &$groupOption) {
1245 $groupOption['label'] = self::getComponent()->replaceEditorSmartCodes($groupOption['label'], $form);
1246 }
1247 unset($groupOption);
1248 $option['label'] = self::getComponent()->replaceEditorSmartCodes($option['label'], $form);
1249 continue;
1250 }
1251
1252 $option['label'] = self::getComponent()->replaceEditorSmartCodes($option['label'], $form);
1253 }
1254 unset($option);
1255
1256 if ($options && 'yes' == ArrayHelper::get($field, 'settings.randomize_options')) {
1257 shuffle($options);
1258 }
1259
1260 return $options;
1261 }
1262
1263 private static function maybeAddOtherOption($field, $question)
1264 {
1265 // Add "Other" option if enabled
1266 $enableOtherOption = ArrayHelper::get($field, 'settings.enable_other_option') === 'yes';
1267 $attributeType = ArrayHelper::get($field, 'attributes.type');
1268 if ($enableOtherOption && in_array($attributeType, ['checkbox', 'radio']) && Helper::hasPro()) {
1269 $otherLabel = ArrayHelper::get($field, 'settings.other_option_label', __('Other', 'fluentform'));
1270 $placeholder = ArrayHelper::get($field, 'settings.other_option_placeholder', __('Please specify', 'fluentform'));
1271 $fieldName = sanitize_text_field(str_replace(['[', ']'], '', ArrayHelper::get($field, 'attributes.name', '')));
1272 $otherValue = '__ff_other_' . $fieldName . '__';
1273 $otherInputName = $fieldName . '__ff_other_input__';
1274 $question['allowOther'] = true;
1275 $question['otherValue'] = $otherValue;
1276 $question['otherInputLabel'] = $otherLabel;
1277 $question['otherInputPlaceholder'] = $placeholder;
1278 $question['otherInputName'] = $otherInputName;
1279 $question['otherInputValue'] = '';
1280 }
1281 return $question;
1282 }
1283
1284 private static function hasFormula($question)
1285 {
1286 return (bool) (
1287 ArrayHelper::get($question, 'calculation_settings.status')
1288 && ArrayHelper::get($question, 'calculation_settings.formula')
1289 );
1290 }
1291
1292 private static function hasSaveAndResume($form)
1293 {
1294 if (!defined('FLUENTFORMPRO')){
1295 return false;
1296 }
1297
1298 if (!version_compare(FLUENTFORMPRO_VERSION,'5.1.13' ,'>=')){
1299 return false;
1300 }
1301
1302 $hasSaveProgressButton = FormFieldsParser::hasElement($form, 'save_progress_button');
1303 $perStepSave = ArrayHelper::get($form->settings, 'conv_form_per_step_save');
1304
1305 if ($perStepSave || $hasSaveProgressButton) {
1306 $saveAndResume = false;
1307 $hash = '';
1308 $form->save_state = false;
1309 $userId = (int)get_current_user_id();
1310 $viaLink = false;
1311
1312 // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Public form state parameter for save & resume functionality, verified by hash comparison in database
1313 $key = isset($_GET['fluent_state']) ? sanitize_text_field(wp_unslash($_GET['fluent_state'])) : false;
1314
1315 if ($key) {
1316 $hash = base64_decode($key);
1317 $form->save_state = true;
1318 $viaLink = true;
1319 } else {
1320 $cookieName = 'fluentform_step_form_hash_' . $form->id;
1321 $hash = isset($_COOKIE[$cookieName])
1322 ? sanitize_text_field(wp_unslash($_COOKIE[$cookieName]))
1323 : wp_generate_uuid4();
1324 }
1325
1326 \FluentFormPro\classes\DraftSubmissionsManager::migrate();
1327
1328 $draftForm = null;
1329
1330 // Logged-in users: prefer their own latest draft for this form so
1331 // cross-device restore works and a stale cookie can't pin them to
1332 // an older row. Link-share path is explicit and skips this.
1333 if (!$viaLink && $userId) {
1334 $draftForm = wpFluent()->table('fluentform_draft_submissions')
1335 ->where('user_id', $userId)
1336 ->where('form_id', $form->id)
1337 ->orderBy('updated_at', 'desc')
1338 ->first();
1339 }
1340
1341 if (!$draftForm) {
1342 $candidate = \FluentFormPro\classes\DraftSubmissionsManager::get($hash);
1343 // Privacy guard: a draft owned by a real user (user_id > 0)
1344 // may only be loaded by that same user. Link-share path is
1345 // explicit sharing and is exempt.
1346 if ($candidate && !$viaLink) {
1347 $entryUserId = (int)$candidate->user_id;
1348 if ($entryUserId > 0 && $entryUserId !== $userId) {
1349 $candidate = null;
1350 }
1351 }
1352 if ($candidate) {
1353 $draftForm = $candidate;
1354 }
1355 }
1356
1357 if ($draftForm) {
1358 $saveAndResume = true;
1359 }
1360
1361 return $saveAndResume;
1362 }
1363
1364 return false;
1365 }
1366
1367
1368 /**
1369 * @param array $field
1370 * @param object $form
1371 * @param string $fieldName
1372 * @return array
1373 */
1374 private static function resolveValidationsRules($field, $form, $fieldName = '')
1375 {
1376 $validationsRules = ArrayHelper::get($field, 'settings.validation_rules', []);
1377 if ($validationsRules) {
1378 if (!$fieldName) {
1379 $fieldName = ArrayHelper::get($field, 'attributes.name');
1380 }
1381 foreach ($validationsRules as $ruleName => $rule) {
1382 if (ArrayHelper::exists($rule, 'message')) {
1383 if (ArrayHelper::isTrue($rule, 'global')) {
1384 $rule['message'] = apply_filters('fluentform/get_global_message_' . $ruleName, $rule['message']);;
1385 }
1386 // Shortcode parse on validation message
1387 $rule['message'] = Helper::shortCodeParseOnValidationMessage($rule['message'], $form, $fieldName);
1388 $validationsRules[$ruleName]['message'] = apply_filters('fluentform/validation_message_' . $ruleName, $rule['message'], $field);
1389 }
1390 }
1391 }
1392 return $validationsRules;
1393 }
1394
1395 private static function getSaveAndResumeData($form)
1396 {
1397 $draftForm = null;
1398 $data = [];
1399 $formId = $form->id;
1400 $userId = (int)get_current_user_id();
1401 $viaLink = false;
1402 // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Public form state parameter for save & resume functionality, verified by hash comparison in database
1403 $key = isset($_GET['fluent_state']) ? sanitize_text_field(wp_unslash($_GET['fluent_state'])) : false;
1404 if ($key) {
1405 $hash = base64_decode($key);
1406 $viaLink = true;
1407 } else {
1408 $cookieName = 'fluentform_step_form_hash_' . $formId;
1409 $hash = isset($_COOKIE[$cookieName])
1410 ? sanitize_text_field(wp_unslash($_COOKIE[$cookieName]))
1411 : wp_generate_uuid4();
1412 }
1413
1414 // Logged-in users: prefer their own latest draft (cross-device sync).
1415 // Link-share path stays explicit and skips this.
1416 if (!$viaLink && $userId) {
1417 $draftForm = wpFluent()->table('fluentform_draft_submissions')
1418 ->where('user_id', $userId)
1419 ->where('form_id', $formId)
1420 ->orderBy('updated_at', 'desc')
1421 ->first();
1422 }
1423
1424 if (!$draftForm && $hash) {
1425 $candidate = \FluentFormPro\classes\DraftSubmissionsManager::get($hash, $formId);
1426 // Privacy guard: a draft owned by a real user (user_id > 0) may
1427 // only be loaded by that same user. Link-share path is exempt.
1428 if ($candidate && !$viaLink) {
1429 $entryUserId = (int)$candidate->user_id;
1430 if ($entryUserId > 0 && $entryUserId !== $userId) {
1431 $candidate = null;
1432 }
1433 }
1434 if ($candidate) {
1435 $draftForm = $candidate;
1436 }
1437 }
1438
1439 if (!$draftForm) {
1440 return $data;
1441 }
1442
1443 if ($draftForm) {
1444 $data['step_completed'] = (int)$draftForm->step_completed;
1445 $data['response'] = json_decode($draftForm->response, true);
1446
1447 $fileFieldTypes = ['input_file', 'input_image'];
1448 if (defined('FLUENTFORM_SIGNATURE')) {
1449 $fileFieldTypes[] = 'signature';
1450 }
1451 $fields = FormFieldsParser::getInputsByElementTypes($form, $fileFieldTypes);
1452 foreach ($fields as $name => $field) {
1453 if ($urls = ArrayHelper::get($data['response'], $name)) {
1454 if (!is_array($urls)) {
1455 // Single value (e.g. signature stored as a string)
1456 $urls = [$urls];
1457 }
1458 foreach ($urls as $index => $url) {
1459 if (is_array($url)) {
1460 // Already wrapped (e.g. from a previous load) — keep as-is
1461 continue;
1462 }
1463 if (!is_string($url) || $url === '[object Object]') {
1464 // Corrupted data — skip this entry
1465 unset($data['response'][$name][$index]);
1466 continue;
1467 }
1468 $data['response'][$name][$index] = [
1469 "data_src" => $url,
1470 "url" => \FluentForm\App\Helpers\Helper::maybeDecryptUrl($url)
1471 ];
1472 }
1473 // Re-index in case entries were removed
1474 $data['response'][$name] = array_values($data['response'][$name]);
1475 }
1476 }
1477 unset(
1478 $data['response']['_wp_http_referer'],
1479 $data['response']['__fluent_form_embded_post_id'],
1480 $data['response']['_fluentform_' . $draftForm->form_id . '_fluentformnonce']
1481 );
1482 }
1483
1484 return $data;
1485 }
1486 }
1487