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 / FrontendFormManager.php

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

1,230 lines 44.9 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 /**
4 * Get set Form,fields
5 */
6
7 namespace BitCode\BitForm\Frontend\Form;
8
9 /**
10 * FrontendFormManager class
11 */
12
13 use BitCode\BitForm\Admin\Form\AdminFormHandler;
14 use BitCode\BitForm\Admin\Form\Helpers;
15 use BitCode\BitForm\Core\Database\FormEntryModel;
16 use BitCode\BitForm\Core\Form\FormManager;
17 use BitCode\BitForm\Core\Form\Validator\FormFieldValidator;
18 use BitCode\BitForm\Core\Integration\IntegrationHandler;
19 use BitCode\BitForm\Core\Messages\SuccessMessageHandler;
20 use BitCode\BitForm\Core\Util\ApiResponse as UtilApiResponse;
21 use BitCode\BitForm\Core\Util\EscapingHelper;
22 use BitCode\BitForm\Core\Util\FieldValueHandler;
23 use BitCode\BitForm\Core\Util\FrontendHelpers;
24 use BitCode\BitForm\Core\Util\HttpHelper;
25 use BitCode\BitForm\Core\Util\IpTool;
26 use BitCode\BitForm\Core\Util\Log;
27 use BitCode\BitForm\Core\Util\Utilities;
28 use BitCode\BitForm\Core\WorkFlow\WorkFlow;
29 use BitCode\BitForm\Frontend\Form\View\FormViewer;
30 use BitCode\BitForm\GlobalHelper;
31 use WP_Error;
32
33 final class FrontendFormManager extends FormManager
34 {
35 private $_form_identifier;
36
37 /** @var array|null reCAPTCHA v3 verdict, reused for the log */
38 private $recaptchaV3Verification;
39 private $_form_token;
40 private $_form_id;
41 private $_conf_messages;
42 private static $_instance = [];
43
44 // private $_has_upload = false;
45 public function __construct($form_id, $shortCodeCounter = null)
46 {
47 parent::__construct($form_id);
48 $this->_form_identifier = 'bitforms_' . $form_id;
49 $this->_form_identifier .= !empty(get_post()->ID) ? '_' . get_post()->ID : '';
50 $this->_form_identifier .= !empty($shortCodeCounter) ? "_$shortCodeCounter" : '';
51 $this->_form_token = wp_create_nonce('bitforms_' . $form_id);
52 $this->_form_id = $form_id;
53 }
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
66 public function getFormIdentifier()
67 {
68 return $this->_form_identifier;
69 }
70
71 public function getFormID()
72 {
73 return $this->_form_id;
74 }
75
76 public function getFormToken()
77 {
78 return $this->_form_token;
79 }
80
81 public function getSubmittedFields($submitted_data)
82 {
83 unset($submitted_data[$this->_form_identifier]);
84 // unset($submitted_data['bit-form-submit-btn']);
85 return array_keys($submitted_data);
86 }
87
88 public function formView($fields = null, $hasFile = false, $errorMessages = null, $previousValue = null, $isEntryEdit = false)
89 {
90 $formContents = $this->getFormContent();
91 $formAtomicClsMap = $this->getAtomicClsMap();
92 if (!empty($fields)) {
93 $formContents->fields = is_string($fields) ? json_decode($fields) : $fields;
94 } else {
95 $workFlowRunHelper = new WorkFlow($this->form_id);
96 $workFlowreturnedOnLoad = $workFlowRunHelper->executeOnLoad(
97 'create',
98 $formContents->fields
99 );
100 $formContents->fields = empty($workFlowreturnedOnLoad['fields']) ? $formContents->fields : $workFlowreturnedOnLoad['fields'];
101 }
102 $formViewer = new FormViewer($this, $formContents, $formAtomicClsMap, $errorMessages, $previousValue);
103 $isRestricted = $this->checkSubmissionRestriction(false, $isEntryEdit);
104 $msg = !empty($isRestricted) ? $isRestricted[0] : '';
105 return $formViewer->getView($hasFile, $msg);
106 }
107
108 public function conversationalFormView($fields = null, $hasFile = false, $errorMessages = null, $previousValue = null, $isEntryEdit = false)
109 {
110 $formContents = $this->getFormContent();
111 $formAtomicClsMap = $this->getAtomicClsMap();
112 if (!empty($fields)) {
113 $formContents->fields = is_string($fields) ? json_decode($fields) : $fields;
114 } else {
115 $workFlowRunHelper = new WorkFlow($this->form_id);
116 $workFlowreturnedOnLoad = $workFlowRunHelper->executeOnLoad(
117 'create',
118 $formContents->fields
119 );
120 $formContents->fields = empty($workFlowreturnedOnLoad['fields']) ? $formContents->fields : $workFlowreturnedOnLoad['fields'];
121 }
122 $formViewer = new FormViewer($this, $formContents, $formAtomicClsMap, $errorMessages, $previousValue);
123 $isRestricted = $this->checkSubmissionRestriction(false, $isEntryEdit);
124 $msg = !empty($isRestricted) ? $isRestricted[0] : '';
125 return $formViewer->getConversationalView($hasFile, $msg);
126 }
127
128 public function checkEmptySubmission($data, $file, $isEntryEdit = false)
129 {
130 $formFields = $this->getFields();
131 foreach ($formFields as $key => $field) {
132 $fieldType = $field['type'];
133 if ('button' === $fieldType) {
134 continue;
135 }
136 $fileUploadFieldTypes = ['file-up', 'advanced-file-up'];
137 if ('decision-box' === $fieldType || 'gdpr' === $fieldType) {
138 continue;
139 }
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 }
149 if ($this->isRepeatedField($key)) {
150 $fileData = !empty($file[$key]) ? $file[$key] : [];
151 $dataVal = !empty($data[$key]) ? $data[$key] : [];
152 if (!$this->checkRepeatedFieldEmptySubmission($isFileType, $dataVal, $fileData)) {
153 return false;
154 }
155 continue;
156 }
157 if (!$isFileType && (!empty($data[$key]) || (isset($data[$key]) && is_numeric($data[$key])))) {
158 return false;
159 }
160 if ($isFileType && !empty($file[$key]['name']) && is_string($file[$key]['name'])) {
161 return false;
162 }
163 if ($isFileType && !empty($file[$key]['name'][0])) {
164 return false;
165 }
166 }
167 return true;
168 }
169
170 private function checkRepeatedFieldEmptySubmission($isFileType, $data, $file = [])
171 {
172 if (!$isFileType) {
173 foreach ($data as $value) {
174 if (!empty($value)) {
175 return false;
176 }
177 }
178 }
179 if ($isFileType) {
180 foreach ($file['name'] as $value) {
181 if (!empty($value) && is_string($value)) {
182 return false;
183 }
184 if (is_array($value) && !empty($value[0])) {
185 return false;
186 }
187 }
188 }
189 return true;
190 }
191
192 private function getParams()
193 {
194 $url = wp_parse_url(wp_get_referer());
195 $parameter = [];
196 if (isset($url['query'])) {
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;
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 }
297 }
298
299 return $postData;
300 }
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
346 public function handleSubmission()
347 {
348 // CSRF verified via verifySubmissionNonce() before this method is called. All $_POST reads below occur after that verification.
349 $this->fieldNameReplaceOfPost();
350
351 $validated = $this->beforeSubmittedValidate();
352
353 $validated = apply_filters('bitform_filter_form_validation', $validated, $this->_form_id);
354
355 if (true === $validated) {
356 do_action('bitform_validation_success', $this->_form_id);
357 $this->discardHiddenFieldValues();
358
359 $redirectPage = '';
360 $regSuccMsg = '';
361
362 $existAuth = (new IntegrationHandler($this->_form_id))->getAllIntegration('wp_user_auth', 'wp_auth', 1);
363 $unslashed_post = wp_unslash($_POST);
364 if (!is_wp_error($existAuth) && count($existAuth) > 0) {
365 $parameter = $this->getParams();
366 $existAuthFilter = has_filter('bitform_wp_user_auth');
367
368 if (true === $existAuthFilter) {
369 $authPostData = $this->resolveConfirmChildValues($unslashed_post);
370 $result = apply_filters('bitform_wp_user_auth', $existAuth[0], $authPostData, $parameter);
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
376 if (isset($result['auth_type']) && 'register' === $result['auth_type']) {
377 if (!$result['success']) {
378 return new WP_Error('errors', self::authErrorMessage($result['message']));
379 } elseif (isset($result['success'])) {
380 $redirectPage = $result['redirectPage'];
381 $regSuccMsg = $result['message'];
382 }
383 } else {
384 if (!$result['success']) {
385 return new WP_Error('errors', self::authErrorMessage($result['message']));
386 } else {
387 return $result;
388 }
389 }
390 }
391 }
392
393 $saveResponse = $this->saveFormEntry($unslashed_post);
394 if (is_wp_error($saveResponse)) {
395 return $saveResponse;
396 }
397
398 $entryID = $saveResponse['entry_id'];
399
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);
404
405 $this->logRecaptchaV3Verification($saveResponse);
406 if (!empty($redirectPage) && empty($saveResponse['redirectPage']) || null === $saveResponse['redirectPage']) {
407 $saveResponse['redirectPage'] = $redirectPage;
408 }
409 if (!empty($regSuccMsg) && isset($saveResponse['dflt_message'])) {
410 $saveResponse['message'] = $regSuccMsg;
411 }
412 $saveResponse['new_nonce'] = wp_create_nonce('bitforms_' . $this->_form_id);
413
414 $saveResponse = IntegrationHandler::maybeSetCronForIntegration($saveResponse, 'create');
415 $entryId = $saveResponse['entry_id'];
416
417 $responseMsg = is_array($saveResponse) && !empty($saveResponse) ? $saveResponse : __('Form Submitted Successfully', 'bit-form');
418 $_POST = [];
419 $responseMsg['entry_id'] = $entryId;
420 return $responseMsg;
421 }
422 do_action('bitform_validation_error', $this->_form_id, $validated);
423 return $validated;
424 }
425
426 public function handleUpdateEntry()
427 {
428 // Entry token or capability verified by caller (FrontendAjax::update_entry). All $_POST reads occur after that check.
429 $this->fieldNameReplaceOfPost();
430 $validated = $this->beforeSubmittedValidate(true, true);
431 $validated = apply_filters('bitform_filter_form_validation', $validated, $this->_form_id);
432
433 $entryID = isset($_REQUEST['entryID']) ? sanitize_text_field(wp_unslash($_REQUEST['entryID'])) : null;
434 $GLOBALS['bitform_entry_id'] = $entryID;
435 if (is_null($entryID)) {
436 return new WP_Error('empty_form', __('Entries id is invalid', 'bit-form'));
437 }
438 if (true === $validated) {
439 do_action('bitform_validation_success', $this->_form_id);
440 $this->discardHiddenFieldValues();
441 unset($_POST['entryID']);
442
443 $redirectPage = '';
444 $regSuccMsg = '';
445 $postData = wp_unslash($_POST);
446
447 $existAuth = (new IntegrationHandler($this->_form_id))->getAllIntegration('wp_user_auth', 'wp_auth', 1);
448 if (!is_wp_error($existAuth) && count($existAuth) > 0) {
449 $parameter = $this->getParams();
450 $existAuthFilter = has_filter('bitform_wp_user_auth');
451
452 if (true === $existAuthFilter) {
453 $authPostData = $this->resolveConfirmChildValues($postData);
454 $result = apply_filters('bitform_wp_user_auth', $existAuth[0], $authPostData, $parameter);
455
456 if (isset($result['auth_type']) && 'register' === $result['auth_type']) {
457 if (!$result['success']) {
458 return new WP_Error('errors', self::authErrorMessage($result['message']));
459 } elseif (isset($result['success'])) {
460 $redirectPage = $result['redirectPage'];
461 $regSuccMsg = $result['message'];
462 }
463 } else {
464 if (!$result['success']) {
465 return new WP_Error('errors', self::authErrorMessage($result['message']));
466 } else {
467 return $result;
468 }
469 }
470 }
471 }
472
473 $updateResponse = $this->updateFormEntry(wp_unslash($_POST), $this->getFormID(), $entryID);
474 if (is_wp_error($updateResponse)) {
475 return $updateResponse;
476 }
477
478 // transformed dropdown value from string to array
479 $newPost = $this->transformDrpdwnValue($postData);
480 $filesData = GlobalHelper::sanitize_files_input($_FILES);
481
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);
487 if (!empty($redirectPage) && empty($updateResponse['redirectPage']) || null === $updateResponse['redirectPage']) {
488 $updateResponse['redirectPage'] = $redirectPage;
489 }
490 if (!empty($regSuccMsg) && isset($updateResponse['dflt_message'])) {
491 $updateResponse['message'] = $regSuccMsg;
492 }
493 $updateResponse['new_nonce'] = wp_create_nonce('bitforms_' . $this->_form_id);
494 $updateResponse = IntegrationHandler::maybeSetCronForIntegration($updateResponse, 'update');
495 $entryId = $updateResponse['entry_id'];
496
497 $responseMsg = is_array($updateResponse) && !empty($updateResponse) ? $updateResponse : __('Entry Update Successfully', 'bit-form');
498
499 $_POST = [];
500 $responseMsg['entry_id'] = $entryId;
501 return $responseMsg;
502 }
503 do_action('bitform_validation_error', $this->_form_id, $validated);
504 return $validated;
505 }
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
592 public function validateFormSubmission($submitted_data)
593 {
594 $hidden_fields = FrontendHelpers::parseHiddenFieldKeys(isset($submitted_data['hidden_fields']) ? $submitted_data['hidden_fields'] : '');
595 $submitted_fields = $this->getSubmittedFields($submitted_data);
596 $form_fields = $this->getFields();
597 $form_fields_names = array_keys($form_fields);
598 if ($this->isGCLIDEnabled()) {
599 array_push($form_fields_names, 'GCLID');
600 }
601 foreach ($submitted_fields as $field) {
602 if ('hidden_fields' !== $field && !in_array($field, $form_fields_names) || FrontendHelpers::isFieldHidden($hidden_fields, $field)) {
603 unset($submitted_data[$field]);
604 }
605 }
606 return $submitted_data;
607 }
608
609 public function beforeSubmittedValidate($verifyCaptcha = true, $isEntryEdit = false)
610 {
611 if ($this->verifySubmissionNonce()) {
612 if ($this->isExist()) {
613 $isRestricted = $this->checkSubmissionRestriction(true, $isEntryEdit);
614 if ($isRestricted && !empty($isRestricted)) {
615 return new WP_Error('spam_detection', $isRestricted[0]);
616 }
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) {
621 return new WP_Error('spam_detection', __('Token verification failed', 'bit-form'));
622 }
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;
629 }
630 $verifyHCaptchaResult = $this->verifyHCaptcha();
631 if (is_wp_error($verifyHCaptchaResult)) {
632 return $verifyHCaptchaResult;
633 }
634 /* Implement Turnstile Captcha start */
635 $verifyTurnstileCaptchaResult = $this->verifyTurnstileCaptcha();
636 if (is_wp_error($verifyTurnstileCaptchaResult)) {
637 return $verifyTurnstileCaptchaResult;
638 }
639 }
640 /* Implement Turnstile Captcha end */
641
642 $existAuth = (new IntegrationHandler($this->_form_id))->getAllIntegration('wp_user_auth', 'wp_auth', 1);
643
644 // check if user is already logged in and form has auth integration
645 do_action('bitform_checked_exist_auth', $this->_form_id, $existAuth);
646 if (!is_wp_error($existAuth) && count($existAuth) > 0 && is_user_logged_in()) {
647 return new WP_Error('auth_error', __('You are already logged in', 'bit-form'));
648 }
649 $validateForm = $this->validateFormSubmission($postData);
650 $validateFormFiles = $this->validateFormSubmission($filesData);
651 $validateForm = array_merge($validateForm, $validateFormFiles);
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();
658 // check if form-current-step is set and form is multi-step
659 $formCurrentStep = isset($_POST['form-current-step']) ? sanitize_text_field(wp_unslash($_POST['form-current-step'])) : null;
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).
666 $formContents = $this->getFormContent();
667 $layout = isset($formContents->layout) ? $formContents->layout : null;
668 $stepIndex = (int) $formCurrentStep - 1;
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;
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 }
697 }
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 }
710 }
711 }
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);
717 $validUniuqFields = [];
718 $existFilter = has_filter('bitform_check_duplicate_entry');
719 if (true === $existFilter) {
720 $validUniuqFields = apply_filters('bitform_check_duplicate_entry', $form_fields, $postData);
721
722 $fieldKeys = array_keys($validUniuqFields);
723 $form_fields_keys = array_keys($form_fields);
724 $uniqueFields = [];
725 foreach ($fieldKeys as $key) {
726 if (in_array($key, $form_fields_keys)) {
727 $uniqueFields[] = $form_fields[$key];
728 }
729 }
730 do_action('bitform_Unique_entry', $uniqueFields, $validUniuqFields, $this->_form_id, $postData);
731 }
732 $validateField = $formFieldValidator->validate('create', $this->_form_id);
733
734 if ($validateForm && $validateField && 0 === count($validUniuqFields)) {
735 return true;
736 } else {
737 $error = __('Please submit form with valid fields', 'bit-form');
738 if (!$validateForm) {
739 $errorMessages = $error;
740 } elseif (count($formFieldValidator->getMessage()) > 0) {
741 $errorMessages = $formFieldValidator->getMessage();
742 } else {
743 $errorMessages = 0 === count($validUniuqFields) ? $error : $validUniuqFields;
744 }
745 return new WP_Error('validation_error', $errorMessages);
746 }
747 }
748 return new WP_Error('unknown_form', __('Form does not exist', 'bit-form'));
749 } else {
750 return new WP_Error('token_expired', __('Token expired', 'bit-form'));
751 }
752 }
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
984 public function verifySubmissionNonce()
985 {
986 if (!isset($_POST['t_identity']) || !isset($_POST['csrf'])) {
987 return false;
988 }
989 $tIdenty = sanitize_text_field(wp_unslash($_POST['t_identity']));
990 $csrf = sanitize_text_field(wp_unslash($_POST['csrf']));
991 unset($_POST['t_identity'], $_POST['action'], $_POST['bitforms_id'], $_POST['csrf']);
992 return Helpers::csrfDecrypted($tIdenty, $csrf);
993 }
994
995 public function setViewCount()
996 {
997 if (!current_user_can('manage_options')) {
998 $update_status = $this->formModel->update(
999 [
1000 'views' => intval($this->form[0]->views) + 1
1001 ],
1002 [
1003 'id' => $this->form_id
1004 ]
1005 );
1006 }
1007 }
1008
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)
1014 {
1015 $formContents = $this->getFormContent();
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)) {
1021 return false;
1022 }
1023
1024 $restrictionMessage = [];
1025 $ipTool = new IpTool();
1026 $ipAddress = $ipTool->getIP();
1027 $currentUserId = get_current_user_id();
1028
1029 foreach ($fromRestrictionSetitingsEnabled as $restrictionKey => $isEnabled) {
1030 if ($isEnabled) {
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
1039 );
1040 if ($skipOnEdit) {
1041 continue;
1042 }
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
1066 if ('onePerIp' === $restrictionKey) {
1067 if ($this->hasCompletedEntryFromIp($ipAddress)) {
1068 $onePerIp = __('Sorry!! You have already submitted from this IP address', 'bit-form');
1069
1070 $onePerIp = apply_filters(
1071 'bitform_filter_restriction_one_per_ip_message',
1072 $onePerIp,
1073 $this->form_id
1074 );
1075
1076 $restrictionMessage[] = $onePerIp;
1077 }
1078 }
1079 if ('is_login' === $restrictionKey && 0 === get_current_user_id()) {
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;
1089 }
1090 if ($checkedEmptySubmitted && 'empty_submission' === $restrictionKey) {
1091 $isEmpty = $this->checkEmptySubmission(wp_unslash($_POST), GlobalHelper::sanitize_files_input($_FILES), $isEntryEdit);
1092 if ($isEmpty) {
1093 $restriction = $fromRestrictionSetitings->empty_submission->message;
1094
1095 $restriction = apply_filters(
1096 'bitform_filter_restriction_empty_submission_message',
1097 $restriction,
1098 $this->form_id
1099 );
1100
1101 $restrictionMessage[] = $restriction;
1102 }
1103 }
1104 }
1105 }
1106 return $restrictionMessage;
1107 }
1108
1109 /**
1110 * Will check if form is submitted by a bot
1111 *
1112 * @return Boolean true - if submitted by bot else false
1113 */
1114 public function isTrappedInHoneypot()
1115 {
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));
1118 }
1119
1120 public function isHoneypotActive()
1121 {
1122 return (bool) apply_filters('bitform_is_honeypot_active', false, $this->_form_id, $this->getFormContent());
1123 }
1124
1125 public function checkPaymentFields()
1126 {
1127 $formContents = $this->getFormContent();
1128 $fields = $formContents->fields;
1129
1130 $payments = [];
1131 foreach ($fields as $fldData) {
1132 if (!is_object($fldData)) {
1133 continue;
1134 }
1135 if ('paypal' === $fldData->typ && property_exists($fldData, 'payIntegID')) {
1136 $payments['paypalKey'] = $this->getClientKey($fldData->payIntegID, 'clientID');
1137 } elseif ('razorpay' === $fldData->typ && isset($fldData->options) && is_object($fldData->options) && property_exists($fldData->options, 'payIntegID')) {
1138 $payments['razorpayKey'] = $this->getClientKey($fldData->options->payIntegID, 'apiKey');
1139 }
1140 }
1141
1142 return $payments;
1143 }
1144
1145 private function getClientKey($integID, $keyName)
1146 {
1147 $client = '';
1148 if (!empty($integID)) {
1149 $integrationHandler = new IntegrationHandler(0);
1150 $integration = $integrationHandler->getAIntegration($integID, 'app', 'payments');
1151 if (!is_wp_error($integration)) {
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 }
1157 }
1158 }
1159 return $client;
1160 }
1161
1162 public function getSuccessMessageMarkups()
1163 {
1164 if (is_null($this->_conf_messages)) {
1165 $successMsgHandler = new SuccessMessageHandler($this->form_id);
1166 $this->_conf_messages = $successMsgHandler->getAllMessage();
1167 }
1168
1169 $messageMarkups = '';
1170 if (is_wp_error($this->_conf_messages)) {
1171 return $messageMarkups;
1172 }
1173
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 }
1179 $messageMarkups .= $this->messageMarkup($msgItem);
1180 }
1181
1182 return $messageMarkups;
1183 }
1184
1185 public function getFormAbandonmentMessage()
1186 {
1187 $msg = apply_filters('bitform_form_abandonment_warning_markup', '', $this->form_id);
1188 return is_string($msg) ? $msg : '';
1189 }
1190
1191 public function getFormAbandonmentSettings()
1192 {
1193 return apply_filters('bitform_form_abandonment_settings', null, $this->form_id);
1194 }
1195
1196 private function messageMarkup($msg)
1197 {
1198 $msgId = $msg->id;
1199 $msgConfig = json_decode($msg->message_config);
1200 $msgType = (is_object($msgConfig) && isset($msgConfig->msgType)) ? $msgConfig->msgType : 'below';
1201 $scrollClass = 'below' === $msgType ? 'scroll' : '';
1202
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>';
1228 }
1229 }
1230