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 +1022 -301 1.4.183.3.1 View file →
@@ -1,345 +1,1066 @@
1 1 <?php
2 2
3 3 namespace BitCode\BitForm\Frontend\Form;
4 4
5 -use WP_Error;
6 -use BitCode\BitForm\Core\WorkFlow\WorkFlowRunHelper;
7 -use BitCode\BitForm\Frontend\Form\FrontendFormManager;
5 +if (!defined('ABSPATH')) {
6 + exit;
7 +}
8 +
9 +use BitCode\BitForm\Admin\Form\AdminFormHandler;
10 +use BitCode\BitForm\Admin\Form\FrontEndScriptGenerator;
11 +use BitCode\BitForm\Admin\Form\Helpers;
12 +use BitCode\BitForm\Core\Database\FormEntryMetaModel;
13 +use BitCode\BitForm\Core\Database\FormModel;
14 +use BitCode\BitForm\Core\Form\FormManager;
8 15 use BitCode\BitForm\Core\Integration\IntegrationHandler;
9 -use BitCode\BitForm\Core\Form\Validator\FormFieldValidator;
16 +use BitCode\BitForm\Core\Util\EscapingHelper;
17 +use BitCode\BitForm\Core\Util\FieldValueHandler;
18 +use BitCode\BitForm\Core\Util\FileDownloadProvider;
19 +use BitCode\BitForm\Core\Util\FileHandler;
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;
25 +use BitCode\BitForm\Core\WorkFlow\WorkFlow;
10 26
11 27 final class FrontendFormHandler
12 28 {
13 - public function __construct()
14 - {
15 - add_action('wp_enqueue_scripts', array($this, 'loadAssets'));
16 - add_shortcode('bitform', array($this, 'handleFrontendRenderRequest'));
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]
35 + add_action('wp_enqueue_scripts', [$this, 'loadAssets']);
36 + // markup loads - formids []
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);
42 + }
43 +
44 + private function validPassowordResetToken($token, $userID, $formId)
45 + {
46 + $existResetInteg = (new IntegrationHandler($formId))->getAllIntegration('wp_user_auth', 'wp_auth', 1);
47 + if (!is_wp_error($existResetInteg) && count($existResetInteg) > 0) {
48 + if ('reset' === $existResetInteg[0]->integration_name) {
49 + $user = get_userdata($userID);
50 + if ($user) {
51 + $validKey = check_password_reset_key($token, $user->user_login);
52 + if (is_wp_error($validKey)) {
53 + echo "<div id='bf-resp' style='display:grid;justify-content:center;color:#860000;'>This password reset token is invalid.</div>";
54 + exit();
55 + }
56 + } else {
57 + echo "<div id='bf-resp' style='display:grid;justify-content:center;color:#860000;'>Invalid User!!</div>";
58 + exit();
59 + }
60 + }
17 61 }
62 + }
18 63
19 - private function validPassowordResetToken($token, $userID, $formId)
20 - {
21 - $existResetInteg = (new IntegrationHandler($formId))->getAllIntegration('wp_user_auth', 'wp_auth', 1);
22 - if (!is_wp_error($existResetInteg) && count($existResetInteg) > 0) {
23 - if ($existResetInteg[0]->integration_name === 'reset') {
24 - $user = get_userdata($userID);
25 - if ($user) {
26 - $validKey = check_password_reset_key($token, $user->user_login);
27 - if (is_wp_error($validKey)) {
28 - echo "<div id='bf-resp' style='display:grid;justify-content:center;color:#860000;'>This password reset token is invalid.</div>";
29 - exit();
30 - }
31 - } else {
32 - echo "<div id='bf-resp' style='display:grid;justify-content:center;color:#860000;'>Invalid User!!</div>";
33 - exit();
34 - }
35 - }
64 + private function getJSFileSrc($postId)
65 + {
66 + $formUpdateVersion = get_option('bitform_form_update_version');
67 + $formScriptSrc = BITFORMS_UPLOAD_BASE_URL . "/form-scripts/$postId/bitform-js-$postId.js?bfv=$formUpdateVersion";
68 +
69 + return $formScriptSrc;
70 + }
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 +
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;
108 + $isFormPreview = get_transient('bitform_form_preview');
109 + if ($isFormPreview && !$formID) {
110 + delete_transient('bitform_form_preview');
111 + return;
112 + }
113 + $frontendScriptGenObj = new FrontEndScriptGenerator();
114 + $isPageBuilder = FrontendHelpers::checkIsPageBuilder($_SERVER);
115 + $bfFrontendFormIds = FrontendHelpers::$bfFrontendFormIds;
116 + if ($isPageBuilder || empty($bfFrontendFormIds)) {
117 + return;
118 + }
119 + // for unique fields ids in the same form (e.g. multiple forms in the same page)
120 + $allFields = [];
121 + $formContents = [];
122 + $contentIds = [];
123 + $formIDs = [];
124 + $previewMode = 'classic';
125 + $postId = '';
126 +
127 + $formUpdateVersion = get_option('bitform_form_update_version');
128 + if ($formID) {
129 + $formIDs[] = $formID;
130 + $FrontendFormManager = FrontendFormManager::getInstance($formID, 1);
131 + $formInfo = $FrontendFormManager->getFormInfo();
132 + $FormIdentifier = esc_js($FrontendFormManager->getFormIdentifier());
133 + $formContent = $FrontendFormManager->getFormContentWithValue($this->getValuesFromQueryParams());
134 + $formContent->formId = $formID;
135 + $formContents[] = $formContent;
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);
142 + array_push($contentIds, $FormIdentifier);
143 +
144 + foreach ($fields as $fk => $field) {
145 + $allFields[$field->typ][] = ['fk' => $fk, 'field' => $field, 'formID' => $formID, 'contentId' => $FormIdentifier];
146 + }
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';
152 + $postId = $formID;
153 + } else {
154 + global $post;
155 + if (!is_object($post) && !isset($post->ID)) {
156 + return;
157 + }
158 + $bfFrontendFormIds = FrontendHelpers::$bfFrontendFormIds;
159 + $bfUniqFormIds = FrontendHelpers::getAllUniqFormIdsInPage();
160 + $formIDs = $bfUniqFormIds;
161 + $regenerateScriptFlag = $this->regenerateScriptChecker($bfUniqFormIds);
162 +
163 + $postId = $post->ID;
164 + if (!$regenerateScriptFlag) {
165 + $regenerateScriptFlag = $this->deleteUnusedFormPageIds($postId, $bfUniqFormIds);
166 + }
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);
170 + if (!$regenerateScriptFlag && !$isJsGenerating && !empty($formIDs)) {
171 + wp_enqueue_script('bit-form-all-script-test', $this->getJSFileSrc($postId), [], $formUpdateVersion, true);
172 + return;
173 + }
174 + foreach ($bfFrontendFormIds as $index => $formId) {
175 + $shortCodeCounter = $index + 1;
176 + $FrontendFormManager = FrontendFormManager::getInstance($formId, $shortCodeCounter);
177 + $formInfo = $FrontendFormManager->getFormInfo();
178 + $FormIdentifier = esc_js($FrontendFormManager->getFormIdentifier());
179 + $formContent = $FrontendFormManager->getFormContentWithValue($this->getValuesFromQueryParams());
180 + $formContent->formId = $formId;
181 + $formContents[] = $formContent;
182 + $fields = $this->triggerWorkflowOnLoad($formId, $shortCodeCounter, $formContent->fields);
183 + $contentIds[] = $FormIdentifier;
184 + $formFields = []; // indivisual form fields array for conversational view
185 + foreach ($fields as $fk => $field) {
186 + $fieldArr = ['fk' => $fk, 'field' => $field, 'formID' => $formId, 'contentId' => $FormIdentifier];
187 + $allFields[$field->typ][] = $fieldArr;
188 + $formFields[$field->typ][] = $fieldArr;
36 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 + }
194 + }
37 195 }
196 + if (empty($formIDs)) {
197 + return;
198 + }
38 199
39 - public function handleFrontendRenderRequest($atts)
40 - {
41 - static $shortCodeCounter = 0;
42 - $shortCodeCounter += 1;
43 - $atts = shortcode_atts(
44 - array('id' => 0),
45 - $atts
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);
210 + }
211 +
212 + private function deleteUnusedFormPageIds($postId, $formIDs)
213 + {
214 + global $post;
215 + if (!is_object($post) && !isset($post->ID)) {
216 + return;
217 + }
218 + $postId = $post->ID;
219 + $formModel = new FormModel();
220 + $forms = $formModel->get(
221 + ['id', 'generated_script_page_ids']
222 + );
223 + $regenerateScriptFlag = false;
224 + foreach ($forms as $form) {
225 + $formId = $form->id;
226 + $generatedScriptPageIdsDecoded = json_decode((string) $form->generated_script_page_ids, true);
227 + $generatedScriptPageIds = is_array($generatedScriptPageIdsDecoded) ? array_keys($generatedScriptPageIdsDecoded) : [];
228 + if (!empty($generatedScriptPageIds) && !in_array($formId, $formIDs) && in_array($postId, $generatedScriptPageIds)) {
229 + unset($generatedScriptPageIdsDecoded[$postId]);
230 + if (empty($generatedScriptPageIdsDecoded)) {
231 + $generatedScriptPageIdsDecoded = new \stdClass();
232 + }
233 + $regenerateScriptFlag = true;
234 + $formModel->update(['generated_script_page_ids' => wp_json_encode($generatedScriptPageIdsDecoded)], ['id' => $formId]);
235 + }
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 + }
246 + return $regenerateScriptFlag;
247 + }
248 +
249 + private function regenerateScriptChecker($formsIds)
250 + {
251 + global $post;
252 + if (!is_a($post, 'WP_Post') && !isset($post->ID)) {
253 + return;
254 + }
255 + $postId = $post->ID;
256 + // Read-only check. Marking the page as generated is deferred to markScriptGenerated(),
257 + // called only after the bundle file is actually written — marking here left the DB
258 + // saying "generated" while the file stayed stale whenever generation failed mid-way.
259 + foreach ($formsIds as $formId) {
260 + $formInstance = FormManager::getInstance($formId);
261 + if (!$formInstance->isExist()) {
262 + continue;
263 + }
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 + }
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)) {
284 + continue;
285 + }
286 + $generatedPages = Utilities::jsonObj($form[0]->generated_script_page_ids ?? '');
287 + if (!is_object($generatedPages)) {
288 + $generatedPages = (object) [];
289 + }
290 + if (!empty($generatedPages->{$postId})) {
291 + continue;
292 + }
293 + $generatedPages->{$postId} = true;
294 + $formModel->update(
295 + [
296 + 'generated_script_page_ids' => \wp_json_encode($generatedPages)
297 + ],
298 + [
299 + 'id' => $formId,
300 + ]
301 + );
302 + }
303 + }
304 +
305 + private function addInlineScript($code, $handle = '', $position = 'after')
306 + {
307 + $scriptHandle = !empty($handle) ? $handle : 'bf-inline-script';
308 + $formUpdateVersion = get_option('bitform_form_update_version');
309 + if (!wp_script_is($scriptHandle)) {
310 + wp_register_script($scriptHandle, '', [], $formUpdateVersion, true);
311 + wp_enqueue_script($scriptHandle);
312 + }
313 + wp_add_inline_script($scriptHandle, $code, $position);
314 + }
315 +
316 + private function addInlineStyle($code, $handle = '')
317 + {
318 + $styleHandle = !empty($handle) ? $handle : 'bf-inline-style';
319 + $formUpdateVersion = get_option('bitform_form_update_version');
320 + if (!wp_style_is($styleHandle)) {
321 + wp_register_style($styleHandle, '', [], $formUpdateVersion);
322 + wp_enqueue_style($styleHandle);
323 + }
324 + wp_add_inline_style($styleHandle, $code);
325 + }
326 +
327 + private function triggerWorkflowOnLoad($formID, $shortCodeCounter, $fields, $workFlowRunType = 'create')
328 + {
329 + $FrontendFormManager = new FrontendFormManager($formID, $shortCodeCounter);
330 + $previousValue = $this->getValuesFromQueryParams();
331 + $formContent = $FrontendFormManager->getFormContentWithValue($previousValue);
332 + if (!empty($formContent->workFlowExist)) {
333 + $workFlowRunHelper = new WorkFlow($formID);
334 + if (!empty($formContent->workFlowExist->onload)) {
335 + $workFlowreturnedOnLoad = $workFlowRunHelper->executeOnLoad(
336 + $workFlowRunType,
337 + $fields
46 338 );
47 - $formID = intval($atts['id']);
48 339
49 - if (!$formID) {
50 - return __('Form ID cannot be empty', 'bitform');
340 + if (!empty($workFlowreturnedOnLoad['fields'])) {
341 + return $workFlowreturnedOnLoad['fields'];
51 342 }
52 - static $formsInpage = array();
53 - $FrontendFormManager = new FrontendFormManager($formID, $shortCodeCounter);
54 - if (!$FrontendFormManager->isExist()) {
55 - return sprintf(__('#%s no. Form doesn\'t exists', 'bitform'), $formID);
343 + }
344 + }
345 +
346 + return $fields;
347 + }
348 +
349 + private function executeOnUserInput($formID, $shortCodeCounter, $workFlowRunType = 'create')
350 + {
351 + $FrontendFormManager = FrontendFormManager::getInstance($formID, $shortCodeCounter);
352 + $previousValue = $this->getValuesFromQueryParams();
353 + $formContent = $FrontendFormManager->getFormContentWithValue($previousValue);
354 + $customCodesExist = strpos(FrontEndScriptGenerator::getCustomCodes($formID)['JavaScript'], 'bfVars');
355 + if ($customCodesExist || (!empty($formContent->workFlowExist) && !empty($formContent->workFlowExist->oninput))) {
356 + $workFlowRunHelper = new WorkFlow($formID);
357 + return $workFlowRunHelper->executeOnUserInput($workFlowRunType);
358 + }
359 + }
360 +
361 + private function getValuesFromQueryParams()
362 + {
363 + // Read-only: query string parsed to pre-fill form fields. Values are sanitized per field before use.
364 + $queryParamsValue = [];
365 + if (isset($_SERVER['QUERY_STRING']) && !empty($_SERVER['QUERY_STRING'])) {
366 + $reqField = wp_unslash($_SERVER['QUERY_STRING']);
367 + foreach (explode('&', $reqField) as $keyValue) {
368 + if (false !== strpos($keyValue, '=')) {
369 + list($field, $value) = explode('=', $keyValue, 2);
370 +
371 + if (!trim($value)) {
372 + continue;
373 + }
374 + $field = sanitize_text_field(urldecode($field));
375 + if (!empty($field)) {
376 + $queryParamsValue[$field][] = sanitize_text_field(urldecode($value));
377 + }
56 378 }
57 - if (!empty($_GET['token']) && !empty($_GET['id'])) {
58 - $this->validPassowordResetToken($_GET['token'], $_GET['id'], $formID);
379 + }
380 + }
381 +
382 + return $queryParamsValue;
383 + }
384 +
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'])));
397 + } else {
398 + $entryId = false;
399 + }
400 + if (isset($atts['id'])) {
401 + $atts = shortcode_atts(['id' => 0], $atts);
402 + $formID = intval($atts['id']);
403 + }
404 +
405 + if (!$formID) {
406 + return __('Form ID cannot be empty', 'bit-form');
407 + }
408 +
409 + if (!$this->isExist($formID)) {
410 + /* translators: %s: form ID */
411 + return sprintf(__('#%s no. Form doesn\'t exists', 'bit-form'), $formID);
412 + }
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 +
429 + $font = $FrontendFormManager->getFont();
430 +
431 + if ($font && !$formPreview) {
432 + wp_enqueue_style('bf-google-font', $font, '1.0.0', true);
433 + }
434 +
435 + // Read-only: password reset token from URL for display-time validation. No state written until form is submitted.
436 + if (!empty($_GET['token']) && !empty($_GET['id'])) {
437 + $this->validPassowordResetToken(sanitize_text_field(wp_unslash($_GET['token'])), sanitize_text_field(wp_unslash($_GET['id'])), $formID);
438 + }
439 +
440 + $previousValue = $this->getValuesFromQueryParams();
441 + $errorMessages = []; // delete
442 + $FormIdentifier = esc_js($FrontendFormManager->getFormIdentifier());
443 + $nonce = $FrontendFormManager->getFormToken();
444 + $file = count($FrontendFormManager->getUploadFields()) > 0 ? $FrontendFormManager->getUploadFields() : false;
445 +
446 + $FrontendFormManager->setViewCount();
447 +
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;
454 +
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 + }
470 +
471 + // if ($entryId) {
472 + // $fields = $this->setFieldsValue($fields, $formID, $entryId);
473 + // }
474 +
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);
487 + }
488 + }
489 + $fieldsKey = $FrontendFormManager->getFieldsKey();
490 +
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) {
504 + $integrationDetails->id = $integration->id;
505 + $reCAPTCHA = $integrationDetails;
506 + $reCAPTCHAVersion = 'v2';
507 + }
508 + }
509 +
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) {
532 + $integrationDetails->id = $integration->id;
533 + $reCAPTCHA = $integrationDetails;
534 + $reCAPTCHAVersion = 'v3';
535 + }
536 + }
537 + }
59 538 }
539 + }
540 + }
60 541
61 - $isBufferStarted = false;
62 - $reqField = $_SERVER['QUERY_STRING'];
63 - $previousValue = [];
64 - $errorMessages = [];
65 - if (!empty($reqField)) {
66 - foreach (explode('&', $reqField) as $keyValue) {
67 - list($field, $value) = explode('=', $keyValue);
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 + }
68 549
69 - if ('' == trim($value)) {
70 - continue;
71 - }
550 + $configs = [
551 + 'bf_separator' => BITFORMS_BF_SEPARATOR,
552 + ];
72 553
73 - $previousValue[$field][] = sanitize_text_field(urldecode($value));
74 - }
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;
75 569 }
76 - $FormIdentifier = esc_js($FrontendFormManager->getFormIdentifier());
77 - $nonce = $FrontendFormManager->getFormToken();
78 - $file = count($FrontendFormManager->getUploadFields()) > 0 ? $FrontendFormManager->getUploadFields() : false;
79 - if ($FrontendFormManager->isSubmitted()) {
80 - $submitResp = $FrontendFormManager->handleSubmission();
81 - if (!is_wp_error($submitResp) && isset($submitResp['redirectPage'])) {
82 - if (!headers_sent()) {
83 - wp_safe_redirect($submitResp['redirectPage']);
84 - exit;
85 - } else {
86 - $isBufferStarted = true;
87 - echo "<script type='text/javascript'>window.onload = function() {window.location.replace('" . $submitResp['redirectPage'] . "')}</script>";
88 - }
89 - } elseif (is_wp_error($submitResp)) {
90 - $errors = $submitResp->get_error_message();
91 - if (\is_string($errors)) {
92 - if (!$isBufferStarted) {
93 - $isBufferStarted = true;
94 - ob_start();
95 - }
96 - echo "<div id='bf-resp' style='display:grid;justify-content:center'>" . wp_kses_post($errors) . "</div>";
97 - } elseif (isset($errors['$form'])) {
98 - if (!$isBufferStarted) {
99 - $isBufferStarted = true;
100 - ob_start();
101 - }
102 - foreach ($errors['$form'] as $error) {
103 - echo "<div id='bf-resp' style='display:grid;justify-content:center'>" . wp_kses_post($error) . "</div>";
104 - }
105 - unset($errors['$form']);
106 - $errorMessages = $errors;
107 - }
108 - if (!empty($errors)) {
109 - $errorMessages = $errors;
110 - }
111 - $previousValue = $_POST;
112 - $_POST = [];
113 - }
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;
114 583 }
115 - if ($FrontendFormManager->checkStatus()) {
116 - $FrontendFormManager->setViewCount();
584 + }
585 + }
117 586
118 - if (isset($_REQUEST) && count($previousValue) > 0) {
119 - $formContent = $FrontendFormManager->getFormContentWithValue($previousValue);
120 - $fields = $formContent->fields;
121 - $layout = $formContent->layout;
122 - $buttons = !empty($formContent->buttons) ? $formContent->buttons : '';
123 - $additional = $formContent->additional;
124 - } else {
125 - $formContent = $FrontendFormManager->getFormContent();
126 - $fields = $formContent->fields;
127 - $layout = $formContent->layout;
128 - $buttons = !empty($formContent->buttons) ? $formContent->buttons : '';
129 - $additional = $formContent->additional;
130 - }
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 + ];
131 610
132 - $captchaV3Settings = $FrontendFormManager->getCaptchaV3Settings();
133 - if ($FrontendFormManager->getCaptchaSettings() || $captchaV3Settings) {
134 - $integrationHandler = new IntegrationHandler(0);
135 - $allFormIntegrations = $integrationHandler->getAllIntegration('app');
136 - if (!is_wp_error($allFormIntegrations)) {
137 - foreach ($allFormIntegrations as $integration) {
138 - if (
139 - $FrontendFormManager->getCaptchaSettings()
140 - && !is_null($integration->integration_type)
141 - && $integration->integration_type === 'gReCaptcha'
142 - ) {
143 - $integrationDetails = json_decode($integration->integration_details);
144 - $integrationDetails->id = $integration->id;
145 - $reCAPTCHA = $integrationDetails;
146 - $reCAPTCHAVersion = 'v2';
147 - }
611 + if ($entryId) {
612 + $bitFormFrontArr['entryId'] = $entryId;
613 + self::markResponseUncacheable();
614 + }
148 615
149 - if ($captchaV3Settings) {
150 - if (!is_null($integration->integration_type) && $integration->integration_type === 'gReCaptchaV3') {
151 - $integrationDetails = json_decode($integration->integration_details);
152 - $integrationDetails->id = $integration->id;
153 - $reCAPTCHA = $integrationDetails;
154 - $reCAPTCHAVersion = 'v3';
155 - }
156 - }
157 - }
158 - }
159 - }
160 - wp_enqueue_script('bitforms-frontend-script');
161 - if ($file) {
162 - wp_enqueue_script('bitforms-frontend-file');
163 - }
616 + if (isset($additional->enabled->validateFocusLost)) {
617 + $bitFormFrontArr['validateFocusLost'] = true;
618 + }
164 619
165 - // if ($captchaV3Settings && !empty($reCAPTCHA->siteKey)) {
166 - // wp_enqueue_script('bitform-recaptchav3', "https://www.google.com/recaptcha/api.js?render={$reCAPTCHA->siteKey}");
167 - // }
168 - $fieldsKey = $FrontendFormManager->getFieldsKey();
169 - $workFlowreturnedOnUserInput = null;
170 - if (!empty($formContent->workFlowExist)) {
171 - $workFlowRunHelper = new WorkFlowRunHelper($formID);
172 - if (!empty($formContent->workFlowExist->onload)) {
173 - $workFlowreturnedOnLoad = $workFlowRunHelper->executeOnLoad(
174 - 'create',
175 - $fields
176 - );
177 - if (!empty($workFlowreturnedOnLoad['fields'])) {
178 - $fields = $workFlowreturnedOnLoad['fields'];
179 - }
180 - }
181 - if (!empty($formContent->workFlowExist->oninput)) {
182 - $workFlowreturnedOnUserInput = $workFlowRunHelper->executeOnUserInput(
183 - 'create',
184 - $fields
185 - );
186 - }
187 - }
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 + }
188 628
189 - if (!wp_style_is('bitform-style' . $formID)) {
190 - $this->loadAssets(true, $formID, $file);
191 - }
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,
638 + ];
639 + $bitFormFrontArr['formInfo'] = (object) [
640 + 'multiStepSettings' => $newTempSettings
641 + ];
642 + }
192 643
193 - if ($captchaV3Settings && !empty($captchaV3Settings->hideReCaptcha)) {
194 - echo '<style>.grecaptcha-badge { visibility: hidden; }</style>';
195 - }
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 + }
196 650
197 - $FrontendFormManager->honeypotTrap();
651 + $formAbandonmentSettings = $FrontendFormManager->getFormAbandonmentSettings();
652 + if (Helpers::property_exists_nested($formAbandonmentSettings, 'active', true)) {
653 + $bitFormFrontArr['formSettings'] = (object)[
654 + 'formAbandonment' => $formAbandonmentSettings
655 + ];
656 + }
198 657
199 - $bitFormFrontArr = array(
200 - 'ajaxURL' => admin_url('admin-ajax.php'),
201 - 'nonce' => $nonce,
202 - 'version' => BITFORMS_VERSION,
203 - // 'contentID' => $FormIdentifier,
204 - 'layout' => $layout,
205 - 'fields' => $fields,
206 - 'buttons' => $buttons,
207 - 'fieldsKey' => $fieldsKey,
208 - 'file' => $file,
209 - 'formId' => $formID,
210 - 'GCLID' => $FrontendFormManager->isGCLIDEnabled(),
211 - 'assetUrl' => BITFORMS_ASSET_URI,
212 - 'gRecaptchaSiteKey' => !empty($reCAPTCHA->siteKey) ? $reCAPTCHA->siteKey : null,
213 - 'gRecaptchaVersion' => !empty($reCAPTCHAVersion) ? $reCAPTCHAVersion : null,
214 - 'conditional' => !empty($workFlowreturnedOnUserInput['conditional']) ? $workFlowreturnedOnUserInput['conditional'] : false,
215 - 'fieldToCheck' => !empty($workFlowreturnedOnUserInput['fieldToCheck']) ? $workFlowreturnedOnUserInput['fieldToCheck'] : false,
216 - 'fieldToChange' => !empty($workFlowreturnedOnUserInput['fieldToChange']) ? $workFlowreturnedOnUserInput['fieldToChange'] : false
217 - );
658 + $layout = wp_json_encode($layout);
659 + $buttons = wp_json_encode($buttons);
660 + $frontArr = wp_json_encode($bitFormFrontArr);
218 661
219 - $paymentKeys = $FrontendFormManager->checkPaymentFields();
220 - if (count($paymentKeys)) {
221 - $bitFormFrontArr['paymentKeys'] = $paymentKeys;
222 - }
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 = {}
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);
223 673
224 - if (isset($additional->enabled->validateFocusLost)) {
225 - $bitFormFrontArr['validateFocusLost'] = true;
226 - }
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);
227 678
228 - $bitFormsFront = apply_filters(
229 - 'bitforms_localized_script',
230 - $bitFormFrontArr
231 - );
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 + }
232 686
233 - $fields = \json_encode($fields);
234 - $layout = \json_encode($layout);
235 - $buttons = \json_encode($buttons);
236 - if (!\in_array($formID, $formsInpage)) {
237 - wp_localize_script('bitforms-frontend-script', $FormIdentifier, $bitFormsFront);
238 - } else {
239 - $formsInpage[] = $formID;
240 - }
241 - $formSpecificScripts = "if(typeof window._bitforms_front==='function'){_bitforms_front('$FormIdentifier')}else{document.addEventListener('DOMContentLoaded',(event)=>{ window._bitforms_front && _bitforms_front('$FormIdentifier')})}";
242 - wp_add_inline_script('bitforms-frontend-script', $formSpecificScripts, 'after');
243 - $html = $FrontendFormManager->formView($fields, $file, $errorMessages);
244 - if (!$isBufferStarted) {
245 - ob_start();
246 - } ?>
247 - <div id=<?php echo esc_attr($FormIdentifier); ?>><?php echo trim($html); ?>
248 - </div>
249 -<?php
250 - return ob_get_clean();
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 + }
698 +
699 + $bfGlobalsHandle = 'bitform-bf-globals-' . sanitize_key($FormIdentifier);
700 + $this->addInlineScript($bfGlobals, $bfGlobalsHandle, 'after');
701 + $this->emitShowPickerBridge();
702 +
703 + // Printed outside wp_kses rather than allowing <script> in form markup.
704 + // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- built by buildFormConfigTag(), JSON_HEX_* escaped.
705 + echo $configTag;
706 + echo wp_kses(trim($html), EscapingHelper::getFormAllowedHtml($formContent));
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
785 + }
786 + // Match '${' . key prefix so keys containing spaces/slashes/commas are handled.
787 + if (false !== strpos($haystack, '${' . $key)) {
788 + $referenced[] = $key;
789 + $frontendSmartTags[$key] = SmartTagRegistry::resolve($key, $ctx);
790 + }
791 + }
792 +
793 + /**
794 + * Escape hatch: a site that genuinely needs an extra tag client-side can opt it
795 + * back in explicitly here, rather than core shipping everything by default.
796 + *
797 + * @param array<string,string> $frontendSmartTags resolved frontend-safe smart tags
798 + * @param int|string $formID
799 + * @param string[] $referenced keys detected in client surfaces
800 + */
801 + return apply_filters('bitform_frontend_smarttags', $frontendSmartTags, $formID, $referenced);
802 + }
803 +
804 + /**
805 + * Delegated listener that opens the native picker on date/time inputs marked
806 + * with data-bf-show-picker. Replaces the legacy hardcoded onclick attribute.
807 + * Registered as inline script once per request via wp_add_inline_script so
808 + * the markup never travels through wp_kses().
809 + */
810 + private function emitShowPickerBridge()
811 + {
812 + static $emitted = false;
813 + if ($emitted) {
814 + return;
815 + }
816 + $emitted = true;
817 + $code = 'if(!window.__bfShowPickerBound){window.__bfShowPickerBound=true;document.addEventListener("click",function(e){var t=e.target;if(t&&t.matches&&t.matches("input[data-bf-show-picker=\"1\"]")&&typeof t.showPicker==="function"){try{t.showPicker();}catch(_){}}});}';
818 + $this->addInlineScript($code, 'bitform-show-picker-bridge', 'after');
819 + }
820 +
821 + /**
822 + * Does this form row exist?
823 + *
824 + * @param int $formID
825 + *
826 + * @return bool
827 + */
828 + private function isExist($formID)
829 + {
830 + $formModel = new FormModel();
831 + $form = $formModel->get(
832 + [
833 + 'id'
834 + ],
835 + [
836 + 'id' => $formID,
837 + ]
838 + );
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;
851 + }
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;
863 + }
864 +
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 + }
251 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 + }
252 902 }
253 903
254 - public function loadAssets($recheck = false, $formID = 0, $isFileExists = false)
255 - {
256 - global $post;
257 - if (!empty($post->post_content)) {
258 - \preg_match_all("/\[bitform\s+id\s*=\s*('|\")\s*(\d+)\s*('|\")\]/", $post->post_content, $shortCode);
259 - } else {
260 - $isSCExists = false;
261 - if (isset($post->ID) && \is_int($post->ID)) {
262 - global $wpdb;
263 - $pMetadata = $wpdb->get_results("SELECT meta_value FROM `" . $wpdb->postmeta . "` WHERE `post_id`={$post->ID} AND meta_value LIKE '%[bitform id=%' LIMIT 1");
264 - if ($pMetadata && !empty($pMetadata[0]->meta_value)) {
265 - $isSCExists = true;
266 - \preg_match_all("/\[bitform\s+id\s*=\s*('|\")\s*(\d+)\s*('|\")\]/", $pMetadata[0]->meta_value, $shortCode);
267 - }
268 - }
269 - if (!$isSCExists) {
270 - $shortCode[0] = null;
271 - $shortCode[1] = null;
272 - $shortCode[2] = [];
273 - $shortCode[3] = null;
274 - }
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;
275 933 }
276 - if ($formID !== 0) {
277 - $shortCode[2][] = $formID;
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 + }
278 947 }
279 - if (count($shortCode) === 4 && count($shortCode[2]) > 0) {
280 - wp_enqueue_style(
281 - 'bitforms-style-frontend',
282 - BITFORMS_ASSET_URI . '/css/components.css',
283 - array(),
284 - BITFORMS_VERSION,
285 - 'all'
286 - );
287 - foreach ($shortCode[2] as $formID) {
288 - if (is_readable(BITFORMS_CONTENT_DIR . '/form-styles/bitform-' . $formID . '.css')) {
289 - $styleModifyTime = filemtime(BITFORMS_CONTENT_DIR . '/form-styles/bitform-' . $formID . '.css');
290 - wp_enqueue_style(
291 - 'bitform-style' . $formID,
292 - BITFORMS_UPLOAD_BASE_URL . '/form-styles/bitform-' . $formID . '.css',
293 - array(),
294 - $styleModifyTime,
295 - 'all'
296 - );
297 - }
298 - if (is_readable(BITFORMS_CONTENT_DIR . '/form-styles/bitform-layout-' . $formID . '.css')) {
299 - $layoutModifyTime = filemtime(BITFORMS_CONTENT_DIR . '/form-styles/bitform-layout-' . $formID . '.css');
300 - wp_enqueue_style(
301 - 'bitform-layout-style' . $formID,
302 - BITFORMS_UPLOAD_BASE_URL . '/form-styles/bitform-layout-' . $formID . '.css',
303 - array(),
304 - $layoutModifyTime,
305 - 'all'
306 - );
307 - }
308 - }
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 + }
309 962
310 - if (!$recheck) {
311 - wp_register_script(
312 - 'bitforms-frontend-script',
313 - BITFORMS_ASSET_FRNT_JS_URI . '/bitformsFrontend.js',
314 - array(),
315 - BITFORMS_VERSION,
316 - true
317 - );
318 - wp_register_script(
319 - 'bitforms-frontend-file',
320 - BITFORMS_ASSET_FRNT_JS_URI . '/bitforms-file.js',
321 - array(),
322 - BITFORMS_VERSION,
323 - true
324 - );
325 - } else {
326 - wp_enqueue_script(
327 - 'bitforms-frontend-script',
328 - BITFORMS_ASSET_FRNT_JS_URI . '/bitformsFrontend.js',
329 - array(),
330 - BITFORMS_VERSION,
331 - true
332 - );
333 - if ($isFileExists) {
334 - wp_enqueue_script(
335 - 'bitforms-frontend-file',
336 - BITFORMS_ASSET_JS_URI . '/bitforms-file.js',
337 - array(),
338 - BITFORMS_VERSION,
339 - true
340 - );
341 - }
342 - }
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)) {
1013 + $formIds = [$formID];
1014 + } else {
1015 + $formIds = $bfUniqFormIds;
1016 + }
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 + }
1022 + if ($bfMultipleFormsExists) {
1023 + $newFormId = $formID . '-formid';
1024 + } else {
1025 + $newFormId = $formID;
1026 + }
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",
1032 + [],
1033 + $formUpdateVersion
1034 + );
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>';
343 1038 }
1039 + }
1040 + if (!wp_style_is('bitform-style-custom-' . $formID) && is_readable(BITFORMS_CONTENT_DIR . '/form-styles/bitform-custom-' . $formID . '.css')) {
1041 + wp_enqueue_style(
1042 + 'bitform-style-custom-' . $formID,
1043 + BITFORMS_UPLOAD_BASE_URL . "/form-styles/bitform-custom-{$formID}.css",
1044 + [],
1045 + $formUpdateVersion
1046 + );
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 + }
1063 + }
344 1064 }
1065 + }
345 1066 }