PluginProbe
Bit Form – Contact Form, Payment Forms, Multi Step Forms, Calculator & Custom Form Builder / 3.3.1
Bit Form – Contact Form, Payment Forms, Multi Step Forms, Calculator & Custom Form Builder v3.3.1
3.3.1 V-3.3.0 3.2.2 3.2.1 3.2.0 3.1.4 3.1.3 3.1.2 3.1.1 3.1.0 V3.0.3 V3.0.2 -3.0.1 V_3.0.0 1.1.1 1.1.8 1.2 1.3 1.4 1.4.18 1.5.2 1.9 2.0 2.10.0 2.10.1 All 138 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.3.1, at includes/Frontend/Form/FrontendFormHandler.php

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