PluginProbe
Bit Form – Contact Form, Payment Forms, Multi Step Forms, Calculator & Custom Form Builder / 3.2.1
Bit Form – Contact Form, Payment Forms, Multi Step Forms, Calculator & Custom Form Builder v3.2.1
V-3.3.0 3.2.2 3.2.1 3.2.0 3.1.4 3.1.3 3.1.2 3.1.1 3.1.0 V3.0.3 V3.0.2 -3.0.1 V_3.0.0 1.1.1 1.1.8 1.2 1.3 1.4 1.4.18 1.5.2 1.9 2.0 2.10.0 2.10.1 2.10.2 All 137 releases
bit-form / includes / Frontend / Form / FrontendFormManager.php

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

988 lines 38.1 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\HttpHelper;
22 use BitCode\BitForm\Core\Util\IpTool;
23 use BitCode\BitForm\Core\Util\Utilities;
24 use BitCode\BitForm\Core\WorkFlow\WorkFlow;
25 use BitCode\BitForm\Frontend\Form\View\FormViewer;
26 use BitCode\BitForm\GlobalHelper;
27 use WP_Error;
28
29 final class FrontendFormManager extends FormManager
30 {
31 private $_form_identifier;
32 private $_form_token;
33 private $_form_id;
34 private $_conf_messages;
35 private static $_instance = [];
36
37 // private $_has_upload = false;
38 public function __construct($form_id, $shortCodeCounter = null)
39 {
40 parent::__construct($form_id);
41 $this->_form_identifier = 'bitforms_' . $form_id;
42 $this->_form_identifier .= !empty(get_post()->ID) ? '_' . get_post()->ID : '';
43 $this->_form_identifier .= !empty($shortCodeCounter) ? "_$shortCodeCounter" : '';
44 $this->_form_token = wp_create_nonce('bitforms_' . $form_id);
45 $this->_form_id = $form_id;
46 }
47
48 public static function getInstance($form_id, $shortCodeCounter = null)
49 {
50 $key = $form_id . ':' . ($shortCodeCounter ?? 'default');
51
52 if (!isset(self::$_instance[$key])) {
53 self::$_instance[$key] = new self($form_id, $shortCodeCounter);
54 }
55
56 return self::$_instance[$key];
57 }
58
59 public function getFormIdentifier()
60 {
61 return $this->_form_identifier;
62 }
63
64 public function getFormID()
65 {
66 return $this->_form_id;
67 }
68
69 public function getFormToken()
70 {
71 return $this->_form_token;
72 }
73
74 public function getSubmittedFields($submitted_data)
75 {
76 unset($submitted_data[$this->_form_identifier]);
77 // unset($submitted_data['bit-form-submit-btn']);
78 return array_keys($submitted_data);
79 }
80
81 public function formView($fields = null, $hasFile = false, $errorMessages = null, $previousValue = null)
82 {
83 $formContents = $this->getFormContent();
84 $formAtomicClsMap = $this->getAtomicClsMap();
85 if (!empty($fields)) {
86 $formContents->fields = is_string($fields) ? json_decode($fields) : $fields;
87 } else {
88 $workFlowRunHelper = new WorkFlow($this->form_id);
89 $workFlowreturnedOnLoad = $workFlowRunHelper->executeOnLoad(
90 'create',
91 $formContents->fields
92 );
93 $formContents->fields = empty($workFlowreturnedOnLoad['fields']) ? $formContents->fields : $workFlowreturnedOnLoad['fields'];
94 }
95 $formViewer = new FormViewer($this, $formContents, $formAtomicClsMap, $errorMessages, $previousValue);
96 $isRestricted = $this->checkSubmissionRestriction(false);
97 $msg = !empty($isRestricted) ? $isRestricted[0] : '';
98 return $formViewer->getView($hasFile, $msg);
99 }
100
101 public function conversationalFormView($fields = null, $hasFile = false, $errorMessages = null, $previousValue = null)
102 {
103 $formContents = $this->getFormContent();
104 $formAtomicClsMap = $this->getAtomicClsMap();
105 if (!empty($fields)) {
106 $formContents->fields = is_string($fields) ? json_decode($fields) : $fields;
107 } else {
108 $workFlowRunHelper = new WorkFlow($this->form_id);
109 $workFlowreturnedOnLoad = $workFlowRunHelper->executeOnLoad(
110 'create',
111 $formContents->fields
112 );
113 $formContents->fields = empty($workFlowreturnedOnLoad['fields']) ? $formContents->fields : $workFlowreturnedOnLoad['fields'];
114 }
115 $formViewer = new FormViewer($this, $formContents, $formAtomicClsMap, $errorMessages, $previousValue);
116 $isRestricted = $this->checkSubmissionRestriction(false);
117 $msg = !empty($isRestricted) ? $isRestricted[0] : '';
118 return $formViewer->getConversationalView($hasFile, $msg);
119 }
120
121 public function checkEmptySubmission($data, $file)
122 {
123 $formFields = $this->getFields();
124 foreach ($formFields as $key => $field) {
125 $fieldType = $field['type'];
126 if ('button' === $fieldType) {
127 continue;
128 }
129 $fileUploadFieldTypes = ['file-up', 'advanced-file-up'];
130 if ('decision-box' === $fieldType || 'gdpr' === $fieldType) {
131 continue;
132 }
133 $isFileType = in_array($fieldType, $fileUploadFieldTypes);
134 if ($this->isRepeatedField($key)) {
135 $fileData = !empty($file[$key]) ? $file[$key] : [];
136 $dataVal = !empty($data[$key]) ? $data[$key] : [];
137 if (!$this->checkRepeatedFieldEmptySubmission($isFileType, $dataVal, $fileData)) {
138 return false;
139 }
140 continue;
141 }
142 if (!$isFileType && (!empty($data[$key]) || (isset($data[$key]) && is_numeric($data[$key])))) {
143 return false;
144 }
145 if ($isFileType && !empty($file[$key]['name']) && is_string($file[$key]['name'])) {
146 return false;
147 }
148 if ($isFileType && !empty($file[$key]['name'][0])) {
149 return false;
150 }
151 }
152 return true;
153 }
154
155 private function checkRepeatedFieldEmptySubmission($isFileType, $data, $file = [])
156 {
157 if (!$isFileType) {
158 foreach ($data as $value) {
159 if (!empty($value)) {
160 return false;
161 }
162 }
163 }
164 if ($isFileType) {
165 foreach ($file['name'] as $value) {
166 if (!empty($value) && is_string($value)) {
167 return false;
168 }
169 if (is_array($value) && !empty($value[0])) {
170 return false;
171 }
172 }
173 }
174 return true;
175 }
176
177 private function getParams()
178 {
179 $url = wp_parse_url(wp_get_referer());
180 $parameter = [];
181 if (isset($url['query'])) {
182 $queries = explode('&', $url['query']);
183 foreach ($queries as $query) {
184 list($field, $value) = explode('=', $query);
185 $parameter[$field] = $value;
186 }
187 }
188 return $parameter;
189 }
190
191 private function getFormFields($formID)
192 {
193 $adminFormHandler = new AdminFormHandler();
194 $post = new \stdClass();
195 $post = (object) [
196 'id' => $formID
197 ];
198 $getForm = $adminFormHandler->getAForm('', $post);
199 $formContainer = $getForm['form_content'];
200
201 return $formContainer['fields'];
202 }
203
204 private function transformDrpdwnValue($post)
205 {
206 $formFields = $this->getFormFields($this->_form_id);
207
208 foreach ($post as $key => $value) {
209 if (!str_starts_with($key, 'repeater') && isset($formFields->{$key}) && 'select' === $formFields->{$key}->typ) {
210 if (is_array($value)) {
211 foreach ($value as $k => $v) {
212 $post[$key][$k] = !is_array($v) && is_string($v) ? explode(BITFORMS_BF_SEPARATOR, $v) : $v;
213 }
214 } else {
215 $post[$key] = explode(BITFORMS_BF_SEPARATOR, $value);
216 }
217 };
218 }
219
220 return $post;
221 }
222
223 public function handleSubmission()
224 {
225 // CSRF verified via verifySubmissionNonce() before this method is called. All $_POST reads below occur after that verification.
226 $this->fieldNameReplaceOfPost();
227
228 $validated = $this->beforeSubmittedValidate();
229
230 $validated = apply_filters('bitform_filter_form_validation', $validated, $this->_form_id);
231
232 if (true === $validated) {
233 do_action('bitform_validation_success', $this->_form_id);
234 unset($_POST['hidden_fields']);
235
236 $redirectPage = '';
237 $regSuccMsg = '';
238
239 $existAuth = (new IntegrationHandler($this->_form_id))->getAllIntegration('wp_user_auth', 'wp_auth', 1);
240 $unslashed_post = wp_unslash($_POST);
241 if (!is_wp_error($existAuth) && count($existAuth) > 0) {
242 $parameter = $this->getParams();
243 $existAuthFilter = has_filter('bitform_wp_user_auth');
244
245 if (true === $existAuthFilter) {
246 $result = apply_filters('bitform_wp_user_auth', $existAuth[0], $unslashed_post, $parameter);
247
248 $result = apply_filters('bitform_filter_wp_user_auth_response', $result, $this->_form_id, $unslashed_post, $parameter);
249
250 do_action('bitform_wp_user_auth_response', $result, $this->_form_id, $unslashed_post, $parameter);
251
252 if (isset($result['auth_type']) && 'register' === $result['auth_type']) {
253 if (!$result['success']) {
254 return new WP_Error('errors', esc_html($result['message']));
255 } elseif (isset($result['success'])) {
256 $redirectPage = $result['redirectPage'];
257 $regSuccMsg = $result['message'];
258 }
259 } else {
260 if (!$result['success']) {
261 return new WP_Error('errors', esc_html($result['message']));
262 } else {
263 return $result;
264 }
265 }
266 }
267 }
268
269 $saveResponse = $this->saveFormEntry($unslashed_post);
270 if (is_wp_error($saveResponse)) {
271 return $saveResponse;
272 }
273
274 $entryID = $saveResponse['entry_id'];
275
276 // transformed dropdown value from string to array
277 $newPost = $this->transformDrpdwnValue($unslashed_post);
278 $filesData = GlobalHelper::sanitize_files_input($_FILES);
279 do_action('bitform_submit_success', $this->_form_id, $entryID, $newPost, $filesData);
280
281 $captchaV3Settings = $this->getCaptchaV3Settings();
282 if ($captchaV3Settings) {
283 $token = isset($_POST['g-recaptcha-response']) ? sanitize_text_field(wp_unslash($_POST['g-recaptcha-response'])) : '';
284 $integrationHandler = new IntegrationHandler(0);
285 $allFormIntegrations = $integrationHandler->getAllIntegration('app', 'gReCaptchaV3');
286 if (!is_wp_error($allFormIntegrations)) {
287 foreach ($allFormIntegrations as $integration) {
288 if (!is_null($integration->integration_type) && 'gReCaptchaV3' === $integration->integration_type) {
289 $integrationDetails = Utilities::jsonObj($integration->integration_details);
290 if ($integrationDetails) {
291 $integrationDetails->id = $integration->id;
292 $reCAPTCHA = $integrationDetails;
293 }
294 }
295 }
296 }
297 if (!empty($reCAPTCHA->secretKey)) {
298 $gRecaptchaResponse = HttpHelper::post(
299 'https://www.google.com/recaptcha/api/siteverify',
300 ['secret' => $reCAPTCHA->secretKey, 'response' => $token]
301 );
302 if ($captchaV3Settings && !empty($saveResponse['triggerData'])) {
303 $logID = $saveResponse['triggerData']['logID'];
304 $integId = $reCAPTCHA->id;
305 $saveApiResponse = new UtilApiResponse();
306 $saveApiResponse->apiResponse($logID, $integId, ['type_name' => 'ReCaptcha', 'type' => 'v3'], 'success', $gRecaptchaResponse);
307 }
308 }
309 unset($_POST['g-recaptcha-response']);
310 }
311 if (!empty($redirectPage) && empty($saveResponse['redirectPage']) || null === $saveResponse['redirectPage']) {
312 $saveResponse['redirectPage'] = $redirectPage;
313 }
314 if (!empty($regSuccMsg) && isset($saveResponse['dflt_message'])) {
315 $saveResponse['message'] = $regSuccMsg;
316 }
317 $saveResponse['new_nonce'] = wp_create_nonce('bitforms_' . $this->_form_id);
318
319 $saveResponse = IntegrationHandler::maybeSetCronForIntegration($saveResponse, 'create');
320 $entryId = $saveResponse['entry_id'];
321
322 $responseMsg = is_array($saveResponse) && !empty($saveResponse) ? $saveResponse : __('Form Submitted Successfully', 'bit-form');
323 $_POST = [];
324 $responseMsg['entry_id'] = $entryId;
325 return $responseMsg;
326 }
327 do_action('bitform_validation_error', $this->_form_id, $validated);
328 return $validated;
329 }
330
331 public function handleUpdateEntry()
332 {
333 // Entry token or capability verified by caller (FrontendAjax::update_entry). All $_POST reads occur after that check.
334 $this->fieldNameReplaceOfPost();
335 $validated = $this->beforeSubmittedValidate();
336 $validated = apply_filters('bitform_filter_form_validation', $validated, $this->_form_id);
337
338 $entryID = isset($_REQUEST['entryID']) ? sanitize_text_field(wp_unslash($_REQUEST['entryID'])) : null;
339 $GLOBALS['bitform_entry_id'] = $entryID;
340 if (is_null($entryID)) {
341 return new WP_Error('empty_form', __('Entries id is invalid', 'bit-form'));
342 }
343 if (true === $validated) {
344 do_action('bitform_validation_success', $this->_form_id);
345 unset($_POST['hidden_fields'], $_POST['entryID']);
346
347 $redirectPage = '';
348 $regSuccMsg = '';
349 $postData = wp_unslash($_POST);
350
351 $existAuth = (new IntegrationHandler($this->_form_id))->getAllIntegration('wp_user_auth', 'wp_auth', 1);
352 if (!is_wp_error($existAuth) && count($existAuth) > 0) {
353 $parameter = $this->getParams();
354 $existAuthFilter = has_filter('bitform_wp_user_auth');
355
356 if (true === $existAuthFilter) {
357 $result = apply_filters('bitform_wp_user_auth', $existAuth[0], $postData, $parameter);
358
359 if (isset($result['auth_type']) && 'register' === $result['auth_type']) {
360 if (!$result['success']) {
361 return new WP_Error('errors', esc_html($result['message']));
362 } elseif (isset($result['success'])) {
363 $redirectPage = $result['redirectPage'];
364 $regSuccMsg = $result['message'];
365 }
366 } else {
367 if (!$result['success']) {
368 return new WP_Error('errors', esc_html($result['message']));
369 } else {
370 return $result;
371 }
372 }
373 }
374 }
375
376 $updateResponse = $this->updateFormEntry(wp_unslash($_POST), $this->getFormID(), $entryID);
377 if (is_wp_error($updateResponse)) {
378 return $updateResponse;
379 }
380
381 // transformed dropdown value from string to array
382 $newPost = $this->transformDrpdwnValue($postData);
383 $filesData = GlobalHelper::sanitize_files_input($_FILES);
384
385 //TO DO:: submit success action temporarily added for solution of a issue
386 do_action('bitform_submit_success', $this->_form_id, $entryID, $newPost, $filesData);
387 do_action('bitform_update_success', $this->_form_id, $entryID, $newPost, $filesData);
388
389 $captchaV3Settings = $this->getCaptchaV3Settings();
390 if ($captchaV3Settings) {
391 $token = isset($_POST['g-recaptcha-response']) ? sanitize_text_field(wp_unslash($_POST['g-recaptcha-response'])) : '';
392 $integrationHandler = new IntegrationHandler(0);
393 $allFormIntegrations = $integrationHandler->getAllIntegration('app', 'gReCaptchaV3');
394 if (!is_wp_error($allFormIntegrations)) {
395 foreach ($allFormIntegrations as $integration) {
396 if (!is_null($integration->integration_type) && 'gReCaptchaV3' === $integration->integration_type) {
397 $integrationDetails = Utilities::jsonObj($integration->integration_details);
398 if ($integrationDetails) {
399 $integrationDetails->id = $integration->id;
400 $reCAPTCHA = $integrationDetails;
401 }
402 }
403 }
404 }
405 if (!empty($reCAPTCHA->secretKey)) {
406 $gRecaptchaResponse = HttpHelper::post(
407 'https://www.google.com/recaptcha/api/siteverify',
408 ['secret' => $reCAPTCHA->secretKey, 'response' => $token]
409 );
410 if ($captchaV3Settings && !empty($updateResponse['triggerData'])) {
411 $logID = $updateResponse['triggerData']['logID'];
412 $integId = $reCAPTCHA->id;
413 $saveApiResponse = new UtilApiResponse();
414 $saveApiResponse->apiResponse($logID, $integId, ['type_name' => 'ReCaptcha', 'type' => 'v3'], 'success', $gRecaptchaResponse);
415 }
416 }
417 unset($_POST['g-recaptcha-response']);
418 }
419 if (!empty($redirectPage) && empty($updateResponse['redirectPage']) || null === $updateResponse['redirectPage']) {
420 $updateResponse['redirectPage'] = $redirectPage;
421 }
422 if (!empty($regSuccMsg) && isset($updateResponse['dflt_message'])) {
423 $updateResponse['message'] = $regSuccMsg;
424 }
425 $updateResponse['new_nonce'] = wp_create_nonce('bitforms_' . $this->_form_id);
426 $updateResponse = IntegrationHandler::maybeSetCronForIntegration($updateResponse, 'update');
427 $entryId = $updateResponse['entry_id'];
428
429 $responseMsg = is_array($updateResponse) && !empty($updateResponse) ? $updateResponse : __('Entry Update Successfully', 'bit-form');
430
431 $_POST = [];
432 $responseMsg['entry_id'] = $entryId;
433 return $responseMsg;
434 }
435 do_action('bitform_validation_error', $this->_form_id, $validated);
436 return $validated;
437 }
438
439 public function validateFormSubmission($submitted_data)
440 {
441 $hidden_fields = isset($submitted_data['hidden_fields']) ? $submitted_data['hidden_fields'] : '';
442 $submitted_fields = $this->getSubmittedFields($submitted_data);
443 $form_fields = $this->getFields();
444 $form_fields_names = array_keys($form_fields);
445 if ($this->isGCLIDEnabled()) {
446 array_push($form_fields_names, 'GCLID');
447 }
448 foreach ($submitted_fields as $field) {
449 if ('hidden_fields' !== $field && !in_array($field, $form_fields_names) || false !== strpos($hidden_fields, $field)) {
450 unset($submitted_data[$field]);
451 }
452 }
453 return $submitted_data;
454 }
455
456 public function beforeSubmittedValidate($verifyCaptcha = true)
457 {
458 if ($this->verifySubmissionNonce()) {
459 if ($this->isExist()) {
460 $isRestricted = $this->checkSubmissionRestriction();
461 if ($isRestricted && !empty($isRestricted)) {
462 return new WP_Error('spam_detection', $isRestricted[0]);
463 }
464 $postData = wp_unslash($_POST);
465 $filesData = GlobalHelper::sanitize_files_input($_FILES);
466 $isHoneypot = apply_filters('bitform_check_honeypot', false, $this->_form_id, $postData);
467 if ($isHoneypot) {
468 return new WP_Error('spam_detection', __('Token verification failed', 'bit-form'));
469 }
470 $formCurrentStep = isset($_POST['form-current-step']) ? sanitize_text_field(wp_unslash($_POST['form-current-step'])) : null;
471 // TODO: Temporary parameter to skip captcha verification in step change of multi step form
472 if ($verifyCaptcha) {
473 $verifyGRecaptchaResult = $this->verifyGRecaptcha();
474 if (is_wp_error($verifyGRecaptchaResult)) {
475 return $verifyGRecaptchaResult;
476 }
477 $verifyHCaptchaResult = $this->verifyHCaptcha();
478 if (is_wp_error($verifyHCaptchaResult)) {
479 return $verifyHCaptchaResult;
480 }
481 /* Implement Turnstile Captcha start */
482 $verifyTurnstileCaptchaResult = $this->verifyTurnstileCaptcha();
483 if (is_wp_error($verifyTurnstileCaptchaResult)) {
484 return $verifyTurnstileCaptchaResult;
485 }
486 }
487 /* Implement Turnstile Captcha end */
488
489 $existAuth = (new IntegrationHandler($this->_form_id))->getAllIntegration('wp_user_auth', 'wp_auth', 1);
490
491 // check if user is already logged in and form has auth integration
492 do_action('bitform_checked_exist_auth', $this->_form_id, $existAuth);
493 if (!is_wp_error($existAuth) && count($existAuth) > 0 && is_user_logged_in()) {
494 return new WP_Error('auth_error', __('You are already logged in', 'bit-form'));
495 }
496 $validateForm = $this->validateFormSubmission($postData);
497 $validateFormFiles = $this->validateFormSubmission($filesData);
498 $validateForm = array_merge($validateForm, $validateFormFiles);
499 // Validate only provably-rendered fields: a field stranded in form_content->fields
500 // with no layout entry (orphan) is never shown to the user and must not block
501 // submission. getRenderedFields() unions ALL breakpoints × steps × nested layouts
502 // + childFields of rendered parents, derives only from DB-stored form_content,
503 // and fails closed (returns all fields) when the layout is unusable.
504 $form_fields = $this->getRenderedFields();
505 // check if form-current-step is set and form is multi-step
506 $formCurrentStep = isset($_POST['form-current-step']) ? sanitize_text_field(wp_unslash($_POST['form-current-step'])) : null;
507 if (!is_null($formCurrentStep)) {
508 // Narrow validation to the current step's fields. SECURITY: the step
509 // key set unions ALL breakpoints (lg/md/sm) — an md/sm-only field was
510 // previously null-skipped by the validator (silent bypass). A forged
511 // step index or malformed layout skips the narrowing entirely so every
512 // rendered field stays validated (fail closed).
513 $formContents = $this->getFormContent();
514 $layout = isset($formContents->layout) ? $formContents->layout : null;
515 $stepIndex = (int) $formCurrentStep - 1;
516 if (is_array($layout) && isset($layout[$stepIndex]->layout) && is_object($layout[$stepIndex]->layout)) {
517 $stepLayout = $layout[$stepIndex]->layout;
518 $nestedLayout = isset($formContents->nestedLayout) && is_object($formContents->nestedLayout)
519 ? $formContents->nestedLayout : null;
520 $stepKeys = [];
521 foreach (['lg', 'md', 'sm'] as $brkpnt) {
522 if (!isset($stepLayout->{$brkpnt}) || !is_array($stepLayout->{$brkpnt})) {
523 continue;
524 }
525 foreach ($stepLayout->{$brkpnt} as $lay) {
526 if (!is_object($lay) || !isset($lay->i)) {
527 continue;
528 }
529 $fk = $lay->i;
530 $stepKeys[$fk] = true;
531 if (!is_null($nestedLayout) && isset($nestedLayout->{$fk})) {
532 foreach (['lg', 'md', 'sm'] as $nBrkpnt) {
533 if (!isset($nestedLayout->{$fk}->{$nBrkpnt}) || !is_array($nestedLayout->{$fk}->{$nBrkpnt})) {
534 continue;
535 }
536 foreach ($nestedLayout->{$fk}->{$nBrkpnt} as $nestedLay) {
537 if (is_object($nestedLay) && isset($nestedLay->i)) {
538 $stepKeys[$nestedLay->i] = true;
539 }
540 }
541 }
542 }
543 }
544 }
545 // Name/Address/Email/Password children live outside layouts; a child
546 // is part of this step iff its parent is.
547 self::expandChildFieldKeys($stepKeys, $form_fields);
548 if (!empty($stepKeys)) {
549 $step_fields = [];
550 foreach (array_keys($stepKeys) as $fk) {
551 if (isset($form_fields[$fk])) {
552 $step_fields[$fk] = $form_fields[$fk];
553 }
554 }
555 $form_fields = $step_fields;
556 }
557 }
558 }
559 $formFieldValidator = new FormFieldValidator($form_fields, $postData, $filesData);
560 $validUniuqFields = [];
561 $existFilter = has_filter('bitform_check_duplicate_entry');
562 if (true === $existFilter) {
563 $validUniuqFields = apply_filters('bitform_check_duplicate_entry', $form_fields, $postData);
564
565 $fieldKeys = array_keys($validUniuqFields);
566 $form_fields_keys = array_keys($form_fields);
567 $uniqueFields = [];
568 foreach ($fieldKeys as $key) {
569 if (in_array($key, $form_fields_keys)) {
570 $uniqueFields[] = $form_fields[$key];
571 }
572 }
573 do_action('bitform_Unique_entry', $uniqueFields, $validUniuqFields, $this->_form_id, $postData);
574 }
575 $validateField = $formFieldValidator->validate('create', $this->_form_id);
576
577 if ($validateForm && $validateField && 0 === count($validUniuqFields)) {
578 return true;
579 } else {
580 $error = __('Please submit form with valid fields', 'bit-form');
581 if (!$validateForm) {
582 $errorMessages = $error;
583 } elseif (count($formFieldValidator->getMessage()) > 0) {
584 $errorMessages = $formFieldValidator->getMessage();
585 } else {
586 $errorMessages = 0 === count($validUniuqFields) ? $error : $validUniuqFields;
587 }
588 return new WP_Error('validation_error', $errorMessages);
589 }
590 }
591 return new WP_Error('unknown_form', __('Form does not exist', 'bit-form'));
592 } else {
593 return new WP_Error('token_expired', __('Token expired', 'bit-form'));
594 }
595 }
596
597 private function verifyGRecaptcha()
598 {
599 $captchaSettings = $this->getCaptchaSettings();
600 $captchaV3Settings = $this->getCaptchaV3Settings();
601 if ($captchaSettings || $captchaV3Settings) {
602 $token = isset($_POST['g-recaptcha-response']) ? sanitize_text_field(wp_unslash($_POST['g-recaptcha-response'])) : '';
603 if (!isset($_POST['g-recaptcha-response'])) {
604 return new WP_Error('spam_detection', __('Please recheck your reCaptcha Configuration', 'bit-form'));
605 }
606 $integrationHandler = new IntegrationHandler(0);
607 $allFormIntegrations = $integrationHandler->getAllIntegration('app', $captchaSettings ? 'gReCaptcha' : 'gReCaptchaV3');
608 if (!is_wp_error($allFormIntegrations)) {
609 foreach ($allFormIntegrations as $integration) {
610 if (!is_null($integration->integration_type) && $integration->integration_type === ($captchaSettings ? 'gReCaptcha' : 'gReCaptchaV3')) {
611 $integrationDetails = Utilities::jsonObj($integration->integration_details);
612 if ($integrationDetails) {
613 $integrationDetails->id = $integration->id;
614 $reCAPTCHA = $integrationDetails;
615 }
616 }
617 }
618 }
619 if (!empty($reCAPTCHA->secretKey)) {
620 $gRecaptchaResponse = HttpHelper::post(
621 'https://www.google.com/recaptcha/api/siteverify',
622 ['secret' => $reCAPTCHA->secretKey, 'response' => $token]
623 );
624 $isgReCaptchaVerified = false;
625 if (!is_wp_error($gRecaptchaResponse)) {
626 if (
627 $captchaV3Settings
628 && !empty($gRecaptchaResponse->score)
629 && ((float) $gRecaptchaResponse->score < (float) $captchaV3Settings->score)
630 ) {
631 wp_send_json_error(
632 sanitize_text_field((string) $captchaV3Settings->message)
633 );
634 }
635
636 $isgReCaptchaVerified = $gRecaptchaResponse->success;
637 }
638 if (!$isgReCaptchaVerified) {
639 return new WP_Error('spam_detection', __('Please verify reCAPTCHA', 'bit-form'));
640 }
641 }
642 }
643 }
644
645 private function verifyHCaptcha()
646 {
647 $hCaptchaExist = $this->isFieldTypeExist('hcaptcha'); // You can rename this to getHCaptchaSettings() if needed
648 if ($hCaptchaExist) {
649 if (!isset($_POST['h-captcha-response'])) {
650 return new WP_Error('spam_detection', __('Please verify hCaptcha', 'bit-form'));
651 }
652
653 $token = sanitize_text_field(wp_unslash($_POST['h-captcha-response']));
654
655 $integrationHandler = new IntegrationHandler(0);
656 $allFormIntegrations = $integrationHandler->getAllIntegration('app', 'hcaptcha');
657
658 if (!is_wp_error($allFormIntegrations)) {
659 foreach ($allFormIntegrations as $integration) {
660 if (!is_null($integration->integration_type) && 'hcaptcha' === $integration->integration_type) {
661 $integrationDetails = Utilities::jsonObj($integration->integration_details);
662 if ($integrationDetails) {
663 $integrationDetails->id = $integration->id;
664 $hCaptcha = $integrationDetails;
665 }
666 }
667 }
668 }
669
670 if (!empty($hCaptcha->secretKey)) {
671 $hCaptchaResponse = HttpHelper::post(
672 'https://api.hcaptcha.com/siteverify',
673 [
674 'secret' => $hCaptcha->secretKey,
675 'response' => $token,
676 'remoteip' => (isset($_SERVER['REMOTE_ADDR']) ? sanitize_text_field(wp_unslash($_SERVER['REMOTE_ADDR'])) : '')
677 ]
678 );
679
680 $isVerified = false;
681 if (!is_wp_error($hCaptchaResponse)) {
682 $isVerified = $hCaptchaResponse->success;
683 }
684
685 if (!$isVerified) {
686 return new WP_Error('spam_detection', __('hCaptcha verification failed', 'bit-form'));
687 }
688 }
689 }
690 }
691
692 private function verifyTurnstileCaptcha()
693 {
694 $turnstileExist = $this->isFieldTypeExist('turnstile');
695 if ($turnstileExist) {
696 if (!isset($_POST['cf-turnstile-response'])) {
697 return new WP_Error('spam_detection', __('Please verify Cloudflare Turnstile Captcha', 'bit-form'));
698 }
699 $token = sanitize_text_field(wp_unslash($_POST['cf-turnstile-response']));
700 $turnstileCaptcha = null;
701 $integrationHandler = new IntegrationHandler(0);
702 $turnstileIntegration = $integrationHandler->getAllIntegration('app', 'turnstileCaptcha')[0];
703 if (!is_wp_error($turnstileIntegration && !is_null($turnstileIntegration->integration_type))) {
704 $turnstileCaptcha = json_decode($turnstileIntegration->integration_details);
705 // $integrationDetails->id = $turnstileIntegration->id;
706 // $turnstileCaptcha = $integrationDetails;
707 }
708 if (!is_null($turnstileCaptcha)) {
709 $isTurnstileCaptchaVerified = false;
710 $turnstileRecaptchaResponse = HttpHelper::post(
711 'https://challenges.cloudflare.com/turnstile/v0/siteverify',
712 ['secret' => $turnstileCaptcha->secretKey, 'response' => $token]
713 );
714 if (!is_wp_error($turnstileRecaptchaResponse)) {
715 if (!$turnstileRecaptchaResponse->success) {
716 $errorCodes = implode(', ', (array) ($turnstileRecaptchaResponse->{'error-codes'} ?? []));
717 wp_send_json_error(
718 sprintf(
719 /* translators: %s: dynamic value. */
720 __('Cloudflare Turnstile Validation Error: %s', 'bit-form'),
721 $errorCodes
722 )
723 );
724 }
725
726 $isTurnstileCaptchaVerified = $turnstileRecaptchaResponse->success;
727 }
728 if (!$isTurnstileCaptchaVerified) {
729 return new WP_Error('spam_detection', __('Please verify Cloudflare Turnstile Captcha', 'bit-form'));
730 }
731 }
732 }
733 }
734
735 public function verifySubmissionNonce()
736 {
737 if (!isset($_POST['t_identity']) || !isset($_POST['csrf'])) {
738 return false;
739 }
740 $tIdenty = sanitize_text_field(wp_unslash($_POST['t_identity']));
741 $csrf = sanitize_text_field(wp_unslash($_POST['csrf']));
742 unset($_POST['t_identity'], $_POST['action'], $_POST['bitforms_id'], $_POST['csrf']);
743 return Helpers::csrfDecrypted($tIdenty, $csrf);
744 }
745
746 public function setViewCount()
747 {
748 if (!current_user_can('manage_options')) {
749 $update_status = $this->formModel->update(
750 [
751 'views' => intval(static::$form[0]->views) + 1
752 ],
753 [
754 'id' => $this->form_id
755 ]
756 );
757 }
758 }
759
760 public function checkSubmissionRestriction($checkedEmptySubmitted = true)
761 {
762 $formContents = $this->getFormContent();
763 $additionalSettings = isset($formContents->additional) ? $formContents->additional : null;
764 $fromRestrictionSetitingsEnabled = empty($additionalSettings->enabled) ? [] : $additionalSettings->enabled;
765 $fromRestrictionSetitings = empty($additionalSettings->settings) ? null : $additionalSettings->settings;
766
767 if (is_null($additionalSettings) || is_null($fromRestrictionSetitings) || empty((array) $fromRestrictionSetitingsEnabled)) {
768 return false;
769 }
770
771 $restrictionMessage = [];
772 $ipTool = new IpTool();
773 $ipAddress = $ipTool->getIP();
774 $currentUserId = get_current_user_id();
775
776 foreach ($fromRestrictionSetitingsEnabled as $restrictionKey => $isEnabled) {
777 if ($isEnabled) {
778 /**
779 * Allow add-ons to handle any restriction key (Pro-only restrictions
780 * should be implemented in the add-on, not shipped in the free plugin).
781 *
782 * Return a non-null string to block submission.
783 */
784 $addonMsg = apply_filters(
785 'bitform_submission_restriction',
786 null,
787 $restrictionKey,
788 $this->form_id,
789 $fromRestrictionSetitingsEnabled,
790 $fromRestrictionSetitings,
791 $ipAddress,
792 $currentUserId
793 );
794
795 if (!is_null($addonMsg) && '' !== $addonMsg) {
796 $restrictionMessage[] = $addonMsg;
797 continue;
798 }
799
800 if ('onePerIp' === $restrictionKey) {
801 $formEntry = new FormEntryModel();
802
803 $getResult = $formEntry->get(
804 ['user_ip', 'status'],
805 [
806 'form_id' => $this->form_id,
807 'user_ip' => (int) ip2long((string) $ipAddress)
808 ],
809 );
810
811 $count = 0;
812 $status = 0;
813
814 if (!is_wp_error($getResult) && count($getResult) > 0) {
815 $count = count($getResult);
816
817 foreach ($getResult as $row) {
818 if (9 === (int) $row->status) {
819 $status = 9;
820 break;
821 }
822 }
823 }
824
825 if ($count > 0 && 9 !== (int) $status) {
826 $onePerIp = __('Sorry!! You have already submitted from this IP address', 'bit-form');
827
828 $onePerIp = apply_filters(
829 'bitform_filter_restriction_one_per_ip_message',
830 $onePerIp,
831 $this->form_id
832 );
833
834 $restrictionMessage[] = $onePerIp;
835 }
836 }
837 if ('is_login' === $restrictionKey && 0 === get_current_user_id()) {
838 $is_login_messages = $fromRestrictionSetitings->is_login->message;
839
840 $is_login_messages = apply_filters(
841 'bitform_filter_restriction_is_login_message',
842 $is_login_messages,
843 $this->form_id
844 );
845
846 $restrictionMessage[] = $is_login_messages;
847 }
848 if ($checkedEmptySubmitted && 'empty_submission' === $restrictionKey) {
849 $isEmpty = $this->checkEmptySubmission(wp_unslash($_POST), GlobalHelper::sanitize_files_input($_FILES));
850 if ($isEmpty) {
851 $restriction = $fromRestrictionSetitings->empty_submission->message;
852
853 $restriction = apply_filters(
854 'bitform_filter_restriction_empty_submission_message',
855 $restriction,
856 $this->form_id
857 );
858
859 $restrictionMessage[] = $restriction;
860 }
861 }
862 }
863 }
864 return $restrictionMessage;
865 }
866
867 /**
868 * Will check if form is submitted by a bot
869 *
870 * @return Boolean true - if submitted by bot else false
871 */
872 public function isTrappedInHoneypot()
873 {
874 // Honeypot is implemented by add-ons (e.g. Pro) via filter.
875 return (bool) apply_filters('bitform_check_honeypot', false, $this->_form_id, wp_unslash($_POST));
876 }
877
878 public function isHoneypotActive()
879 {
880 return (bool) apply_filters('bitform_is_honeypot_active', false, $this->_form_id, $this->getFormContent());
881 }
882
883 public function checkPaymentFields()
884 {
885 $formContents = $this->getFormContent();
886 $fields = $formContents->fields;
887
888 $payments = [];
889 foreach ($fields as $fldData) {
890 if (!is_object($fldData)) {
891 continue;
892 }
893 if ('paypal' === $fldData->typ && property_exists($fldData, 'payIntegID')) {
894 $payments['paypalKey'] = $this->getClientKey($fldData->payIntegID, 'clientID');
895 } elseif ('razorpay' === $fldData->typ && isset($fldData->options) && is_object($fldData->options) && property_exists($fldData->options, 'payIntegID')) {
896 $payments['razorpayKey'] = $this->getClientKey($fldData->options->payIntegID, 'apiKey');
897 }
898 }
899
900 return $payments;
901 }
902
903 private function getClientKey($integID, $keyName)
904 {
905 $client = '';
906 if (!empty($integID)) {
907 $integrationHandler = new IntegrationHandler(0);
908 $integration = $integrationHandler->getAIntegration($integID, 'app', 'payments');
909 if (!is_wp_error($integration)) {
910 $integrationRow = Utilities::firstRow($integration);
911 $integration_details = Utilities::jsonObj($integrationRow->integration_details ?? '');
912 if ($integration_details && isset($integration_details->{$keyName})) {
913 $client = base64_encode($integration_details->{$keyName});
914 }
915 }
916 }
917 return $client;
918 }
919
920 public function getSuccessMessageMarkups()
921 {
922 if (is_null($this->_conf_messages)) {
923 $successMsgHandler = new SuccessMessageHandler($this->form_id);
924 $this->_conf_messages = $successMsgHandler->getAllMessage();
925 }
926
927 $messageMarkups = '';
928 if (is_wp_error($this->_conf_messages)) {
929 return $messageMarkups;
930 }
931
932 foreach ($this->_conf_messages as $msgItem) {
933 $msgConfig = json_decode($msgItem->message_config);
934 if (is_object($msgConfig) && property_exists($msgConfig, 'status') && empty($msgConfig->status)) {
935 continue;
936 }
937 $messageMarkups .= $this->messageMarkup($msgItem);
938 }
939
940 return $messageMarkups;
941 }
942
943 public function getFormAbandonmentMessage()
944 {
945 $msg = apply_filters('bitform_form_abandonment_warning_markup', '', $this->form_id);
946 return is_string($msg) ? $msg : '';
947 }
948
949 public function getFormAbandonmentSettings()
950 {
951 return apply_filters('bitform_form_abandonment_settings', null, $this->form_id);
952 }
953
954 private function messageMarkup($msg)
955 {
956 $msgId = $msg->id;
957 $msgConfig = json_decode($msg->message_config);
958 $msgType = (is_object($msgConfig) && isset($msgConfig->msgType)) ? $msgConfig->msgType : 'below';
959 $scrollClass = 'below' === $msgType ? 'scroll' : '';
960
961 return '<div
962 role="dialog"
963 aria-hidden="true"
964 data-modal-backdrop="true"
965 class="' . $this->getAtomicCls("msg-container-{$msgId}") . ' deactive ' . $scrollClass . '">
966 <div
967 data-contentid="' . $this->getFormIdentifier() . '"
968 data-msgid="' . $msgId . '"
969 role="button"
970 class="' . $this->getAtomicCls("msg-background-{$msgId}") . ' msg-backdrop">
971 <div class="bf-msg-content ' . $this->getAtomicCls("msg-content-{$msgId}") . '">
972 <button
973 data-contentid="' . $this->getFormIdentifier() . '"
974 data-msgid="' . $msgId . '"
975 class="' . $this->getAtomicCls("close-{$msgId}") . ' bf-msg-close"
976 type="button">
977 <svg class="' . $this->getAtomicCls("close-icn-{$msgId}") . '" viewBox="0 0 30 30">
978 <line fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" x1="4" y1="3.88" x2="26" y2="26.12"></line>
979 <line fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" x1="26" y1="3.88" x2="4" y2="26.12"></line>
980 </svg>
981 </button>
982 <div class="msg-content"></div>
983 </div>
984 </div>
985 </div>';
986 }
987 }
988