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/FrontendFormManager.php +764 -442 2.10.0 → V3.3.2 View file →
@@ -9,8 +9,9 @@
9 9 /**
10 10 * FrontendFormManager class
11 11 */
12 12
13 +use BitCode\BitForm\Admin\Form\AdminFormHandler;
13 14 use BitCode\BitForm\Admin\Form\Helpers;
14 15 use BitCode\BitForm\Core\Database\FormEntryModel;
15 16 use BitCode\BitForm\Core\Form\FormManager;
16 17 use BitCode\BitForm\Core\Form\Validator\FormFieldValidator;
@@ -16,24 +17,30 @@
16 17 use BitCode\BitForm\Core\Form\Validator\FormFieldValidator;
17 18 use BitCode\BitForm\Core\Integration\IntegrationHandler;
18 19 use BitCode\BitForm\Core\Messages\SuccessMessageHandler;
19 20 use BitCode\BitForm\Core\Util\ApiResponse as UtilApiResponse;
20 -use BitCode\BitForm\Core\Util\DateTimeHelper;
21 +use BitCode\BitForm\Core\Util\EscapingHelper;
22 +use BitCode\BitForm\Core\Util\FieldValueHandler;
23 +use BitCode\BitForm\Core\Util\FrontendHelpers;
21 24 use BitCode\BitForm\Core\Util\HttpHelper;
22 25 use BitCode\BitForm\Core\Util\IpTool;
26 +use BitCode\BitForm\Core\Util\Log;
27 +use BitCode\BitForm\Core\Util\Utilities;
23 28 use BitCode\BitForm\Core\WorkFlow\WorkFlow;
24 -use BitCode\BitForm\Core\WorkFlow\WorkFlowHandler;
25 29 use BitCode\BitForm\Frontend\Form\View\FormViewer;
26 -use BitCode\BitFormPro\Admin\FormSettings\FormAbandonment;
30 +use BitCode\BitForm\GlobalHelper;
27 31 use WP_Error;
28 32
29 33 final class FrontendFormManager extends FormManager
30 34 {
31 35 private $_form_identifier;
36 +
37 + /** @var array|null reCAPTCHA v3 verdict, reused for the log */
38 + private $recaptchaV3Verification;
32 39 private $_form_token;
33 40 private $_form_id;
34 - private $_work_flows;
35 41 private $_conf_messages;
42 + private static $_instance = [];
36 43
37 44 // private $_has_upload = false;
38 45 public function __construct($form_id, $shortCodeCounter = null)
39 46 {
@@ -44,8 +51,19 @@
44 51 $this->_form_token = wp_create_nonce('bitforms_' . $form_id);
45 52 $this->_form_id = $form_id;
46 53 }
47 54
55 + public static function getInstance($form_id, $shortCodeCounter = null)
56 + {
57 + $key = $form_id . ':' . ($shortCodeCounter ?? 'default');
58 +
59 + if (!isset(self::$_instance[$key])) {
60 + self::$_instance[$key] = new self($form_id, $shortCodeCounter);
61 + }
62 +
63 + return self::$_instance[$key];
64 + }
65 +
48 66 public function getFormIdentifier()
49 67 {
50 68 return $this->_form_identifier;
51 69 }
@@ -59,14 +77,8 @@
59 77 {
60 78 return $this->_form_token;
61 79 }
62 80
63 - public function isSubmitted()
64 - {
65 - // return isset($_POST[$this->_form_identifier]) ? true : false;
66 - return (isset($_POST['bitforms_id']) && $_POST['bitforms_id'] === $this->_form_identifier) ? true : false;
67 - }
68 -
69 81 public function getSubmittedFields($submitted_data)
70 82 {
71 83 unset($submitted_data[$this->_form_identifier]);
72 84 // unset($submitted_data['bit-form-submit-btn']);
@@ -72,9 +84,9 @@
72 84 // unset($submitted_data['bit-form-submit-btn']);
73 85 return array_keys($submitted_data);
74 86 }
75 87
76 - public function formView($fields = null, $hasFile = false, $errorMessages = null, $previousValue = null)
88 + public function formView($fields = null, $hasFile = false, $errorMessages = null, $previousValue = null, $isEntryEdit = false)
77 89 {
78 90 $formContents = $this->getFormContent();
79 91 $formAtomicClsMap = $this->getAtomicClsMap();
80 92 if (!empty($fields)) {
@@ -87,14 +99,14 @@
87 99 );
88 100 $formContents->fields = empty($workFlowreturnedOnLoad['fields']) ? $formContents->fields : $workFlowreturnedOnLoad['fields'];
89 101 }
90 102 $formViewer = new FormViewer($this, $formContents, $formAtomicClsMap, $errorMessages, $previousValue);
91 - $isRestricted = $this->checkSubmissionRestriction(false);
103 + $isRestricted = $this->checkSubmissionRestriction(false, $isEntryEdit);
92 104 $msg = !empty($isRestricted) ? $isRestricted[0] : '';
93 105 return $formViewer->getView($hasFile, $msg);
94 106 }
95 107
96 - public function conversationalFormView($fields = null, $hasFile = false, $errorMessages = null, $previousValue = null)
108 + public function conversationalFormView($fields = null, $hasFile = false, $errorMessages = null, $previousValue = null, $isEntryEdit = false)
97 109 {
98 110 $formContents = $this->getFormContent();
99 111 $formAtomicClsMap = $this->getAtomicClsMap();
100 112 if (!empty($fields)) {
@@ -107,23 +119,34 @@
107 119 );
108 120 $formContents->fields = empty($workFlowreturnedOnLoad['fields']) ? $formContents->fields : $workFlowreturnedOnLoad['fields'];
109 121 }
110 122 $formViewer = new FormViewer($this, $formContents, $formAtomicClsMap, $errorMessages, $previousValue);
111 - $isRestricted = $this->checkSubmissionRestriction(false);
123 + $isRestricted = $this->checkSubmissionRestriction(false, $isEntryEdit);
112 124 $msg = !empty($isRestricted) ? $isRestricted[0] : '';
113 125 return $formViewer->getConversationalView($hasFile, $msg);
114 126 }
115 127
116 - private function checkEmptySubmission($data, $file)
128 + public function checkEmptySubmission($data, $file, $isEntryEdit = false)
117 129 {
118 130 $formFields = $this->getFields();
119 131 foreach ($formFields as $key => $field) {
120 132 $fieldType = $field['type'];
133 + if ('button' === $fieldType) {
134 + continue;
135 + }
121 136 $fileUploadFieldTypes = ['file-up', 'advanced-file-up'];
122 - if ('decision-box' === $fieldType) {
137 + if ('decision-box' === $fieldType || 'gdpr' === $fieldType) {
123 138 continue;
124 139 }
125 140 $isFileType = in_array($fieldType, $fileUploadFieldTypes);
141 + // An edit keeps an untouched file/signature as `<fieldKey>_old`, not as an upload.
142 + if (
143 + $isEntryEdit
144 + && ($isFileType || 'signature' === $fieldType)
145 + && !empty(FieldValueHandler::retainedOldValues($data, $key))
146 + ) {
147 + return false;
148 + }
126 149 if ($this->isRepeatedField($key)) {
127 150 $fileData = !empty($file[$key]) ? $file[$key] : [];
128 151 $dataVal = !empty($data[$key]) ? $data[$key] : [];
129 152 if (!$this->checkRepeatedFieldEmptySubmission($isFileType, $dataVal, $fileData)) {
@@ -170,48 +193,197 @@
170 193 {
171 194 $url = wp_parse_url(wp_get_referer());
172 195 $parameter = [];
173 196 if (isset($url['query'])) {
174 - $queries = explode('&', $url['query']);
175 - foreach ($queries as $query) {
176 - list($field, $value) = explode('=', $query);
177 - $parameter[$field] = $value;
197 + // a bare key (?preview) made the old explode() split warn; this also URL-decodes values
198 + wp_parse_str($url['query'], $parameter);
199 + }
200 + return $parameter;
201 + }
202 +
203 + private function getFormFields($formID)
204 + {
205 + $adminFormHandler = new AdminFormHandler();
206 + $post = new \stdClass();
207 + $post = (object) [
208 + 'id' => $formID
209 + ];
210 + $getForm = $adminFormHandler->getAForm('', $post);
211 + $formContainer = $getForm['form_content'];
212 +
213 + return $formContainer['fields'];
214 + }
215 +
216 + private function transformDrpdwnValue($post)
217 + {
218 + $formFields = $this->getFormFields($this->_form_id);
219 +
220 + foreach ($post as $key => $value) {
221 + if (!str_starts_with($key, 'repeater') && isset($formFields->{$key}) && 'select' === $formFields->{$key}->typ) {
222 + if (is_array($value)) {
223 + foreach ($value as $k => $v) {
224 + $post[$key][$k] = !is_array($v) && is_string($v) ? explode(BITFORMS_BF_SEPARATOR, $v) : $v;
225 + }
226 + } else {
227 + $post[$key] = explode(BITFORMS_BF_SEPARATOR, $value);
228 + }
229 + };
230 + }
231 +
232 + return $post;
233 + }
234 +
235 + /**
236 + * WP auth errors carry markup and the confirmation box paints them with innerHTML,
237 + * so esc_html() would show the tags as text. kses keeps only the safe markup.
238 + *
239 + * @param mixed $message
240 + *
241 + * @return string
242 + */
243 + private static function authErrorMessage($message)
244 + {
245 + return wp_kses(is_string($message) ? $message : '', EscapingHelper::getAllowedHtmlTags());
246 + }
247 +
248 + /**
249 + * A confirm-enabled email/password field posts as one composite and the validator collapses it
250 + * to the primary value, so the confirm child's own field key never reaches $_POST. WP auth
251 + * integrations map fields by key, so fill those child keys on a copy for the auth filter.
252 + *
253 + * @param mixed $postData
254 + *
255 + * @return mixed
256 + */
257 + private function resolveConfirmChildValues($postData)
258 + {
259 + if (!is_array($postData)) {
260 + return $postData;
261 + }
262 + $fields = $this->getFields();
263 + $postData = $this->resolveCompositeChildValues($postData, $fields);
264 + foreach ($fields as $fieldKey => $fieldData) {
265 + if (
266 + empty($fieldData['childFields'])
267 + || !isset($fieldData['type'])
268 + || !in_array($fieldData['type'], ['email', 'password'], true)
269 + || !empty($fieldData['repeated'])
270 + || !isset($postData[$fieldKey])
271 + ) {
272 + continue;
178 273 }
274 + $parentValue = $postData[$fieldKey];
275 + foreach ((array) $fieldData['childFields'] as $childFieldRef) {
276 + $childKey = is_object($childFieldRef) && isset($childFieldRef->fldKey) ? $childFieldRef->fldKey : '';
277 + if (
278 + empty($childKey)
279 + || !isset($fields[$childKey])
280 + || !empty($fields[$childKey]['isDeactive'])
281 + || isset($postData[$childKey])
282 + ) {
283 + continue;
284 + }
285 + if (is_array($parentValue)) {
286 + if (array_key_exists('confirm', $parentValue)) {
287 + $postData[$childKey] = $parentValue['confirm'];
288 + }
289 + continue;
290 + }
291 + // Validation matched primary against confirm before collapsing, so this is that value.
292 + $postData[$childKey] = $parentValue;
293 + }
294 + if (is_array($parentValue) && array_key_exists('primary', $parentValue)) {
295 + $postData[$fieldKey] = $parentValue['primary'];
296 + }
179 297 }
180 - return $parameter;
298 +
299 + return $postData;
181 300 }
182 301
302 + /**
303 + * Name/Address parts post as one array under the parent key ("parent[first_name]"), so a WP
304 + * auth mapping that points at a part's own key (First Name, Last Name) found nothing. Copy each
305 + * part's value to its key; the parent value is left as it was.
306 + *
307 + * @param array $postData
308 + * @param array $fields getFields() output
309 + *
310 + * @return array
311 + */
312 + private function resolveCompositeChildValues($postData, $fields)
313 + {
314 + foreach ($fields as $fieldKey => $fieldData) {
315 + if (
316 + empty($fieldData['childFields'])
317 + || !isset($fieldData['type'])
318 + || !in_array($fieldData['type'], ['name', 'address'], true)
319 + || !empty($fieldData['repeated'])
320 + || !isset($postData[$fieldKey])
321 + || !is_array($postData[$fieldKey])
322 + ) {
323 + continue;
324 + }
325 + foreach ((array) $fieldData['childFields'] as $childFieldRef) {
326 + $childKey = is_object($childFieldRef) && isset($childFieldRef->fldKey) ? $childFieldRef->fldKey : '';
327 + if (
328 + empty($childKey)
329 + || !isset($fields[$childKey])
330 + || !empty($fields[$childKey]['isDeactive'])
331 + || isset($postData[$childKey])
332 + || !preg_match('/\[([^\]]+)\]$/', (string) ($fields[$childKey]['name'] ?? ''), $matches)
333 + ) {
334 + continue;
335 + }
336 + $value = FieldValueHandler::extractChildValueFromParentValue($postData[$fieldKey], $matches[1], $childKey);
337 + if (null !== $value) {
338 + $postData[$childKey] = $value;
339 + }
340 + }
341 + }
342 +
343 + return $postData;
344 + }
345 +
183 346 public function handleSubmission()
184 347 {
348 + // CSRF verified via verifySubmissionNonce() before this method is called. All $_POST reads below occur after that verification.
185 349 $this->fieldNameReplaceOfPost();
186 350
187 351 $validated = $this->beforeSubmittedValidate();
188 352
353 + $validated = apply_filters('bitform_filter_form_validation', $validated, $this->_form_id);
354 +
189 355 if (true === $validated) {
190 - unset($_POST['hidden_fields']);
356 + do_action('bitform_validation_success', $this->_form_id);
357 + $this->discardHiddenFieldValues();
191 358
192 359 $redirectPage = '';
193 360 $regSuccMsg = '';
194 361
195 362 $existAuth = (new IntegrationHandler($this->_form_id))->getAllIntegration('wp_user_auth', 'wp_auth', 1);
363 + $unslashed_post = wp_unslash($_POST);
196 364 if (!is_wp_error($existAuth) && count($existAuth) > 0) {
197 365 $parameter = $this->getParams();
198 - $existAuthFilter = has_filter('bf_wp_user_auth');
366 + $existAuthFilter = has_filter('bitform_wp_user_auth');
199 367
200 368 if (true === $existAuthFilter) {
201 - $result = apply_filters('bf_wp_user_auth', $existAuth[0], $_POST, $parameter);
369 + $authPostData = $this->resolveConfirmChildValues($unslashed_post);
370 + $result = apply_filters('bitform_wp_user_auth', $existAuth[0], $authPostData, $parameter);
202 371
372 + $result = apply_filters('bitform_filter_wp_user_auth_response', $result, $this->_form_id, $authPostData, $parameter);
373 +
374 + do_action('bitform_wp_user_auth_response', $result, $this->_form_id, $authPostData, $parameter);
375 +
203 376 if (isset($result['auth_type']) && 'register' === $result['auth_type']) {
204 377 if (!$result['success']) {
205 - return new WP_Error('errors', __($result['message'], 'bit-form'));
378 + return new WP_Error('errors', self::authErrorMessage($result['message']));
206 379 } elseif (isset($result['success'])) {
207 - $redirectPage = $result['redirect_url'];
380 + $redirectPage = $result['redirectPage'];
208 381 $regSuccMsg = $result['message'];
209 - $newNonce = wp_create_nonce('bitforms_' . $this->_form_id);
210 382 }
211 383 } else {
212 384 if (!$result['success']) {
213 - return new WP_Error('errors', __($result['message'], 'bit-form'));
385 + return new WP_Error('errors', self::authErrorMessage($result['message']));
214 386 } else {
215 387 return $result;
216 388 }
217 389 }
@@ -217,55 +389,21 @@
217 389 }
218 390 }
219 391 }
220 392
221 - $saveResponse = $this->saveFormEntry($_POST);
393 + $saveResponse = $this->saveFormEntry($unslashed_post);
222 394 if (is_wp_error($saveResponse)) {
223 395 return $saveResponse;
224 396 }
225 397
226 398 $entryID = $saveResponse['entry_id'];
227 - do_action('bitform_submit_success', $this->_form_id, $entryID, $_POST);
228 399
229 - // check and replace signature field value
230 - $formFields = $this->getFields();
231 - $uploadPath = BITFORMS_UPLOAD_BASE_URL . "/uploads/{$this->_form_id}/{$entryID}";
400 + // transformed dropdown value from string to array
401 + $newPost = $this->transformDrpdwnValue($unslashed_post);
402 + $filesData = GlobalHelper::sanitize_files_input($_FILES);
403 + do_action('bitform_submit_success', $this->_form_id, $entryID, $newPost, $filesData);
232 404
233 - foreach ($formFields as $key => $field) {
234 - if ('signature' === $field['type']) {
235 - $saveResponse['fields'][$key] = $uploadPath . '/' . $saveResponse['fields'][$key];
236 - break;
237 - }
238 - }
239 -
240 - $captchaV3Settings = $this->getCaptchaV3Settings();
241 - if ($captchaV3Settings) {
242 - $token = $_POST['g-recaptcha-response'];
243 - $integrationHandler = new IntegrationHandler(0);
244 - $allFormIntegrations = $integrationHandler->getAllIntegration('app', 'gReCaptchaV3');
245 - if (!is_wp_error($allFormIntegrations)) {
246 - foreach ($allFormIntegrations as $integration) {
247 - if (!is_null($integration->integration_type) && 'gReCaptchaV3' === $integration->integration_type) {
248 - $integrationDetails = json_decode($integration->integration_details);
249 - $integrationDetails->id = $integration->id;
250 - $reCAPTCHA = $integrationDetails;
251 - }
252 - }
253 - }
254 - if (!empty($reCAPTCHA->secretKey)) {
255 - $gRecaptchaResponse = HttpHelper::post(
256 - 'https://www.google.com/recaptcha/api/siteverify',
257 - ['secret' => $reCAPTCHA->secretKey, 'response' => $token]
258 - );
259 - if ($captchaV3Settings && !empty($saveResponse['triggerData'])) {
260 - $logID = $saveResponse['triggerData']['logID'];
261 - $integId = $reCAPTCHA->id;
262 - $saveApiResponse = new UtilApiResponse();
263 - $saveApiResponse->apiResponse($logID, $integId, ['type_name' => 'ReCaptcha', 'type' => 'v3'], 'success', $gRecaptchaResponse);
264 - }
265 - }
266 - unset($_POST['g-recaptcha-response']);
267 - }
405 + $this->logRecaptchaV3Verification($saveResponse);
268 406 if (!empty($redirectPage) && empty($saveResponse['redirectPage']) || null === $saveResponse['redirectPage']) {
269 407 $saveResponse['redirectPage'] = $redirectPage;
270 408 }
271 409 if (!empty($regSuccMsg) && isset($saveResponse['dflt_message'])) {
@@ -270,15 +408,14 @@
270 408 }
271 409 if (!empty($regSuccMsg) && isset($saveResponse['dflt_message'])) {
272 410 $saveResponse['message'] = $regSuccMsg;
273 411 }
412 + $saveResponse['new_nonce'] = wp_create_nonce('bitforms_' . $this->_form_id);
413 +
274 414 $saveResponse = IntegrationHandler::maybeSetCronForIntegration($saveResponse, 'create');
275 415 $entryId = $saveResponse['entry_id'];
276 416
277 417 $responseMsg = is_array($saveResponse) && !empty($saveResponse) ? $saveResponse : __('Form Submitted Successfully', 'bit-form');
278 - if (isset($newNonce)) {
279 - $responseMsg['new_nonce'] = $newNonce;
280 - }
281 418 $_POST = [];
282 419 $responseMsg['entry_id'] = $entryId;
283 420 return $responseMsg;
284 421 }
@@ -287,40 +424,46 @@
287 424 }
288 425
289 426 public function handleUpdateEntry()
290 427 {
428 + // Entry token or capability verified by caller (FrontendAjax::update_entry). All $_POST reads occur after that check.
291 429 $this->fieldNameReplaceOfPost();
292 - $validated = $this->beforeSubmittedValidate();
430 + $validated = $this->beforeSubmittedValidate(true, true);
431 + $validated = apply_filters('bitform_filter_form_validation', $validated, $this->_form_id);
293 432
294 - $entryID = $_REQUEST['entryID'];
433 + $entryID = isset($_REQUEST['entryID']) ? sanitize_text_field(wp_unslash($_REQUEST['entryID'])) : null;
434 + $GLOBALS['bitform_entry_id'] = $entryID;
295 435 if (is_null($entryID)) {
296 436 return new WP_Error('empty_form', __('Entries id is invalid', 'bit-form'));
297 437 }
298 438 if (true === $validated) {
299 - unset($_POST['hidden_fields'], $_POST['entryID']);
439 + do_action('bitform_validation_success', $this->_form_id);
440 + $this->discardHiddenFieldValues();
441 + unset($_POST['entryID']);
300 442
301 443 $redirectPage = '';
302 444 $regSuccMsg = '';
445 + $postData = wp_unslash($_POST);
303 446
304 447 $existAuth = (new IntegrationHandler($this->_form_id))->getAllIntegration('wp_user_auth', 'wp_auth', 1);
305 448 if (!is_wp_error($existAuth) && count($existAuth) > 0) {
306 449 $parameter = $this->getParams();
307 - $existAuthFilter = has_filter('bf_wp_user_auth');
450 + $existAuthFilter = has_filter('bitform_wp_user_auth');
308 451
309 452 if (true === $existAuthFilter) {
310 - $result = apply_filters('bf_wp_user_auth', $existAuth[0], $_POST, $parameter);
453 + $authPostData = $this->resolveConfirmChildValues($postData);
454 + $result = apply_filters('bitform_wp_user_auth', $existAuth[0], $authPostData, $parameter);
311 455
312 456 if (isset($result['auth_type']) && 'register' === $result['auth_type']) {
313 457 if (!$result['success']) {
314 - return new WP_Error('errors', __($result['message'], 'bit-form'));
458 + return new WP_Error('errors', self::authErrorMessage($result['message']));
315 459 } elseif (isset($result['success'])) {
316 - $redirectPage = $result['redirect_url'];
460 + $redirectPage = $result['redirectPage'];
317 461 $regSuccMsg = $result['message'];
318 - $newNonce = wp_create_nonce('bitforms_' . $this->_form_id);
319 462 }
320 463 } else {
321 464 if (!$result['success']) {
322 - return new WP_Error('errors', __($result['message'], 'bit-form'));
465 + return new WP_Error('errors', self::authErrorMessage($result['message']));
323 466 } else {
324 467 return $result;
325 468 }
326 469 }
@@ -326,43 +469,22 @@
326 469 }
327 470 }
328 471 }
329 472
330 - $updateResponse = $this->updateFormEntry($_POST, $this->getFormID(), $entryID);
473 + $updateResponse = $this->updateFormEntry(wp_unslash($_POST), $this->getFormID(), $entryID);
331 474 if (is_wp_error($updateResponse)) {
332 475 return $updateResponse;
333 476 }
334 477
335 - do_action('bitform_submit_success', $this->_form_id, $entryID, $_POST);
478 + // transformed dropdown value from string to array
479 + $newPost = $this->transformDrpdwnValue($postData);
480 + $filesData = GlobalHelper::sanitize_files_input($_FILES);
336 481
337 - $captchaV3Settings = $this->getCaptchaV3Settings();
338 - if ($captchaV3Settings) {
339 - $token = $_POST['g-recaptcha-response'];
340 - $integrationHandler = new IntegrationHandler(0);
341 - $allFormIntegrations = $integrationHandler->getAllIntegration('app', 'gReCaptchaV3');
342 - if (!is_wp_error($allFormIntegrations)) {
343 - foreach ($allFormIntegrations as $integration) {
344 - if (!is_null($integration->integration_type) && 'gReCaptchaV3' === $integration->integration_type) {
345 - $integrationDetails = json_decode($integration->integration_details);
346 - $integrationDetails->id = $integration->id;
347 - $reCAPTCHA = $integrationDetails;
348 - }
349 - }
350 - }
351 - if (!empty($reCAPTCHA->secretKey)) {
352 - $gRecaptchaResponse = HttpHelper::post(
353 - 'https://www.google.com/recaptcha/api/siteverify',
354 - ['secret' => $reCAPTCHA->secretKey, 'response' => $token]
355 - );
356 - if ($captchaV3Settings && !empty($updateResponse['triggerData'])) {
357 - $logID = $updateResponse['triggerData']['logID'];
358 - $integId = $reCAPTCHA->id;
359 - $saveApiResponse = new UtilApiResponse();
360 - $saveApiResponse->apiResponse($logID, $integId, ['type_name' => 'ReCaptcha', 'type' => 'v3'], 'success', $gRecaptchaResponse);
361 - }
362 - }
363 - unset($_POST['g-recaptcha-response']);
364 - }
482 + //TO DO:: submit success action temporarily added for solution of a issue
483 + do_action('bitform_submit_success', $this->_form_id, $entryID, $newPost, $filesData);
484 + do_action('bitform_update_success', $this->_form_id, $entryID, $newPost, $filesData);
485 +
486 + $this->logRecaptchaV3Verification($updateResponse);
365 487 if (!empty($redirectPage) && empty($updateResponse['redirectPage']) || null === $updateResponse['redirectPage']) {
366 488 $updateResponse['redirectPage'] = $redirectPage;
367 489 }
368 490 if (!empty($regSuccMsg) && isset($updateResponse['dflt_message'])) {
@@ -367,15 +489,14 @@
367 489 }
368 490 if (!empty($regSuccMsg) && isset($updateResponse['dflt_message'])) {
369 491 $updateResponse['message'] = $regSuccMsg;
370 492 }
371 - $updateResponse = IntegrationHandler::maybeSetCronForIntegration($updateResponse, 'create');
493 + $updateResponse['new_nonce'] = wp_create_nonce('bitforms_' . $this->_form_id);
494 + $updateResponse = IntegrationHandler::maybeSetCronForIntegration($updateResponse, 'update');
372 495 $entryId = $updateResponse['entry_id'];
373 496
374 497 $responseMsg = is_array($updateResponse) && !empty($updateResponse) ? $updateResponse : __('Entry Update Successfully', 'bit-form');
375 - if (isset($newNonce)) {
376 - $responseMsg['new_nonce'] = $newNonce;
377 - }
498 +
378 499 $_POST = [];
379 500 $responseMsg['entry_id'] = $entryId;
380 501 return $responseMsg;
381 502 }
@@ -382,11 +503,96 @@
382 503 do_action('bitform_validation_error', $this->_form_id, $validated);
383 504 return $validated;
384 505 }
385 506
507 + /**
508 + * Drop the posted `hidden_fields` transport key and, when the form opts in, the values of
509 + * the fields it names.
510 + *
511 + * A hidden field keeps its typed value in the DOM, so the browser still submits it. Runs
512 + * here because it is the last point before entry, notifications and integrations are built
513 + * from $_POST.
514 + *
515 + * @return void
516 + */
517 + private function discardHiddenFieldValues()
518 + {
519 + // CSRF verified upstream via verifySubmissionNonce(); $_POST is only being narrowed here.
520 + $rawHiddenFields = isset($_POST['hidden_fields']) ? wp_unslash($_POST['hidden_fields']) : '';
521 + unset($_POST['hidden_fields']);
522 +
523 + if (!$this->shouldDiscardHiddenFieldValues()) {
524 + return;
525 + }
526 + $hiddenFieldKeys = FrontendHelpers::parseHiddenFieldKeys($rawHiddenFields);
527 + if (empty($hiddenFieldKeys)) {
528 + return;
529 + }
530 +
531 + $formFields = $this->getFields();
532 + foreach ($hiddenFieldKeys as $fieldKey) {
533 + if (!isset($formFields[$fieldKey])) {
534 + continue;
535 + }
536 + $field = $formFields[$fieldKey];
537 + // The posted list also names builder-hidden and hidden-type fields, which carry a value
538 + // on purpose. Only what conditional logic hid is discarded.
539 + if ('hidden' === $field['type'] || !empty($field['valid']['hide'])) {
540 + continue;
541 + }
542 + // Hiding flags a repeater child once, not per row, so discarding would wipe the column
543 + // in every row.
544 + if (!empty($field['repeated'])) {
545 + continue;
546 + }
547 + // Calculation and tracking fields opt out.
548 + if (!empty($field['valid']['keepValueWhenHidden'])) {
549 + continue;
550 + }
551 + // A composite child (name/address/confirm) posts nested under its parent key.
552 + if (!empty($field['parentFieldKey'])) {
553 + $this->discardCompositeChildValue($formFields, $field, $fieldKey);
554 + continue;
555 + }
556 + unset($_POST[$fieldKey], $_FILES[$fieldKey]);
557 + }
558 + }
559 +
560 + /**
561 + * @param array $formFields
562 + * @param array $field the child field's config
563 + * @param string $fieldKey the child field's key
564 + *
565 + * @return void
566 + */
567 + private function discardCompositeChildValue($formFields, $field, $fieldKey)
568 + {
569 + $parentKey = $field['parentFieldKey'];
570 + if (!isset($_POST[$parentKey]) || !is_array($_POST[$parentKey])) {
571 + return;
572 + }
573 + $parentName = isset($formFields[$parentKey]['name']) ? $formFields[$parentKey]['name'] : '';
574 + $childName = FieldValueHandler::deriveChildName(isset($field['name']) ? $field['name'] : '', $parentName);
575 + unset($_POST[$parentKey][$childName], $_POST[$parentKey][$fieldKey]);
576 + }
577 +
578 + /**
579 + * @return bool
580 + */
581 + private function shouldDiscardHiddenFieldValues()
582 + {
583 + $formInfo = $this->getFormInfo();
584 + if (!is_object($formInfo) || !isset($formInfo->submissionSettings)) {
585 + return false;
586 + }
587 + $submissionSettings = (object) $formInfo->submissionSettings;
588 +
589 + return !empty($submissionSettings->discardHiddenFieldValues);
590 + }
591 +
386 592 public function validateFormSubmission($submitted_data)
387 593 {
388 - $hidden_fields = isset($submitted_data['hidden_fields']) ? $submitted_data['hidden_fields'] : '';
594 + $hidden_fields = FrontendHelpers::parseHiddenFieldKeys(isset($submitted_data['hidden_fields']) ? $submitted_data['hidden_fields'] : '');
389 595 $submitted_fields = $this->getSubmittedFields($submitted_data);
390 596 $form_fields = $this->getFields();
391 597 $form_fields_names = array_keys($form_fields);
392 598 if ($this->isGCLIDEnabled()) {
@@ -392,9 +598,9 @@
392 598 if ($this->isGCLIDEnabled()) {
393 599 array_push($form_fields_names, 'GCLID');
394 600 }
395 601 foreach ($submitted_fields as $field) {
396 - if ('hidden_fields' !== $field && !in_array($field, $form_fields_names) || false !== strpos($hidden_fields, $field)) {
602 + if ('hidden_fields' !== $field && !in_array($field, $form_fields_names) || FrontendHelpers::isFieldHidden($hidden_fields, $field)) {
397 603 unset($submitted_data[$field]);
398 604 }
399 605 }
400 606 return $submitted_data;
@@ -399,105 +605,41 @@
399 605 }
400 606 return $submitted_data;
401 607 }
402 608
403 - public function beforeSubmittedValidate()
609 + public function beforeSubmittedValidate($verifyCaptcha = true, $isEntryEdit = false)
404 610 {
405 611 if ($this->verifySubmissionNonce()) {
406 612 if ($this->isExist()) {
407 - $isRestricted = $this->checkSubmissionRestriction();
613 + $isRestricted = $this->checkSubmissionRestriction(true, $isEntryEdit);
408 614 if ($isRestricted && !empty($isRestricted)) {
409 615 return new WP_Error('spam_detection', $isRestricted[0]);
410 616 }
411 - if ($this->isTrappedInHoneypot()) {
617 + $postData = wp_unslash($_POST);
618 + $filesData = GlobalHelper::sanitize_files_input($_FILES);
619 + $isHoneypot = apply_filters('bitform_check_honeypot', false, $this->_form_id, $postData);
620 + if ($isHoneypot) {
412 621 return new WP_Error('spam_detection', __('Token verification failed', 'bit-form'));
413 622 }
414 - $captchaSettings = $this->getCaptchaSettings();
415 - $captchaV3Settings = $this->getCaptchaV3Settings();
416 - if ($captchaSettings || $captchaV3Settings) {
417 - $token = $_POST['g-recaptcha-response'];
418 - if (!isset($_POST['g-recaptcha-response'])) {
419 - return new WP_Error('spam_detection', __('Please verify reCAPTCHA', 'bit-form'));
623 + $formCurrentStep = isset($_POST['form-current-step']) ? sanitize_text_field(wp_unslash($_POST['form-current-step'])) : null;
624 + // TODO: Temporary parameter to skip captcha verification in step change of multi step form
625 + if ($verifyCaptcha) {
626 + $verifyGRecaptchaResult = $this->verifyGRecaptcha();
627 + if (is_wp_error($verifyGRecaptchaResult)) {
628 + return $verifyGRecaptchaResult;
420 629 }
421 - $integrationHandler = new IntegrationHandler(0);
422 - $allFormIntegrations = $integrationHandler->getAllIntegration('app', $captchaSettings ? 'gReCaptcha' : 'gReCaptchaV3');
423 - if (!is_wp_error($allFormIntegrations)) {
424 - foreach ($allFormIntegrations as $integration) {
425 - if (!is_null($integration->integration_type) && $integration->integration_type === ($captchaSettings ? 'gReCaptcha' : 'gReCaptchaV3')) {
426 - $integrationDetails = json_decode($integration->integration_details);
427 - $integrationDetails->id = $integration->id;
428 - $reCAPTCHA = $integrationDetails;
429 - }
430 - }
630 + $verifyHCaptchaResult = $this->verifyHCaptcha();
631 + if (is_wp_error($verifyHCaptchaResult)) {
632 + return $verifyHCaptchaResult;
431 633 }
432 - if (!empty($reCAPTCHA->secretKey)) {
433 - $gRecaptchaResponse = HttpHelper::post(
434 - 'https://www.google.com/recaptcha/api/siteverify',
435 - ['secret' => $reCAPTCHA->secretKey, 'response' => $token]
436 - );
437 - $isgReCaptchaVerified = false;
438 - if (!is_wp_error($gRecaptchaResponse)) {
439 - if (
440 - $captchaV3Settings
441 - && !empty($gRecaptchaResponse->score)
442 - && ((float) $gRecaptchaResponse->score < (float) $captchaV3Settings->score)
443 - ) {
444 - wp_send_json_error(
445 - __(
446 - $captchaV3Settings->message,
447 - 'bit-form'
448 - )
449 - );
450 - }
451 -
452 - $isgReCaptchaVerified = $gRecaptchaResponse->success;
453 - }
454 - if (!$isgReCaptchaVerified) {
455 - return new WP_Error('spam_detection', __('Please verify reCAPTCHA', 'bit-form'));
456 - }
634 + /* Implement Turnstile Captcha start */
635 + $verifyTurnstileCaptchaResult = $this->verifyTurnstileCaptcha();
636 + if (is_wp_error($verifyTurnstileCaptchaResult)) {
637 + return $verifyTurnstileCaptchaResult;
457 638 }
458 639 }
640 + /* Implement Turnstile Captcha end */
459 641
460 - /* Implement Turnstile Captcha start */
461 - $turnstileSetting = $this->getTurnstileSettings();
462 - if ($turnstileSetting) {
463 - if (!isset($_POST['cf-turnstile-response'])) {
464 - return new WP_Error('spam_detection', __('Please verify Cloudflare Turnstile Captcha', 'bit-form'));
465 - }
466 - $token = $_POST['cf-turnstile-response'];
467 - $turnstileCaptcha = null;
468 - $integrationHandler = new IntegrationHandler(0);
469 - $turnstileIntegration = $integrationHandler->getAllIntegration('app', 'turnstileCaptcha')[0];
470 - if (!is_wp_error($turnstileIntegration && !is_null($turnstileIntegration->integration_type))) {
471 - $turnstileCaptcha = json_decode($turnstileIntegration->integration_details);
472 - // $integrationDetails->id = $turnstileIntegration->id;
473 - // $turnstileCaptcha = $integrationDetails;
474 - }
475 - if (!is_null($turnstileCaptcha)) {
476 - $isTurnstileCaptchaVerified = false;
477 - $turnstileRecaptchaResponse = HttpHelper::post(
478 - 'https://challenges.cloudflare.com/turnstile/v0/siteverify',
479 - ['secret' => $turnstileCaptcha->secretKey, 'response' => $token]
480 - );
481 - if (!is_wp_error($turnstileRecaptchaResponse)) {
482 - if (!$turnstileRecaptchaResponse->success) {
483 - wp_send_json_error(
484 - __(
485 - 'Cloudflare Turnstile Validation Error: ' . implode(', ', $turnstileRecaptchaResponse->{'error-codes'}),
486 - 'bit-form'
487 - )
488 - );
489 - }
490 -
491 - $isTurnstileCaptchaVerified = $turnstileRecaptchaResponse->success;
492 - }
493 - if (!$isTurnstileCaptchaVerified) {
494 - return new WP_Error('spam_detection', __('Please verify Cloudflare Turnstile Captcha', 'bit-form'));
495 - }
496 - }
497 - }
498 -
499 - /* Implement Turnstile Captcha end */
500 642 $existAuth = (new IntegrationHandler($this->_form_id))->getAllIntegration('wp_user_auth', 'wp_auth', 1);
501 643
502 644 // check if user is already logged in and form has auth integration
503 645 do_action('bitform_checked_exist_auth', $this->_form_id, $existAuth);
@@ -503,39 +645,80 @@
503 645 do_action('bitform_checked_exist_auth', $this->_form_id, $existAuth);
504 646 if (!is_wp_error($existAuth) && count($existAuth) > 0 && is_user_logged_in()) {
505 647 return new WP_Error('auth_error', __('You are already logged in', 'bit-form'));
506 648 }
507 - $validateForm = $this->validateFormSubmission($_POST);
508 - $validateFormFiles = $this->validateFormSubmission($_FILES);
649 + $validateForm = $this->validateFormSubmission($postData);
650 + $validateFormFiles = $this->validateFormSubmission($filesData);
509 651 $validateForm = array_merge($validateForm, $validateFormFiles);
510 - $form_fields = $this->getFields();
652 + // Validate only provably-rendered fields: a field stranded in form_content->fields
653 + // with no layout entry (orphan) is never shown to the user and must not block
654 + // submission. getRenderedFields() unions ALL breakpoints × steps × nested layouts
655 + // + childFields of rendered parents, derives only from DB-stored form_content,
656 + // and fails closed (returns all fields) when the layout is unusable.
657 + $form_fields = $this->getRenderedFields();
511 658 // check if form-current-step is set and form is multi-step
512 - $formCurrentStep = isset($_POST['form-current-step']) ? $_POST['form-current-step'] : null;
659 + $formCurrentStep = isset($_POST['form-current-step']) ? sanitize_text_field(wp_unslash($_POST['form-current-step'])) : null;
513 660 if (!is_null($formCurrentStep)) {
661 + // Narrow validation to the current step's fields. SECURITY: the step
662 + // key set unions ALL breakpoints (lg/md/sm) — an md/sm-only field was
663 + // previously null-skipped by the validator (silent bypass). A forged
664 + // step index or malformed layout skips the narrowing entirely so every
665 + // rendered field stays validated (fail closed).
514 666 $formContents = $this->getFormContent();
515 - $layout = $formContents->layout;
667 + $layout = isset($formContents->layout) ? $formContents->layout : null;
516 668 $stepIndex = (int) $formCurrentStep - 1;
517 - $stepLayout = $layout[$stepIndex]->layout->lg;
518 - $nestedLayout = $formContents->nestedLayout;
519 - $step_fields = [];
520 - foreach ($stepLayout as $lay) {
521 - $fk = $lay->i;
522 - if (isset($nestedLayout->{$fk})) {
523 - $nestedLg = $nestedLayout->{$fk}->lg;
524 - foreach ($nestedLg as $nestedLay) {
525 - $nestedFk = $nestedLay->i;
526 - $step_fields[$nestedFk] = $form_fields[$nestedFk];
669 + if (is_array($layout) && isset($layout[$stepIndex]->layout) && is_object($layout[$stepIndex]->layout)) {
670 + $stepLayout = $layout[$stepIndex]->layout;
671 + $nestedLayout = isset($formContents->nestedLayout) && is_object($formContents->nestedLayout)
672 + ? $formContents->nestedLayout : null;
673 + $stepKeys = [];
674 + foreach (['lg', 'md', 'sm'] as $brkpnt) {
675 + if (!isset($stepLayout->{$brkpnt}) || !is_array($stepLayout->{$brkpnt})) {
676 + continue;
527 677 }
678 + foreach ($stepLayout->{$brkpnt} as $lay) {
679 + if (!is_object($lay) || !isset($lay->i)) {
680 + continue;
681 + }
682 + $fk = $lay->i;
683 + $stepKeys[$fk] = true;
684 + if (!is_null($nestedLayout) && isset($nestedLayout->{$fk})) {
685 + foreach (['lg', 'md', 'sm'] as $nBrkpnt) {
686 + if (!isset($nestedLayout->{$fk}->{$nBrkpnt}) || !is_array($nestedLayout->{$fk}->{$nBrkpnt})) {
687 + continue;
688 + }
689 + foreach ($nestedLayout->{$fk}->{$nBrkpnt} as $nestedLay) {
690 + if (is_object($nestedLay) && isset($nestedLay->i)) {
691 + $stepKeys[$nestedLay->i] = true;
692 + }
693 + }
694 + }
695 + }
696 + }
528 697 }
529 - $step_fields[$fk] = $form_fields[$fk];
698 + // Name/Address/Email/Password children live outside layouts; a child
699 + // is part of this step iff its parent is.
700 + self::expandChildFieldKeys($stepKeys, $form_fields);
701 + if (!empty($stepKeys)) {
702 + $step_fields = [];
703 + foreach (array_keys($stepKeys) as $fk) {
704 + if (isset($form_fields[$fk])) {
705 + $step_fields[$fk] = $form_fields[$fk];
706 + }
707 + }
708 + $form_fields = $step_fields;
709 + }
530 710 }
531 - $form_fields = $step_fields;
532 711 }
533 - $formFieldValidator = new FormFieldValidator($form_fields, $_POST, $_FILES);
712 + // Only an edit may satisfy a required upload/signature from a `_old` marker.
713 + $editedEntryID = $isEntryEdit && isset($_REQUEST['entryID'])
714 + ? sanitize_text_field(wp_unslash($_REQUEST['entryID']))
715 + : null;
716 + $formFieldValidator = new FormFieldValidator($form_fields, $postData, $filesData, $editedEntryID);
534 717 $validUniuqFields = [];
535 - $existFilter = has_filter('bf_check_duplicate_entry');
718 + $existFilter = has_filter('bitform_check_duplicate_entry');
536 719 if (true === $existFilter) {
537 - $validUniuqFields = apply_filters('bf_check_duplicate_entry', $form_fields, $_POST);
720 + $validUniuqFields = apply_filters('bitform_check_duplicate_entry', $form_fields, $postData);
538 721
539 722 $fieldKeys = array_keys($validUniuqFields);
540 723 $form_fields_keys = array_keys($form_fields);
541 724 $uniqueFields = [];
@@ -543,9 +726,9 @@
543 726 if (in_array($key, $form_fields_keys)) {
544 727 $uniqueFields[] = $form_fields[$key];
545 728 }
546 729 }
547 - do_action('bitform_Unique_entry', $uniqueFields, $validUniuqFields, $this->_form_id, $_POST);
730 + do_action('bitform_Unique_entry', $uniqueFields, $validUniuqFields, $this->_form_id, $postData);
548 731 }
549 732 $validateField = $formFieldValidator->validate('create', $this->_form_id);
550 733
551 734 if ($validateForm && $validateField && 0 === count($validUniuqFields)) {
@@ -567,15 +750,245 @@
567 750 return new WP_Error('token_expired', __('Token expired', 'bit-form'));
568 751 }
569 752 }
570 753
754 + /**
755 + * Whether the IP already has a completed entry; drafts (status 9) don't count.
756 + *
757 + * @param string $ipAddress Visitor IP
758 + *
759 + * @return bool
760 + */
761 + private function hasCompletedEntryFromIp($ipAddress)
762 + {
763 + $storedIp = IpTool::storageValue($ipAddress);
764 + if ('' === $storedIp) {
765 + return false;
766 + }
767 +
768 + $entries = (new FormEntryModel())->get(
769 + ['status'],
770 + [
771 + 'form_id' => $this->form_id,
772 + 'user_ip' => $storedIp,
773 + ]
774 + );
775 +
776 + if (is_wp_error($entries) || !is_array($entries)) {
777 + return false;
778 + }
779 +
780 + foreach ($entries as $entry) {
781 + if (9 !== (int) $entry->status) {
782 + return true;
783 + }
784 + }
785 +
786 + return false;
787 + }
788 +
789 + private function verifyGRecaptcha()
790 + {
791 + $captchaSettings = $this->getCaptchaSettings();
792 + $captchaV3Settings = $this->getCaptchaV3Settings();
793 + if (!$captchaSettings && !$captchaV3Settings) {
794 + return;
795 + }
796 +
797 + if (!isset($_POST['g-recaptcha-response'])) {
798 + return new WP_Error('spam_detection', __('Please recheck your reCaptcha Configuration', 'bit-form'));
799 + }
800 + $token = sanitize_text_field(wp_unslash($_POST['g-recaptcha-response']));
801 +
802 + $appType = $captchaSettings ? 'gReCaptcha' : 'gReCaptchaV3';
803 + $reCAPTCHA = $this->getCaptchaApp($appType);
804 +
805 + // Fail closed: no secret key must not skip verification.
806 + if (empty($reCAPTCHA->secretKey)) {
807 + Log::debug_log("reCAPTCHA ({$appType}) is enabled on form {$this->form_id}, but no secret key is set in App Settings.");
808 + return new WP_Error('spam_detection', __('reCAPTCHA is not configured. Please contact the site administrator.', 'bit-form'));
809 + }
810 +
811 + $gRecaptchaResponse = HttpHelper::post(
812 + 'https://www.google.com/recaptcha/api/siteverify',
813 + ['secret' => $reCAPTCHA->secretKey, 'response' => $token]
814 + );
815 + if (is_wp_error($gRecaptchaResponse)) {
816 + return new WP_Error('spam_detection', __('Please verify reCAPTCHA', 'bit-form'));
817 + }
818 +
819 + // Tokens are single-use; keep this verdict for the log.
820 + if ('gReCaptchaV3' === $appType) {
821 + $this->recaptchaV3Verification = ['integId' => $reCAPTCHA->id, 'response' => $gRecaptchaResponse];
822 + }
823 +
824 + if ($captchaV3Settings && self::isRecaptchaScoreTooLow($gRecaptchaResponse, $captchaV3Settings)) {
825 + wp_send_json_error(
826 + sanitize_text_field((string) $captchaV3Settings->message)
827 + );
828 + }
829 +
830 + if (!is_object($gRecaptchaResponse) || empty($gRecaptchaResponse->success)) {
831 + return new WP_Error('spam_detection', __('Please verify reCAPTCHA', 'bit-form'));
832 + }
833 + }
834 +
835 + /**
836 + * Connected captcha app from App Settings, with its integration id.
837 + *
838 + * @param string $appType gReCaptcha, gReCaptchaV3, hcaptcha or turnstileCaptcha
839 + *
840 + * @return object|null
841 + */
842 + private function getCaptchaApp($appType)
843 + {
844 + $apps = (new IntegrationHandler(0))->getAllIntegration('app', $appType);
845 + if (is_wp_error($apps) || !is_array($apps)) {
846 + return null;
847 + }
848 +
849 + $reCAPTCHA = null;
850 + foreach ($apps as $integration) {
851 + if ($appType !== $integration->integration_type) {
852 + continue;
853 + }
854 + $details = Utilities::jsonObj($integration->integration_details);
855 + if (is_object($details)) {
856 + $details->id = $integration->id;
857 + $reCAPTCHA = $details;
858 + }
859 + }
860 +
861 + return $reCAPTCHA;
862 + }
863 +
864 + /**
865 + * Log the reCAPTCHA v3 verdict for this submission.
866 + *
867 + * @param array $saveResponse Entry save/update response
868 + *
869 + * @return void
870 + */
871 + private function logRecaptchaV3Verification($saveResponse)
872 + {
873 + if (!$this->getCaptchaV3Settings()) {
874 + return;
875 + }
876 + unset($_POST['g-recaptcha-response']);
877 +
878 + if (empty($this->recaptchaV3Verification) || empty($saveResponse['triggerData']['logID'])) {
879 + return;
880 + }
881 +
882 + $response = $this->recaptchaV3Verification['response'];
883 + $status = is_object($response) && !empty($response->success) ? 'success' : 'error';
884 + (new UtilApiResponse())->apiResponse(
885 + $saveResponse['triggerData']['logID'],
886 + $this->recaptchaV3Verification['integId'],
887 + ['type_name' => 'ReCaptcha', 'type' => 'v3'],
888 + $status,
889 + $response
890 + );
891 + }
892 +
893 + /**
894 + * Whether the v3 score is below tolerance; 0.0 counts, blank tolerance means 0.6.
895 + *
896 + * @param mixed $response Decoded siteverify response
897 + * @param mixed $settings Form's recaptchav3 settings
898 + *
899 + * @return bool
900 + */
901 + private static function isRecaptchaScoreTooLow($response, $settings)
902 + {
903 + if (!is_object($response) || !isset($response->score) || !is_numeric($response->score)) {
904 + return false;
905 + }
906 +
907 + $threshold = is_object($settings) && isset($settings->score) && is_numeric($settings->score)
908 + ? (float) $settings->score
909 + : 0.6;
910 +
911 + return (float) $response->score < $threshold;
912 + }
913 +
914 + private function verifyHCaptcha()
915 + {
916 + if (!$this->isFieldTypeExist('hcaptcha')) {
917 + return;
918 + }
919 +
920 + if (!isset($_POST['h-captcha-response'])) {
921 + return new WP_Error('spam_detection', __('Please verify hCaptcha', 'bit-form'));
922 + }
923 + $token = sanitize_text_field(wp_unslash($_POST['h-captcha-response']));
924 + $hCaptcha = $this->getCaptchaApp('hcaptcha');
925 +
926 + // Fail closed: no secret key must not skip verification.
927 + if (empty($hCaptcha->secretKey)) {
928 + Log::debug_log("hCaptcha is enabled on form {$this->form_id}, but no secret key is set in App Settings.");
929 + return new WP_Error('spam_detection', __('hCaptcha is not configured. Please contact the site administrator.', 'bit-form'));
930 + }
931 +
932 + $hCaptchaResponse = HttpHelper::post(
933 + 'https://api.hcaptcha.com/siteverify',
934 + [
935 + 'secret' => $hCaptcha->secretKey,
936 + 'response' => $token,
937 + 'remoteip' => (isset($_SERVER['REMOTE_ADDR']) ? sanitize_text_field(wp_unslash($_SERVER['REMOTE_ADDR'])) : '')
938 + ]
939 + );
940 +
941 + if (is_wp_error($hCaptchaResponse) || !is_object($hCaptchaResponse) || empty($hCaptchaResponse->success)) {
942 + return new WP_Error('spam_detection', __('hCaptcha verification failed', 'bit-form'));
943 + }
944 + }
945 +
946 + private function verifyTurnstileCaptcha()
947 + {
948 + if (!$this->isFieldTypeExist('turnstile')) {
949 + return;
950 + }
951 +
952 + if (!isset($_POST['cf-turnstile-response'])) {
953 + return new WP_Error('spam_detection', __('Please verify Cloudflare Turnstile Captcha', 'bit-form'));
954 + }
955 + $token = sanitize_text_field(wp_unslash($_POST['cf-turnstile-response']));
956 + $turnstileCaptcha = $this->getCaptchaApp('turnstileCaptcha');
957 +
958 + // Fail closed: no secret key must not skip verification.
959 + if (empty($turnstileCaptcha->secretKey)) {
960 + Log::debug_log("Turnstile is enabled on form {$this->form_id}, but no secret key is set in App Settings.");
961 + return new WP_Error('spam_detection', __('Cloudflare Turnstile is not configured. Please contact the site administrator.', 'bit-form'));
962 + }
963 +
964 + $turnstileResponse = HttpHelper::post(
965 + 'https://challenges.cloudflare.com/turnstile/v0/siteverify',
966 + ['secret' => $turnstileCaptcha->secretKey, 'response' => $token]
967 + );
968 + if (is_wp_error($turnstileResponse) || !is_object($turnstileResponse)) {
969 + return new WP_Error('spam_detection', __('Please verify Cloudflare Turnstile Captcha', 'bit-form'));
970 + }
971 +
972 + if (empty($turnstileResponse->success)) {
973 + $errorCodes = implode(', ', (array) ($turnstileResponse->{'error-codes'} ?? []));
974 + wp_send_json_error(
975 + sprintf(
976 + /* translators: %s: dynamic value. */
977 + __('Cloudflare Turnstile Validation Error: %s', 'bit-form'),
978 + $errorCodes
979 + )
980 + );
981 + }
982 + }
983 +
571 984 public function verifySubmissionNonce()
572 985 {
573 - if (!isset($_POST['t_identity']) && !isset($_POST['csrf'])) {
986 + if (!isset($_POST['t_identity']) || !isset($_POST['csrf'])) {
574 987 return false;
575 988 }
576 - $tIdenty = sanitize_text_field($_POST['t_identity']);
577 - $csrf = sanitize_text_field($_POST['csrf']);
989 + $tIdenty = sanitize_text_field(wp_unslash($_POST['t_identity']));
990 + $csrf = sanitize_text_field(wp_unslash($_POST['csrf']));
578 991 unset($_POST['t_identity'], $_POST['action'], $_POST['bitforms_id'], $_POST['csrf']);
579 992 return Helpers::csrfDecrypted($tIdenty, $csrf);
580 993 }
581 994
@@ -583,9 +996,9 @@
583 996 {
584 997 if (!current_user_can('manage_options')) {
585 998 $update_status = $this->formModel->update(
586 999 [
587 - 'views' => intval(static::$form[0]->views) + 1
1000 + 'views' => intval($this->form[0]->views) + 1
588 1001 ],
589 1002 [
590 1003 'id' => $this->form_id
591 1004 ]
@@ -592,156 +1005,103 @@
592 1005 );
593 1006 }
594 1007 }
595 1008
596 - public function checkSubmissionRestriction($checkedEmptySubmitted = true)
1009 + /**
1010 + * @param bool $checkedEmptySubmitted whether the empty-submission rule applies here
1011 + * @param bool $isEntryEdit true when an existing entry is being updated
1012 + */
1013 + public function checkSubmissionRestriction($checkedEmptySubmitted = true, $isEntryEdit = false)
597 1014 {
598 1015 $formContents = $this->getFormContent();
599 - $fromRestrictionSetitingsEnabled = empty($formContents->additional->enabled) ? [] : $formContents->additional->enabled;
600 - $fromRestrictionSetitings = empty($formContents->additional->settings) ? null : $formContents->additional->settings;
601 - if (is_null($formContents->additional->enabled) || is_null($formContents->additional->settings)) {
1016 + $additionalSettings = isset($formContents->additional) ? $formContents->additional : null;
1017 + $fromRestrictionSetitingsEnabled = empty($additionalSettings->enabled) ? [] : $additionalSettings->enabled;
1018 + $fromRestrictionSetitings = empty($additionalSettings->settings) ? null : $additionalSettings->settings;
1019 +
1020 + if (is_null($additionalSettings) || is_null($fromRestrictionSetitings) || empty((array) $fromRestrictionSetitingsEnabled)) {
602 1021 return false;
603 1022 }
1023 +
604 1024 $restrictionMessage = [];
605 1025 $ipTool = new IpTool();
606 1026 $ipAddress = $ipTool->getIP();
1027 + $currentUserId = get_current_user_id();
1028 +
607 1029 foreach ($fromRestrictionSetitingsEnabled as $restrictionKey => $isEnabled) {
608 1030 if ($isEnabled) {
609 - if ('entry_limit' === $restrictionKey && isset($fromRestrictionSetitings->{$restrictionKey})) {
610 - $formEntry = new FormEntryModel();
611 - $countResult = $formEntry->count(
612 - [
613 - 'form_id' => $this->form_id
614 - ]
1031 + // Quota rules gate creating an entry, so an edit skips them; access-control keys stay.
1032 + $skippableOnEdit = ['onePerIp', 'entry_limit', 'entry_limit_by_user', 'restrict_form'];
1033 + if ($isEntryEdit && in_array($restrictionKey, $skippableOnEdit, true)) {
1034 + $skipOnEdit = apply_filters(
1035 + 'bitform_skip_restriction_on_entry_edit',
1036 + true,
1037 + $restrictionKey,
1038 + $this->form_id
615 1039 );
616 - $count = !empty($countResult[0]) && !empty($countResult[0]->count) ? $countResult[0]->count : false;
617 - if ($count && $count >= intval($fromRestrictionSetitings->{$restrictionKey})) {
618 - $restrictionMessage[] = __('Sorry!! Entry limit exceeded', 'bit-form');
1040 + if ($skipOnEdit) {
1041 + continue;
619 1042 }
620 1043 }
1044 + /**
1045 + * Allow add-ons to handle any restriction key (Pro-only restrictions
1046 + * should be implemented in the add-on, not shipped in the free plugin).
1047 + *
1048 + * Return a non-null string to block submission.
1049 + */
1050 + $addonMsg = apply_filters(
1051 + 'bitform_submission_restriction',
1052 + null,
1053 + $restrictionKey,
1054 + $this->form_id,
1055 + $fromRestrictionSetitingsEnabled,
1056 + $fromRestrictionSetitings,
1057 + $ipAddress,
1058 + $currentUserId
1059 + );
1060 +
1061 + if (!is_null($addonMsg) && '' !== $addonMsg) {
1062 + $restrictionMessage[] = $addonMsg;
1063 + continue;
1064 + }
1065 +
621 1066 if ('onePerIp' === $restrictionKey) {
622 - $formEntry = new FormEntryModel();
623 - $countResult = $formEntry->count(
624 - [
625 - 'form_id' => $this->form_id,
626 - 'user_ip' => ip2long($ipAddress)
627 - ]
628 - );
629 - $count = !empty($countResult[0]) && !empty($countResult[0]->count) ? $countResult[0]->count : false;
1067 + if ($this->hasCompletedEntryFromIp($ipAddress)) {
1068 + $onePerIp = __('Sorry!! You have already submitted from this IP address', 'bit-form');
630 1069
631 - if ($count && $count > 0) {
632 - $restrictionMessage[] = __('Sorry!! You have already submitted', 'bit-form');
1070 + $onePerIp = apply_filters(
1071 + 'bitform_filter_restriction_one_per_ip_message',
1072 + $onePerIp,
1073 + $this->form_id
1074 + );
1075 +
1076 + $restrictionMessage[] = $onePerIp;
633 1077 }
634 1078 }
635 1079 if ('is_login' === $restrictionKey && 0 === get_current_user_id()) {
636 - $restrictionMessage[] = __($fromRestrictionSetitings->is_login->message, 'bit-form');
1080 + $is_login_messages = $fromRestrictionSetitings->is_login->message;
1081 +
1082 + $is_login_messages = apply_filters(
1083 + 'bitform_filter_restriction_is_login_message',
1084 + $is_login_messages,
1085 + $this->form_id
1086 + );
1087 +
1088 + $restrictionMessage[] = $is_login_messages;
637 1089 }
638 1090 if ($checkedEmptySubmitted && 'empty_submission' === $restrictionKey) {
639 - $isEmpty = $this->checkEmptySubmission($_POST, $_FILES);
1091 + $isEmpty = $this->checkEmptySubmission(wp_unslash($_POST), GlobalHelper::sanitize_files_input($_FILES), $isEntryEdit);
640 1092 if ($isEmpty) {
641 - $restrictionMessage[] = __($fromRestrictionSetitings->empty_submission->message, 'bit-form');
642 - }
643 - }
644 - if ('restrict_form' === $restrictionKey && isset($fromRestrictionSetitings->{$restrictionKey})) {
645 - $day = empty($fromRestrictionSetitings->{$restrictionKey}->day) ? null : $fromRestrictionSetitings->{$restrictionKey}->day;
646 - $date = empty($fromRestrictionSetitings->{$restrictionKey}->date) ? null : $fromRestrictionSetitings->{$restrictionKey}->date;
647 - $time = empty($fromRestrictionSetitings->{$restrictionKey}->time) ? null : $fromRestrictionSetitings->{$restrictionKey}->time;
1093 + $restriction = $fromRestrictionSetitings->empty_submission->message;
648 1094
649 - $isdayOk = $isdateOk = $istimeOk = true;
650 - $dayNotOkMsg = $dateNotOkMsg = $timeNotOkMsg = '';
651 - $dateTimeHelper = new DateTimeHelper();
652 - if (
653 - !empty($day)
654 - && is_array($day)
655 - && (in_array('Friday', $day)
656 - || in_array('Saturday', $day)
657 - || in_array('Sunday', $day)
658 - || in_array('Monday', $day)
659 - || in_array('Tuesday', $day)
660 - || in_array('Wednesday', $day)
661 - || in_array('Thursday', $day))
662 - && (!in_array($dateTimeHelper->getDay('full-name'), $day))
663 - ) {
664 - $isdayOk = false;
665 - $dayMsgVarsFormat = '';
666 - foreach ($day as $dayIndex => $dayValue) {
667 - if ($dayIndex > 0) {
668 - $dayMsgVarsFormat .= ', ';
669 - }
670 - $dayMsgVarsFormat .= '%s';
671 - }
672 - $dayNotOkMsg = vsprintf(__("in $dayMsgVarsFormat", 'bit-form'), $day);
673 - }
674 - if (
675 - !empty($day)
676 - && is_array($day)
677 - && (in_array('Custom', $day))
678 - ) {
679 - $startDate = empty($date->from) ? '00-00-0000' : $date->from;
680 - $endDate = empty($date->to) ? '00-00-0000' : $date->to;
681 - $dateFormat = preg_match('/^[0-9]{4}-[0-9]{2}-[0-9]{2}$/', $startDate) ? 'Y-m-d' : 'm-d-Y';
682 - if (!empty($date->from) && false !== strpos($startDate, 'T')) {
683 - $startDate = $dateTimeHelper->getDate($startDate, false, null, $dateFormat);
684 - }
685 - if (!empty($date->to) && false !== strpos($endDate, 'T')) {
686 - $endDate = $dateTimeHelper->getDate($endDate, false, null, $dateFormat);
687 - }
688 - $currentDate = $dateTimeHelper->getDate(null, null, null, $dateFormat);
689 - if (!($currentDate >= $startDate && $currentDate <= $endDate)) {
690 - $isdateOk = false;
691 - $dateNotOkMsg = sprintf(__('within %s to %s', 'bit-form'), $startDate, $endDate);
692 - }
693 - }
1095 + $restriction = apply_filters(
1096 + 'bitform_filter_restriction_empty_submission_message',
1097 + $restriction,
1098 + $this->form_id
1099 + );
694 1100
695 - if (!empty($time)) {
696 - $startTime = empty($time->from) ? '00:00' : $time->from;
697 - $endTime = empty($time->to) ? '23:59.999' : $time->to;
698 - $currentTime = $dateTimeHelper->getTime(null, null, null, 'H:i');
699 - if (!($currentTime >= $startTime && $currentTime <= $endTime)) {
700 - $istimeOk = false;
701 - $startTime = $dateTimeHelper->getTime($startTime, 'H:i', null);
702 - $endTime = $dateTimeHelper->getTime($endTime, 'H:i', null);
703 - $isTimeOk = false;
704 - $timeNotOkMsg = sprintf(__('%s to %s', 'bit-form'), $startTime, $endTime);
705 - }
1101 + $restrictionMessage[] = $restriction;
706 1102 }
707 -
708 - if (!($isdateOk && $isdayOk && $istimeOk)) {
709 - if (!$isdayOk) {
710 - $restrictionMessage[] = !empty($timeNotOkMsg) ? sprintf(__('Form is available %s From %s', 'bit-form'), $dayNotOkMsg, $timeNotOkMsg) :
711 - sprintf(__('Form is available %s', 'bit-form'), $dayNotOkMsg, $timeNotOkMsg);
712 - } elseif (!$isdateOk) {
713 - $restrictionMessage[] = !empty($timeNotOkMsg) ? sprintf(__('Form is available %s From %s', 'bit-form'), $dateNotOkMsg, $timeNotOkMsg) :
714 - sprintf(__('Form is available %s', 'bit-form'), $dateNotOkMsg, $timeNotOkMsg);
715 - } elseif (!$istimeOk) {
716 - $restrictionMessage[] = sprintf(__('Form is available on %s', 'bit-form'), $timeNotOkMsg);
717 - }
718 - }
719 1103 }
720 - if ('blocked_ip' === $restrictionKey && isset($fromRestrictionSetitings->{$restrictionKey})) {
721 - $isIpBlocked = false;
722 - foreach ($fromRestrictionSetitings->{$restrictionKey} as $ipIndex => $ipDetails) {
723 - if (!empty($ipDetails->status) && $ipDetails->status && !empty($ipDetails->ip) && $ipDetails->ip === $ipAddress) {
724 - $isIpBlocked = true;
725 - break;
726 - }
727 - }
728 - if ($isIpBlocked) {
729 - $restrictionMessage[] = sprintf(__('Sorry!! Your IP address is %s, Blocked from submitting the form', 'bit-form'), $ipAddress);
730 - }
731 - }
732 - if ('private_ip' === $restrictionKey && isset($fromRestrictionSetitings->{$restrictionKey})) {
733 - $isIpWhiteListed = false;
734 - foreach ($fromRestrictionSetitings->{$restrictionKey} as $ipIndex => $ipDetails) {
735 - if (!empty($ipDetails->status) && $ipDetails->status && !empty($ipDetails->ip) && $ipDetails->ip === $ipAddress) {
736 - $isIpWhiteListed = true;
737 - break;
738 - }
739 - }
740 - if (!$isIpWhiteListed) {
741 - $restrictionMessage[] = sprintf(__('Sorry!! Your IP address is %s, Blocked from submitting the form', 'bit-form'), $ipAddress);
742 - }
743 - }
744 1104 }
745 1105 }
746 1106 return $restrictionMessage;
747 1107 }
@@ -746,51 +1106,21 @@
746 1106 return $restrictionMessage;
747 1107 }
748 1108
749 1109 /**
750 - * Will check if form is submitted by a bot
751 - *
752 - * @return Boolean true - if submitted by bot else false
753 - */
1110 + * Will check if form is submitted by a bot
1111 + *
1112 + * @return Boolean true - if submitted by bot else false
1113 + */
754 1114 public function isTrappedInHoneypot()
755 1115 {
756 - $isHoneyPot = false;
757 -
758 - if (!$this->isHoneypotActive()) {
759 - return false;
760 - }
761 -
762 - $token = $_POST['b_h_t'];
763 - $pattern = '/^([a-zA-Z0-9]*_[a-zA-Z0-9]*){4}$/';
764 - $decryptedToken = base64_decode(base64_decode($token));
765 -
766 - preg_match($pattern, $decryptedToken, $validToken);
767 -
768 - if ($validToken) {
769 - if (isset($_POST[$token]) && empty($_POST[$token])) {
770 - $isHoneyPot = false;
771 - } else {
772 - $isHoneyPot = true;
773 - }
774 - } else {
775 - $isHoneyPot = true;
776 - }
777 -
778 - if (isset($_POST[$token])) {
779 - unset($_POST[$token]);
780 - }
781 - unset($_POST['b_h_t']);
782 - return $isHoneyPot;
1116 + // Honeypot is implemented by add-ons (e.g. Pro) via filter.
1117 + return (bool) apply_filters('bitform_check_honeypot', false, $this->_form_id, wp_unslash($_POST));
783 1118 }
784 1119
785 1120 public function isHoneypotActive()
786 1121 {
787 - $formContents = $this->getFormContent();
788 - $enabled = empty($formContents->additional->enabled) ? null : $formContents->additional->enabled;
789 - if (!empty($enabled->honeypot) && $enabled->honeypot) {
790 - return true;
791 - }
792 - return false;
1122 + return (bool) apply_filters('bitform_is_honeypot_active', false, $this->_form_id, $this->getFormContent());
793 1123 }
794 1124
795 1125 public function checkPaymentFields()
796 1126 {
@@ -798,11 +1128,14 @@
798 1128 $fields = $formContents->fields;
799 1129
800 1130 $payments = [];
801 1131 foreach ($fields as $fldData) {
1132 + if (!is_object($fldData)) {
1133 + continue;
1134 + }
802 1135 if ('paypal' === $fldData->typ && property_exists($fldData, 'payIntegID')) {
803 1136 $payments['paypalKey'] = $this->getClientKey($fldData->payIntegID, 'clientID');
804 - } elseif ('razorpay' === $fldData->typ && property_exists($fldData->options, 'payIntegID')) {
1137 + } elseif ('razorpay' === $fldData->typ && isset($fldData->options) && is_object($fldData->options) && property_exists($fldData->options, 'payIntegID')) {
805 1138 $payments['razorpayKey'] = $this->getClientKey($fldData->options->payIntegID, 'apiKey');
806 1139 }
807 1140 }
808 1141
@@ -815,10 +1148,13 @@
815 1148 if (!empty($integID)) {
816 1149 $integrationHandler = new IntegrationHandler(0);
817 1150 $integration = $integrationHandler->getAIntegration($integID, 'app', 'payments');
818 1151 if (!is_wp_error($integration)) {
819 - $integration_details = json_decode($integration[0]->integration_details);
820 - $client = base64_encode($integration_details->{$keyName});
1152 + $integrationRow = Utilities::firstRow($integration);
1153 + $integration_details = Utilities::jsonObj($integrationRow->integration_details ?? '');
1154 + if ($integration_details && isset($integration_details->{$keyName})) {
1155 + $client = base64_encode($integration_details->{$keyName});
1156 + }
821 1157 }
822 1158 }
823 1159 return $client;
824 1160 }
@@ -824,38 +1160,11 @@
824 1160 }
825 1161
826 1162 public function getSuccessMessageMarkups()
827 1163 {
828 - if (is_null($this->_work_flows)) {
829 - $workFlowManager = new WorkFlowHandler($this->form_id);
830 - $this->_work_flows = $workFlowManager->getAllworkFlow();
831 - }
832 -
833 - $ids = [];
834 - foreach ($this->_work_flows as $msgItem) {
835 - foreach ($msgItem['conditions'] as $condition) {
836 - if (isset($condition->actions->success)) {
837 - foreach ($condition->actions->success as $msg) {
838 - if ('successMsg' === $msg->type && isset($msg->details->id)) {
839 - $idObj = json_decode(stripslashes($msg->details->id));
840 - if (is_object($idObj) && !empty($idObj->id)) {
841 - array_push($ids, $idObj->id);
842 - }
843 - }
844 - }
845 - }
846 - if (isset($condition->actions->failure)) {
847 - $idObj = json_decode(stripslashes($condition->actions->failure));
848 - if (is_object($idObj) && !empty($idObj->id)) {
849 - array_push($ids, $idObj->id);
850 - }
851 - }
852 - }
853 - }
854 - $ids = array_unique($ids);
855 1164 if (is_null($this->_conf_messages)) {
856 1165 $successMsgHandler = new SuccessMessageHandler($this->form_id);
857 - $this->_conf_messages = $successMsgHandler->getMessages($ids);
1166 + $this->_conf_messages = $successMsgHandler->getAllMessage();
858 1167 }
859 1168
860 1169 $messageMarkups = '';
861 1170 if (is_wp_error($this->_conf_messages)) {
@@ -862,8 +1171,12 @@
862 1171 return $messageMarkups;
863 1172 }
864 1173
865 1174 foreach ($this->_conf_messages as $msgItem) {
1175 + $msgConfig = json_decode($msgItem->message_config);
1176 + if (is_object($msgConfig) && property_exists($msgConfig, 'status') && empty($msgConfig->status)) {
1177 + continue;
1178 + }
866 1179 $messageMarkups .= $this->messageMarkup($msgItem);
867 1180 }
868 1181
869 1182 return $messageMarkups;
@@ -870,38 +1183,47 @@
870 1183 }
871 1184
872 1185 public function getFormAbandonmentMessage()
873 1186 {
874 - if (class_exists('\BitCode\BitFormPro\Admin\FormSettings\FormAbandonment')) {
875 - $formAbandonmentSettings = FormAbandonment::getFormAbandonmentSettings($this->form_id);
876 - $msg = '';
877 - if (isset($formAbandonmentSettings->showWarningMsg) && $formAbandonmentSettings->showWarningMsg && !empty($formAbandonmentSettings->warningMsg)) {
878 - $msg = $formAbandonmentSettings->warningMsg;
879 - $msg = '<div class="bf-form-msg active warning">' . wp_kses_post($msg) . '</div>';
880 - }
881 - return $msg;
882 - }
1187 + $msg = apply_filters('bitform_form_abandonment_warning_markup', '', $this->form_id);
1188 + return is_string($msg) ? $msg : '';
883 1189 }
884 1190
1191 + public function getFormAbandonmentSettings()
1192 + {
1193 + return apply_filters('bitform_form_abandonment_settings', null, $this->form_id);
1194 + }
1195 +
885 1196 private function messageMarkup($msg)
886 1197 {
887 1198 $msgId = $msg->id;
888 1199 $msgConfig = json_decode($msg->message_config);
889 - $scrollClass = 'below' === $msgConfig->msgType ? 'scroll' : '';
1200 + $msgType = (is_object($msgConfig) && isset($msgConfig->msgType)) ? $msgConfig->msgType : 'below';
1201 + $scrollClass = 'below' === $msgType ? 'scroll' : '';
890 1202
891 - return <<<SUCCESSMSG
892 - <div role="dialog" aria-hidden="true" data-modal-backdrop="true" class="{$this->getAtomicCls("msg-container-{$msgId}")} deactive {$scrollClass}">
893 - <div data-contentid="{$this->getFormIdentifier()}" data-msgid="{$msgId}" role="button" class="{$this->getAtomicCls("msg-background-{$msgId}")} msg-backdrop">
894 - <div class="bf-msg-content {$this->getAtomicCls("msg-content-{$msgId}")}">
895 - <button data-contentid="{$this->getFormIdentifier()}" data-msgid="{$msgId}" class="{$this->getAtomicCls("close-{$msgId}")} bf-msg-close" type="button">
896 - <svg class="{$this->getAtomicCls("close-icn-{$msgId}")}" viewBox="0 0 30 30">
897 - <line fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" x1="4" y1="3.88" x2="26" y2="26.12"></line>
898 - <line fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" x1="26" y1="3.88" x2="4" y2="26.12"></line>
899 - </svg>
900 - </button>
901 - <div class="msg-content"></div>
902 - </div>
903 - </div>
904 - </div>
905 -SUCCESSMSG;
1203 + return '<div
1204 + role="dialog"
1205 + aria-hidden="true"
1206 + data-modal-backdrop="true"
1207 + class="' . $this->getAtomicCls("msg-container-{$msgId}") . ' deactive ' . $scrollClass . '">
1208 + <div
1209 + data-contentid="' . $this->getFormIdentifier() . '"
1210 + data-msgid="' . $msgId . '"
1211 + role="button"
1212 + class="' . $this->getAtomicCls("msg-background-{$msgId}") . ' msg-backdrop">
1213 + <div class="bf-msg-content ' . $this->getAtomicCls("msg-content-{$msgId}") . '">
1214 + <button
1215 + data-contentid="' . $this->getFormIdentifier() . '"
1216 + data-msgid="' . $msgId . '"
1217 + class="' . $this->getAtomicCls("close-{$msgId}") . ' bf-msg-close"
1218 + type="button">
1219 + <svg class="' . $this->getAtomicCls("close-icn-{$msgId}") . '" viewBox="0 0 30 30">
1220 + <line fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" x1="4" y1="3.88" x2="26" y2="26.12"></line>
1221 + <line fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" x1="26" y1="3.88" x2="4" y2="26.12"></line>
1222 + </svg>
1223 + </button>
1224 + <div class="msg-content"></div>
1225 + </div>
1226 + </div>
1227 + </div>';
906 1228 }
907 1229 }