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
← All changes | includes/Frontend/Form/FrontendFormHandler.php +268 -61 3.1.13.3.1 View file →
@@ -15,16 +15,21 @@
15 15 use BitCode\BitForm\Core\Integration\IntegrationHandler;
16 16 use BitCode\BitForm\Core\Util\EscapingHelper;
17 17 use BitCode\BitForm\Core\Util\FieldValueHandler;
18 18 use BitCode\BitForm\Core\Util\FileDownloadProvider;
19 +use BitCode\BitForm\Core\Util\FileHandler;
19 20 use BitCode\BitForm\Core\Util\FrontendHelpers;
21 +use BitCode\BitForm\Core\Util\Log;
22 +use BitCode\BitForm\Core\Util\SmartTagRegistry;
20 23 use BitCode\BitForm\Core\Util\SmartTags;
21 -use BitCode\BitForm\Core\Util\SmartTagRegistry;
24 +use BitCode\BitForm\Core\Util\Utilities;
22 25 use BitCode\BitForm\Core\WorkFlow\WorkFlow;
23 -use Error;
24 26
25 27 final class FrontendFormHandler
26 28 {
29 + /** Largest stored signature inlined into the page as a data URI. */
30 + private const MAX_INLINE_SIGNATURE_BYTES = 2097152;
31 +
27 32 public function __construct()
28 33 {
29 34 // before markup load - formids [], posts [1,2]
30 35 add_action('wp_enqueue_scripts', [$this, 'loadAssets']);
@@ -30,9 +35,11 @@
30 35 add_action('wp_enqueue_scripts', [$this, 'loadAssets']);
31 36 // markup loads - formids []
32 37 add_shortcode('bitform', [$this, 'handleFrontendRenderRequest']);
33 38 // after markup load - formids [1,35,3]
34 - add_action('wp_footer', [$this, 'generateJS']);
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);
35 42 }
36 43
37 44 private function validPassowordResetToken($token, $userID, $formId)
38 45 {
@@ -61,10 +68,43 @@
61 68
62 69 return $formScriptSrc;
63 70 }
64 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 +
65 103 public function generateJs($formID = null, $entryID = null, $formType = null)
66 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.
67 107 // return true;
68 108 $isFormPreview = get_transient('bitform_form_preview');
69 109 if ($isFormPreview && !$formID) {
70 110 delete_transient('bitform_form_preview');
@@ -124,8 +164,10 @@
124 164 if (!$regenerateScriptFlag) {
125 165 $regenerateScriptFlag = $this->deleteUnusedFormPageIds($postId, $bfUniqFormIds);
126 166 }
127 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);
128 170 if (!$regenerateScriptFlag && !$isJsGenerating && !empty($formIDs)) {
129 171 wp_enqueue_script('bit-form-all-script-test', $this->getJSFileSrc($postId), [], $formUpdateVersion, true);
130 172 return;
131 173 }
@@ -158,8 +200,13 @@
158 200 $frontendScriptGenObj->generateJsFile($formContents, $allFields, $contentIds, $postId, $formIDs, $previewMode);
159 201 if ('preview' === $previewMode) {
160 202 return;
161 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 + }
162 209 wp_enqueue_script('bit-form-all-script-test', $this->getJSFileSrc($postId), [], $formUpdateVersion, true);
163 210 }
164 211
165 212 private function deleteUnusedFormPageIds($postId, $formIDs)
@@ -175,9 +222,9 @@
175 222 );
176 223 $regenerateScriptFlag = false;
177 224 foreach ($forms as $form) {
178 225 $formId = $form->id;
179 - $generatedScriptPageIdsDecoded = json_decode($form->generated_script_page_ids, true);
226 + $generatedScriptPageIdsDecoded = json_decode((string) $form->generated_script_page_ids, true);
180 227 $generatedScriptPageIds = is_array($generatedScriptPageIdsDecoded) ? array_keys($generatedScriptPageIdsDecoded) : [];
181 228 if (!empty($generatedScriptPageIds) && !in_array($formId, $formIDs) && in_array($postId, $generatedScriptPageIds)) {
182 229 unset($generatedScriptPageIdsDecoded[$postId]);
183 230 if (empty($generatedScriptPageIdsDecoded)) {
@@ -205,10 +252,11 @@
205 252 if (!is_a($post, 'WP_Post') && !isset($post->ID)) {
206 253 return;
207 254 }
208 255 $postId = $post->ID;
209 - $regenerateScriptFlag = false;
210 - $formModel = new FormModel();
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.
211 259 foreach ($formsIds as $formId) {
212 260 $formInstance = FormManager::getInstance($formId);
213 261 if (!$formInstance->isExist()) {
214 262 continue;
@@ -214,18 +262,35 @@
214 262 continue;
215 263 }
216 264 $generatedPages = $formInstance->getFormData('generated_script_page_ids');
217 265 if (empty($generatedPages)) {
218 - $regenerateScriptFlag = true;
219 - } elseif (is_object($generatedPages) && (!isset($generatedPages->{$postId}) || (isset($generatedPages->{$postId}) && false === $generatedPages->{$postId}))) {
220 - $regenerateScriptFlag = true;
266 + return true;
221 267 }
222 - if (!$regenerateScriptFlag) {
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)) {
223 284 continue;
224 285 }
286 + $generatedPages = Utilities::jsonObj($form[0]->generated_script_page_ids ?? '');
225 287 if (!is_object($generatedPages)) {
226 288 $generatedPages = (object) [];
227 289 }
290 + if (!empty($generatedPages->{$postId})) {
291 + continue;
292 + }
228 293 $generatedPages->{$postId} = true;
229 294 $formModel->update(
230 295 [
231 296 'generated_script_page_ids' => \wp_json_encode($generatedPages)
@@ -234,9 +299,8 @@
234 299 'id' => $formId,
235 300 ]
236 301 );
237 302 }
238 - return $regenerateScriptFlag;
239 303 }
240 304
241 305 private function addInlineScript($code, $handle = '', $position = 'after')
242 306 {
@@ -281,9 +345,9 @@
281 345
282 346 return $fields;
283 347 }
284 348
285 - private function executeOnUserInput($formID, $shortCodeCounter, $fields)
349 + private function executeOnUserInput($formID, $shortCodeCounter, $workFlowRunType = 'create')
286 350 {
287 351 $FrontendFormManager = FrontendFormManager::getInstance($formID, $shortCodeCounter);
288 352 $previousValue = $this->getValuesFromQueryParams();
289 353 $formContent = $FrontendFormManager->getFormContentWithValue($previousValue);
@@ -289,9 +353,9 @@
289 353 $formContent = $FrontendFormManager->getFormContentWithValue($previousValue);
290 354 $customCodesExist = strpos(FrontEndScriptGenerator::getCustomCodes($formID)['JavaScript'], 'bfVars');
291 355 if ($customCodesExist || (!empty($formContent->workFlowExist) && !empty($formContent->workFlowExist->oninput))) {
292 356 $workFlowRunHelper = new WorkFlow($formID);
293 - return $workFlowRunHelper->executeOnUserInput('create', $fields);
357 + return $workFlowRunHelper->executeOnUserInput($workFlowRunType);
294 358 }
295 359 }
296 360
297 361 private function getValuesFromQueryParams()
@@ -298,21 +362,20 @@
298 362 {
299 363 // Read-only: query string parsed to pre-fill form fields. Values are sanitized per field before use.
300 364 $queryParamsValue = [];
301 365 if (isset($_SERVER['QUERY_STRING']) && !empty($_SERVER['QUERY_STRING'])) {
302 - $reqField = sanitize_text_field(wp_unslash($_SERVER['QUERY_STRING']));
366 + $reqField = wp_unslash($_SERVER['QUERY_STRING']);
303 367 foreach (explode('&', $reqField) as $keyValue) {
304 - // $pattern = '/([a-zA-Z0-9])([a-zA-Z])\=+/';
305 - $pattern = '/([^.]+)=(.*?)([^.]+)/';
306 - $matches = preg_match($pattern, $keyValue, $matchFormat);
307 - if ($matches) {
368 + if (false !== strpos($keyValue, '=')) {
308 369 list($field, $value) = explode('=', $keyValue, 2);
309 370
310 371 if (!trim($value)) {
311 372 continue;
312 373 }
313 -
314 - $queryParamsValue[$field][] = sanitize_text_field(urldecode($value));
374 + $field = sanitize_text_field(urldecode($field));
375 + if (!empty($field)) {
376 + $queryParamsValue[$field][] = sanitize_text_field(urldecode($value));
377 + }
315 378 }
316 379 }
317 380 }
318 381
@@ -412,9 +475,9 @@
412 475 $fields = apply_filters('bitform_filter_before_workflow_onload_fields', $fields, $formID);
413 476 $fields = $this->triggerWorkflowOnLoad($formID, $shortCodeCounter, $fields, $workFlowRunType);
414 477 $fields = apply_filters('bitform_filter_after_workflow_onload_fields', $fields, $formID);
415 478 do_action('bitform_onload_fields', $fields, $formID);
416 - $workFlowreturnedOnUserInput = $this->executeOnUserInput($formID, $shortCodeCounter, $fields);
479 + $workFlowreturnedOnUserInput = $this->executeOnUserInput($formID, $shortCodeCounter, $workFlowRunType);
417 480
418 481 // test for form before remove
419 482 $noLabelFieldTypes = ['decision-box', 'gdpr', 'html', 'shortcode', 'button', 'paypal', 'razorpay', 'recaptcha', 'turnstile', 'hcaptcha', 'stripe', 'spacer'];
420 483 foreach ($fields as $fldKey => $field) {
@@ -435,12 +498,14 @@
435 498 $FrontendFormManager->getCaptchaSettings()
436 499 && !is_null($integration->integration_type)
437 500 && 'gReCaptcha' === $integration->integration_type
438 501 ) {
439 - $integrationDetails = json_decode($integration->integration_details);
440 - $integrationDetails->id = $integration->id;
441 - $reCAPTCHA = $integrationDetails;
442 - $reCAPTCHAVersion = 'v2';
502 + $integrationDetails = Utilities::jsonObj($integration->integration_details);
503 + if ($integrationDetails) {
504 + $integrationDetails->id = $integration->id;
505 + $reCAPTCHA = $integrationDetails;
506 + $reCAPTCHAVersion = 'v2';
507 + }
443 508 }
444 509
445 510 if (
446 511 $FrontendFormManager->getTurnstileSettings()
@@ -446,10 +511,10 @@
446 511 $FrontendFormManager->getTurnstileSettings()
447 512 && !is_null($integration->integration_type)
448 513 && 'turnstileCaptcha' === $integration->integration_type
449 514 ) {
450 - $integrationDetails = json_decode($integration->integration_details);
451 - $turnstileSiteKey = $integrationDetails->siteKey;
515 + $integrationDetails = Utilities::jsonObj($integration->integration_details);
516 + $turnstileSiteKey = $integrationDetails->siteKey ?? '';
452 517 }
453 518
454 519 if (
455 520 $FrontendFormManager->isFieldTypeExist('hcaptcha')
@@ -455,18 +520,20 @@
455 520 $FrontendFormManager->isFieldTypeExist('hcaptcha')
456 521 && !is_null($integration->integration_type)
457 522 && 'hcaptcha' === $integration->integration_type
458 523 ) {
459 - $integrationDetails = json_decode($integration->integration_details);
460 - $hCaptchaSiteKey = $integrationDetails->siteKey;
524 + $integrationDetails = Utilities::jsonObj($integration->integration_details);
525 + $hCaptchaSiteKey = $integrationDetails->siteKey ?? '';
461 526 }
462 527
463 528 if ($captchaV3Settings) {
464 529 if (!is_null($integration->integration_type) && 'gReCaptchaV3' === $integration->integration_type) {
465 - $integrationDetails = json_decode($integration->integration_details);
466 - $integrationDetails->id = $integration->id;
467 - $reCAPTCHA = $integrationDetails;
468 - $reCAPTCHAVersion = 'v3';
530 + $integrationDetails = Utilities::jsonObj($integration->integration_details);
531 + if ($integrationDetails) {
532 + $integrationDetails->id = $integration->id;
533 + $reCAPTCHA = $integrationDetails;
534 + $reCAPTCHAVersion = 'v3';
535 + }
469 536 }
470 537 }
471 538 }
472 539 }
@@ -499,19 +566,20 @@
499 566 $paymentIntegration = $integrationHandler->getAIntegration($fldData->payIntegID);
500 567 if (is_wp_error($paymentIntegration)) {
501 568 continue;
502 569 }
570 + $paymentIntegrationRow = Utilities::firstRow($paymentIntegration);
503 571 if ('paypal' === $fldData->typ) {
504 - $integrationDetails = json_decode($paymentIntegration[0]->integration_details);
505 - $clientID = $integrationDetails->clientID;
572 + $integrationDetails = Utilities::jsonObj($paymentIntegrationRow->integration_details ?? '');
573 + $clientID = $integrationDetails->clientID ?? '';
506 574 $fields->{$fldKey}->clientId = $clientID;
507 575 } elseif ('razorpay' === $fldData->typ) {
508 - $integrationDetails = json_decode($paymentIntegration[0]->integration_details);
509 - $clientID = $integrationDetails->apiKey;
576 + $integrationDetails = Utilities::jsonObj($paymentIntegrationRow->integration_details ?? '');
577 + $clientID = $integrationDetails->apiKey ?? '';
510 578 $fields->{$fldKey}->clientId = $clientID;
511 579 } elseif ('stripe' === $fldData->typ) {
512 - $integrationDetails = json_decode($paymentIntegration[0]->integration_details);
513 - $publishableKey = $integrationDetails->publishableKey;
580 + $integrationDetails = Utilities::jsonObj($paymentIntegrationRow->integration_details ?? '');
581 + $publishableKey = $integrationDetails->publishableKey ?? '';
514 582 $fields->{$fldKey}->publishableKey = $publishableKey;
515 583 }
516 584 }
517 585 }
@@ -541,8 +609,9 @@
541 609 ];
542 610
543 611 if ($entryId) {
544 612 $bitFormFrontArr['entryId'] = $entryId;
613 + self::markResponseUncacheable();
545 614 }
546 615
547 616 if (isset($additional->enabled->validateFocusLost)) {
548 617 $bitFormFrontArr['validateFocusLost'] = true;
@@ -548,11 +617,13 @@
548 617 $bitFormFrontArr['validateFocusLost'] = true;
549 618 }
550 619
551 620 if (!empty($isAbandoned)) {
621 + // One visitor's typed values, so this response must not be page-cached.
552 622 $bitFormFrontArr['oldValues'] = $this->getFieldsValue($formID, $isAbandoned);
623 + self::markResponseUncacheable();
553 624 if (empty($entryId)) {
554 - $bitFormFrontArr['entryId'] = $isAbandoned;
625 + $bitFormFrontArr['entryId'] = $entryId;
555 626 }
556 627 }
557 628
558 629 $formInfo = $FrontendFormManager->getFormInfo();
@@ -587,27 +658,31 @@
587 658 $layout = wp_json_encode($layout);
588 659 $buttons = wp_json_encode($buttons);
589 660 $frontArr = wp_json_encode($bitFormFrontArr);
590 661
591 - $bfGlobals = sprintf('
592 - if(!window.bf_globals) {
593 - window.bf_globals = {}
594 - } if(!window.bf_globals.%1$s) {
595 - window.bf_globals.%1$s = {}
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 = {}
596 667 }
597 - if(document.getElementById("%1$s")) {
598 - window.bf_globals.%1$s = {
599 - ...window.bf_globals.%1$s,
600 - ...%2$s
601 - };
602 - }', $FormIdentifier, $frontArr);
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);
603 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 +
604 679 if ('conversational' === $formType
605 680 && isset($formContent->formInfo->conversationalSettings->enable)
606 681 && $formContent->formInfo->conversationalSettings->enable) {
607 - $html = $FrontendFormManager->conversationalFormView($fields, $file, $errorMessages);
682 + $html = $FrontendFormManager->conversationalFormView($fields, $file, $errorMessages, null, !empty($entryId));
608 683 } else {
609 - $html = $FrontendFormManager->formView($fields, $file, $errorMessages);
684 + $html = $FrontendFormManager->formView($fields, $file, $errorMessages, null, !empty($entryId));
610 685 }
611 686
612 687 // if form preview then return html otherwise echo with output buffer
613 688 if ($formPreview) {
@@ -615,8 +690,9 @@
615 690 $formViewObject = new \stdClass();
616 691 $formViewObject->html = $html;
617 692 $formViewObject->font = $font;
618 693 $formViewObject->bfGlobals = $bfGlobals;
694 + $formViewObject->configTag = $configTag;
619 695 $formViewObject->formContent = $formContent;
620 696 return $formViewObject;
621 697 }
622 698
@@ -623,13 +699,60 @@
623 699 $bfGlobalsHandle = 'bitform-bf-globals-' . sanitize_key($FormIdentifier);
624 700 $this->addInlineScript($bfGlobals, $bfGlobalsHandle, 'after');
625 701 $this->emitShowPickerBridge();
626 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;
627 706 echo wp_kses(trim($html), EscapingHelper::getFormAllowedHtml($formContent));
628 707 return ob_get_clean();
629 708 }
630 709
631 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 + /**
632 755 * Build the smart-tag map exposed to the browser in window.bf_globals[formId].smartTags.
633 756 *
634 757 * Security: the legacy code shipped the ENTIRE ~43-tag map to every visitor, leaking
635 758 * PII (admin/user/author email) and freezing per-visitor request data (IP, time,
@@ -661,9 +784,9 @@
661 784 continue; // identity / request / param tags never travel to the browser
662 785 }
663 786 // Match '${' . key prefix so keys containing spaces/slashes/commas are handled.
664 787 if (false !== strpos($haystack, '${' . $key)) {
665 - $referenced[] = $key;
788 + $referenced[] = $key;
666 789 $frontendSmartTags[$key] = SmartTagRegistry::resolve($key, $ctx);
667 790 }
668 791 }
669 792
@@ -694,8 +817,15 @@
694 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(_){}}});}';
695 818 $this->addInlineScript($code, 'bitform-show-picker-bridge', 'after');
696 819 }
697 820
821 + /**
822 + * Does this form row exist?
823 + *
824 + * @param int $formID
825 + *
826 + * @return bool
827 + */
698 828 private function isExist($formID)
699 829 {
700 830 $formModel = new FormModel();
701 831 $form = $formModel->get(
@@ -705,12 +835,32 @@
705 835 [
706 836 'id' => $formID,
707 837 ]
708 838 );
709 - if (!is_wp_error($form)) {
710 - return true;
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;
711 851 }
712 - return false;
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;
713 863 }
714 864
715 865 private function getFieldsValue($formID, $entryID)
716 866 {
@@ -766,8 +916,23 @@
766 916 'bitforms_form_entry_id' => $entryID,
767 917 ]
768 918 );
769 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 + }
770 935 foreach ($metaValues as $metaValue) {
771 936 $metaKey = $metaValue->meta_key;
772 937 $metaVal = $metaValue->meta_value;
773 938 // if meta value is array then convert to string
@@ -784,17 +949,59 @@
784 949 $fields->{$metaKey}->val = $metaVal;
785 950 if ('file-up' === $fields->{$metaKey}->typ || 'advanced-file-up' === $fields->{$metaKey}->typ) {
786 951 $fields->{$metaKey}->val = $metaValue->meta_value;
787 952 $fields->{$metaKey}->config->oldFiles = $metaValue->meta_value;
788 - $urlQuery = wp_parse_url(FileDownloadProvider::getBaseDownloadURL(), PHP_URL_QUERY);
789 - $baseDLURL = FileDownloadProvider::getBaseDownloadURL();
790 - $baseDLURL = empty($urlQuery) ? $baseDLURL . '?' : $baseDLURL . '&';
791 - $fields->{$metaKey}->config->baseDLURL = $baseDLURL . "formID={$formID}&entryID={$entryID}";
792 953 }
954 + if ('signature' === $fields->{$metaKey}->typ) {
955 + $this->setOldSignature($fields->{$metaKey}, $formID, $entryID, $metaValue->meta_value);
956 + }
793 957 }
794 958 }
795 959 }
796 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);
797 1004 }
798 1005
799 1006 public function loadAssets($formID = 0, $fromType = 'classic')
800 1007 {