PluginProbe
Bit Form – Contact Form, Payment Forms, Multi Step Forms, Calculator & Custom Form Builder / 3.3.1
Bit Form – Contact Form, Payment Forms, Multi Step Forms, Calculator & Custom Form Builder v3.3.1
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 3.3.1, at includes/Frontend/Form/FrontendFormManager.php

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