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