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

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