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 +124 -15 3.2.23.3.1 View file →
@@ -15,8 +15,9 @@
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;
20 21 use BitCode\BitForm\Core\Util\Log;
21 22 use BitCode\BitForm\Core\Util\SmartTagRegistry;
22 23 use BitCode\BitForm\Core\Util\SmartTags;
@@ -24,8 +25,11 @@
24 25 use BitCode\BitForm\Core\WorkFlow\WorkFlow;
25 26
26 27 final class FrontendFormHandler
27 28 {
29 + /** Largest stored signature inlined into the page as a data URI. */
30 + private const MAX_INLINE_SIGNATURE_BYTES = 2097152;
31 +
28 32 public function __construct()
29 33 {
30 34 // before markup load - formids [], posts [1,2]
31 35 add_action('wp_enqueue_scripts', [$this, 'loadAssets']);
@@ -31,9 +35,11 @@
31 35 add_action('wp_enqueue_scripts', [$this, 'loadAssets']);
32 36 // markup loads - formids []
33 37 add_shortcode('bitform', [$this, 'handleFrontendRenderRequest']);
34 38 // after markup load - formids [1,35,3]
35 - 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);
36 42 }
37 43
38 44 private function validPassowordResetToken($token, $userID, $formId)
39 45 {
@@ -95,8 +101,10 @@
95 101 }
96 102
97 103 public function generateJs($formID = null, $entryID = null, $formType = null)
98 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.
99 107 // return true;
100 108 $isFormPreview = get_transient('bitform_form_preview');
101 109 if ($isFormPreview && !$formID) {
102 110 delete_transient('bitform_form_preview');
@@ -214,9 +222,9 @@
214 222 );
215 223 $regenerateScriptFlag = false;
216 224 foreach ($forms as $form) {
217 225 $formId = $form->id;
218 - $generatedScriptPageIdsDecoded = json_decode($form->generated_script_page_ids, true);
226 + $generatedScriptPageIdsDecoded = json_decode((string) $form->generated_script_page_ids, true);
219 227 $generatedScriptPageIds = is_array($generatedScriptPageIdsDecoded) ? array_keys($generatedScriptPageIdsDecoded) : [];
220 228 if (!empty($generatedScriptPageIds) && !in_array($formId, $formIDs) && in_array($postId, $generatedScriptPageIds)) {
221 229 unset($generatedScriptPageIdsDecoded[$postId]);
222 230 if (empty($generatedScriptPageIdsDecoded)) {
@@ -601,8 +609,9 @@
601 609 ];
602 610
603 611 if ($entryId) {
604 612 $bitFormFrontArr['entryId'] = $entryId;
613 + self::markResponseUncacheable();
605 614 }
606 615
607 616 if (isset($additional->enabled->validateFocusLost)) {
608 617 $bitFormFrontArr['validateFocusLost'] = true;
@@ -608,9 +617,11 @@
608 617 $bitFormFrontArr['validateFocusLost'] = true;
609 618 }
610 619
611 620 if (!empty($isAbandoned)) {
621 + // One visitor's typed values, so this response must not be page-cached.
612 622 $bitFormFrontArr['oldValues'] = $this->getFieldsValue($formID, $isAbandoned);
623 + self::markResponseUncacheable();
613 624 if (empty($entryId)) {
614 625 $bitFormFrontArr['entryId'] = $entryId;
615 626 }
616 627 }
@@ -647,27 +658,31 @@
647 658 $layout = wp_json_encode($layout);
648 659 $buttons = wp_json_encode($buttons);
649 660 $frontArr = wp_json_encode($bitFormFrontArr);
650 661
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 = {}
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 = {}
656 667 }
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);
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);
663 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 +
664 679 if ('conversational' === $formType
665 680 && isset($formContent->formInfo->conversationalSettings->enable)
666 681 && $formContent->formInfo->conversationalSettings->enable) {
667 - $html = $FrontendFormManager->conversationalFormView($fields, $file, $errorMessages);
682 + $html = $FrontendFormManager->conversationalFormView($fields, $file, $errorMessages, null, !empty($entryId));
668 683 } else {
669 - $html = $FrontendFormManager->formView($fields, $file, $errorMessages);
684 + $html = $FrontendFormManager->formView($fields, $file, $errorMessages, null, !empty($entryId));
670 685 }
671 686
672 687 // if form preview then return html otherwise echo with output buffer
673 688 if ($formPreview) {
@@ -675,8 +690,9 @@
675 690 $formViewObject = new \stdClass();
676 691 $formViewObject->html = $html;
677 692 $formViewObject->font = $font;
678 693 $formViewObject->bfGlobals = $bfGlobals;
694 + $formViewObject->configTag = $configTag;
679 695 $formViewObject->formContent = $formContent;
680 696 return $formViewObject;
681 697 }
682 698
@@ -683,13 +699,60 @@
683 699 $bfGlobalsHandle = 'bitform-bf-globals-' . sanitize_key($FormIdentifier);
684 700 $this->addInlineScript($bfGlobals, $bfGlobalsHandle, 'after');
685 701 $this->emitShowPickerBridge();
686 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;
687 706 echo wp_kses(trim($html), EscapingHelper::getFormAllowedHtml($formContent));
688 707 return ob_get_clean();
689 708 }
690 709
691 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 + /**
692 755 * Build the smart-tag map exposed to the browser in window.bf_globals[formId].smartTags.
693 756 *
694 757 * Security: the legacy code shipped the ENTIRE ~43-tag map to every visitor, leaking
695 758 * PII (admin/user/author email) and freezing per-visitor request data (IP, time,
@@ -887,12 +950,58 @@
887 950 if ('file-up' === $fields->{$metaKey}->typ || 'advanced-file-up' === $fields->{$metaKey}->typ) {
888 951 $fields->{$metaKey}->val = $metaValue->meta_value;
889 952 $fields->{$metaKey}->config->oldFiles = $metaValue->meta_value;
890 953 }
954 + if ('signature' === $fields->{$metaKey}->typ) {
955 + $this->setOldSignature($fields->{$metaKey}, $formID, $entryID, $metaValue->meta_value);
956 + }
891 957 }
892 958 }
893 959 }
894 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);
895 1004 }
896 1005
897 1006 public function loadAssets($formID = 0, $fromType = 'classic')
898 1007 {