PluginProbe
Bit Form – Contact Form, Payment Forms, Multi Step Forms, Calculator & Custom Form Builder / 3.2.1
Bit Form – Contact Form, Payment Forms, Multi Step Forms, Calculator & Custom Form Builder v3.2.1
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.2.1, at includes/Frontend/Form/FrontendFormHandler.php

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