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
← All changes | includes/Frontend/Form/FrontendFormManager.php +1117 -201 1.1.13.3.1 View file →
@@ -9,249 +9,1165 @@
9 9 /**
10 10 * FrontendFormManager class
11 11 */
12 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;
13 25 use BitCode\BitForm\Core\Util\IpTool;
14 -use BitCode\BitForm\Core\Form\FormManager;
15 -use BitCode\BitForm\Core\Util\DateTimeHelper;
16 -use BitCode\BitForm\Core\Database\FormEntryModel;
26 +use BitCode\BitForm\Core\Util\Utilities;
27 +use BitCode\BitForm\Core\WorkFlow\WorkFlow;
17 28 use BitCode\BitForm\Frontend\Form\View\FormViewer;
18 -use BitCode\BitForm\Core\WorkFlow\WorkFlowRunHelper;
29 +use BitCode\BitForm\GlobalHelper;
30 +use WP_Error;
19 31
20 32 final class FrontendFormManager extends FormManager
21 33 {
22 - private $_form_identifier;
23 - private $_form_token;
24 - // private $_has_upload = false;
25 - public function __construct($form_id, $shortCodeCounter = null)
26 - {
27 - parent::__construct($form_id);
28 - $this->_form_identifier = 'bitforms_' . $form_id . '_submit_';
29 - $this->_form_identifier .= !empty(get_post()->ID) ? get_post()->ID : '';
30 - $this->_form_identifier .= !empty($shortCodeCounter) ? "_$shortCodeCounter" : '';
31 - $this->_form_token = wp_create_nonce('bitforms_' . $form_id);
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);
32 57 }
33 58
34 - public function getFormIdentifier()
35 - {
36 - return $this->_form_identifier;
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'];
37 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 + }
38 103
39 - public function getFormID()
40 - {
41 - return $this->form_id;
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'];
42 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 + }
43 123
44 - public function getFormToken()
45 - {
46 - return $this->_form_token;
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 + }
47 162 }
163 + return true;
164 + }
48 165
49 - public function isSubmitted()
50 - {
51 - return isset($_POST[$this->_form_identifier]) ? true : false;
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 + }
52 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 + }
53 187
54 - public function getSubmittedFields($submitted_data)
55 - {
56 - unset($submitted_data[$this->_form_identifier]);
57 - return array_keys($submitted_data);
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 + }
58 198 }
199 + return $parameter;
200 + }
59 201
60 - public function formView($fields = null, $hasFile = false, $errorMessages = null, $previousValue = null)
61 - {
62 - $formContents = $this->getFormContent();
63 - if (!empty($fields)) {
64 - $formContents->fields = is_string($fields) ? json_decode($fields) : $fields;
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 + }
65 225 } else {
66 - $workFlowRunHelper = new WorkFlowRunHelper($this->form_id);
67 - $workFlowreturnedOnLoad = $workFlowRunHelper->executeOnLoad(
68 - 'create',
69 - $formContents->fields
70 - );
71 - $formContents->fields = empty($workFlowreturnedOnLoad['fields']) ? $formContents->fields : $workFlowreturnedOnLoad['fields'];
226 + $post[$key] = explode(BITFORMS_BF_SEPARATOR, $value);
72 227 }
73 - $formViewer = new FormViewer($this, $formContents, $errorMessages, $previousValue);
74 - return $formViewer->getView($hasFile);
228 + };
75 229 }
76 230
77 - public function validateFormSubmission($submitted_data)
78 - {
79 - $submitted_fields = $this->getSubmittedFields($submitted_data);
80 - $form_fields = $this->getFields();
81 - $form_fields_names = array_keys($form_fields);
82 - if ($this->isGCLIDEnabled()) {
83 - array_push($form_fields_names, 'GCLID');
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;
84 282 }
85 - foreach ($submitted_fields as $key => $field) {
86 - if (!in_array($field, $form_fields_names)) {
87 - unset($submitted_data[$field]);
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'];
88 336 }
337 + } else {
338 + if (!$result['success']) {
339 + return new WP_Error('errors', self::authErrorMessage($result['message']));
340 + } else {
341 + return $result;
342 + }
343 + }
89 344 }
90 - return $submitted_data;
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;
91 404 }
405 + do_action('bitform_validation_error', $this->_form_id, $validated);
406 + return $validated;
407 + }
92 408
93 - public function verifySubmissionNonce($submitted_data)
94 - {
95 - if (!isset($submitted_data['bitforms_token'])) {
96 - return false;
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 + }
97 453 }
98 - return wp_verify_nonce(sanitize_text_field($submitted_data['bitforms_token']), "bitforms_{$this->form_id}");
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;
99 514 }
515 + do_action('bitform_validation_error', $this->_form_id, $validated);
516 + return $validated;
517 + }
100 518
101 - public function setViewCount()
102 - {
103 - if (!current_user_can('manage_options')) {
104 - $update_status = $this->formModel->update(
105 - array(
106 - 'views' => intval(static::$form[0]->views) + 1
107 - ),
108 - array(
109 - 'id' => $this->form_id
110 - )
111 - );
112 - }
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;
113 537 }
538 + $hiddenFieldKeys = FrontendHelpers::parseHiddenFieldKeys($rawHiddenFields);
539 + if (empty($hiddenFieldKeys)) {
540 + return;
541 + }
114 542
115 - public function checkSubmissionRestriction()
116 - {
117 - $formContents = $this->getFormContent();
118 - $fromRestrictionSetitingsEnabled = empty($formContents->additional->enabled) ? null : $formContents->additional->enabled;
119 - $fromRestrictionSetitings = empty($formContents->additional->settings) ? null : $formContents->additional->settings;
120 - if (is_null($formContents->additional->enabled) || is_null($formContents->additional->settings)) {
121 - return false;
122 - }
123 - $restrictionMessage = array();
124 - $ipTool = new IpTool();
125 - $ipAddress = $ipTool->getIP();
126 - foreach ($fromRestrictionSetitingsEnabled as $restrictionKey => $isEnabled) {
127 - if ($isEnabled) {
128 - if ($restrictionKey === 'entry_limit' && isset($fromRestrictionSetitings->{$restrictionKey})) {
129 - $formEntry = new FormEntryModel();
130 - $countResult = $formEntry->count(
131 - array(
132 - 'form_id' => $this->form_id
133 - )
134 - );
135 - $count = !empty($countResult[0]) && !empty($countResult[0]->count) ? $countResult[0]->count : false;
136 - if ($count && $count >= intval($fromRestrictionSetitings->{$restrictionKey})) {
137 - $restrictionMessage[] = __('Sorry!! Entry limit exceeded', "bitform");
138 - }
139 - }
140 - if ($restrictionKey === 'onePerIp') {
141 - $formEntry = new FormEntryModel();
142 - $countResult = $formEntry->count(
143 - array(
144 - 'form_id' => $this->form_id,
145 - 'user_ip' => ip2long($ipAddress)
146 - )
147 - );
148 - $count = !empty($countResult[0]) && !empty($countResult[0]->count) ? $countResult[0]->count : false;
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 + }
149 571
150 - if ($count && $count > 0) {
151 - $restrictionMessage[] = __('Sorry!! You have already submitted', "bitform");
152 - }
153 - }
154 - if ($restrictionKey === 'restrict_form' && isset($fromRestrictionSetitings->{$restrictionKey})) {
155 - $day = empty($fromRestrictionSetitings->{$restrictionKey}->day) ? null : $fromRestrictionSetitings->{$restrictionKey}->day;
156 - $date = empty($fromRestrictionSetitings->{$restrictionKey}->date) ? null : $fromRestrictionSetitings->{$restrictionKey}->date;
157 - $time = empty($fromRestrictionSetitings->{$restrictionKey}->time) ? null : $fromRestrictionSetitings->{$restrictionKey}->time;
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 + }
158 589
159 - $isdayOk = $isdateOk = $istimeOk = true;
160 - $dayNotOkMsg = $dateNotOkMsg = $timeNotOkMsg = '';
161 - $dateTimeHelper = new DateTimeHelper();
162 - if (
163 - !empty($day)
164 - && is_array($day)
165 - && (in_array("Friday", $day)
166 - || in_array("Saturday", $day)
167 - || in_array("Sunday", $day)
168 - || in_array("Monday", $day)
169 - || in_array("Tuesday", $day)
170 - || in_array("Wednesday", $day)
171 - || in_array("Thursday", $day))
172 - && (!in_array($dateTimeHelper->getDay('full-name'), $day))
173 - ) {
174 - $isdayOk = false;
175 - $dayMsgVarsFormat = '';
176 - foreach ($day as $dayIndex => $dayValue) {
177 - if ($dayIndex > 0) {
178 - $dayMsgVarsFormat .= ', ';
179 - }
180 - $dayMsgVarsFormat .= '%s';
181 - }
182 - $dayNotOkMsg = vsprintf(__("in $dayMsgVarsFormat", 'bitform'), $day);
183 - }
184 - if (
185 - !empty($day)
186 - && is_array($day)
187 - && (in_array("Custom", $day))
188 - ) {
189 - $startDate = empty($date->from) ? '00-00-0000' : $date->from;
190 - $endDate = empty($date->to) ? '00-00-0000' : $date->to;
191 - if (!empty($date->from) && strpos($startDate, 'T') !== false) {
192 - $startDate = $dateTimeHelper->getDate($startDate, false, null, 'm-d-Y');
193 - }
194 - if (!empty($date->to) && strpos($endDate, 'T') !== false) {
195 - $endDate = $dateTimeHelper->getDate($endDate, false, null, 'm-d-Y');
196 - }
197 - $currentDate = $dateTimeHelper->getDate(null, null, null, 'm-d-Y');
198 - if (!($currentDate >= $startDate && $currentDate <= $endDate)) {
199 - $isdateOk = false;
200 - $dateNotOkMsg = sprintf(__("within %s to %s", 'bitform'), $startDate, $endDate);
201 - }
202 - }
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;
203 600
204 - if (!empty($time)) {
205 - $startTime = empty($time->from) ? '00:00' : $time->from;
206 - $endTime = empty($time->to) ? '23:59.999' : $time->to;
207 - $currentTime = $dateTimeHelper->getTime(null, null, null, 'H:i');
208 - if (!($currentTime >= $startTime && $currentTime <= $endTime)) {
209 - $istimeOk = false;
210 - $startTime = $dateTimeHelper->getTime($startTime, 'H:i', null);
211 - $endTime = $dateTimeHelper->getTime($endTime, 'H:i', null);
212 - $isTimeOk = false;
213 - $timeNotOkMsg = sprintf(__("%s to %s", 'bitform'), $startTime, $endTime);
214 - }
215 - }
601 + return !empty($submissionSettings->discardHiddenFieldValues);
602 + }
216 603
217 - if (!($isdateOk && $isdayOk && $istimeOk)) {
218 - if (!$isdayOk) {
219 - $restrictionMessage[] = !empty($timeNotOkMsg) ? sprintf(__("Form is available %s From %s", 'bitform'), $dayNotOkMsg, $timeNotOkMsg) :
220 - sprintf(__("Form is available %s", 'bitform'), $dayNotOkMsg, $timeNotOkMsg);
221 - } elseif (!$isdateOk) {
222 - $restrictionMessage[] = !empty($timeNotOkMsg) ? sprintf(__("Form is available %s From %s", 'bitform'), $dateNotOkMsg, $timeNotOkMsg) :
223 - sprintf(__("Form is available %s", 'bitform'), $dateNotOkMsg, $timeNotOkMsg);
224 - } elseif (!$istimeOk) {
225 - $restrictionMessage[] = sprintf(__("Form is available on %s", 'bitform'), $timeNotOkMsg);
226 - }
227 - }
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;
228 693 }
229 - if ($restrictionKey === 'blocked_ip' && isset($fromRestrictionSetitings->{$restrictionKey})) {
230 - $isIpBlocked = false;
231 - foreach ($fromRestrictionSetitings->{$restrictionKey} as $ipIndex => $ipDetails) {
232 - if (!empty($ipDetails->status) && $ipDetails->status && !empty($ipDetails->ip) && $ipDetails->ip === $ipAddress) {
233 - $isIpBlocked = true;
234 - break;
235 - }
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;
236 700 }
237 - if ($isIpBlocked) {
238 - $restrictionMessage[] = sprintf(__("Sorry!! Your IP address is %s, Blocked from submitting the form", 'bitform'), $ipAddress);
701 + foreach ($nestedLayout->{$fk}->{$nBrkpnt} as $nestedLay) {
702 + if (is_object($nestedLay) && isset($nestedLay->i)) {
703 + $stepKeys[$nestedLay->i] = true;
704 + }
239 705 }
706 + }
240 707 }
241 - if ($restrictionKey === 'private_ip' && isset($fromRestrictionSetitings->{$restrictionKey})) {
242 - $isIpWhiteListed = false;
243 - foreach ($fromRestrictionSetitings->{$restrictionKey} as $ipIndex => $ipDetails) {
244 - if (!empty($ipDetails->status) && $ipDetails->status && !empty($ipDetails->ip) && $ipDetails->ip === $ipAddress) {
245 - $isIpWhiteListed = true;
246 - break;
247 - }
248 - }
249 - if (!$isIpWhiteListed) {
250 - $restrictionMessage[] = sprintf(__("Sorry!! Your IP address is %s, Blocked from submitting the form", 'bitform'), $ipAddress);
251 - }
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];
252 718 }
719 + }
720 + $form_fields = $step_fields;
253 721 }
722 + }
254 723 }
255 - return $restrictionMessage;
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'));
256 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 + }
257 1173 }