PluginProbe
Bit Form – Contact Form, Payment Forms, Multi Step Forms, Calculator & Custom Form Builder / 3.1.3
Bit Form – Contact Form, Payment Forms, Multi Step Forms, Calculator & Custom Form Builder v3.1.3
V-3.3.0 3.2.2 3.2.1 3.2.0 3.1.4 3.1.3 3.1.2 3.1.1 3.1.0 V3.0.3 V3.0.2 -3.0.1 V_3.0.0 1.1.1 1.1.8 1.2 1.3 1.4 1.4.18 1.5.2 1.9 2.0 2.10.0 2.10.1 2.10.2 All 137 releases
bit-form / includes / Frontend / Form / FrontendFormHandler.php

FrontendFormHandler.php in Bit Form – Contact Form, Payment Forms, Multi Step Forms, Calculator & Custom Form Builder 3.1.3, at includes/Frontend/Form/FrontendFormHandler.php

869 lines 34.8 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 namespace BitCode\BitForm\Frontend\Form;
4
5 if (!defined('ABSPATH')) {
6 exit;
7 }
8
9 use BitCode\BitForm\Admin\Form\AdminFormHandler;
10 use BitCode\BitForm\Admin\Form\FrontEndScriptGenerator;
11 use BitCode\BitForm\Admin\Form\Helpers;
12 use BitCode\BitForm\Core\Database\FormEntryMetaModel;
13 use BitCode\BitForm\Core\Database\FormModel;
14 use BitCode\BitForm\Core\Form\FormManager;
15 use BitCode\BitForm\Core\Integration\IntegrationHandler;
16 use BitCode\BitForm\Core\Util\EscapingHelper;
17 use BitCode\BitForm\Core\Util\FieldValueHandler;
18 use BitCode\BitForm\Core\Util\FileDownloadProvider;
19 use BitCode\BitForm\Core\Util\FrontendHelpers;
20 use BitCode\BitForm\Core\Util\SmartTagRegistry;
21 use BitCode\BitForm\Core\Util\SmartTags;
22 use BitCode\BitForm\Core\WorkFlow\WorkFlow;
23
24 final class FrontendFormHandler
25 {
26 public function __construct()
27 {
28 // before markup load - formids [], posts [1,2]
29 add_action('wp_enqueue_scripts', [$this, 'loadAssets']);
30 // markup loads - formids []
31 add_shortcode('bitform', [$this, 'handleFrontendRenderRequest']);
32 // after markup load - formids [1,35,3]
33 add_action('wp_footer', [$this, 'generateJS']);
34 }
35
36 private function validPassowordResetToken($token, $userID, $formId)
37 {
38 $existResetInteg = (new IntegrationHandler($formId))->getAllIntegration('wp_user_auth', 'wp_auth', 1);
39 if (!is_wp_error($existResetInteg) && count($existResetInteg) > 0) {
40 if ('reset' === $existResetInteg[0]->integration_name) {
41 $user = get_userdata($userID);
42 if ($user) {
43 $validKey = check_password_reset_key($token, $user->user_login);
44 if (is_wp_error($validKey)) {
45 echo "<div id='bf-resp' style='display:grid;justify-content:center;color:#860000;'>This password reset token is invalid.</div>";
46 exit();
47 }
48 } else {
49 echo "<div id='bf-resp' style='display:grid;justify-content:center;color:#860000;'>Invalid User!!</div>";
50 exit();
51 }
52 }
53 }
54 }
55
56 private function getJSFileSrc($postId)
57 {
58 $formUpdateVersion = get_option('bitform_form_update_version');
59 $formScriptSrc = BITFORMS_UPLOAD_BASE_URL . "/form-scripts/$postId/bitform-js-$postId.js?bfv=$formUpdateVersion";
60
61 return $formScriptSrc;
62 }
63
64 public function generateJs($formID = null, $entryID = null, $formType = null)
65 {
66 // return true;
67 $isFormPreview = get_transient('bitform_form_preview');
68 if ($isFormPreview && !$formID) {
69 delete_transient('bitform_form_preview');
70 return;
71 }
72 $frontendScriptGenObj = new FrontEndScriptGenerator();
73 $isPageBuilder = FrontendHelpers::checkIsPageBuilder($_SERVER);
74 $bfFrontendFormIds = FrontendHelpers::$bfFrontendFormIds;
75 if ($isPageBuilder || empty($bfFrontendFormIds)) {
76 return;
77 }
78 // for unique fields ids in the same form (e.g. multiple forms in the same page)
79 $allFields = [];
80 $formContents = [];
81 $contentIds = [];
82 $formIDs = [];
83 $previewMode = 'classic';
84 $postId = '';
85
86 $formUpdateVersion = get_option('bitform_form_update_version');
87 if ($formID) {
88 $formIDs[] = $formID;
89 $FrontendFormManager = FrontendFormManager::getInstance($formID, 1);
90 $formInfo = $FrontendFormManager->getFormInfo();
91 $FormIdentifier = esc_js($FrontendFormManager->getFormIdentifier());
92 $formContent = $FrontendFormManager->getFormContentWithValue($this->getValuesFromQueryParams());
93 $formContent->formId = $formID;
94 $formContents[] = $formContent;
95 $workFlowRunType = $entryID ? 'edit' : 'create';
96 $fields = $formContent->fields;
97 if ($entryID) {
98 $fields = $this->setFieldsValue($fields, $formID, $entryID);
99 }
100 $fields = $this->triggerWorkflowOnLoad($formID, 1, $fields, $workFlowRunType);
101 array_push($contentIds, $FormIdentifier);
102
103 foreach ($fields as $fk => $field) {
104 $allFields[$field->typ][] = ['fk' => $fk, 'field' => $field, 'formID' => $formID, 'contentId' => $FormIdentifier];
105 }
106 //Generate JS file for conversational form
107 if (!empty($formInfo->conversationalSettings->enable) && $formInfo->conversationalSettings->enable) {
108 $frontendScriptGenObj->generateJsFile([$formContent], $allFields, [$FormIdentifier], $formID, [$formID], 'conversational');
109 }
110 $previewMode = 'preview';
111 $postId = $formID;
112 } else {
113 global $post;
114 if (!is_object($post) && !isset($post->ID)) {
115 return;
116 }
117 $bfFrontendFormIds = FrontendHelpers::$bfFrontendFormIds;
118 $bfUniqFormIds = FrontendHelpers::getAllUniqFormIdsInPage();
119 $formIDs = $bfUniqFormIds;
120 $regenerateScriptFlag = $this->regenerateScriptChecker($bfUniqFormIds);
121
122 $postId = $post->ID;
123 if (!$regenerateScriptFlag) {
124 $regenerateScriptFlag = $this->deleteUnusedFormPageIds($postId, $bfUniqFormIds);
125 }
126 $isJsGenerating = get_option('bitforms_frontend_js_generating');
127 if (!$regenerateScriptFlag && !$isJsGenerating && !empty($formIDs)) {
128 wp_enqueue_script('bit-form-all-script-test', $this->getJSFileSrc($postId), [], $formUpdateVersion, true);
129 return;
130 }
131 foreach ($bfFrontendFormIds as $index => $formId) {
132 $shortCodeCounter = $index + 1;
133 $FrontendFormManager = FrontendFormManager::getInstance($formId, $shortCodeCounter);
134 $formInfo = $FrontendFormManager->getFormInfo();
135 $FormIdentifier = esc_js($FrontendFormManager->getFormIdentifier());
136 $formContent = $FrontendFormManager->getFormContentWithValue($this->getValuesFromQueryParams());
137 $formContent->formId = $formId;
138 $formContents[] = $formContent;
139 $fields = $this->triggerWorkflowOnLoad($formId, $shortCodeCounter, $formContent->fields);
140 $contentIds[] = $FormIdentifier;
141 $formFields = []; // indivisual form fields array for conversational view
142 foreach ($fields as $fk => $field) {
143 $fieldArr = ['fk' => $fk, 'field' => $field, 'formID' => $formId, 'contentId' => $FormIdentifier];
144 $allFields[$field->typ][] = $fieldArr;
145 $formFields[$field->typ][] = $fieldArr;
146 }
147 //Generate JS file for conversational form
148 if (!empty($formInfo->conversationalSettings->enable) && $formInfo->conversationalSettings->enable) {
149 $frontendScriptGenObj->generateJsFile([$formContent], $formFields, [$FormIdentifier], $formId, [$formId], 'conversational');
150 }
151 }
152 }
153 if (empty($formIDs)) {
154 return;
155 }
156
157 $frontendScriptGenObj->generateJsFile($formContents, $allFields, $contentIds, $postId, $formIDs, $previewMode);
158 if ('preview' === $previewMode) {
159 return;
160 }
161 wp_enqueue_script('bit-form-all-script-test', $this->getJSFileSrc($postId), [], $formUpdateVersion, true);
162 }
163
164 private function deleteUnusedFormPageIds($postId, $formIDs)
165 {
166 global $post;
167 if (!is_object($post) && !isset($post->ID)) {
168 return;
169 }
170 $postId = $post->ID;
171 $formModel = new FormModel();
172 $forms = $formModel->get(
173 ['id', 'generated_script_page_ids']
174 );
175 $regenerateScriptFlag = false;
176 foreach ($forms as $form) {
177 $formId = $form->id;
178 $generatedScriptPageIdsDecoded = json_decode($form->generated_script_page_ids, true);
179 $generatedScriptPageIds = is_array($generatedScriptPageIdsDecoded) ? array_keys($generatedScriptPageIdsDecoded) : [];
180 if (!empty($generatedScriptPageIds) && !in_array($formId, $formIDs) && in_array($postId, $generatedScriptPageIds)) {
181 unset($generatedScriptPageIdsDecoded[$postId]);
182 if (empty($generatedScriptPageIdsDecoded)) {
183 $generatedScriptPageIdsDecoded = new \stdClass();
184 }
185 $regenerateScriptFlag = true;
186 $formModel->update(['generated_script_page_ids' => wp_json_encode($generatedScriptPageIdsDecoded)], ['id' => $formId]);
187 }
188 }
189 if ($regenerateScriptFlag) {
190 $formUpdateVersion = get_option('bitform_form_update_version');
191 if (!$formUpdateVersion) {
192 $formUpdateVersion = 1;
193 } else {
194 $formUpdateVersion = (int) $formUpdateVersion + 1;
195 }
196 update_option('bitform_form_update_version', $formUpdateVersion);
197 }
198 return $regenerateScriptFlag;
199 }
200
201 private function regenerateScriptChecker($formsIds)
202 {
203 global $post;
204 if (!is_a($post, 'WP_Post') && !isset($post->ID)) {
205 return;
206 }
207 $postId = $post->ID;
208 $regenerateScriptFlag = false;
209 $formModel = new FormModel();
210 foreach ($formsIds as $formId) {
211 $formInstance = FormManager::getInstance($formId);
212 if (!$formInstance->isExist()) {
213 continue;
214 }
215 $generatedPages = $formInstance->getFormData('generated_script_page_ids');
216 if (empty($generatedPages)) {
217 $regenerateScriptFlag = true;
218 } elseif (is_object($generatedPages) && (!isset($generatedPages->{$postId}) || (isset($generatedPages->{$postId}) && false === $generatedPages->{$postId}))) {
219 $regenerateScriptFlag = true;
220 }
221 if (!$regenerateScriptFlag) {
222 continue;
223 }
224 if (!is_object($generatedPages)) {
225 $generatedPages = (object) [];
226 }
227 $generatedPages->{$postId} = true;
228 $formModel->update(
229 [
230 'generated_script_page_ids' => \wp_json_encode($generatedPages)
231 ],
232 [
233 'id' => $formId,
234 ]
235 );
236 }
237 return $regenerateScriptFlag;
238 }
239
240 private function addInlineScript($code, $handle = '', $position = 'after')
241 {
242 $scriptHandle = !empty($handle) ? $handle : 'bf-inline-script';
243 $formUpdateVersion = get_option('bitform_form_update_version');
244 if (!wp_script_is($scriptHandle)) {
245 wp_register_script($scriptHandle, '', [], $formUpdateVersion, true);
246 wp_enqueue_script($scriptHandle);
247 }
248 wp_add_inline_script($scriptHandle, $code, $position);
249 }
250
251 private function addInlineStyle($code, $handle = '')
252 {
253 $styleHandle = !empty($handle) ? $handle : 'bf-inline-style';
254 $formUpdateVersion = get_option('bitform_form_update_version');
255 if (!wp_style_is($styleHandle)) {
256 wp_register_style($styleHandle, '', [], $formUpdateVersion);
257 wp_enqueue_style($styleHandle);
258 }
259 wp_add_inline_style($styleHandle, $code);
260 }
261
262 private function triggerWorkflowOnLoad($formID, $shortCodeCounter, $fields, $workFlowRunType = 'create')
263 {
264 $FrontendFormManager = new FrontendFormManager($formID, $shortCodeCounter);
265 $previousValue = $this->getValuesFromQueryParams();
266 $formContent = $FrontendFormManager->getFormContentWithValue($previousValue);
267 if (!empty($formContent->workFlowExist)) {
268 $workFlowRunHelper = new WorkFlow($formID);
269 if (!empty($formContent->workFlowExist->onload)) {
270 $workFlowreturnedOnLoad = $workFlowRunHelper->executeOnLoad(
271 $workFlowRunType,
272 $fields
273 );
274
275 if (!empty($workFlowreturnedOnLoad['fields'])) {
276 return $workFlowreturnedOnLoad['fields'];
277 }
278 }
279 }
280
281 return $fields;
282 }
283
284 private function executeOnUserInput($formID, $shortCodeCounter, $fields)
285 {
286 $FrontendFormManager = FrontendFormManager::getInstance($formID, $shortCodeCounter);
287 $previousValue = $this->getValuesFromQueryParams();
288 $formContent = $FrontendFormManager->getFormContentWithValue($previousValue);
289 $customCodesExist = strpos(FrontEndScriptGenerator::getCustomCodes($formID)['JavaScript'], 'bfVars');
290 if ($customCodesExist || (!empty($formContent->workFlowExist) && !empty($formContent->workFlowExist->oninput))) {
291 $workFlowRunHelper = new WorkFlow($formID);
292 return $workFlowRunHelper->executeOnUserInput('create', $fields);
293 }
294 }
295
296 private function getValuesFromQueryParams()
297 {
298 // Read-only: query string parsed to pre-fill form fields. Values are sanitized per field before use.
299 $queryParamsValue = [];
300 if (isset($_SERVER['QUERY_STRING']) && !empty($_SERVER['QUERY_STRING'])) {
301 $reqField = wp_unslash($_SERVER['QUERY_STRING']);
302 foreach (explode('&', $reqField) as $keyValue) {
303 if (false !== strpos($keyValue, '=')) {
304 list($field, $value) = explode('=', $keyValue, 2);
305
306 if (!trim($value)) {
307 continue;
308 }
309 $field = sanitize_text_field(urldecode($field));
310 if (!empty($field)) {
311 $queryParamsValue[$field][] = sanitize_text_field(urldecode($value));
312 }
313 }
314 }
315 }
316
317 return $queryParamsValue;
318 }
319
320 public function handleFrontendRenderRequest($atts)
321 {
322 $formType = isset($atts['type']) ? $atts['type'] : 'classic';
323 $formPreview = isset($atts['form_preview']) ? $atts['form_preview'] : false;
324 if (isset($atts['form_id'])) {
325 $formID = intval($atts['form_id']);
326 }
327 if (isset($atts['entry_id'])) {
328 $entryId = intval($atts['entry_id']);
329 // Read-only: entry ID from query string for shortcode render. No state mutation.
330 } elseif (isset($_GET['bf_entry_id']) && !is_array($_GET['bf_entry_id'])) {
331 $entryId = intval(sanitize_text_field(wp_unslash($_GET['bf_entry_id'])));
332 } else {
333 $entryId = false;
334 }
335 if (isset($atts['id'])) {
336 $atts = shortcode_atts(['id' => 0], $atts);
337 $formID = intval($atts['id']);
338 }
339
340 if (!$formID) {
341 return __('Form ID cannot be empty', 'bit-form');
342 }
343
344 if (!$this->isExist($formID)) {
345 /* translators: %s: form ID */
346 return sprintf(__('#%s no. Form doesn\'t exists', 'bit-form'), $formID);
347 }
348
349 // Add-ons may detect whether the current visitor is resuming an abandoned entry.
350 $isAbandoned = (bool) apply_filters('bitform_is_abandoned_entry', false, $formID, $entryId, $atts);
351
352 FrontendHelpers::setBfFrontendFormIds($formID);
353 $bfFrontendFormIds = FrontendHelpers::$bfFrontendFormIds;
354 $shortCodeCounter = count($bfFrontendFormIds);
355 $FrontendFormManager = FrontendFormManager::getInstance($formID, $shortCodeCounter);
356
357 if (!$FrontendFormManager->checkStatus()) {
358 /* translators: %s: form ID */
359 return sprintf(__('#%s no. Form is not active', 'bit-form'), $formID);
360 }
361 ob_start();
362 $this->loadAssets($formID, $formType);
363
364 $font = $FrontendFormManager->getFont();
365
366 if ($font && !$formPreview) {
367 wp_enqueue_style('bf-google-font', $font, '1.0.0', true);
368 }
369
370 // Read-only: password reset token from URL for display-time validation. No state written until form is submitted.
371 if (!empty($_GET['token']) && !empty($_GET['id'])) {
372 $this->validPassowordResetToken(sanitize_text_field(wp_unslash($_GET['token'])), sanitize_text_field(wp_unslash($_GET['id'])), $formID);
373 }
374
375 $previousValue = $this->getValuesFromQueryParams();
376 $errorMessages = []; // delete
377 $FormIdentifier = esc_js($FrontendFormManager->getFormIdentifier());
378 $nonce = $FrontendFormManager->getFormToken();
379 $file = count($FrontendFormManager->getUploadFields()) > 0 ? $FrontendFormManager->getUploadFields() : false;
380
381 $FrontendFormManager->setViewCount();
382
383 $formContent = $FrontendFormManager->getFormContentWithValue($previousValue);
384 $fields = $formContent->fields;
385 $layout = $formContent->layout;
386 $nestedLayout = isset($formContent->nestedLayout) ? $formContent->nestedLayout : (object) [];
387 $buttons = !empty($formContent->buttons) ? $formContent->buttons : '';
388 $additional = $formContent->additional;
389
390 // $workFlowRunType = $entryId ? 'edit' : 'create';
391 if ($entryId && (FrontendHelpers::is_current_user_can_access($formID, 'entryEditAccess'))) {
392 $workFlowRunType = 'edit';
393 $adminFormHandler = new AdminFormHandler();
394 $getEntry = $adminFormHandler->getSingleEntry($formID, $entryId);
395 if (FrontendHelpers::is_current_user_can_access($formID, 'entryEditAccess', '', $getEntry->__user_id)) {
396 $fields = $this->setFieldsValue($fields, $formID, $entryId);
397 } elseif (!$isAbandoned) {
398 $entryId = false;
399 $workFlowRunType = 'create';
400 }
401 } else {
402 $entryId = false;
403 $workFlowRunType = 'create';
404 }
405
406 // if ($entryId) {
407 // $fields = $this->setFieldsValue($fields, $formID, $entryId);
408 // }
409
410 $fields = apply_filters('bitform_filter_before_workflow_onload_fields', $fields, $formID);
411 $fields = $this->triggerWorkflowOnLoad($formID, $shortCodeCounter, $fields, $workFlowRunType);
412 $fields = apply_filters('bitform_filter_after_workflow_onload_fields', $fields, $formID);
413 do_action('bitform_onload_fields', $fields, $formID);
414 $workFlowreturnedOnUserInput = $this->executeOnUserInput($formID, $shortCodeCounter, $fields);
415
416 // test for form before remove
417 $noLabelFieldTypes = ['decision-box', 'gdpr', 'html', 'shortcode', 'button', 'paypal', 'razorpay', 'recaptcha', 'turnstile', 'hcaptcha', 'stripe', 'spacer'];
418 foreach ($fields as $fldKey => $field) {
419 if (!in_array($field->typ, $noLabelFieldTypes) && isset($field->lbl)) {
420 $lblReplaceToBackslash = str_replace('$_bf_$', '\\', $field->lbl);
421 $fields->{$fldKey}->lbl = FieldValueHandler::replaceSmartTagWithValue($lblReplaceToBackslash);
422 }
423 }
424 $fieldsKey = $FrontendFormManager->getFieldsKey();
425
426 $captchaV3Settings = $FrontendFormManager->getCaptchaV3Settings();
427 if ($FrontendFormManager->getCaptchaSettings() || $captchaV3Settings || $FrontendFormManager->getTurnstileSettings() || $FrontendFormManager->isFieldTypeExist('hcaptcha')) {
428 $integrationHandler = new IntegrationHandler(0);
429 $allFormIntegrations = $integrationHandler->getAllIntegration('app');
430 if (!is_wp_error($allFormIntegrations)) {
431 foreach ($allFormIntegrations as $integration) {
432 if (
433 $FrontendFormManager->getCaptchaSettings()
434 && !is_null($integration->integration_type)
435 && 'gReCaptcha' === $integration->integration_type
436 ) {
437 $integrationDetails = json_decode($integration->integration_details);
438 $integrationDetails->id = $integration->id;
439 $reCAPTCHA = $integrationDetails;
440 $reCAPTCHAVersion = 'v2';
441 }
442
443 if (
444 $FrontendFormManager->getTurnstileSettings()
445 && !is_null($integration->integration_type)
446 && 'turnstileCaptcha' === $integration->integration_type
447 ) {
448 $integrationDetails = json_decode($integration->integration_details);
449 $turnstileSiteKey = $integrationDetails->siteKey;
450 }
451
452 if (
453 $FrontendFormManager->isFieldTypeExist('hcaptcha')
454 && !is_null($integration->integration_type)
455 && 'hcaptcha' === $integration->integration_type
456 ) {
457 $integrationDetails = json_decode($integration->integration_details);
458 $hCaptchaSiteKey = $integrationDetails->siteKey;
459 }
460
461 if ($captchaV3Settings) {
462 if (!is_null($integration->integration_type) && 'gReCaptchaV3' === $integration->integration_type) {
463 $integrationDetails = json_decode($integration->integration_details);
464 $integrationDetails->id = $integration->id;
465 $reCAPTCHA = $integrationDetails;
466 $reCAPTCHAVersion = 'v3';
467 }
468 }
469 }
470 }
471 }
472
473 if ($captchaV3Settings && !empty($reCAPTCHA->siteKey)) {
474 // DANGER: no matter what, DONT CHANGE THE SCRIPT ID OF THIS SCRIPT
475 $scriptId = BITFORMS_PREFIX . 'recaptcha';
476 // External Google reCAPTCHA script; version managed by URL query param. Loaded in header because
477 // standalone form views do not render wp_footer(), making footer enqueue unreliable.
478 wp_enqueue_script($scriptId, "https://www.google.com/recaptcha/api.js?render={$reCAPTCHA->siteKey}", [], null, false);
479 }
480
481 $configs = [
482 'bf_separator' => BITFORMS_BF_SEPARATOR,
483 ];
484
485 // check if fields has paypal or razorpay
486 $paymentFields = ['paypal', 'razorpay', 'stripe'];
487 $paymentFieldData = [];
488 foreach ($fields as $key => $field) {
489 if (in_array($field->typ, $paymentFields)) {
490 $paymentFieldData[$key] = $field;
491 }
492 }
493
494 if (!empty($paymentFieldData)) {
495 $integrationHandler = new IntegrationHandler(0);
496 foreach ($paymentFieldData as $fldKey => $fldData) {
497 $paymentIntegration = $integrationHandler->getAIntegration($fldData->payIntegID);
498 if (is_wp_error($paymentIntegration)) {
499 continue;
500 }
501 if ('paypal' === $fldData->typ) {
502 $integrationDetails = json_decode($paymentIntegration[0]->integration_details);
503 $clientID = $integrationDetails->clientID;
504 $fields->{$fldKey}->clientId = $clientID;
505 } elseif ('razorpay' === $fldData->typ) {
506 $integrationDetails = json_decode($paymentIntegration[0]->integration_details);
507 $clientID = $integrationDetails->apiKey;
508 $fields->{$fldKey}->clientId = $clientID;
509 } elseif ('stripe' === $fldData->typ) {
510 $integrationDetails = json_decode($paymentIntegration[0]->integration_details);
511 $publishableKey = $integrationDetails->publishableKey;
512 $fields->{$fldKey}->publishableKey = $publishableKey;
513 }
514 }
515 }
516
517 $bitFormFrontArr = [
518 'ajaxURL' => admin_url('admin-ajax.php'),
519 'nonce' => $nonce,
520 'version' => BITFORMS_VERSION,
521 'layout' => $layout,
522 'nestedLayout' => $nestedLayout,
523 'fields' => $fields,
524 'buttons' => $buttons,
525 'fieldsKey' => $fieldsKey,
526 'file' => $file,
527 'configs' => $configs,
528 'formId' => $formID,
529 'appID' => "bitforms_{$formID}",
530 'GCLID' => $FrontendFormManager->isGCLIDEnabled(),
531 'assetUrl' => BITFORMS_ASSET_URI,
532 'onfieldCondition' => !empty($workFlowreturnedOnUserInput['onfield_input_conditions']) ? $workFlowreturnedOnUserInput['onfield_input_conditions'] : false,
533 'smartTags' => $this->buildFrontendSmartTags($formID, $workFlowreturnedOnUserInput, $fields),
534 'paymentCallbackUrl' => get_rest_url() . 'bitform/v1/payments/razorpay',
535 'gRecaptchaSiteKey' => !empty($reCAPTCHA->siteKey) ? $reCAPTCHA->siteKey : null,
536 'gRecaptchaVersion' => !empty($reCAPTCHAVersion) ? $reCAPTCHAVersion : null,
537 'turnstileSiteKey' => !empty($turnstileSiteKey) ? $turnstileSiteKey : null,
538 'hCaptchaSiteKey' => !empty($hCaptchaSiteKey) ? $hCaptchaSiteKey : null,
539 ];
540
541 if ($entryId) {
542 $bitFormFrontArr['entryId'] = $entryId;
543 }
544
545 if (isset($additional->enabled->validateFocusLost)) {
546 $bitFormFrontArr['validateFocusLost'] = true;
547 }
548
549 if (!empty($isAbandoned)) {
550 $bitFormFrontArr['oldValues'] = $this->getFieldsValue($formID, $isAbandoned);
551 if (empty($entryId)) {
552 $bitFormFrontArr['entryId'] = $entryId;
553 }
554 }
555
556 $formInfo = $FrontendFormManager->getFormInfo();
557 $bitFormFrontArr['formName'] = $formInfo->formName ?? '';
558 if (is_array($layout) && count($layout) > 1) {
559 $multiStepSettings = isset($formInfo->multiStepSettings) ? $formInfo->multiStepSettings : null;
560 $newTempSettings = (object) [
561 'validateOnStepChange' => isset($multiStepSettings->validateOnStepChange) ? $multiStepSettings->validateOnStepChange : false,
562 'maintainStepHistory' => isset($multiStepSettings->maintainStepHistory) ? $multiStepSettings->maintainStepHistory : false,
563 'saveProgress' => isset($multiStepSettings->saveProgress) ? $multiStepSettings->saveProgress : false,
564 'showPercentage' => isset($multiStepSettings->progressSettings->showPercentage) ? $multiStepSettings->progressSettings->showPercentage : false,
565 ];
566 $bitFormFrontArr['formInfo'] = (object) [
567 'multiStepSettings' => $newTempSettings
568 ];
569 }
570
571 if (Helpers::property_exists_nested($formInfo, 'conversationalSettings->enable', true)) {
572 if (!isset($bitFormFrontArr['formInfo'])) {
573 $bitFormFrontArr['formInfo'] = new \stdClass();
574 }
575 $bitFormFrontArr['formInfo']->conversationalSettings = $formInfo->conversationalSettings;
576 }
577
578 $formAbandonmentSettings = $FrontendFormManager->getFormAbandonmentSettings();
579 if (Helpers::property_exists_nested($formAbandonmentSettings, 'active', true)) {
580 $bitFormFrontArr['formSettings'] = (object)[
581 'formAbandonment' => $formAbandonmentSettings
582 ];
583 }
584
585 $layout = wp_json_encode($layout);
586 $buttons = wp_json_encode($buttons);
587 $frontArr = wp_json_encode($bitFormFrontArr);
588
589 $bfGlobals = sprintf('
590 if(!window.bf_globals) {
591 window.bf_globals = {}
592 } if(!window.bf_globals.%1$s) {
593 window.bf_globals.%1$s = {}
594 }
595 if(document.getElementById("%1$s")) {
596 window.bf_globals.%1$s = {
597 ...window.bf_globals.%1$s,
598 ...%2$s
599 };
600 }', $FormIdentifier, $frontArr);
601
602 if ('conversational' === $formType
603 && isset($formContent->formInfo->conversationalSettings->enable)
604 && $formContent->formInfo->conversationalSettings->enable) {
605 $html = $FrontendFormManager->conversationalFormView($fields, $file, $errorMessages);
606 } else {
607 $html = $FrontendFormManager->formView($fields, $file, $errorMessages);
608 }
609
610 // if form preview then return html otherwise echo with output buffer
611 if ($formPreview) {
612 ob_clean();
613 $formViewObject = new \stdClass();
614 $formViewObject->html = $html;
615 $formViewObject->font = $font;
616 $formViewObject->bfGlobals = $bfGlobals;
617 $formViewObject->formContent = $formContent;
618 return $formViewObject;
619 }
620
621 $bfGlobalsHandle = 'bitform-bf-globals-' . sanitize_key($FormIdentifier);
622 $this->addInlineScript($bfGlobals, $bfGlobalsHandle, 'after');
623 $this->emitShowPickerBridge();
624
625 echo wp_kses(trim($html), EscapingHelper::getFormAllowedHtml($formContent));
626 return ob_get_clean();
627 }
628
629 /**
630 * Build the smart-tag map exposed to the browser in window.bf_globals[formId].smartTags.
631 *
632 * Security: the legacy code shipped the ENTIRE ~43-tag map to every visitor, leaking
633 * PII (admin/user/author email) and freezing per-visitor request data (IP, time,
634 * browser, referer) into cacheable HTML. We now emit ONLY tags that are (a) actually
635 * referenced by this form's client-evaluated surfaces — conditional logic, payment
636 * notes, admin custom JS — AND (b) flagged frontend-safe in the registry (static/post
637 * context only). Sensitive (identity) and request/visitor tags are never emitted; they
638 * resolve server-side at submit time instead.
639 *
640 * @param int|string $formID
641 * @param mixed $workflowConditions on-field input conditions (client-evaluated)
642 * @param mixed $fields form fields object (carries payment notes, etc.)
643 * @return array<string,string>
644 */
645 private function buildFrontendSmartTags($formID, $workflowConditions, $fields)
646 {
647 // Haystack = only surfaces the browser actually evaluates against smartTags.
648 $haystack = wp_json_encode($workflowConditions) . ' ' . wp_json_encode($fields);
649 $customJs = FrontEndScriptGenerator::getCustomCodes($formID)['JavaScript'];
650 if (is_string($customJs) && '' !== $customJs) {
651 $haystack .= ' ' . $customJs;
652 }
653
654 $ctx = SmartTags::getPostUserData();
655 $frontendSmartTags = [];
656 $referenced = [];
657 foreach (SmartTags::smartTagFieldKeys() as $key) {
658 if (!SmartTagRegistry::isFrontendExposable($key)) {
659 continue; // identity / request / param tags never travel to the browser
660 }
661 // Match '${' . key prefix so keys containing spaces/slashes/commas are handled.
662 if (false !== strpos($haystack, '${' . $key)) {
663 $referenced[] = $key;
664 $frontendSmartTags[$key] = SmartTagRegistry::resolve($key, $ctx);
665 }
666 }
667
668 /**
669 * Escape hatch: a site that genuinely needs an extra tag client-side can opt it
670 * back in explicitly here, rather than core shipping everything by default.
671 *
672 * @param array<string,string> $frontendSmartTags resolved frontend-safe smart tags
673 * @param int|string $formID
674 * @param string[] $referenced keys detected in client surfaces
675 */
676 return apply_filters('bitform_frontend_smarttags', $frontendSmartTags, $formID, $referenced);
677 }
678
679 /**
680 * Delegated listener that opens the native picker on date/time inputs marked
681 * with data-bf-show-picker. Replaces the legacy hardcoded onclick attribute.
682 * Registered as inline script once per request via wp_add_inline_script so
683 * the markup never travels through wp_kses().
684 */
685 private function emitShowPickerBridge()
686 {
687 static $emitted = false;
688 if ($emitted) {
689 return;
690 }
691 $emitted = true;
692 $code = 'if(!window.__bfShowPickerBound){window.__bfShowPickerBound=true;document.addEventListener("click",function(e){var t=e.target;if(t&&t.matches&&t.matches("input[data-bf-show-picker=\"1\"]")&&typeof t.showPicker==="function"){try{t.showPicker();}catch(_){}}});}';
693 $this->addInlineScript($code, 'bitform-show-picker-bridge', 'after');
694 }
695
696 private function isExist($formID)
697 {
698 $formModel = new FormModel();
699 $form = $formModel->get(
700 [
701 'id'
702 ],
703 [
704 'id' => $formID,
705 ]
706 );
707 if (!is_wp_error($form)) {
708 return true;
709 }
710 return false;
711 }
712
713 private function getFieldsValue($formID, $entryID)
714 {
715 $FrontendFormManager = FrontendFormManager::getInstance($formID, 1);
716 $formEntryModel = new FormEntryMetaModel();
717 $metaValues = $formEntryModel->get(
718 [
719 'meta_key',
720 'meta_value'
721 ],
722 [
723 'bitforms_form_entry_id' => $entryID,
724 ]
725 );
726 $formFields = $FrontendFormManager->getFields();
727 $fldsData = (object) [];
728 if (!is_wp_error($metaValues)) {
729 foreach ($metaValues as $metaValue) {
730 $metaKey = $metaValue->meta_key;
731 $metaVal = $metaValue->meta_value;
732 // if meta value is array then convert to string
733 if (preg_match('/^\[.*\]$/', $metaVal)) {
734 $metaVal = json_decode($metaVal);
735 //check is it array of objects
736 if (is_array($metaVal) && is_object($metaVal[0])) {
737 $metaVal = $metaValue->meta_value;
738 } else {
739 $metaVal = implode(BITFORMS_BF_SEPARATOR, $metaVal);
740 }
741 }
742 if (!isset($fldsData->{$metaKey})) {
743 $fldsData->{$metaKey} = '';
744 }
745 $fldsData->{$metaKey} = $metaVal;
746 if (isset($formFields[$metaKey]['type']) && in_array($formFields[$metaKey]['type'], ['file-up', 'advanced-file-up'])) {
747 $fldsData->{$metaKey} = $metaValue->meta_value;
748 }
749 }
750 }
751
752 return $fldsData;
753 }
754
755 public function setFieldsValue($fields, $formID, $entryID)
756 {
757 $formEntryModel = new FormEntryMetaModel();
758 $metaValues = $formEntryModel->get(
759 [
760 'meta_key',
761 'meta_value'
762 ],
763 [
764 'bitforms_form_entry_id' => $entryID,
765 ]
766 );
767 if (!is_wp_error($metaValues)) {
768 $urlQuery = wp_parse_url(FileDownloadProvider::getBaseDownloadURL(), PHP_URL_QUERY);
769 $baseDLURL = FileDownloadProvider::getBaseDownloadURL();
770 $baseDLURL = empty($urlQuery) ? $baseDLURL . '?' : $baseDLURL . '&';
771 $baseDLURL .= "formID={$formID}&entryID={$entryID}";
772
773 foreach ($fields as $field) {
774 if ('file-up' === $field->typ || 'advanced-file-up' === $field->typ) {
775 if (!isset($field->config)) {
776 $field->config = (object) [];
777 } elseif (is_array($field->config)) {
778 $field->config = (object) $field->config;
779 }
780 $field->config->baseDLURL = $baseDLURL;
781 }
782 }
783 foreach ($metaValues as $metaValue) {
784 $metaKey = $metaValue->meta_key;
785 $metaVal = $metaValue->meta_value;
786 // if meta value is array then convert to string
787 if (preg_match('/^\[.*\]$/', $metaVal)) {
788 $metaVal = json_decode($metaVal);
789 //check is it array of objects
790 if (is_array($metaVal) && is_object($metaVal[0])) {
791 $metaVal = $metaValue->meta_value;
792 } else {
793 $metaVal = implode(BITFORMS_BF_SEPARATOR, $metaVal);
794 }
795 }
796 if (property_exists($fields, $metaKey)) {
797 $fields->{$metaKey}->val = $metaVal;
798 if ('file-up' === $fields->{$metaKey}->typ || 'advanced-file-up' === $fields->{$metaKey}->typ) {
799 $fields->{$metaKey}->val = $metaValue->meta_value;
800 $fields->{$metaKey}->config->oldFiles = $metaValue->meta_value;
801 }
802 }
803 }
804 }
805 return $fields;
806 }
807
808 public function loadAssets($formID = 0, $fromType = 'classic')
809 {
810 $bfUniqFormIds = FrontendHelpers::getAllFormIdsInPage();
811 $isPageBuilder = FrontendHelpers::$isPageBuilder;
812 $bfMultipleFormsExists = $isPageBuilder ? true : count($bfUniqFormIds) > 1;
813
814 if (!empty($formID)) {
815 $formIds = [$formID];
816 } else {
817 $formIds = $bfUniqFormIds;
818 }
819 foreach ($formIds as $formID) {
820 global $bitform_dequeued_styles;
821 if (is_array($bitform_dequeued_styles) && in_array($formID, $bitform_dequeued_styles)) {
822 continue;
823 }
824 if ($bfMultipleFormsExists) {
825 $newFormId = $formID . '-formid';
826 } else {
827 $newFormId = $formID;
828 }
829 $formUpdateVersion = get_option('bitform_form_update_version');
830 if (!wp_style_is('bitform-style-' . $newFormId) && is_readable(BITFORMS_CONTENT_DIR . '/form-styles/bitform-' . $newFormId . '.css')) {
831 wp_enqueue_style(
832 'bitform-style-' . $newFormId,
833 BITFORMS_UPLOAD_BASE_URL . "/form-styles/bitform-{$newFormId}.css",
834 [],
835 $formUpdateVersion
836 );
837 if ($isPageBuilder) {
838 $formStyle = file_get_contents(BITFORMS_CONTENT_DIR . '/form-styles/bitform-' . $newFormId . '.css');
839 echo '<style id="bitform-style-' . esc_attr((string) $newFormId) . '">' . wp_kses($formStyle, []) . '</style>';
840 }
841 }
842 if (!wp_style_is('bitform-style-custom-' . $formID) && is_readable(BITFORMS_CONTENT_DIR . '/form-styles/bitform-custom-' . $formID . '.css')) {
843 wp_enqueue_style(
844 'bitform-style-custom-' . $formID,
845 BITFORMS_UPLOAD_BASE_URL . "/form-styles/bitform-custom-{$formID}.css",
846 [],
847 $formUpdateVersion
848 );
849 if ($isPageBuilder) {
850 $formStyle = file_get_contents(BITFORMS_CONTENT_DIR . '/form-styles/bitform-custom-' . $formID . '.css');
851 echo '<style id="bitform-style-custom-' . esc_attr((string) $formID) . '">' . wp_kses($formStyle, []) . '</style>';
852 }
853 }
854 // load conversational form css
855 if ('conversational' === $fromType) {
856 if (!wp_style_is('bitform-conversational-style-' . $formID) &&
857 is_readable(BITFORMS_CONTENT_DIR . "/form-styles/bitform-conversational-{$formID}.css")) {
858 wp_enqueue_style(
859 'bitform-conversational-style',
860 BITFORMS_UPLOAD_BASE_URL . "/form-styles/bitform-conversational-{$formID}.css",
861 [],
862 $formUpdateVersion
863 );
864 }
865 }
866 }
867 }
868 }
869