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