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

875 lines 35.2 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\Util\Utilities;
23 use BitCode\BitForm\Core\WorkFlow\WorkFlow;
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, $workFlowRunType = 'create')
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($workFlowRunType);
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 = wp_unslash($_SERVER['QUERY_STRING']);
303 foreach (explode('&', $reqField) as $keyValue) {
304 if (false !== strpos($keyValue, '=')) {
305 list($field, $value) = explode('=', $keyValue, 2);
306
307 if (!trim($value)) {
308 continue;
309 }
310 $field = sanitize_text_field(urldecode($field));
311 if (!empty($field)) {
312 $queryParamsValue[$field][] = sanitize_text_field(urldecode($value));
313 }
314 }
315 }
316 }
317
318 return $queryParamsValue;
319 }
320
321 public function handleFrontendRenderRequest($atts)
322 {
323 $formType = isset($atts['type']) ? $atts['type'] : 'classic';
324 $formPreview = isset($atts['form_preview']) ? $atts['form_preview'] : false;
325 if (isset($atts['form_id'])) {
326 $formID = intval($atts['form_id']);
327 }
328 if (isset($atts['entry_id'])) {
329 $entryId = intval($atts['entry_id']);
330 // Read-only: entry ID from query string for shortcode render. No state mutation.
331 } elseif (isset($_GET['bf_entry_id']) && !is_array($_GET['bf_entry_id'])) {
332 $entryId = intval(sanitize_text_field(wp_unslash($_GET['bf_entry_id'])));
333 } else {
334 $entryId = false;
335 }
336 if (isset($atts['id'])) {
337 $atts = shortcode_atts(['id' => 0], $atts);
338 $formID = intval($atts['id']);
339 }
340
341 if (!$formID) {
342 return __('Form ID cannot be empty', 'bit-form');
343 }
344
345 if (!$this->isExist($formID)) {
346 /* translators: %s: form ID */
347 return sprintf(__('#%s no. Form doesn\'t exists', 'bit-form'), $formID);
348 }
349
350 // Add-ons may detect whether the current visitor is resuming an abandoned entry.
351 $isAbandoned = (bool) apply_filters('bitform_is_abandoned_entry', false, $formID, $entryId, $atts);
352
353 FrontendHelpers::setBfFrontendFormIds($formID);
354 $bfFrontendFormIds = FrontendHelpers::$bfFrontendFormIds;
355 $shortCodeCounter = count($bfFrontendFormIds);
356 $FrontendFormManager = FrontendFormManager::getInstance($formID, $shortCodeCounter);
357
358 if (!$FrontendFormManager->checkStatus()) {
359 /* translators: %s: form ID */
360 return sprintf(__('#%s no. Form is not active', 'bit-form'), $formID);
361 }
362 ob_start();
363 $this->loadAssets($formID, $formType);
364
365 $font = $FrontendFormManager->getFont();
366
367 if ($font && !$formPreview) {
368 wp_enqueue_style('bf-google-font', $font, '1.0.0', true);
369 }
370
371 // Read-only: password reset token from URL for display-time validation. No state written until form is submitted.
372 if (!empty($_GET['token']) && !empty($_GET['id'])) {
373 $this->validPassowordResetToken(sanitize_text_field(wp_unslash($_GET['token'])), sanitize_text_field(wp_unslash($_GET['id'])), $formID);
374 }
375
376 $previousValue = $this->getValuesFromQueryParams();
377 $errorMessages = []; // delete
378 $FormIdentifier = esc_js($FrontendFormManager->getFormIdentifier());
379 $nonce = $FrontendFormManager->getFormToken();
380 $file = count($FrontendFormManager->getUploadFields()) > 0 ? $FrontendFormManager->getUploadFields() : false;
381
382 $FrontendFormManager->setViewCount();
383
384 $formContent = $FrontendFormManager->getFormContentWithValue($previousValue);
385 $fields = $formContent->fields;
386 $layout = $formContent->layout;
387 $nestedLayout = isset($formContent->nestedLayout) ? $formContent->nestedLayout : (object) [];
388 $buttons = !empty($formContent->buttons) ? $formContent->buttons : '';
389 $additional = $formContent->additional;
390
391 // $workFlowRunType = $entryId ? 'edit' : 'create';
392 if ($entryId && (FrontendHelpers::is_current_user_can_access($formID, 'entryEditAccess'))) {
393 $workFlowRunType = 'edit';
394 $adminFormHandler = new AdminFormHandler();
395 $getEntry = $adminFormHandler->getSingleEntry($formID, $entryId);
396 if (FrontendHelpers::is_current_user_can_access($formID, 'entryEditAccess', '', $getEntry->__user_id)) {
397 $fields = $this->setFieldsValue($fields, $formID, $entryId);
398 } elseif (!$isAbandoned) {
399 $entryId = false;
400 $workFlowRunType = 'create';
401 }
402 } else {
403 $entryId = false;
404 $workFlowRunType = 'create';
405 }
406
407 // if ($entryId) {
408 // $fields = $this->setFieldsValue($fields, $formID, $entryId);
409 // }
410
411 $fields = apply_filters('bitform_filter_before_workflow_onload_fields', $fields, $formID);
412 $fields = $this->triggerWorkflowOnLoad($formID, $shortCodeCounter, $fields, $workFlowRunType);
413 $fields = apply_filters('bitform_filter_after_workflow_onload_fields', $fields, $formID);
414 do_action('bitform_onload_fields', $fields, $formID);
415 $workFlowreturnedOnUserInput = $this->executeOnUserInput($formID, $shortCodeCounter, $workFlowRunType);
416
417 // test for form before remove
418 $noLabelFieldTypes = ['decision-box', 'gdpr', 'html', 'shortcode', 'button', 'paypal', 'razorpay', 'recaptcha', 'turnstile', 'hcaptcha', 'stripe', 'spacer'];
419 foreach ($fields as $fldKey => $field) {
420 if (!in_array($field->typ, $noLabelFieldTypes) && isset($field->lbl)) {
421 $lblReplaceToBackslash = str_replace('$_bf_$', '\\', $field->lbl);
422 $fields->{$fldKey}->lbl = FieldValueHandler::replaceSmartTagWithValue($lblReplaceToBackslash);
423 }
424 }
425 $fieldsKey = $FrontendFormManager->getFieldsKey();
426
427 $captchaV3Settings = $FrontendFormManager->getCaptchaV3Settings();
428 if ($FrontendFormManager->getCaptchaSettings() || $captchaV3Settings || $FrontendFormManager->getTurnstileSettings() || $FrontendFormManager->isFieldTypeExist('hcaptcha')) {
429 $integrationHandler = new IntegrationHandler(0);
430 $allFormIntegrations = $integrationHandler->getAllIntegration('app');
431 if (!is_wp_error($allFormIntegrations)) {
432 foreach ($allFormIntegrations as $integration) {
433 if (
434 $FrontendFormManager->getCaptchaSettings()
435 && !is_null($integration->integration_type)
436 && 'gReCaptcha' === $integration->integration_type
437 ) {
438 $integrationDetails = Utilities::jsonObj($integration->integration_details);
439 if ($integrationDetails) {
440 $integrationDetails->id = $integration->id;
441 $reCAPTCHA = $integrationDetails;
442 $reCAPTCHAVersion = 'v2';
443 }
444 }
445
446 if (
447 $FrontendFormManager->getTurnstileSettings()
448 && !is_null($integration->integration_type)
449 && 'turnstileCaptcha' === $integration->integration_type
450 ) {
451 $integrationDetails = Utilities::jsonObj($integration->integration_details);
452 $turnstileSiteKey = $integrationDetails->siteKey ?? '';
453 }
454
455 if (
456 $FrontendFormManager->isFieldTypeExist('hcaptcha')
457 && !is_null($integration->integration_type)
458 && 'hcaptcha' === $integration->integration_type
459 ) {
460 $integrationDetails = Utilities::jsonObj($integration->integration_details);
461 $hCaptchaSiteKey = $integrationDetails->siteKey ?? '';
462 }
463
464 if ($captchaV3Settings) {
465 if (!is_null($integration->integration_type) && 'gReCaptchaV3' === $integration->integration_type) {
466 $integrationDetails = Utilities::jsonObj($integration->integration_details);
467 if ($integrationDetails) {
468 $integrationDetails->id = $integration->id;
469 $reCAPTCHA = $integrationDetails;
470 $reCAPTCHAVersion = 'v3';
471 }
472 }
473 }
474 }
475 }
476 }
477
478 if ($captchaV3Settings && !empty($reCAPTCHA->siteKey)) {
479 // DANGER: no matter what, DONT CHANGE THE SCRIPT ID OF THIS SCRIPT
480 $scriptId = BITFORMS_PREFIX . 'recaptcha';
481 // External Google reCAPTCHA script; version managed by URL query param. Loaded in header because
482 // standalone form views do not render wp_footer(), making footer enqueue unreliable.
483 wp_enqueue_script($scriptId, "https://www.google.com/recaptcha/api.js?render={$reCAPTCHA->siteKey}", [], null, false);
484 }
485
486 $configs = [
487 'bf_separator' => BITFORMS_BF_SEPARATOR,
488 ];
489
490 // check if fields has paypal or razorpay
491 $paymentFields = ['paypal', 'razorpay', 'stripe'];
492 $paymentFieldData = [];
493 foreach ($fields as $key => $field) {
494 if (in_array($field->typ, $paymentFields)) {
495 $paymentFieldData[$key] = $field;
496 }
497 }
498
499 if (!empty($paymentFieldData)) {
500 $integrationHandler = new IntegrationHandler(0);
501 foreach ($paymentFieldData as $fldKey => $fldData) {
502 $paymentIntegration = $integrationHandler->getAIntegration($fldData->payIntegID);
503 if (is_wp_error($paymentIntegration)) {
504 continue;
505 }
506 $paymentIntegrationRow = Utilities::firstRow($paymentIntegration);
507 if ('paypal' === $fldData->typ) {
508 $integrationDetails = Utilities::jsonObj($paymentIntegrationRow->integration_details ?? '');
509 $clientID = $integrationDetails->clientID ?? '';
510 $fields->{$fldKey}->clientId = $clientID;
511 } elseif ('razorpay' === $fldData->typ) {
512 $integrationDetails = Utilities::jsonObj($paymentIntegrationRow->integration_details ?? '');
513 $clientID = $integrationDetails->apiKey ?? '';
514 $fields->{$fldKey}->clientId = $clientID;
515 } elseif ('stripe' === $fldData->typ) {
516 $integrationDetails = Utilities::jsonObj($paymentIntegrationRow->integration_details ?? '');
517 $publishableKey = $integrationDetails->publishableKey ?? '';
518 $fields->{$fldKey}->publishableKey = $publishableKey;
519 }
520 }
521 }
522
523 $bitFormFrontArr = [
524 'ajaxURL' => admin_url('admin-ajax.php'),
525 'nonce' => $nonce,
526 'version' => BITFORMS_VERSION,
527 'layout' => $layout,
528 'nestedLayout' => $nestedLayout,
529 'fields' => $fields,
530 'buttons' => $buttons,
531 'fieldsKey' => $fieldsKey,
532 'file' => $file,
533 'configs' => $configs,
534 'formId' => $formID,
535 'appID' => "bitforms_{$formID}",
536 'GCLID' => $FrontendFormManager->isGCLIDEnabled(),
537 'assetUrl' => BITFORMS_ASSET_URI,
538 'onfieldCondition' => !empty($workFlowreturnedOnUserInput['onfield_input_conditions']) ? $workFlowreturnedOnUserInput['onfield_input_conditions'] : false,
539 'smartTags' => $this->buildFrontendSmartTags($formID, $workFlowreturnedOnUserInput, $fields),
540 'paymentCallbackUrl' => get_rest_url() . 'bitform/v1/payments/razorpay',
541 'gRecaptchaSiteKey' => !empty($reCAPTCHA->siteKey) ? $reCAPTCHA->siteKey : null,
542 'gRecaptchaVersion' => !empty($reCAPTCHAVersion) ? $reCAPTCHAVersion : null,
543 'turnstileSiteKey' => !empty($turnstileSiteKey) ? $turnstileSiteKey : null,
544 'hCaptchaSiteKey' => !empty($hCaptchaSiteKey) ? $hCaptchaSiteKey : null,
545 ];
546
547 if ($entryId) {
548 $bitFormFrontArr['entryId'] = $entryId;
549 }
550
551 if (isset($additional->enabled->validateFocusLost)) {
552 $bitFormFrontArr['validateFocusLost'] = true;
553 }
554
555 if (!empty($isAbandoned)) {
556 $bitFormFrontArr['oldValues'] = $this->getFieldsValue($formID, $isAbandoned);
557 if (empty($entryId)) {
558 $bitFormFrontArr['entryId'] = $entryId;
559 }
560 }
561
562 $formInfo = $FrontendFormManager->getFormInfo();
563 $bitFormFrontArr['formName'] = $formInfo->formName ?? '';
564 if (is_array($layout) && count($layout) > 1) {
565 $multiStepSettings = isset($formInfo->multiStepSettings) ? $formInfo->multiStepSettings : null;
566 $newTempSettings = (object) [
567 'validateOnStepChange' => isset($multiStepSettings->validateOnStepChange) ? $multiStepSettings->validateOnStepChange : false,
568 'maintainStepHistory' => isset($multiStepSettings->maintainStepHistory) ? $multiStepSettings->maintainStepHistory : false,
569 'saveProgress' => isset($multiStepSettings->saveProgress) ? $multiStepSettings->saveProgress : false,
570 'showPercentage' => isset($multiStepSettings->progressSettings->showPercentage) ? $multiStepSettings->progressSettings->showPercentage : false,
571 ];
572 $bitFormFrontArr['formInfo'] = (object) [
573 'multiStepSettings' => $newTempSettings
574 ];
575 }
576
577 if (Helpers::property_exists_nested($formInfo, 'conversationalSettings->enable', true)) {
578 if (!isset($bitFormFrontArr['formInfo'])) {
579 $bitFormFrontArr['formInfo'] = new \stdClass();
580 }
581 $bitFormFrontArr['formInfo']->conversationalSettings = $formInfo->conversationalSettings;
582 }
583
584 $formAbandonmentSettings = $FrontendFormManager->getFormAbandonmentSettings();
585 if (Helpers::property_exists_nested($formAbandonmentSettings, 'active', true)) {
586 $bitFormFrontArr['formSettings'] = (object)[
587 'formAbandonment' => $formAbandonmentSettings
588 ];
589 }
590
591 $layout = wp_json_encode($layout);
592 $buttons = wp_json_encode($buttons);
593 $frontArr = wp_json_encode($bitFormFrontArr);
594
595 $bfGlobals = sprintf('
596 if(!window.bf_globals) {
597 window.bf_globals = {}
598 } if(!window.bf_globals.%1$s) {
599 window.bf_globals.%1$s = {}
600 }
601 if(document.getElementById("%1$s")) {
602 window.bf_globals.%1$s = {
603 ...window.bf_globals.%1$s,
604 ...%2$s
605 };
606 }', $FormIdentifier, $frontArr);
607
608 if ('conversational' === $formType
609 && isset($formContent->formInfo->conversationalSettings->enable)
610 && $formContent->formInfo->conversationalSettings->enable) {
611 $html = $FrontendFormManager->conversationalFormView($fields, $file, $errorMessages);
612 } else {
613 $html = $FrontendFormManager->formView($fields, $file, $errorMessages);
614 }
615
616 // if form preview then return html otherwise echo with output buffer
617 if ($formPreview) {
618 ob_clean();
619 $formViewObject = new \stdClass();
620 $formViewObject->html = $html;
621 $formViewObject->font = $font;
622 $formViewObject->bfGlobals = $bfGlobals;
623 $formViewObject->formContent = $formContent;
624 return $formViewObject;
625 }
626
627 $bfGlobalsHandle = 'bitform-bf-globals-' . sanitize_key($FormIdentifier);
628 $this->addInlineScript($bfGlobals, $bfGlobalsHandle, 'after');
629 $this->emitShowPickerBridge();
630
631 echo wp_kses(trim($html), EscapingHelper::getFormAllowedHtml($formContent));
632 return ob_get_clean();
633 }
634
635 /**
636 * Build the smart-tag map exposed to the browser in window.bf_globals[formId].smartTags.
637 *
638 * Security: the legacy code shipped the ENTIRE ~43-tag map to every visitor, leaking
639 * PII (admin/user/author email) and freezing per-visitor request data (IP, time,
640 * browser, referer) into cacheable HTML. We now emit ONLY tags that are (a) actually
641 * referenced by this form's client-evaluated surfaces — conditional logic, payment
642 * notes, admin custom JS — AND (b) flagged frontend-safe in the registry (static/post
643 * context only). Sensitive (identity) and request/visitor tags are never emitted; they
644 * resolve server-side at submit time instead.
645 *
646 * @param int|string $formID
647 * @param mixed $workflowConditions on-field input conditions (client-evaluated)
648 * @param mixed $fields form fields object (carries payment notes, etc.)
649 * @return array<string,string>
650 */
651 private function buildFrontendSmartTags($formID, $workflowConditions, $fields)
652 {
653 // Haystack = only surfaces the browser actually evaluates against smartTags.
654 $haystack = wp_json_encode($workflowConditions) . ' ' . wp_json_encode($fields);
655 $customJs = FrontEndScriptGenerator::getCustomCodes($formID)['JavaScript'];
656 if (is_string($customJs) && '' !== $customJs) {
657 $haystack .= ' ' . $customJs;
658 }
659
660 $ctx = SmartTags::getPostUserData();
661 $frontendSmartTags = [];
662 $referenced = [];
663 foreach (SmartTags::smartTagFieldKeys() as $key) {
664 if (!SmartTagRegistry::isFrontendExposable($key)) {
665 continue; // identity / request / param tags never travel to the browser
666 }
667 // Match '${' . key prefix so keys containing spaces/slashes/commas are handled.
668 if (false !== strpos($haystack, '${' . $key)) {
669 $referenced[] = $key;
670 $frontendSmartTags[$key] = SmartTagRegistry::resolve($key, $ctx);
671 }
672 }
673
674 /**
675 * Escape hatch: a site that genuinely needs an extra tag client-side can opt it
676 * back in explicitly here, rather than core shipping everything by default.
677 *
678 * @param array<string,string> $frontendSmartTags resolved frontend-safe smart tags
679 * @param int|string $formID
680 * @param string[] $referenced keys detected in client surfaces
681 */
682 return apply_filters('bitform_frontend_smarttags', $frontendSmartTags, $formID, $referenced);
683 }
684
685 /**
686 * Delegated listener that opens the native picker on date/time inputs marked
687 * with data-bf-show-picker. Replaces the legacy hardcoded onclick attribute.
688 * Registered as inline script once per request via wp_add_inline_script so
689 * the markup never travels through wp_kses().
690 */
691 private function emitShowPickerBridge()
692 {
693 static $emitted = false;
694 if ($emitted) {
695 return;
696 }
697 $emitted = true;
698 $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(_){}}});}';
699 $this->addInlineScript($code, 'bitform-show-picker-bridge', 'after');
700 }
701
702 private function isExist($formID)
703 {
704 $formModel = new FormModel();
705 $form = $formModel->get(
706 [
707 'id'
708 ],
709 [
710 'id' => $formID,
711 ]
712 );
713 if (!is_wp_error($form)) {
714 return true;
715 }
716 return false;
717 }
718
719 private function getFieldsValue($formID, $entryID)
720 {
721 $FrontendFormManager = FrontendFormManager::getInstance($formID, 1);
722 $formEntryModel = new FormEntryMetaModel();
723 $metaValues = $formEntryModel->get(
724 [
725 'meta_key',
726 'meta_value'
727 ],
728 [
729 'bitforms_form_entry_id' => $entryID,
730 ]
731 );
732 $formFields = $FrontendFormManager->getFields();
733 $fldsData = (object) [];
734 if (!is_wp_error($metaValues)) {
735 foreach ($metaValues as $metaValue) {
736 $metaKey = $metaValue->meta_key;
737 $metaVal = $metaValue->meta_value;
738 // if meta value is array then convert to string
739 if (preg_match('/^\[.*\]$/', $metaVal)) {
740 $metaVal = json_decode($metaVal);
741 //check is it array of objects
742 if (is_array($metaVal) && is_object($metaVal[0])) {
743 $metaVal = $metaValue->meta_value;
744 } else {
745 $metaVal = implode(BITFORMS_BF_SEPARATOR, $metaVal);
746 }
747 }
748 if (!isset($fldsData->{$metaKey})) {
749 $fldsData->{$metaKey} = '';
750 }
751 $fldsData->{$metaKey} = $metaVal;
752 if (isset($formFields[$metaKey]['type']) && in_array($formFields[$metaKey]['type'], ['file-up', 'advanced-file-up'])) {
753 $fldsData->{$metaKey} = $metaValue->meta_value;
754 }
755 }
756 }
757
758 return $fldsData;
759 }
760
761 public function setFieldsValue($fields, $formID, $entryID)
762 {
763 $formEntryModel = new FormEntryMetaModel();
764 $metaValues = $formEntryModel->get(
765 [
766 'meta_key',
767 'meta_value'
768 ],
769 [
770 'bitforms_form_entry_id' => $entryID,
771 ]
772 );
773 if (!is_wp_error($metaValues)) {
774 $urlQuery = wp_parse_url(FileDownloadProvider::getBaseDownloadURL(), PHP_URL_QUERY);
775 $baseDLURL = FileDownloadProvider::getBaseDownloadURL();
776 $baseDLURL = empty($urlQuery) ? $baseDLURL . '?' : $baseDLURL . '&';
777 $baseDLURL .= "formID={$formID}&entryID={$entryID}";
778
779 foreach ($fields as $field) {
780 if ('file-up' === $field->typ || 'advanced-file-up' === $field->typ) {
781 if (!isset($field->config)) {
782 $field->config = (object) [];
783 } elseif (is_array($field->config)) {
784 $field->config = (object) $field->config;
785 }
786 $field->config->baseDLURL = $baseDLURL;
787 }
788 }
789 foreach ($metaValues as $metaValue) {
790 $metaKey = $metaValue->meta_key;
791 $metaVal = $metaValue->meta_value;
792 // if meta value is array then convert to string
793 if (preg_match('/^\[.*\]$/', $metaVal)) {
794 $metaVal = json_decode($metaVal);
795 //check is it array of objects
796 if (is_array($metaVal) && is_object($metaVal[0])) {
797 $metaVal = $metaValue->meta_value;
798 } else {
799 $metaVal = implode(BITFORMS_BF_SEPARATOR, $metaVal);
800 }
801 }
802 if (property_exists($fields, $metaKey)) {
803 $fields->{$metaKey}->val = $metaVal;
804 if ('file-up' === $fields->{$metaKey}->typ || 'advanced-file-up' === $fields->{$metaKey}->typ) {
805 $fields->{$metaKey}->val = $metaValue->meta_value;
806 $fields->{$metaKey}->config->oldFiles = $metaValue->meta_value;
807 }
808 }
809 }
810 }
811 return $fields;
812 }
813
814 public function loadAssets($formID = 0, $fromType = 'classic')
815 {
816 $bfUniqFormIds = FrontendHelpers::getAllFormIdsInPage();
817 $isPageBuilder = FrontendHelpers::$isPageBuilder;
818 $bfMultipleFormsExists = $isPageBuilder ? true : count($bfUniqFormIds) > 1;
819
820 if (!empty($formID)) {
821 $formIds = [$formID];
822 } else {
823 $formIds = $bfUniqFormIds;
824 }
825 foreach ($formIds as $formID) {
826 global $bitform_dequeued_styles;
827 if (is_array($bitform_dequeued_styles) && in_array($formID, $bitform_dequeued_styles)) {
828 continue;
829 }
830 if ($bfMultipleFormsExists) {
831 $newFormId = $formID . '-formid';
832 } else {
833 $newFormId = $formID;
834 }
835 $formUpdateVersion = get_option('bitform_form_update_version');
836 if (!wp_style_is('bitform-style-' . $newFormId) && is_readable(BITFORMS_CONTENT_DIR . '/form-styles/bitform-' . $newFormId . '.css')) {
837 wp_enqueue_style(
838 'bitform-style-' . $newFormId,
839 BITFORMS_UPLOAD_BASE_URL . "/form-styles/bitform-{$newFormId}.css",
840 [],
841 $formUpdateVersion
842 );
843 if ($isPageBuilder) {
844 $formStyle = file_get_contents(BITFORMS_CONTENT_DIR . '/form-styles/bitform-' . $newFormId . '.css');
845 echo '<style id="bitform-style-' . esc_attr((string) $newFormId) . '">' . wp_kses($formStyle, []) . '</style>';
846 }
847 }
848 if (!wp_style_is('bitform-style-custom-' . $formID) && is_readable(BITFORMS_CONTENT_DIR . '/form-styles/bitform-custom-' . $formID . '.css')) {
849 wp_enqueue_style(
850 'bitform-style-custom-' . $formID,
851 BITFORMS_UPLOAD_BASE_URL . "/form-styles/bitform-custom-{$formID}.css",
852 [],
853 $formUpdateVersion
854 );
855 if ($isPageBuilder) {
856 $formStyle = file_get_contents(BITFORMS_CONTENT_DIR . '/form-styles/bitform-custom-' . $formID . '.css');
857 echo '<style id="bitform-style-custom-' . esc_attr((string) $formID) . '">' . wp_kses($formStyle, []) . '</style>';
858 }
859 }
860 // load conversational form css
861 if ('conversational' === $fromType) {
862 if (!wp_style_is('bitform-conversational-style-' . $formID) &&
863 is_readable(BITFORMS_CONTENT_DIR . "/form-styles/bitform-conversational-{$formID}.css")) {
864 wp_enqueue_style(
865 'bitform-conversational-style',
866 BITFORMS_UPLOAD_BASE_URL . "/form-styles/bitform-conversational-{$formID}.css",
867 [],
868 $formUpdateVersion
869 );
870 }
871 }
872 }
873 }
874 }
875