PluginProbe
Bit Form – Contact Form, Payment Forms, Multi Step Forms, Calculator & Custom Form Builder / V3.3.2
Bit Form – Contact Form, Payment Forms, Multi Step Forms, Calculator & Custom Form Builder vV3.3.2
V3.3.2 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 All 139 releases
← All changes | includes/Frontend/Form/FrontendFormHandler.php +179 -39 3.2.2 → V3.3.2 View file →
@@ -8,8 +8,9 @@
8 8
9 9 use BitCode\BitForm\Admin\Form\AdminFormHandler;
10 10 use BitCode\BitForm\Admin\Form\FrontEndScriptGenerator;
11 11 use BitCode\BitForm\Admin\Form\Helpers;
12 +use BitCode\BitForm\Core\Cryptography\Cryptography;
12 13 use BitCode\BitForm\Core\Database\FormEntryMetaModel;
13 14 use BitCode\BitForm\Core\Database\FormModel;
14 15 use BitCode\BitForm\Core\Form\FormManager;
15 16 use BitCode\BitForm\Core\Integration\IntegrationHandler;
@@ -15,8 +16,9 @@
15 16 use BitCode\BitForm\Core\Integration\IntegrationHandler;
16 17 use BitCode\BitForm\Core\Util\EscapingHelper;
17 18 use BitCode\BitForm\Core\Util\FieldValueHandler;
18 19 use BitCode\BitForm\Core\Util\FileDownloadProvider;
20 +use BitCode\BitForm\Core\Util\FileHandler;
19 21 use BitCode\BitForm\Core\Util\FrontendHelpers;
20 22 use BitCode\BitForm\Core\Util\Log;
21 23 use BitCode\BitForm\Core\Util\SmartTagRegistry;
22 24 use BitCode\BitForm\Core\Util\SmartTags;
@@ -24,8 +26,11 @@
24 26 use BitCode\BitForm\Core\WorkFlow\WorkFlow;
25 27
26 28 final class FrontendFormHandler
27 29 {
30 + /** Largest stored signature inlined into the page as a data URI. */
31 + private const MAX_INLINE_SIGNATURE_BYTES = 2097152;
32 +
28 33 public function __construct()
29 34 {
30 35 // before markup load - formids [], posts [1,2]
31 36 add_action('wp_enqueue_scripts', [$this, 'loadAssets']);
@@ -31,29 +36,37 @@
31 36 add_action('wp_enqueue_scripts', [$this, 'loadAssets']);
32 37 // markup loads - formids []
33 38 add_shortcode('bitform', [$this, 'handleFrontendRenderRequest']);
34 39 // after markup load - formids [1,35,3]
35 - add_action('wp_footer', [$this, 'generateJS']);
40 + // After popup plugins render at 10 (that is when popup-only forms register
41 + // their formID), before wp_print_footer_scripts at 20.
42 + add_action('wp_footer', [$this, 'generateJS'], 15);
36 43 }
37 44
38 - private function validPassowordResetToken($token, $userID, $formId)
45 + /**
46 + * A reset form opened from a used or expired link cannot succeed, so show why instead of
47 + * the form. Returns the notice markup, or '' when the link is fine or the form is not a
48 + * reset form. Rendered in place of the form (the old exit() cut the page off mid-theme).
49 + *
50 + * @param string $token key from the reset link
51 + * @param string $userID user ID from the reset link
52 + * @param int $formId form being rendered
53 + *
54 + * @return string
55 + */
56 + private function invalidPasswordResetNotice($token, $userID, $formId)
39 57 {
40 58 $existResetInteg = (new IntegrationHandler($formId))->getAllIntegration('wp_user_auth', 'wp_auth', 1);
41 - if (!is_wp_error($existResetInteg) && count($existResetInteg) > 0) {
42 - if ('reset' === $existResetInteg[0]->integration_name) {
43 - $user = get_userdata($userID);
44 - if ($user) {
45 - $validKey = check_password_reset_key($token, $user->user_login);
46 - if (is_wp_error($validKey)) {
47 - echo "<div id='bf-resp' style='display:grid;justify-content:center;color:#860000;'>This password reset token is invalid.</div>";
48 - exit();
49 - }
50 - } else {
51 - echo "<div id='bf-resp' style='display:grid;justify-content:center;color:#860000;'>Invalid User!!</div>";
52 - exit();
53 - }
54 - }
59 + if (is_wp_error($existResetInteg) || empty($existResetInteg) || 'reset' !== $existResetInteg[0]->integration_name) {
60 + return '';
55 61 }
62 + $user = get_userdata((int) $userID);
63 + if ($user && !is_wp_error(check_password_reset_key($token, $user->user_login))) {
64 + return '';
65 + }
66 + return "<div id='bf-resp' role='alert' style='display:grid;justify-content:center;color:#860000;'>"
67 + . esc_html__('This password reset link is invalid or has expired. Please request a new one.', 'bit-form')
68 + . '</div>';
56 69 }
57 70
58 71 private function getJSFileSrc($postId)
59 72 {
@@ -95,8 +108,10 @@
95 108 }
96 109
97 110 public function generateJs($formID = null, $entryID = null, $formType = null)
98 111 {
112 + // bitform-js-{postId}.js is disk-cached per post ID with no language key.
113 + // Display strings must stay out of it and travel in bf_globals per request.
99 114 // return true;
100 115 $isFormPreview = get_transient('bitform_form_preview');
101 116 if ($isFormPreview && !$formID) {
102 117 delete_transient('bitform_form_preview');
@@ -214,9 +229,9 @@
214 229 );
215 230 $regenerateScriptFlag = false;
216 231 foreach ($forms as $form) {
217 232 $formId = $form->id;
218 - $generatedScriptPageIdsDecoded = json_decode($form->generated_script_page_ids, true);
233 + $generatedScriptPageIdsDecoded = json_decode((string) $form->generated_script_page_ids, true);
219 234 $generatedScriptPageIds = is_array($generatedScriptPageIdsDecoded) ? array_keys($generatedScriptPageIdsDecoded) : [];
220 235 if (!empty($generatedScriptPageIds) && !in_array($formId, $formIDs) && in_array($postId, $generatedScriptPageIds)) {
221 236 unset($generatedScriptPageIdsDecoded[$postId]);
222 237 if (empty($generatedScriptPageIdsDecoded)) {
@@ -402,10 +417,13 @@
402 417 /* translators: %s: form ID */
403 418 return sprintf(__('#%s no. Form doesn\'t exists', 'bit-form'), $formID);
404 419 }
405 420
406 - // Add-ons may detect whether the current visitor is resuming an abandoned entry.
407 - $isAbandoned = (bool) apply_filters('bitform_is_abandoned_entry', false, $formID, $entryId, $atts);
421 + // Add-ons return the id of the abandoned entry this visitor can resume. A bare
422 + // true (older Pro) carries no id, so nothing is restored rather than entry #1.
423 + $abandonedEntryId = apply_filters('bitform_is_abandoned_entry', false, $formID, $entryId, $atts);
424 + $abandonedEntryId = (!is_bool($abandonedEntryId) && is_numeric($abandonedEntryId)) ? (int) $abandonedEntryId : 0;
425 + $isAbandoned = $abandonedEntryId > 0;
408 426
409 427 FrontendHelpers::setBfFrontendFormIds($formID);
410 428 $bfFrontendFormIds = FrontendHelpers::$bfFrontendFormIds;
411 429 $shortCodeCounter = count($bfFrontendFormIds);
@@ -425,9 +443,13 @@
425 443 }
426 444
427 445 // Read-only: password reset token from URL for display-time validation. No state written until form is submitted.
428 446 if (!empty($_GET['token']) && !empty($_GET['id'])) {
429 - $this->validPassowordResetToken(sanitize_text_field(wp_unslash($_GET['token'])), sanitize_text_field(wp_unslash($_GET['id'])), $formID);
447 + $resetNotice = $this->invalidPasswordResetNotice(sanitize_text_field(wp_unslash($_GET['token'])), sanitize_text_field(wp_unslash($_GET['id'])), $formID);
448 + if ('' !== $resetNotice) {
449 + ob_end_clean();
450 + return $resetNotice;
451 + }
430 452 }
431 453
432 454 $previousValue = $this->getValuesFromQueryParams();
433 455 $errorMessages = []; // delete
@@ -601,8 +623,9 @@
601 623 ];
602 624
603 625 if ($entryId) {
604 626 $bitFormFrontArr['entryId'] = $entryId;
627 + self::markResponseUncacheable();
605 628 }
606 629
607 630 if (isset($additional->enabled->validateFocusLost)) {
608 631 $bitFormFrontArr['validateFocusLost'] = true;
@@ -607,12 +630,19 @@
607 630 if (isset($additional->enabled->validateFocusLost)) {
608 631 $bitFormFrontArr['validateFocusLost'] = true;
609 632 }
610 633
611 - if (!empty($isAbandoned)) {
612 - $bitFormFrontArr['oldValues'] = $this->getFieldsValue($formID, $isAbandoned);
634 + if ($isAbandoned) {
635 + // One visitor's typed values, so this response must not be page-cached.
636 + $bitFormFrontArr['oldValues'] = $this->getFieldsValue($formID, $abandonedEntryId);
637 + self::markResponseUncacheable();
613 638 if (empty($entryId)) {
614 - $bitFormFrontArr['entryId'] = $entryId;
639 + // Later draft saves must update this entry, not start a new one each time.
640 + $bitFormFrontArr['entryId'] = $abandonedEntryId;
641 + $entryToken = Cryptography::encrypt($abandonedEntryId, Helpers::getBitformSalt());
642 + if (is_string($entryToken)) {
643 + $bitFormFrontArr['entryToken'] = $entryToken;
644 + }
615 645 }
616 646 }
617 647
618 648 $formInfo = $FrontendFormManager->getFormInfo();
@@ -647,27 +677,31 @@
647 677 $layout = wp_json_encode($layout);
648 678 $buttons = wp_json_encode($buttons);
649 679 $frontArr = wp_json_encode($bitFormFrontArr);
650 680
651 - $bfGlobals = sprintf('
652 - if(!window.bf_globals) {
653 - window.bf_globals = {}
654 - } if(!window.bf_globals.%1$s) {
655 - window.bf_globals.%1$s = {}
681 + $bfGlobals = sprintf('
682 + if(!window.bf_globals) {
683 + window.bf_globals = {}
684 + } if(!window.bf_globals.%1$s) {
685 + window.bf_globals.%1$s = {}
656 686 }
657 - if(document.getElementById("%1$s")) {
658 - window.bf_globals.%1$s = {
659 - ...window.bf_globals.%1$s,
660 - ...%2$s
661 - };
662 - }', $FormIdentifier, $frontArr);
687 + window.bf_globals.%1$s = {
688 + ...window.bf_globals.%1$s,
689 + ...%2$s
690 + };
691 + if (typeof window.bitformInit === "function") { window.bitformInit("%1$s"); }', $FormIdentifier, $frontArr);
663 692
693 + // Inert copy of the config. Optimizers only rewrite executable scripts, so
694 + // this survives and travels with the markup; the runtime hydrates from it
695 + // whenever bf_globals is missing.
696 + $configTag = self::buildFormConfigTag($FormIdentifier, $bitFormFrontArr);
697 +
664 698 if ('conversational' === $formType
665 699 && isset($formContent->formInfo->conversationalSettings->enable)
666 700 && $formContent->formInfo->conversationalSettings->enable) {
667 - $html = $FrontendFormManager->conversationalFormView($fields, $file, $errorMessages);
701 + $html = $FrontendFormManager->conversationalFormView($fields, $file, $errorMessages, null, !empty($entryId));
668 702 } else {
669 - $html = $FrontendFormManager->formView($fields, $file, $errorMessages);
703 + $html = $FrontendFormManager->formView($fields, $file, $errorMessages, null, !empty($entryId));
670 704 }
671 705
672 706 // if form preview then return html otherwise echo with output buffer
673 707 if ($formPreview) {
@@ -675,8 +709,9 @@
675 709 $formViewObject = new \stdClass();
676 710 $formViewObject->html = $html;
677 711 $formViewObject->font = $font;
678 712 $formViewObject->bfGlobals = $bfGlobals;
713 + $formViewObject->configTag = $configTag;
679 714 $formViewObject->formContent = $formContent;
680 715 return $formViewObject;
681 716 }
682 717
@@ -683,13 +718,60 @@
683 718 $bfGlobalsHandle = 'bitform-bf-globals-' . sanitize_key($FormIdentifier);
684 719 $this->addInlineScript($bfGlobals, $bfGlobalsHandle, 'after');
685 720 $this->emitShowPickerBridge();
686 721
722 + // Printed outside wp_kses rather than allowing <script> in form markup.
723 + // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- built by buildFormConfigTag(), JSON_HEX_* escaped.
724 + echo $configTag;
687 725 echo wp_kses(trim($html), EscapingHelper::getFormAllowedHtml($formContent));
688 726 return ob_get_clean();
689 727 }
690 728
691 729 /**
730 + * Keep per-visitor config (oldValues, entryId) out of full-page caches.
731 + *
732 + * @return void
733 + */
734 + public static function markResponseUncacheable()
735 + {
736 + // DONOTCACHEPAGE does the work; caches read it at shutdown. Rendering
737 + // usually runs after headers are sent, so nocache_headers() is a bonus.
738 + if (!defined('DONOTCACHEPAGE')) {
739 + define('DONOTCACHEPAGE', true);
740 + }
741 + if (!headers_sent() && function_exists('nocache_headers')) {
742 + nocache_headers();
743 + }
744 + }
745 +
746 + /**
747 + * Build the inert JSON config block for a rendered form.
748 + *
749 + * JSON_HEX_* escapes < > & as \u00XX so no field value can close the script
750 + * element or inject markup.
751 + *
752 + * @param string $formIdentifier
753 + * @param array $config
754 + *
755 + * @return string
756 + */
757 + public static function buildFormConfigTag($formIdentifier, $config)
758 + {
759 + $json = wp_json_encode($config, JSON_HEX_TAG | JSON_HEX_AMP | JSON_HEX_APOS | JSON_HEX_QUOT);
760 + if (false === $json) {
761 + return '';
762 + }
763 + // Some optimizers wrap any inline <script>, including application/json,
764 + // in DOMContentLoaded boilerplate that corrupts the JSON. These attributes
765 + // make the common ones skip it; the JS side also salvage-parses.
766 + return sprintf(
767 + '<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>',
768 + esc_attr($formIdentifier),
769 + $json
770 + );
771 + }
772 +
773 + /**
692 774 * Build the smart-tag map exposed to the browser in window.bf_globals[formId].smartTags.
693 775 *
694 776 * Security: the legacy code shipped the ENTIRE ~43-tag map to every visitor, leaking
695 777 * PII (admin/user/author email) and freezing per-visitor request data (IP, time,
@@ -827,8 +909,12 @@
827 909 } else {
828 910 $metaVal = implode(BITFORMS_BF_SEPARATOR, $metaVal);
829 911 }
830 912 }
913 + // A restored draft never gets the password back; the visitor types it again.
914 + if (isset($formFields[$metaKey]['type']) && 'password' === $formFields[$metaKey]['type']) {
915 + continue;
916 + }
831 917 if (!isset($fldsData->{$metaKey})) {
832 918 $fldsData->{$metaKey} = '';
833 919 }
834 920 $fldsData->{$metaKey} = $metaVal;
@@ -881,8 +967,16 @@
881 967 } else {
882 968 $metaVal = implode(BITFORMS_BF_SEPARATOR, $metaVal);
883 969 }
884 970 }
971 + if (property_exists($fields, $metaKey) && 'password' === $fields->{$metaKey}->typ) {
972 + // Never hand a stored password back to the browser; blank keeps it on update.
973 + $fields->{$metaKey}->val = '';
974 + if ('' !== (string) $metaValue->meta_value && isset($fields->{$metaKey}->valid) && is_object($fields->{$metaKey}->valid)) {
975 + $fields->{$metaKey}->valid->req = false;
976 + }
977 + continue;
978 + }
885 979 if (property_exists($fields, $metaKey)) {
886 980 $fields->{$metaKey}->val = $metaVal;
887 981 if ('file-up' === $fields->{$metaKey}->typ || 'advanced-file-up' === $fields->{$metaKey}->typ) {
888 982 $fields->{$metaKey}->val = $metaValue->meta_value;
@@ -887,8 +981,11 @@
887 981 if ('file-up' === $fields->{$metaKey}->typ || 'advanced-file-up' === $fields->{$metaKey}->typ) {
888 982 $fields->{$metaKey}->val = $metaValue->meta_value;
889 983 $fields->{$metaKey}->config->oldFiles = $metaValue->meta_value;
890 984 }
985 + if ('signature' === $fields->{$metaKey}->typ) {
986 + $this->setOldSignature($fields->{$metaKey}, $formID, $entryID, $metaValue->meta_value);
987 + }
891 988 }
892 989 }
893 990 }
894 991 return $fields;
@@ -893,8 +990,51 @@
893 990 }
894 991 return $fields;
895 992 }
896 993
994 + /** Give the signature field its stored signature: a data URI to redraw, and the name it posts back as `_old`. */
995 + private function setOldSignature($field, $formID, $entryID, $storedValue)
996 + {
997 + $fileName = is_string($storedValue) ? trim($storedValue) : '';
998 + $decoded = json_decode($fileName, true);
999 + if (is_array($decoded)) {
1000 + $fileName = empty($decoded) ? '' : trim((string) reset($decoded));
1001 + }
1002 + // signature-failed.png means the stored signature was never usable.
1003 + if ('' === $fileName || 'signature-failed.png' === $fileName) {
1004 + return;
1005 + }
1006 + $fileName = sanitize_file_name($fileName);
1007 + if (!isset($field->config)) {
1008 + $field->config = (object) [];
1009 + } elseif (is_array($field->config)) {
1010 + $field->config = (object) $field->config;
1011 + }
1012 + $field->config->oldSignatureFile = $fileName;
1013 +
1014 + $filePath = FileHandler::getEntriesFileUploadDir($formID, $entryID) . DIRECTORY_SEPARATOR . $fileName;
1015 + if (!is_file($filePath) || !is_readable($filePath)) {
1016 + return;
1017 + }
1018 + // The types getSignatureFilePath() writes; wp_check_filetype() reports none for SVG.
1019 + $signatureMimeTypes = ['png' => 'image/png', 'jpg' => 'image/jpeg', 'svg' => 'image/svg+xml'];
1020 + $extension = strtolower((string) pathinfo($fileName, PATHINFO_EXTENSION));
1021 + if (!isset($signatureMimeTypes[$extension])) {
1022 + return;
1023 + }
1024 + $mimeType = $signatureMimeTypes[$extension];
1025 + // A hand-drawn signature is a few KB; a larger file is not worth inlining.
1026 + if (filesize($filePath) > self::MAX_INLINE_SIGNATURE_BYTES) {
1027 + return;
1028 + }
1029 + // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_file_get_contents -- local upload dir read, inlined as a data URI for the signature pad.
1030 + $contents = file_get_contents($filePath);
1031 + if (false === $contents || '' === $contents) {
1032 + return;
1033 + }
1034 + $field->config->oldSignature = 'data:' . $mimeType . ';base64,' . base64_encode($contents);
1035 + }
1036 +
897 1037 public function loadAssets($formID = 0, $fromType = 'classic')
898 1038 {
899 1039 $bfUniqFormIds = FrontendHelpers::getAllFormIdsInPage();
900 1040 $isPageBuilder = FrontendHelpers::$isPageBuilder;
@@ -918,9 +1058,9 @@
918 1058 $formUpdateVersion = get_option('bitform_form_update_version');
919 1059 if (!wp_style_is('bitform-style-' . $newFormId) && is_readable(BITFORMS_CONTENT_DIR . '/form-styles/bitform-' . $newFormId . '.css')) {
920 1060 wp_enqueue_style(
921 1061 'bitform-style-' . $newFormId,
922 - BITFORMS_UPLOAD_BASE_URL . "/form-styles/bitform-{$newFormId}.css",
1062 + FrontendHelpers::styleSrc("form-styles/bitform-{$newFormId}.css"),
923 1063 [],
924 1064 $formUpdateVersion
925 1065 );
926 1066 if ($isPageBuilder) {
@@ -930,9 +1070,9 @@
930 1070 }
931 1071 if (!wp_style_is('bitform-style-custom-' . $formID) && is_readable(BITFORMS_CONTENT_DIR . '/form-styles/bitform-custom-' . $formID . '.css')) {
932 1072 wp_enqueue_style(
933 1073 'bitform-style-custom-' . $formID,
934 - BITFORMS_UPLOAD_BASE_URL . "/form-styles/bitform-custom-{$formID}.css",
1074 + FrontendHelpers::styleSrc("form-styles/bitform-custom-{$formID}.css"),
935 1075 [],
936 1076 $formUpdateVersion
937 1077 );
938 1078 if ($isPageBuilder) {
@@ -945,9 +1085,9 @@
945 1085 if (!wp_style_is('bitform-conversational-style-' . $formID) &&
946 1086 is_readable(BITFORMS_CONTENT_DIR . "/form-styles/bitform-conversational-{$formID}.css")) {
947 1087 wp_enqueue_style(
948 1088 'bitform-conversational-style',
949 - BITFORMS_UPLOAD_BASE_URL . "/form-styles/bitform-conversational-{$formID}.css",
1089 + FrontendHelpers::styleSrc("form-styles/bitform-conversational-{$formID}.css"),
950 1090 [],
951 1091 $formUpdateVersion
952 1092 );
953 1093 }