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

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