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
bit-form / includes / Frontend / Form / FrontendFormHandler.php

FrontendFormHandler.php in Bit Form – Contact Form, Payment Forms, Multi Step Forms, Calculator & Custom Form Builder V3.3.2, at includes/Frontend/Form/FrontendFormHandler.php

1,098 lines 44.5 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 namespace BitCode\BitForm\Frontend\Form;
4
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;
16 use BitCode\BitForm\Core\Integration\IntegrationHandler;
17 use BitCode\BitForm\Core\Util\EscapingHelper;
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;
27
28 final class FrontendFormHandler
29 {
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 '';
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 }
70
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;
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 }
202 }
203 if (empty($formIDs)) {
204 return;
205 }
206
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
345 );
346
347 if (!empty($workFlowreturnedOnLoad['fields'])) {
348 return $workFlowreturnedOnLoad['fields'];
349 }
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 }
385 }
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 }
552 }
553 }
554 }
555
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 }
563
564 $configs = [
565 'bf_separator' => BITFORMS_BF_SEPARATOR,
566 ];
567
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 }
576
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;
583 }
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;
597 }
598 }
599 }
600
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 ];
624
625 if ($entryId) {
626 $bitFormFrontArr['entryId'] = $entryId;
627 self::markResponseUncacheable();
628 }
629
630 if (isset($additional->enabled->validateFocusLost)) {
631 $bitFormFrontArr['validateFocusLost'] = true;
632 }
633
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 }
647
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 }
662
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 }
669
670 $formAbandonmentSettings = $FrontendFormManager->getFormAbandonmentSettings();
671 if (Helpers::property_exists_nested($formAbandonmentSettings, 'active', true)) {
672 $bitFormFrontArr['formSettings'] = (object)[
673 'formAbandonment' => $formAbandonmentSettings
674 ];
675 }
676
677 $layout = wp_json_encode($layout);
678 $buttons = wp_json_encode($buttons);
679 $frontArr = wp_json_encode($bitFormFrontArr);
680
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);
692
693 // Inert copy of the config. Optimizers only rewrite executable scripts, so
694 // this survives and travels with the markup; the runtime hydrates from it
695 // whenever bf_globals is missing.
696 $configTag = self::buildFormConfigTag($FormIdentifier, $bitFormFrontArr);
697
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 }
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 }
925 }
926
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;
956 }
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 }
970 }
971 if (property_exists($fields, $metaKey) && 'password' === $fields->{$metaKey}->typ) {
972 // Never hand a stored password back to the browser; blank keeps it on update.
973 $fields->{$metaKey}->val = '';
974 if ('' !== (string) $metaValue->meta_value && isset($fields->{$metaKey}->valid) && is_object($fields->{$metaKey}->valid)) {
975 $fields->{$metaKey}->valid->req = false;
976 }
977 continue;
978 }
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 }
993
994 /** Give the signature field its stored signature: a data URI to redraw, and the name it posts back as `_old`. */
995 private function setOldSignature($field, $formID, $entryID, $storedValue)
996 {
997 $fileName = is_string($storedValue) ? trim($storedValue) : '';
998 $decoded = json_decode($fileName, true);
999 if (is_array($decoded)) {
1000 $fileName = empty($decoded) ? '' : trim((string) reset($decoded));
1001 }
1002 // signature-failed.png means the stored signature was never usable.
1003 if ('' === $fileName || 'signature-failed.png' === $fileName) {
1004 return;
1005 }
1006 $fileName = sanitize_file_name($fileName);
1007 if (!isset($field->config)) {
1008 $field->config = (object) [];
1009 } elseif (is_array($field->config)) {
1010 $field->config = (object) $field->config;
1011 }
1012 $field->config->oldSignatureFile = $fileName;
1013
1014 $filePath = FileHandler::getEntriesFileUploadDir($formID, $entryID) . DIRECTORY_SEPARATOR . $fileName;
1015 if (!is_file($filePath) || !is_readable($filePath)) {
1016 return;
1017 }
1018 // The types getSignatureFilePath() writes; wp_check_filetype() reports none for SVG.
1019 $signatureMimeTypes = ['png' => 'image/png', 'jpg' => 'image/jpeg', 'svg' => 'image/svg+xml'];
1020 $extension = strtolower((string) pathinfo($fileName, PATHINFO_EXTENSION));
1021 if (!isset($signatureMimeTypes[$extension])) {
1022 return;
1023 }
1024 $mimeType = $signatureMimeTypes[$extension];
1025 // A hand-drawn signature is a few KB; a larger file is not worth inlining.
1026 if (filesize($filePath) > self::MAX_INLINE_SIGNATURE_BYTES) {
1027 return;
1028 }
1029 // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_file_get_contents -- local upload dir read, inlined as a data URI for the signature pad.
1030 $contents = file_get_contents($filePath);
1031 if (false === $contents || '' === $contents) {
1032 return;
1033 }
1034 $field->config->oldSignature = 'data:' . $mimeType . ';base64,' . base64_encode($contents);
1035 }
1036
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>';
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 }
1095 }
1096 }
1097 }
1098