PluginProbe
Bit Form – Contact Form, Payment Forms, Multi Step Forms, Calculator & Custom Form Builder / 2.15.3
Bit Form – Contact Form, Payment Forms, Multi Step Forms, Calculator & Custom Form Builder v2.15.3
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
← All changes | includes/Frontend/Form/FrontendFormHandler.php +96 -407 3.3.12.15.3 View file →
@@ -1,13 +1,8 @@
1 1 <?php
2 2
3 3 namespace BitCode\BitForm\Frontend\Form;
4 4
5 -if (!defined('ABSPATH')) {
6 - exit;
7 -}
8 -
9 -use BitCode\BitForm\Admin\Form\AdminFormHandler;
10 5 use BitCode\BitForm\Admin\Form\FrontEndScriptGenerator;
11 6 use BitCode\BitForm\Admin\Form\Helpers;
12 7 use BitCode\BitForm\Core\Database\FormEntryMetaModel;
13 8 use BitCode\BitForm\Core\Database\FormModel;
@@ -12,24 +7,18 @@
12 7 use BitCode\BitForm\Core\Database\FormEntryMetaModel;
13 8 use BitCode\BitForm\Core\Database\FormModel;
14 9 use BitCode\BitForm\Core\Form\FormManager;
15 10 use BitCode\BitForm\Core\Integration\IntegrationHandler;
16 -use BitCode\BitForm\Core\Util\EscapingHelper;
17 11 use BitCode\BitForm\Core\Util\FieldValueHandler;
18 12 use BitCode\BitForm\Core\Util\FileDownloadProvider;
19 -use BitCode\BitForm\Core\Util\FileHandler;
20 13 use BitCode\BitForm\Core\Util\FrontendHelpers;
21 -use BitCode\BitForm\Core\Util\Log;
22 -use BitCode\BitForm\Core\Util\SmartTagRegistry;
23 14 use BitCode\BitForm\Core\Util\SmartTags;
24 15 use BitCode\BitForm\Core\Util\Utilities;
25 16 use BitCode\BitForm\Core\WorkFlow\WorkFlow;
17 +use BitCode\BitFormPro\Admin\FormSettings\FormAbandonment;
26 18
27 19 final class FrontendFormHandler
28 20 {
29 - /** Largest stored signature inlined into the page as a data URI. */
30 - private const MAX_INLINE_SIGNATURE_BYTES = 2097152;
31 -
32 21 public function __construct()
33 22 {
34 23 // before markup load - formids [], posts [1,2]
35 24 add_action('wp_enqueue_scripts', [$this, 'loadAssets']);
@@ -35,11 +24,9 @@
35 24 add_action('wp_enqueue_scripts', [$this, 'loadAssets']);
36 25 // markup loads - formids []
37 26 add_shortcode('bitform', [$this, 'handleFrontendRenderRequest']);
38 27 // 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);
28 + add_action('wp_footer', [$this, 'generateJS']);
42 29 }
43 30
44 31 private function validPassowordResetToken($token, $userID, $formId)
45 32 {
@@ -62,49 +49,16 @@
62 49 }
63 50
64 51 private function getJSFileSrc($postId)
65 52 {
66 - $formUpdateVersion = get_option('bitform_form_update_version');
53 + $formUpdateVersion = get_option('bit-form_form_update_version');
67 54 $formScriptSrc = BITFORMS_UPLOAD_BASE_URL . "/form-scripts/$postId/bitform-js-$postId.js?bfv=$formUpdateVersion";
68 55
69 56 return $formScriptSrc;
70 57 }
71 58
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 59 public function generateJs($formID = null, $entryID = null, $formType = null)
104 60 {
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 61 // return true;
108 62 $isFormPreview = get_transient('bitform_form_preview');
109 63 if ($isFormPreview && !$formID) {
110 64 delete_transient('bitform_form_preview');
@@ -123,12 +77,12 @@
123 77 $formIDs = [];
124 78 $previewMode = 'classic';
125 79 $postId = '';
126 80
127 - $formUpdateVersion = get_option('bitform_form_update_version');
81 + $formUpdateVersion = get_option('bit-form_form_update_version');
128 82 if ($formID) {
129 83 $formIDs[] = $formID;
130 - $FrontendFormManager = FrontendFormManager::getInstance($formID, 1);
84 + $FrontendFormManager = new FrontendFormManager($formID, 1);
131 85 $formInfo = $FrontendFormManager->getFormInfo();
132 86 $FormIdentifier = esc_js($FrontendFormManager->getFormIdentifier());
133 87 $formContent = $FrontendFormManager->getFormContentWithValue($this->getValuesFromQueryParams());
134 88 $formContent->formId = $formID;
@@ -164,10 +118,8 @@
164 118 if (!$regenerateScriptFlag) {
165 119 $regenerateScriptFlag = $this->deleteUnusedFormPageIds($postId, $bfUniqFormIds);
166 120 }
167 121 $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 122 if (!$regenerateScriptFlag && !$isJsGenerating && !empty($formIDs)) {
171 123 wp_enqueue_script('bit-form-all-script-test', $this->getJSFileSrc($postId), [], $formUpdateVersion, true);
172 124 return;
173 125 }
@@ -172,9 +124,9 @@
172 124 return;
173 125 }
174 126 foreach ($bfFrontendFormIds as $index => $formId) {
175 127 $shortCodeCounter = $index + 1;
176 - $FrontendFormManager = FrontendFormManager::getInstance($formId, $shortCodeCounter);
128 + $FrontendFormManager = new FrontendFormManager($formId, $shortCodeCounter);
177 129 $formInfo = $FrontendFormManager->getFormInfo();
178 130 $FormIdentifier = esc_js($FrontendFormManager->getFormIdentifier());
179 131 $formContent = $FrontendFormManager->getFormContentWithValue($this->getValuesFromQueryParams());
180 132 $formContent->formId = $formId;
@@ -197,16 +149,8 @@
197 149 return;
198 150 }
199 151
200 152 $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 153 wp_enqueue_script('bit-form-all-script-test', $this->getJSFileSrc($postId), [], $formUpdateVersion, true);
210 154 }
211 155
212 156 private function deleteUnusedFormPageIds($postId, $formIDs)
@@ -222,9 +166,9 @@
222 166 );
223 167 $regenerateScriptFlag = false;
224 168 foreach ($forms as $form) {
225 169 $formId = $form->id;
226 - $generatedScriptPageIdsDecoded = json_decode((string) $form->generated_script_page_ids, true);
170 + $generatedScriptPageIdsDecoded = json_decode($form->generated_script_page_ids, true);
227 171 $generatedScriptPageIds = is_array($generatedScriptPageIdsDecoded) ? array_keys($generatedScriptPageIdsDecoded) : [];
228 172 if (!empty($generatedScriptPageIds) && !in_array($formId, $formIDs) && in_array($postId, $generatedScriptPageIds)) {
229 173 unset($generatedScriptPageIdsDecoded[$postId]);
230 174 if (empty($generatedScriptPageIdsDecoded)) {
@@ -234,15 +178,15 @@
234 178 $formModel->update(['generated_script_page_ids' => wp_json_encode($generatedScriptPageIdsDecoded)], ['id' => $formId]);
235 179 }
236 180 }
237 181 if ($regenerateScriptFlag) {
238 - $formUpdateVersion = get_option('bitform_form_update_version');
182 + $formUpdateVersion = get_option('bit-form_form_update_version');
239 183 if (!$formUpdateVersion) {
240 184 $formUpdateVersion = 1;
241 185 } else {
242 186 $formUpdateVersion = (int) $formUpdateVersion + 1;
243 187 }
244 - update_option('bitform_form_update_version', $formUpdateVersion);
188 + update_option('bit-form_form_update_version', $formUpdateVersion);
245 189 }
246 190 return $regenerateScriptFlag;
247 191 }
248 192
@@ -252,45 +196,27 @@
252 196 if (!is_a($post, 'WP_Post') && !isset($post->ID)) {
253 197 return;
254 198 }
255 199 $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.
200 + $regenerateScriptFlag = false;
201 + $formModel = new FormModel();
259 202 foreach ($formsIds as $formId) {
260 - $formInstance = FormManager::getInstance($formId);
203 + $formInstance = new FormManager($formId);
261 204 if (!$formInstance->isExist()) {
262 205 continue;
263 206 }
264 207 $generatedPages = $formInstance->getFormData('generated_script_page_ids');
265 208 if (empty($generatedPages)) {
266 - return true;
209 + $regenerateScriptFlag = true;
210 + } elseif (is_object($generatedPages) && (!isset($generatedPages->{$postId}) || (isset($generatedPages->{$postId}) && false === $generatedPages->{$postId}))) {
211 + $regenerateScriptFlag = true;
267 212 }
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)) {
213 + if (!$regenerateScriptFlag) {
284 214 continue;
285 215 }
286 - $generatedPages = Utilities::jsonObj($form[0]->generated_script_page_ids ?? '');
287 216 if (!is_object($generatedPages)) {
288 217 $generatedPages = (object) [];
289 218 }
290 - if (!empty($generatedPages->{$postId})) {
291 - continue;
292 - }
293 219 $generatedPages->{$postId} = true;
294 220 $formModel->update(
295 221 [
296 222 'generated_script_page_ids' => \wp_json_encode($generatedPages)
@@ -299,16 +225,16 @@
299 225 'id' => $formId,
300 226 ]
301 227 );
302 228 }
229 + return $regenerateScriptFlag;
303 230 }
304 231
305 232 private function addInlineScript($code, $handle = '', $position = 'after')
306 233 {
307 234 $scriptHandle = !empty($handle) ? $handle : 'bf-inline-script';
308 - $formUpdateVersion = get_option('bitform_form_update_version');
309 235 if (!wp_script_is($scriptHandle)) {
310 - wp_register_script($scriptHandle, '', [], $formUpdateVersion, true);
236 + wp_register_script($scriptHandle, '', [], '', true);
311 237 wp_enqueue_script($scriptHandle);
312 238 }
313 239 wp_add_inline_script($scriptHandle, $code, $position);
314 240 }
@@ -315,11 +241,10 @@
315 241
316 242 private function addInlineStyle($code, $handle = '')
317 243 {
318 244 $styleHandle = !empty($handle) ? $handle : 'bf-inline-style';
319 - $formUpdateVersion = get_option('bitform_form_update_version');
320 245 if (!wp_style_is($styleHandle)) {
321 - wp_register_style($styleHandle, '', [], $formUpdateVersion);
246 + wp_register_style($styleHandle, '', [], '', true);
322 247 wp_enqueue_style($styleHandle);
323 248 }
324 249 wp_add_inline_style($styleHandle, $code);
325 250 }
@@ -345,37 +270,37 @@
345 270
346 271 return $fields;
347 272 }
348 273
349 - private function executeOnUserInput($formID, $shortCodeCounter, $workFlowRunType = 'create')
274 + private function executeOnUserInput($formID, $shortCodeCounter, $fields)
350 275 {
351 - $FrontendFormManager = FrontendFormManager::getInstance($formID, $shortCodeCounter);
276 + $FrontendFormManager = new FrontendFormManager($formID, $shortCodeCounter);
352 277 $previousValue = $this->getValuesFromQueryParams();
353 278 $formContent = $FrontendFormManager->getFormContentWithValue($previousValue);
354 279 $customCodesExist = strpos(FrontEndScriptGenerator::getCustomCodes($formID)['JavaScript'], 'bfVars');
355 280 if ($customCodesExist || (!empty($formContent->workFlowExist) && !empty($formContent->workFlowExist->oninput))) {
356 281 $workFlowRunHelper = new WorkFlow($formID);
357 - return $workFlowRunHelper->executeOnUserInput($workFlowRunType);
282 + return $workFlowRunHelper->executeOnUserInput('create', $fields);
358 283 }
359 284 }
360 285
361 286 private function getValuesFromQueryParams()
362 287 {
363 - // Read-only: query string parsed to pre-fill form fields. Values are sanitized per field before use.
364 288 $queryParamsValue = [];
365 289 if (isset($_SERVER['QUERY_STRING']) && !empty($_SERVER['QUERY_STRING'])) {
366 - $reqField = wp_unslash($_SERVER['QUERY_STRING']);
290 + $reqField = $_SERVER['QUERY_STRING'];
367 291 foreach (explode('&', $reqField) as $keyValue) {
368 - if (false !== strpos($keyValue, '=')) {
292 + // $pattern = '/([a-zA-Z0-9])([a-zA-Z])\=+/';
293 + $pattern = '/([^.]+)=(.*?)([^.]+)/';
294 + $matches = preg_match($pattern, $keyValue, $matchFormat);
295 + if ($matches) {
369 296 list($field, $value) = explode('=', $keyValue, 2);
370 297
371 298 if (!trim($value)) {
372 299 continue;
373 300 }
374 - $field = sanitize_text_field(urldecode($field));
375 - if (!empty($field)) {
376 - $queryParamsValue[$field][] = sanitize_text_field(urldecode($value));
377 - }
301 +
302 + $queryParamsValue[$field][] = sanitize_text_field(urldecode($value));
378 303 }
379 304 }
380 305 }
381 306
@@ -390,11 +315,10 @@
390 315 $formID = intval($atts['form_id']);
391 316 }
392 317 if (isset($atts['entry_id'])) {
393 318 $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'])));
319 + } elseif (isset($_GET['bf_entry_id'])) {
320 + $entryId = $_GET['bf_entry_id'];
397 321 } else {
398 322 $entryId = false;
399 323 }
400 324 if (isset($atts['id'])) {
@@ -406,22 +330,24 @@
406 330 return __('Form ID cannot be empty', 'bit-form');
407 331 }
408 332
409 333 if (!$this->isExist($formID)) {
410 - /* translators: %s: form ID */
411 334 return sprintf(__('#%s no. Form doesn\'t exists', 'bit-form'), $formID);
412 335 }
413 336
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);
337 + // check for abandoned form entry id
338 + $isAbandoned = false;
339 + if (empty($entryId) && Utilities::isPro() && class_exists('\BitCode\BitFormPro\Admin\FormSettings\FormAbandonment')) {
340 + $FormAbandonment = new FormAbandonment($formID);
341 + $isAbandoned = $FormAbandonment->checkAbandonedFormEntryId();
342 + }
416 343
417 344 FrontendHelpers::setBfFrontendFormIds($formID);
418 345 $bfFrontendFormIds = FrontendHelpers::$bfFrontendFormIds;
419 346 $shortCodeCounter = count($bfFrontendFormIds);
420 - $FrontendFormManager = FrontendFormManager::getInstance($formID, $shortCodeCounter);
347 + $FrontendFormManager = new FrontendFormManager($formID, $shortCodeCounter);
421 348
422 349 if (!$FrontendFormManager->checkStatus()) {
423 - /* translators: %s: form ID */
424 350 return sprintf(__('#%s no. Form is not active', 'bit-form'), $formID);
425 351 }
426 352 ob_start();
427 353 $this->loadAssets($formID, $formType);
@@ -431,11 +357,10 @@
431 357 if ($font && !$formPreview) {
432 358 wp_enqueue_style('bf-google-font', $font, '1.0.0', true);
433 359 }
434 360
435 - // Read-only: password reset token from URL for display-time validation. No state written until form is submitted.
436 361 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);
362 + $this->validPassowordResetToken($_GET['token'], $_GET['id'], $formID);
438 363 }
439 364
440 365 $previousValue = $this->getValuesFromQueryParams();
441 366 $errorMessages = []; // delete
@@ -452,20 +377,13 @@
452 377 $buttons = !empty($formContent->buttons) ? $formContent->buttons : '';
453 378 $additional = $formContent->additional;
454 379
455 380 // $workFlowRunType = $entryId ? 'edit' : 'create';
456 - if ($entryId && (FrontendHelpers::is_current_user_can_access($formID, 'entryEditAccess'))) {
381 + if ($entryId && (get_current_user_id() || is_admin())) {
382 + // return sprintf(__('Sorry!, You cannot edit #%s no form.', 'bit-form'), $formID);
457 383 $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 - }
384 + $fields = $this->setFieldsValue($fields, $formID, $entryId);
466 385 } else {
467 - $entryId = false;
468 386 $workFlowRunType = 'create';
469 387 }
470 388
471 389 // if ($entryId) {
@@ -475,14 +393,14 @@
475 393 $fields = apply_filters('bitform_filter_before_workflow_onload_fields', $fields, $formID);
476 394 $fields = $this->triggerWorkflowOnLoad($formID, $shortCodeCounter, $fields, $workFlowRunType);
477 395 $fields = apply_filters('bitform_filter_after_workflow_onload_fields', $fields, $formID);
478 396 do_action('bitform_onload_fields', $fields, $formID);
479 - $workFlowreturnedOnUserInput = $this->executeOnUserInput($formID, $shortCodeCounter, $workFlowRunType);
397 + $workFlowreturnedOnUserInput = $this->executeOnUserInput($formID, $shortCodeCounter, $fields);
480 398
481 399 // test for form before remove
482 - $noLabelFieldTypes = ['decision-box', 'gdpr', 'html', 'shortcode', 'button', 'paypal', 'razorpay', 'recaptcha', 'turnstile', 'hcaptcha', 'stripe', 'spacer'];
400 + $noLabel = ['decision-box', 'html', 'shortcode', 'button', 'paypal', 'razorpay', 'recaptcha'];
483 401 foreach ($fields as $fldKey => $field) {
484 - if (!in_array($field->typ, $noLabelFieldTypes) && isset($field->lbl)) {
402 + if (!in_array($field->typ, $noLabel) && isset($field->lbl)) {
485 403 $lblReplaceToBackslash = str_replace('$_bf_$', '\\', $field->lbl);
486 404 $fields->{$fldKey}->lbl = FieldValueHandler::replaceSmartTagWithValue($lblReplaceToBackslash);
487 405 }
488 406 }
@@ -488,9 +406,9 @@
488 406 }
489 407 $fieldsKey = $FrontendFormManager->getFieldsKey();
490 408
491 409 $captchaV3Settings = $FrontendFormManager->getCaptchaV3Settings();
492 - if ($FrontendFormManager->getCaptchaSettings() || $captchaV3Settings || $FrontendFormManager->getTurnstileSettings() || $FrontendFormManager->isFieldTypeExist('hcaptcha')) {
410 + if ($FrontendFormManager->getCaptchaSettings() || $captchaV3Settings || $FrontendFormManager->getTurnstileSettings()) {
493 411 $integrationHandler = new IntegrationHandler(0);
494 412 $allFormIntegrations = $integrationHandler->getAllIntegration('app');
495 413 if (!is_wp_error($allFormIntegrations)) {
496 414 foreach ($allFormIntegrations as $integration) {
@@ -498,14 +416,12 @@
498 416 $FrontendFormManager->getCaptchaSettings()
499 417 && !is_null($integration->integration_type)
500 418 && 'gReCaptcha' === $integration->integration_type
501 419 ) {
502 - $integrationDetails = Utilities::jsonObj($integration->integration_details);
503 - if ($integrationDetails) {
504 - $integrationDetails->id = $integration->id;
505 - $reCAPTCHA = $integrationDetails;
506 - $reCAPTCHAVersion = 'v2';
507 - }
420 + $integrationDetails = json_decode($integration->integration_details);
421 + $integrationDetails->id = $integration->id;
422 + $reCAPTCHA = $integrationDetails;
423 + $reCAPTCHAVersion = 'v2';
508 424 }
509 425
510 426 if (
511 427 $FrontendFormManager->getTurnstileSettings()
@@ -511,29 +427,18 @@
511 427 $FrontendFormManager->getTurnstileSettings()
512 428 && !is_null($integration->integration_type)
513 429 && 'turnstileCaptcha' === $integration->integration_type
514 430 ) {
515 - $integrationDetails = Utilities::jsonObj($integration->integration_details);
516 - $turnstileSiteKey = $integrationDetails->siteKey ?? '';
431 + $integrationDetails = json_decode($integration->integration_details);
432 + $turnstileSiteKey = $integrationDetails->siteKey;
517 433 }
518 434
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 435 if ($captchaV3Settings) {
529 436 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 - }
437 + $integrationDetails = json_decode($integration->integration_details);
438 + $integrationDetails->id = $integration->id;
439 + $reCAPTCHA = $integrationDetails;
440 + $reCAPTCHAVersion = 'v3';
536 441 }
537 442 }
538 443 }
539 444 }
@@ -541,11 +446,9 @@
541 446
542 447 if ($captchaV3Settings && !empty($reCAPTCHA->siteKey)) {
543 448 // DANGER: no matter what, DONT CHANGE THE SCRIPT ID OF THIS SCRIPT
544 449 $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);
450 + wp_enqueue_script($scriptId, "https://www.google.com/recaptcha/api.js?render={$reCAPTCHA->siteKey}");
548 451 }
549 452
550 453 $configs = [
551 454 'bf_separator' => BITFORMS_BF_SEPARATOR,
@@ -566,20 +469,19 @@
566 469 $paymentIntegration = $integrationHandler->getAIntegration($fldData->payIntegID);
567 470 if (is_wp_error($paymentIntegration)) {
568 471 continue;
569 472 }
570 - $paymentIntegrationRow = Utilities::firstRow($paymentIntegration);
571 473 if ('paypal' === $fldData->typ) {
572 - $integrationDetails = Utilities::jsonObj($paymentIntegrationRow->integration_details ?? '');
573 - $clientID = $integrationDetails->clientID ?? '';
474 + $integrationDetails = json_decode($paymentIntegration[0]->integration_details);
475 + $clientID = $integrationDetails->clientID;
574 476 $fields->{$fldKey}->clientId = $clientID;
575 477 } elseif ('razorpay' === $fldData->typ) {
576 - $integrationDetails = Utilities::jsonObj($paymentIntegrationRow->integration_details ?? '');
577 - $clientID = $integrationDetails->apiKey ?? '';
478 + $integrationDetails = json_decode($paymentIntegration[0]->integration_details);
479 + $clientID = $integrationDetails->apiKey;
578 480 $fields->{$fldKey}->clientId = $clientID;
579 481 } elseif ('stripe' === $fldData->typ) {
580 - $integrationDetails = Utilities::jsonObj($paymentIntegrationRow->integration_details ?? '');
581 - $publishableKey = $integrationDetails->publishableKey ?? '';
482 + $integrationDetails = json_decode($paymentIntegration[0]->integration_details);
483 + $publishableKey = $integrationDetails->publishableKey;
582 484 $fields->{$fldKey}->publishableKey = $publishableKey;
583 485 }
584 486 }
585 487 }
@@ -599,19 +501,17 @@
599 501 'appID' => "bitforms_{$formID}",
600 502 'GCLID' => $FrontendFormManager->isGCLIDEnabled(),
601 503 'assetUrl' => BITFORMS_ASSET_URI,
602 504 'onfieldCondition' => !empty($workFlowreturnedOnUserInput['onfield_input_conditions']) ? $workFlowreturnedOnUserInput['onfield_input_conditions'] : false,
603 - 'smartTags' => $this->buildFrontendSmartTags($formID, $workFlowreturnedOnUserInput, $fields),
505 + 'smartTags' => SmartTags::smartTags(SmartTags::getPostUserData()),
604 506 'paymentCallbackUrl' => get_rest_url() . 'bitform/v1/payments/razorpay',
605 507 'gRecaptchaSiteKey' => !empty($reCAPTCHA->siteKey) ? $reCAPTCHA->siteKey : null,
606 508 'gRecaptchaVersion' => !empty($reCAPTCHAVersion) ? $reCAPTCHAVersion : null,
607 - 'turnstileSiteKey' => !empty($turnstileSiteKey) ? $turnstileSiteKey : null,
608 - 'hCaptchaSiteKey' => !empty($hCaptchaSiteKey) ? $hCaptchaSiteKey : null,
509 + 'turnstileSiteKey' => !empty($turnstileSiteKey) ? $turnstileSiteKey : null
609 510 ];
610 511
611 512 if ($entryId) {
612 513 $bitFormFrontArr['entryId'] = $entryId;
613 - self::markResponseUncacheable();
614 514 }
615 515
616 516 if (isset($additional->enabled->validateFocusLost)) {
617 517 $bitFormFrontArr['validateFocusLost'] = true;
@@ -617,18 +517,15 @@
617 517 $bitFormFrontArr['validateFocusLost'] = true;
618 518 }
619 519
620 520 if (!empty($isAbandoned)) {
621 - // One visitor's typed values, so this response must not be page-cached.
622 521 $bitFormFrontArr['oldValues'] = $this->getFieldsValue($formID, $isAbandoned);
623 - self::markResponseUncacheable();
624 522 if (empty($entryId)) {
625 - $bitFormFrontArr['entryId'] = $entryId;
523 + $bitFormFrontArr['entryId'] = $isAbandoned;
626 524 }
627 525 }
628 526
629 527 $formInfo = $FrontendFormManager->getFormInfo();
630 - $bitFormFrontArr['formName'] = $formInfo->formName ?? '';
631 528 if (is_array($layout) && count($layout) > 1) {
632 529 $multiStepSettings = isset($formInfo->multiStepSettings) ? $formInfo->multiStepSettings : null;
633 530 $newTempSettings = (object) [
634 531 'validateOnStepChange' => isset($multiStepSettings->validateOnStepChange) ? $multiStepSettings->validateOnStepChange : false,
@@ -654,35 +551,34 @@
654 551 'formAbandonment' => $formAbandonmentSettings
655 552 ];
656 553 }
657 554
555 + $bitFormsFront = apply_filters(
556 + 'bitforms_localized_script',
557 + $bitFormFrontArr
558 + );
559 +
658 560 $layout = wp_json_encode($layout);
659 561 $buttons = wp_json_encode($buttons);
660 562 $frontArr = wp_json_encode($bitFormFrontArr);
661 563
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 = {}
564 + $bfGlobals = <<<BFGLOBALS
565 + if(!window.bf_globals) {
566 + window.bf_globals = {}
567 + } if(!window.bf_globals.{$FormIdentifier}) {
568 + window.bf_globals.{$FormIdentifier} = {}
667 569 }
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);
570 + if(document.getElementById('{$FormIdentifier}')) {
571 + window.bf_globals.{$FormIdentifier} = {...window.bf_globals.{$FormIdentifier}, ...{$frontArr}};
572 + }
573 +BFGLOBALS;
673 574
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 575 if ('conversational' === $formType
680 576 && isset($formContent->formInfo->conversationalSettings->enable)
681 577 && $formContent->formInfo->conversationalSettings->enable) {
682 - $html = $FrontendFormManager->conversationalFormView($fields, $file, $errorMessages, null, !empty($entryId));
578 + $html = $FrontendFormManager->conversationalFormView($fields, $file, $errorMessages);
683 579 } else {
684 - $html = $FrontendFormManager->formView($fields, $file, $errorMessages, null, !empty($entryId));
580 + $html = $FrontendFormManager->formView($fields, $file, $errorMessages);
685 581 }
686 582
687 583 // if form preview then return html otherwise echo with output buffer
688 584 if ($formPreview) {
@@ -690,142 +586,17 @@
690 586 $formViewObject = new \stdClass();
691 587 $formViewObject->html = $html;
692 588 $formViewObject->font = $font;
693 589 $formViewObject->bfGlobals = $bfGlobals;
694 - $formViewObject->configTag = $configTag;
695 - $formViewObject->formContent = $formContent;
696 590 return $formViewObject;
697 591 }
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));
592 + $html .= <<<BFGLOBALSSCRIPT
593 + <script id="bit-form-bf-globals-{$FormIdentifier}">{$bfGlobals}</script>
594 +BFGLOBALSSCRIPT;
595 + echo trim($html);
707 596 return ob_get_clean();
708 597 }
709 598
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 599 private function isExist($formID)
829 600 {
830 601 $formModel = new FormModel();
831 602 $form = $formModel->get(
@@ -835,37 +606,16 @@
835 606 [
836 607 'id' => $formID,
837 608 ]
838 609 );
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;
610 + if (!is_wp_error($form)) {
611 + return true;
851 612 }
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;
613 + return false;
863 614 }
864 615
865 616 private function getFieldsValue($formID, $entryID)
866 617 {
867 - $FrontendFormManager = FrontendFormManager::getInstance($formID, 1);
868 618 $formEntryModel = new FormEntryMetaModel();
869 619 $metaValues = $formEntryModel->get(
870 620 [
871 621 'meta_key',
@@ -874,9 +624,8 @@
874 624 [
875 625 'bitforms_form_entry_id' => $entryID,
876 626 ]
877 627 );
878 - $formFields = $FrontendFormManager->getFields();
879 628 $fldsData = (object) [];
880 629 if (!is_wp_error($metaValues)) {
881 630 foreach ($metaValues as $metaValue) {
882 631 $metaKey = $metaValue->meta_key;
@@ -894,11 +643,8 @@
894 643 if (!isset($fldsData->{$metaKey})) {
895 644 $fldsData->{$metaKey} = '';
896 645 }
897 646 $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 647 }
902 648 }
903 649
904 650 return $fldsData;
@@ -916,23 +662,8 @@
916 662 'bitforms_form_entry_id' => $entryID,
917 663 ]
918 664 );
919 665 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 666 foreach ($metaValues as $metaValue) {
936 667 $metaKey = $metaValue->meta_key;
937 668 $metaVal = $metaValue->meta_value;
938 669 // if meta value is array then convert to string
@@ -949,12 +680,13 @@
949 680 $fields->{$metaKey}->val = $metaVal;
950 681 if ('file-up' === $fields->{$metaKey}->typ || 'advanced-file-up' === $fields->{$metaKey}->typ) {
951 682 $fields->{$metaKey}->val = $metaValue->meta_value;
952 683 $fields->{$metaKey}->config->oldFiles = $metaValue->meta_value;
684 + $urlQuery = wp_parse_url(FileDownloadProvider::getBaseDownloadURL(), PHP_URL_QUERY);
685 + $baseDLURL = FileDownloadProvider::getBaseDownloadURL();
686 + $baseDLURL = empty($urlQuery) ? $baseDLURL . '?' : $baseDLURL . '&';
687 + $fields->{$metaKey}->config->baseDLURL = $baseDLURL . "formID={$formID}&entryID={$entryID}";
953 688 }
954 - if ('signature' === $fields->{$metaKey}->typ) {
955 - $this->setOldSignature($fields->{$metaKey}, $formID, $entryID, $metaValue->meta_value);
956 - }
957 689 }
958 690 }
959 691 }
960 692 return $fields;
@@ -959,51 +691,8 @@
959 691 }
960 692 return $fields;
961 693 }
962 694
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 695 public function loadAssets($formID = 0, $fromType = 'classic')
1007 696 {
1008 697 $bfUniqFormIds = FrontendHelpers::getAllFormIdsInPage();
1009 698 $isPageBuilder = FrontendHelpers::$isPageBuilder;
@@ -1023,9 +712,9 @@
1023 712 $newFormId = $formID . '-formid';
1024 713 } else {
1025 714 $newFormId = $formID;
1026 715 }
1027 - $formUpdateVersion = get_option('bitform_form_update_version');
716 + $formUpdateVersion = get_option('bit-form_form_update_version');
1028 717 if (!wp_style_is('bitform-style-' . $newFormId) && is_readable(BITFORMS_CONTENT_DIR . '/form-styles/bitform-' . $newFormId . '.css')) {
1029 718 wp_enqueue_style(
1030 719 'bitform-style-' . $newFormId,
1031 720 BITFORMS_UPLOAD_BASE_URL . "/form-styles/bitform-{$newFormId}.css",
@@ -1033,9 +722,9 @@
1033 722 $formUpdateVersion
1034 723 );
1035 724 if ($isPageBuilder) {
1036 725 $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>';
726 + echo sprintf("<style id='bitform-style-{$newFormId}'>%s</style>", $formStyle);
1038 727 }
1039 728 }
1040 729 if (!wp_style_is('bitform-style-custom-' . $formID) && is_readable(BITFORMS_CONTENT_DIR . '/form-styles/bitform-custom-' . $formID . '.css')) {
1041 730 wp_enqueue_style(
@@ -1045,9 +734,9 @@
1045 734 $formUpdateVersion
1046 735 );
1047 736 if ($isPageBuilder) {
1048 737 $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>';
738 + echo sprintf("<style id='bitform-style-custom-{$formID}'>%s</style>", $formStyle);
1050 739 }
1051 740 }
1052 741 // load conversational form css
1053 742 if ('conversational' === $fromType) {