PluginProbe
Bit Form – Contact Form, Payment Forms, Multi Step Forms, Calculator & Custom Form Builder / V-3.3.0
Bit Form – Contact Form, Payment Forms, Multi Step Forms, Calculator & Custom Form Builder vV-3.3.0
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 2.10.1 All 138 releases
bit-form / includes / Frontend / Form / FrontendFormManager.php

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

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