PluginProbe
Fluent Forms – Customizable Contact Forms, Survey, Quiz, & Conversational Form Builder / trunk
Fluent Forms – Customizable Contact Forms, Survey, Quiz, & Conversational Form Builder vtrunk
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
app/Services/FluentConversational/Classes/Converter
Converter.php 74.8 KB 50 minutes ago

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

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