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 +650 -384 2.10.03.3.1 View file →
@@ -9,8 +9,9 @@
9 9 /**
10 10 * FrontendFormManager class
11 11 */
12 12
13 +use BitCode\BitForm\Admin\Form\AdminFormHandler;
13 14 use BitCode\BitForm\Admin\Form\Helpers;
14 15 use BitCode\BitForm\Core\Database\FormEntryModel;
15 16 use BitCode\BitForm\Core\Form\FormManager;
16 17 use BitCode\BitForm\Core\Form\Validator\FormFieldValidator;
@@ -16,15 +17,17 @@
16 17 use BitCode\BitForm\Core\Form\Validator\FormFieldValidator;
17 18 use BitCode\BitForm\Core\Integration\IntegrationHandler;
18 19 use BitCode\BitForm\Core\Messages\SuccessMessageHandler;
19 20 use BitCode\BitForm\Core\Util\ApiResponse as UtilApiResponse;
20 -use BitCode\BitForm\Core\Util\DateTimeHelper;
21 +use BitCode\BitForm\Core\Util\EscapingHelper;
22 +use BitCode\BitForm\Core\Util\FieldValueHandler;
23 +use BitCode\BitForm\Core\Util\FrontendHelpers;
21 24 use BitCode\BitForm\Core\Util\HttpHelper;
22 25 use BitCode\BitForm\Core\Util\IpTool;
26 +use BitCode\BitForm\Core\Util\Utilities;
23 27 use BitCode\BitForm\Core\WorkFlow\WorkFlow;
24 -use BitCode\BitForm\Core\WorkFlow\WorkFlowHandler;
25 28 use BitCode\BitForm\Frontend\Form\View\FormViewer;
26 -use BitCode\BitFormPro\Admin\FormSettings\FormAbandonment;
29 +use BitCode\BitForm\GlobalHelper;
27 30 use WP_Error;
28 31
29 32 final class FrontendFormManager extends FormManager
30 33 {
@@ -30,10 +33,10 @@
30 33 {
31 34 private $_form_identifier;
32 35 private $_form_token;
33 36 private $_form_id;
34 - private $_work_flows;
35 37 private $_conf_messages;
38 + private static $_instance = [];
36 39
37 40 // private $_has_upload = false;
38 41 public function __construct($form_id, $shortCodeCounter = null)
39 42 {
@@ -44,8 +47,19 @@
44 47 $this->_form_token = wp_create_nonce('bitforms_' . $form_id);
45 48 $this->_form_id = $form_id;
46 49 }
47 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 +
48 62 public function getFormIdentifier()
49 63 {
50 64 return $this->_form_identifier;
51 65 }
@@ -59,14 +73,8 @@
59 73 {
60 74 return $this->_form_token;
61 75 }
62 76
63 - public function isSubmitted()
64 - {
65 - // return isset($_POST[$this->_form_identifier]) ? true : false;
66 - return (isset($_POST['bitforms_id']) && $_POST['bitforms_id'] === $this->_form_identifier) ? true : false;
67 - }
68 -
69 77 public function getSubmittedFields($submitted_data)
70 78 {
71 79 unset($submitted_data[$this->_form_identifier]);
72 80 // unset($submitted_data['bit-form-submit-btn']);
@@ -72,9 +80,9 @@
72 80 // unset($submitted_data['bit-form-submit-btn']);
73 81 return array_keys($submitted_data);
74 82 }
75 83
76 - public function formView($fields = null, $hasFile = false, $errorMessages = null, $previousValue = null)
84 + public function formView($fields = null, $hasFile = false, $errorMessages = null, $previousValue = null, $isEntryEdit = false)
77 85 {
78 86 $formContents = $this->getFormContent();
79 87 $formAtomicClsMap = $this->getAtomicClsMap();
80 88 if (!empty($fields)) {
@@ -87,14 +95,14 @@
87 95 );
88 96 $formContents->fields = empty($workFlowreturnedOnLoad['fields']) ? $formContents->fields : $workFlowreturnedOnLoad['fields'];
89 97 }
90 98 $formViewer = new FormViewer($this, $formContents, $formAtomicClsMap, $errorMessages, $previousValue);
91 - $isRestricted = $this->checkSubmissionRestriction(false);
99 + $isRestricted = $this->checkSubmissionRestriction(false, $isEntryEdit);
92 100 $msg = !empty($isRestricted) ? $isRestricted[0] : '';
93 101 return $formViewer->getView($hasFile, $msg);
94 102 }
95 103
96 - public function conversationalFormView($fields = null, $hasFile = false, $errorMessages = null, $previousValue = null)
104 + public function conversationalFormView($fields = null, $hasFile = false, $errorMessages = null, $previousValue = null, $isEntryEdit = false)
97 105 {
98 106 $formContents = $this->getFormContent();
99 107 $formAtomicClsMap = $this->getAtomicClsMap();
100 108 if (!empty($fields)) {
@@ -107,23 +115,34 @@
107 115 );
108 116 $formContents->fields = empty($workFlowreturnedOnLoad['fields']) ? $formContents->fields : $workFlowreturnedOnLoad['fields'];
109 117 }
110 118 $formViewer = new FormViewer($this, $formContents, $formAtomicClsMap, $errorMessages, $previousValue);
111 - $isRestricted = $this->checkSubmissionRestriction(false);
119 + $isRestricted = $this->checkSubmissionRestriction(false, $isEntryEdit);
112 120 $msg = !empty($isRestricted) ? $isRestricted[0] : '';
113 121 return $formViewer->getConversationalView($hasFile, $msg);
114 122 }
115 123
116 - private function checkEmptySubmission($data, $file)
124 + public function checkEmptySubmission($data, $file, $isEntryEdit = false)
117 125 {
118 126 $formFields = $this->getFields();
119 127 foreach ($formFields as $key => $field) {
120 128 $fieldType = $field['type'];
129 + if ('button' === $fieldType) {
130 + continue;
131 + }
121 132 $fileUploadFieldTypes = ['file-up', 'advanced-file-up'];
122 - if ('decision-box' === $fieldType) {
133 + if ('decision-box' === $fieldType || 'gdpr' === $fieldType) {
123 134 continue;
124 135 }
125 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 + }
126 145 if ($this->isRepeatedField($key)) {
127 146 $fileData = !empty($file[$key]) ? $file[$key] : [];
128 147 $dataVal = !empty($data[$key]) ? $data[$key] : [];
129 148 if (!$this->checkRepeatedFieldEmptySubmission($isFileType, $dataVal, $fileData)) {
@@ -179,39 +198,146 @@
179 198 }
180 199 return $parameter;
181 200 }
182 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 +
183 300 public function handleSubmission()
184 301 {
302 + // CSRF verified via verifySubmissionNonce() before this method is called. All $_POST reads below occur after that verification.
185 303 $this->fieldNameReplaceOfPost();
186 304
187 305 $validated = $this->beforeSubmittedValidate();
188 306
307 + $validated = apply_filters('bitform_filter_form_validation', $validated, $this->_form_id);
308 +
189 309 if (true === $validated) {
190 - unset($_POST['hidden_fields']);
310 + do_action('bitform_validation_success', $this->_form_id);
311 + $this->discardHiddenFieldValues();
191 312
192 313 $redirectPage = '';
193 314 $regSuccMsg = '';
194 315
195 316 $existAuth = (new IntegrationHandler($this->_form_id))->getAllIntegration('wp_user_auth', 'wp_auth', 1);
317 + $unslashed_post = wp_unslash($_POST);
196 318 if (!is_wp_error($existAuth) && count($existAuth) > 0) {
197 319 $parameter = $this->getParams();
198 - $existAuthFilter = has_filter('bf_wp_user_auth');
320 + $existAuthFilter = has_filter('bitform_wp_user_auth');
199 321
200 322 if (true === $existAuthFilter) {
201 - $result = apply_filters('bf_wp_user_auth', $existAuth[0], $_POST, $parameter);
323 + $authPostData = $this->resolveConfirmChildValues($unslashed_post);
324 + $result = apply_filters('bitform_wp_user_auth', $existAuth[0], $authPostData, $parameter);
202 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 +
203 330 if (isset($result['auth_type']) && 'register' === $result['auth_type']) {
204 331 if (!$result['success']) {
205 - return new WP_Error('errors', __($result['message'], 'bit-form'));
332 + return new WP_Error('errors', self::authErrorMessage($result['message']));
206 333 } elseif (isset($result['success'])) {
207 - $redirectPage = $result['redirect_url'];
334 + $redirectPage = $result['redirectPage'];
208 335 $regSuccMsg = $result['message'];
209 - $newNonce = wp_create_nonce('bitforms_' . $this->_form_id);
210 336 }
211 337 } else {
212 338 if (!$result['success']) {
213 - return new WP_Error('errors', __($result['message'], 'bit-form'));
339 + return new WP_Error('errors', self::authErrorMessage($result['message']));
214 340 } else {
215 341 return $result;
216 342 }
217 343 }
@@ -217,38 +343,33 @@
217 343 }
218 344 }
219 345 }
220 346
221 - $saveResponse = $this->saveFormEntry($_POST);
347 + $saveResponse = $this->saveFormEntry($unslashed_post);
222 348 if (is_wp_error($saveResponse)) {
223 349 return $saveResponse;
224 350 }
225 351
226 352 $entryID = $saveResponse['entry_id'];
227 - do_action('bitform_submit_success', $this->_form_id, $entryID, $_POST);
228 353
229 - // check and replace signature field value
230 - $formFields = $this->getFields();
231 - $uploadPath = BITFORMS_UPLOAD_BASE_URL . "/uploads/{$this->_form_id}/{$entryID}";
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);
232 358
233 - foreach ($formFields as $key => $field) {
234 - if ('signature' === $field['type']) {
235 - $saveResponse['fields'][$key] = $uploadPath . '/' . $saveResponse['fields'][$key];
236 - break;
237 - }
238 - }
239 -
240 359 $captchaV3Settings = $this->getCaptchaV3Settings();
241 360 if ($captchaV3Settings) {
242 - $token = $_POST['g-recaptcha-response'];
361 + $token = isset($_POST['g-recaptcha-response']) ? sanitize_text_field(wp_unslash($_POST['g-recaptcha-response'])) : '';
243 362 $integrationHandler = new IntegrationHandler(0);
244 363 $allFormIntegrations = $integrationHandler->getAllIntegration('app', 'gReCaptchaV3');
245 364 if (!is_wp_error($allFormIntegrations)) {
246 365 foreach ($allFormIntegrations as $integration) {
247 366 if (!is_null($integration->integration_type) && 'gReCaptchaV3' === $integration->integration_type) {
248 - $integrationDetails = json_decode($integration->integration_details);
249 - $integrationDetails->id = $integration->id;
250 - $reCAPTCHA = $integrationDetails;
367 + $integrationDetails = Utilities::jsonObj($integration->integration_details);
368 + if ($integrationDetails) {
369 + $integrationDetails->id = $integration->id;
370 + $reCAPTCHA = $integrationDetails;
371 + }
251 372 }
252 373 }
253 374 }
254 375 if (!empty($reCAPTCHA->secretKey)) {
@@ -270,15 +391,14 @@
270 391 }
271 392 if (!empty($regSuccMsg) && isset($saveResponse['dflt_message'])) {
272 393 $saveResponse['message'] = $regSuccMsg;
273 394 }
395 + $saveResponse['new_nonce'] = wp_create_nonce('bitforms_' . $this->_form_id);
396 +
274 397 $saveResponse = IntegrationHandler::maybeSetCronForIntegration($saveResponse, 'create');
275 398 $entryId = $saveResponse['entry_id'];
276 399
277 400 $responseMsg = is_array($saveResponse) && !empty($saveResponse) ? $saveResponse : __('Form Submitted Successfully', 'bit-form');
278 - if (isset($newNonce)) {
279 - $responseMsg['new_nonce'] = $newNonce;
280 - }
281 401 $_POST = [];
282 402 $responseMsg['entry_id'] = $entryId;
283 403 return $responseMsg;
284 404 }
@@ -287,40 +407,46 @@
287 407 }
288 408
289 409 public function handleUpdateEntry()
290 410 {
411 + // Entry token or capability verified by caller (FrontendAjax::update_entry). All $_POST reads occur after that check.
291 412 $this->fieldNameReplaceOfPost();
292 - $validated = $this->beforeSubmittedValidate();
413 + $validated = $this->beforeSubmittedValidate(true, true);
414 + $validated = apply_filters('bitform_filter_form_validation', $validated, $this->_form_id);
293 415
294 - $entryID = $_REQUEST['entryID'];
416 + $entryID = isset($_REQUEST['entryID']) ? sanitize_text_field(wp_unslash($_REQUEST['entryID'])) : null;
417 + $GLOBALS['bitform_entry_id'] = $entryID;
295 418 if (is_null($entryID)) {
296 419 return new WP_Error('empty_form', __('Entries id is invalid', 'bit-form'));
297 420 }
298 421 if (true === $validated) {
299 - unset($_POST['hidden_fields'], $_POST['entryID']);
422 + do_action('bitform_validation_success', $this->_form_id);
423 + $this->discardHiddenFieldValues();
424 + unset($_POST['entryID']);
300 425
301 426 $redirectPage = '';
302 427 $regSuccMsg = '';
428 + $postData = wp_unslash($_POST);
303 429
304 430 $existAuth = (new IntegrationHandler($this->_form_id))->getAllIntegration('wp_user_auth', 'wp_auth', 1);
305 431 if (!is_wp_error($existAuth) && count($existAuth) > 0) {
306 432 $parameter = $this->getParams();
307 - $existAuthFilter = has_filter('bf_wp_user_auth');
433 + $existAuthFilter = has_filter('bitform_wp_user_auth');
308 434
309 435 if (true === $existAuthFilter) {
310 - $result = apply_filters('bf_wp_user_auth', $existAuth[0], $_POST, $parameter);
436 + $authPostData = $this->resolveConfirmChildValues($postData);
437 + $result = apply_filters('bitform_wp_user_auth', $existAuth[0], $authPostData, $parameter);
311 438
312 439 if (isset($result['auth_type']) && 'register' === $result['auth_type']) {
313 440 if (!$result['success']) {
314 - return new WP_Error('errors', __($result['message'], 'bit-form'));
441 + return new WP_Error('errors', self::authErrorMessage($result['message']));
315 442 } elseif (isset($result['success'])) {
316 - $redirectPage = $result['redirect_url'];
443 + $redirectPage = $result['redirectPage'];
317 444 $regSuccMsg = $result['message'];
318 - $newNonce = wp_create_nonce('bitforms_' . $this->_form_id);
319 445 }
320 446 } else {
321 447 if (!$result['success']) {
322 - return new WP_Error('errors', __($result['message'], 'bit-form'));
448 + return new WP_Error('errors', self::authErrorMessage($result['message']));
323 449 } else {
324 450 return $result;
325 451 }
326 452 }
@@ -326,26 +452,34 @@
326 452 }
327 453 }
328 454 }
329 455
330 - $updateResponse = $this->updateFormEntry($_POST, $this->getFormID(), $entryID);
456 + $updateResponse = $this->updateFormEntry(wp_unslash($_POST), $this->getFormID(), $entryID);
331 457 if (is_wp_error($updateResponse)) {
332 458 return $updateResponse;
333 459 }
334 460
335 - do_action('bitform_submit_success', $this->_form_id, $entryID, $_POST);
461 + // transformed dropdown value from string to array
462 + $newPost = $this->transformDrpdwnValue($postData);
463 + $filesData = GlobalHelper::sanitize_files_input($_FILES);
336 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 +
337 469 $captchaV3Settings = $this->getCaptchaV3Settings();
338 470 if ($captchaV3Settings) {
339 - $token = $_POST['g-recaptcha-response'];
471 + $token = isset($_POST['g-recaptcha-response']) ? sanitize_text_field(wp_unslash($_POST['g-recaptcha-response'])) : '';
340 472 $integrationHandler = new IntegrationHandler(0);
341 473 $allFormIntegrations = $integrationHandler->getAllIntegration('app', 'gReCaptchaV3');
342 474 if (!is_wp_error($allFormIntegrations)) {
343 475 foreach ($allFormIntegrations as $integration) {
344 476 if (!is_null($integration->integration_type) && 'gReCaptchaV3' === $integration->integration_type) {
345 - $integrationDetails = json_decode($integration->integration_details);
346 - $integrationDetails->id = $integration->id;
347 - $reCAPTCHA = $integrationDetails;
477 + $integrationDetails = Utilities::jsonObj($integration->integration_details);
478 + if ($integrationDetails) {
479 + $integrationDetails->id = $integration->id;
480 + $reCAPTCHA = $integrationDetails;
481 + }
348 482 }
349 483 }
350 484 }
351 485 if (!empty($reCAPTCHA->secretKey)) {
@@ -367,15 +501,14 @@
367 501 }
368 502 if (!empty($regSuccMsg) && isset($updateResponse['dflt_message'])) {
369 503 $updateResponse['message'] = $regSuccMsg;
370 504 }
371 - $updateResponse = IntegrationHandler::maybeSetCronForIntegration($updateResponse, 'create');
505 + $updateResponse['new_nonce'] = wp_create_nonce('bitforms_' . $this->_form_id);
506 + $updateResponse = IntegrationHandler::maybeSetCronForIntegration($updateResponse, 'update');
372 507 $entryId = $updateResponse['entry_id'];
373 508
374 509 $responseMsg = is_array($updateResponse) && !empty($updateResponse) ? $updateResponse : __('Entry Update Successfully', 'bit-form');
375 - if (isset($newNonce)) {
376 - $responseMsg['new_nonce'] = $newNonce;
377 - }
510 +
378 511 $_POST = [];
379 512 $responseMsg['entry_id'] = $entryId;
380 513 return $responseMsg;
381 514 }
@@ -382,11 +515,96 @@
382 515 do_action('bitform_validation_error', $this->_form_id, $validated);
383 516 return $validated;
384 517 }
385 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 +
386 604 public function validateFormSubmission($submitted_data)
387 605 {
388 - $hidden_fields = isset($submitted_data['hidden_fields']) ? $submitted_data['hidden_fields'] : '';
606 + $hidden_fields = FrontendHelpers::parseHiddenFieldKeys(isset($submitted_data['hidden_fields']) ? $submitted_data['hidden_fields'] : '');
389 607 $submitted_fields = $this->getSubmittedFields($submitted_data);
390 608 $form_fields = $this->getFields();
391 609 $form_fields_names = array_keys($form_fields);
392 610 if ($this->isGCLIDEnabled()) {
@@ -392,9 +610,9 @@
392 610 if ($this->isGCLIDEnabled()) {
393 611 array_push($form_fields_names, 'GCLID');
394 612 }
395 613 foreach ($submitted_fields as $field) {
396 - if ('hidden_fields' !== $field && !in_array($field, $form_fields_names) || false !== strpos($hidden_fields, $field)) {
614 + if ('hidden_fields' !== $field && !in_array($field, $form_fields_names) || FrontendHelpers::isFieldHidden($hidden_fields, $field)) {
397 615 unset($submitted_data[$field]);
398 616 }
399 617 }
400 618 return $submitted_data;
@@ -399,105 +617,41 @@
399 617 }
400 618 return $submitted_data;
401 619 }
402 620
403 - public function beforeSubmittedValidate()
621 + public function beforeSubmittedValidate($verifyCaptcha = true, $isEntryEdit = false)
404 622 {
405 623 if ($this->verifySubmissionNonce()) {
406 624 if ($this->isExist()) {
407 - $isRestricted = $this->checkSubmissionRestriction();
625 + $isRestricted = $this->checkSubmissionRestriction(true, $isEntryEdit);
408 626 if ($isRestricted && !empty($isRestricted)) {
409 627 return new WP_Error('spam_detection', $isRestricted[0]);
410 628 }
411 - if ($this->isTrappedInHoneypot()) {
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) {
412 633 return new WP_Error('spam_detection', __('Token verification failed', 'bit-form'));
413 634 }
414 - $captchaSettings = $this->getCaptchaSettings();
415 - $captchaV3Settings = $this->getCaptchaV3Settings();
416 - if ($captchaSettings || $captchaV3Settings) {
417 - $token = $_POST['g-recaptcha-response'];
418 - if (!isset($_POST['g-recaptcha-response'])) {
419 - return new WP_Error('spam_detection', __('Please verify reCAPTCHA', 'bit-form'));
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;
420 641 }
421 - $integrationHandler = new IntegrationHandler(0);
422 - $allFormIntegrations = $integrationHandler->getAllIntegration('app', $captchaSettings ? 'gReCaptcha' : 'gReCaptchaV3');
423 - if (!is_wp_error($allFormIntegrations)) {
424 - foreach ($allFormIntegrations as $integration) {
425 - if (!is_null($integration->integration_type) && $integration->integration_type === ($captchaSettings ? 'gReCaptcha' : 'gReCaptchaV3')) {
426 - $integrationDetails = json_decode($integration->integration_details);
427 - $integrationDetails->id = $integration->id;
428 - $reCAPTCHA = $integrationDetails;
429 - }
430 - }
642 + $verifyHCaptchaResult = $this->verifyHCaptcha();
643 + if (is_wp_error($verifyHCaptchaResult)) {
644 + return $verifyHCaptchaResult;
431 645 }
432 - if (!empty($reCAPTCHA->secretKey)) {
433 - $gRecaptchaResponse = HttpHelper::post(
434 - 'https://www.google.com/recaptcha/api/siteverify',
435 - ['secret' => $reCAPTCHA->secretKey, 'response' => $token]
436 - );
437 - $isgReCaptchaVerified = false;
438 - if (!is_wp_error($gRecaptchaResponse)) {
439 - if (
440 - $captchaV3Settings
441 - && !empty($gRecaptchaResponse->score)
442 - && ((float) $gRecaptchaResponse->score < (float) $captchaV3Settings->score)
443 - ) {
444 - wp_send_json_error(
445 - __(
446 - $captchaV3Settings->message,
447 - 'bit-form'
448 - )
449 - );
450 - }
451 -
452 - $isgReCaptchaVerified = $gRecaptchaResponse->success;
453 - }
454 - if (!$isgReCaptchaVerified) {
455 - return new WP_Error('spam_detection', __('Please verify reCAPTCHA', 'bit-form'));
456 - }
646 + /* Implement Turnstile Captcha start */
647 + $verifyTurnstileCaptchaResult = $this->verifyTurnstileCaptcha();
648 + if (is_wp_error($verifyTurnstileCaptchaResult)) {
649 + return $verifyTurnstileCaptchaResult;
457 650 }
458 651 }
652 + /* Implement Turnstile Captcha end */
459 653
460 - /* Implement Turnstile Captcha start */
461 - $turnstileSetting = $this->getTurnstileSettings();
462 - if ($turnstileSetting) {
463 - if (!isset($_POST['cf-turnstile-response'])) {
464 - return new WP_Error('spam_detection', __('Please verify Cloudflare Turnstile Captcha', 'bit-form'));
465 - }
466 - $token = $_POST['cf-turnstile-response'];
467 - $turnstileCaptcha = null;
468 - $integrationHandler = new IntegrationHandler(0);
469 - $turnstileIntegration = $integrationHandler->getAllIntegration('app', 'turnstileCaptcha')[0];
470 - if (!is_wp_error($turnstileIntegration && !is_null($turnstileIntegration->integration_type))) {
471 - $turnstileCaptcha = json_decode($turnstileIntegration->integration_details);
472 - // $integrationDetails->id = $turnstileIntegration->id;
473 - // $turnstileCaptcha = $integrationDetails;
474 - }
475 - if (!is_null($turnstileCaptcha)) {
476 - $isTurnstileCaptchaVerified = false;
477 - $turnstileRecaptchaResponse = HttpHelper::post(
478 - 'https://challenges.cloudflare.com/turnstile/v0/siteverify',
479 - ['secret' => $turnstileCaptcha->secretKey, 'response' => $token]
480 - );
481 - if (!is_wp_error($turnstileRecaptchaResponse)) {
482 - if (!$turnstileRecaptchaResponse->success) {
483 - wp_send_json_error(
484 - __(
485 - 'Cloudflare Turnstile Validation Error: ' . implode(', ', $turnstileRecaptchaResponse->{'error-codes'}),
486 - 'bit-form'
487 - )
488 - );
489 - }
490 -
491 - $isTurnstileCaptchaVerified = $turnstileRecaptchaResponse->success;
492 - }
493 - if (!$isTurnstileCaptchaVerified) {
494 - return new WP_Error('spam_detection', __('Please verify Cloudflare Turnstile Captcha', 'bit-form'));
495 - }
496 - }
497 - }
498 -
499 - /* Implement Turnstile Captcha end */
500 654 $existAuth = (new IntegrationHandler($this->_form_id))->getAllIntegration('wp_user_auth', 'wp_auth', 1);
501 655
502 656 // check if user is already logged in and form has auth integration
503 657 do_action('bitform_checked_exist_auth', $this->_form_id, $existAuth);
@@ -503,39 +657,80 @@
503 657 do_action('bitform_checked_exist_auth', $this->_form_id, $existAuth);
504 658 if (!is_wp_error($existAuth) && count($existAuth) > 0 && is_user_logged_in()) {
505 659 return new WP_Error('auth_error', __('You are already logged in', 'bit-form'));
506 660 }
507 - $validateForm = $this->validateFormSubmission($_POST);
508 - $validateFormFiles = $this->validateFormSubmission($_FILES);
661 + $validateForm = $this->validateFormSubmission($postData);
662 + $validateFormFiles = $this->validateFormSubmission($filesData);
509 663 $validateForm = array_merge($validateForm, $validateFormFiles);
510 - $form_fields = $this->getFields();
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();
511 670 // check if form-current-step is set and form is multi-step
512 - $formCurrentStep = isset($_POST['form-current-step']) ? $_POST['form-current-step'] : null;
671 + $formCurrentStep = isset($_POST['form-current-step']) ? sanitize_text_field(wp_unslash($_POST['form-current-step'])) : null;
513 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).
514 678 $formContents = $this->getFormContent();
515 - $layout = $formContents->layout;
679 + $layout = isset($formContents->layout) ? $formContents->layout : null;
516 680 $stepIndex = (int) $formCurrentStep - 1;
517 - $stepLayout = $layout[$stepIndex]->layout->lg;
518 - $nestedLayout = $formContents->nestedLayout;
519 - $step_fields = [];
520 - foreach ($stepLayout as $lay) {
521 - $fk = $lay->i;
522 - if (isset($nestedLayout->{$fk})) {
523 - $nestedLg = $nestedLayout->{$fk}->lg;
524 - foreach ($nestedLg as $nestedLay) {
525 - $nestedFk = $nestedLay->i;
526 - $step_fields[$nestedFk] = $form_fields[$nestedFk];
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;
527 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 + }
528 709 }
529 - $step_fields[$fk] = $form_fields[$fk];
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 + }
530 722 }
531 - $form_fields = $step_fields;
532 723 }
533 - $formFieldValidator = new FormFieldValidator($form_fields, $_POST, $_FILES);
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);
534 729 $validUniuqFields = [];
535 - $existFilter = has_filter('bf_check_duplicate_entry');
730 + $existFilter = has_filter('bitform_check_duplicate_entry');
536 731 if (true === $existFilter) {
537 - $validUniuqFields = apply_filters('bf_check_duplicate_entry', $form_fields, $_POST);
732 + $validUniuqFields = apply_filters('bitform_check_duplicate_entry', $form_fields, $postData);
538 733
539 734 $fieldKeys = array_keys($validUniuqFields);
540 735 $form_fields_keys = array_keys($form_fields);
541 736 $uniqueFields = [];
@@ -543,9 +738,9 @@
543 738 if (in_array($key, $form_fields_keys)) {
544 739 $uniqueFields[] = $form_fields[$key];
545 740 }
546 741 }
547 - do_action('bitform_Unique_entry', $uniqueFields, $validUniuqFields, $this->_form_id, $_POST);
742 + do_action('bitform_Unique_entry', $uniqueFields, $validUniuqFields, $this->_form_id, $postData);
548 743 }
549 744 $validateField = $formFieldValidator->validate('create', $this->_form_id);
550 745
551 746 if ($validateForm && $validateField && 0 === count($validUniuqFields)) {
@@ -567,15 +762,153 @@
567 762 return new WP_Error('token_expired', __('Token expired', 'bit-form'));
568 763 }
569 764 }
570 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 +
571 904 public function verifySubmissionNonce()
572 905 {
573 - if (!isset($_POST['t_identity']) && !isset($_POST['csrf'])) {
906 + if (!isset($_POST['t_identity']) || !isset($_POST['csrf'])) {
574 907 return false;
575 908 }
576 - $tIdenty = sanitize_text_field($_POST['t_identity']);
577 - $csrf = sanitize_text_field($_POST['csrf']);
909 + $tIdenty = sanitize_text_field(wp_unslash($_POST['t_identity']));
910 + $csrf = sanitize_text_field(wp_unslash($_POST['csrf']));
578 911 unset($_POST['t_identity'], $_POST['action'], $_POST['bitforms_id'], $_POST['csrf']);
579 912 return Helpers::csrfDecrypted($tIdenty, $csrf);
580 913 }
581 914
@@ -583,9 +916,9 @@
583 916 {
584 917 if (!current_user_can('manage_options')) {
585 918 $update_status = $this->formModel->update(
586 919 [
587 - 'views' => intval(static::$form[0]->views) + 1
920 + 'views' => intval($this->form[0]->views) + 1
588 921 ],
589 922 [
590 923 'id' => $this->form_id
591 924 ]
@@ -592,155 +925,126 @@
592 925 );
593 926 }
594 927 }
595 928
596 - public function checkSubmissionRestriction($checkedEmptySubmitted = true)
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)
597 934 {
598 935 $formContents = $this->getFormContent();
599 - $fromRestrictionSetitingsEnabled = empty($formContents->additional->enabled) ? [] : $formContents->additional->enabled;
600 - $fromRestrictionSetitings = empty($formContents->additional->settings) ? null : $formContents->additional->settings;
601 - if (is_null($formContents->additional->enabled) || is_null($formContents->additional->settings)) {
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)) {
602 941 return false;
603 942 }
943 +
604 944 $restrictionMessage = [];
605 945 $ipTool = new IpTool();
606 946 $ipAddress = $ipTool->getIP();
947 + $currentUserId = get_current_user_id();
948 +
607 949 foreach ($fromRestrictionSetitingsEnabled as $restrictionKey => $isEnabled) {
608 950 if ($isEnabled) {
609 - if ('entry_limit' === $restrictionKey && isset($fromRestrictionSetitings->{$restrictionKey})) {
610 - $formEntry = new FormEntryModel();
611 - $countResult = $formEntry->count(
612 - [
613 - 'form_id' => $this->form_id
614 - ]
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
615 959 );
616 - $count = !empty($countResult[0]) && !empty($countResult[0]->count) ? $countResult[0]->count : false;
617 - if ($count && $count >= intval($fromRestrictionSetitings->{$restrictionKey})) {
618 - $restrictionMessage[] = __('Sorry!! Entry limit exceeded', 'bit-form');
960 + if ($skipOnEdit) {
961 + continue;
619 962 }
620 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 +
621 986 if ('onePerIp' === $restrictionKey) {
622 987 $formEntry = new FormEntryModel();
623 - $countResult = $formEntry->count(
988 +
989 + $getResult = $formEntry->get(
990 + ['user_ip', 'status'],
624 991 [
625 992 'form_id' => $this->form_id,
626 - 'user_ip' => ip2long($ipAddress)
627 - ]
993 + 'user_ip' => (int) ip2long((string) $ipAddress)
994 + ],
628 995 );
629 - $count = !empty($countResult[0]) && !empty($countResult[0]->count) ? $countResult[0]->count : false;
630 996
631 - if ($count && $count > 0) {
632 - $restrictionMessage[] = __('Sorry!! You have already submitted', 'bit-form');
633 - }
634 - }
635 - if ('is_login' === $restrictionKey && 0 === get_current_user_id()) {
636 - $restrictionMessage[] = __($fromRestrictionSetitings->is_login->message, 'bit-form');
637 - }
638 - if ($checkedEmptySubmitted && 'empty_submission' === $restrictionKey) {
639 - $isEmpty = $this->checkEmptySubmission($_POST, $_FILES);
640 - if ($isEmpty) {
641 - $restrictionMessage[] = __($fromRestrictionSetitings->empty_submission->message, 'bit-form');
642 - }
643 - }
644 - if ('restrict_form' === $restrictionKey && isset($fromRestrictionSetitings->{$restrictionKey})) {
645 - $day = empty($fromRestrictionSetitings->{$restrictionKey}->day) ? null : $fromRestrictionSetitings->{$restrictionKey}->day;
646 - $date = empty($fromRestrictionSetitings->{$restrictionKey}->date) ? null : $fromRestrictionSetitings->{$restrictionKey}->date;
647 - $time = empty($fromRestrictionSetitings->{$restrictionKey}->time) ? null : $fromRestrictionSetitings->{$restrictionKey}->time;
997 + $count = 0;
998 + $status = 0;
648 999
649 - $isdayOk = $isdateOk = $istimeOk = true;
650 - $dayNotOkMsg = $dateNotOkMsg = $timeNotOkMsg = '';
651 - $dateTimeHelper = new DateTimeHelper();
652 - if (
653 - !empty($day)
654 - && is_array($day)
655 - && (in_array('Friday', $day)
656 - || in_array('Saturday', $day)
657 - || in_array('Sunday', $day)
658 - || in_array('Monday', $day)
659 - || in_array('Tuesday', $day)
660 - || in_array('Wednesday', $day)
661 - || in_array('Thursday', $day))
662 - && (!in_array($dateTimeHelper->getDay('full-name'), $day))
663 - ) {
664 - $isdayOk = false;
665 - $dayMsgVarsFormat = '';
666 - foreach ($day as $dayIndex => $dayValue) {
667 - if ($dayIndex > 0) {
668 - $dayMsgVarsFormat .= ', ';
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;
669 1007 }
670 - $dayMsgVarsFormat .= '%s';
671 1008 }
672 - $dayNotOkMsg = vsprintf(__("in $dayMsgVarsFormat", 'bit-form'), $day);
673 1009 }
674 - if (
675 - !empty($day)
676 - && is_array($day)
677 - && (in_array('Custom', $day))
678 - ) {
679 - $startDate = empty($date->from) ? '00-00-0000' : $date->from;
680 - $endDate = empty($date->to) ? '00-00-0000' : $date->to;
681 - $dateFormat = preg_match('/^[0-9]{4}-[0-9]{2}-[0-9]{2}$/', $startDate) ? 'Y-m-d' : 'm-d-Y';
682 - if (!empty($date->from) && false !== strpos($startDate, 'T')) {
683 - $startDate = $dateTimeHelper->getDate($startDate, false, null, $dateFormat);
684 - }
685 - if (!empty($date->to) && false !== strpos($endDate, 'T')) {
686 - $endDate = $dateTimeHelper->getDate($endDate, false, null, $dateFormat);
687 - }
688 - $currentDate = $dateTimeHelper->getDate(null, null, null, $dateFormat);
689 - if (!($currentDate >= $startDate && $currentDate <= $endDate)) {
690 - $isdateOk = false;
691 - $dateNotOkMsg = sprintf(__('within %s to %s', 'bit-form'), $startDate, $endDate);
692 - }
693 - }
694 1010
695 - if (!empty($time)) {
696 - $startTime = empty($time->from) ? '00:00' : $time->from;
697 - $endTime = empty($time->to) ? '23:59.999' : $time->to;
698 - $currentTime = $dateTimeHelper->getTime(null, null, null, 'H:i');
699 - if (!($currentTime >= $startTime && $currentTime <= $endTime)) {
700 - $istimeOk = false;
701 - $startTime = $dateTimeHelper->getTime($startTime, 'H:i', null);
702 - $endTime = $dateTimeHelper->getTime($endTime, 'H:i', null);
703 - $isTimeOk = false;
704 - $timeNotOkMsg = sprintf(__('%s to %s', 'bit-form'), $startTime, $endTime);
705 - }
706 - }
1011 + if ($count > 0 && 9 !== (int) $status) {
1012 + $onePerIp = __('Sorry!! You have already submitted from this IP address', 'bit-form');
707 1013
708 - if (!($isdateOk && $isdayOk && $istimeOk)) {
709 - if (!$isdayOk) {
710 - $restrictionMessage[] = !empty($timeNotOkMsg) ? sprintf(__('Form is available %s From %s', 'bit-form'), $dayNotOkMsg, $timeNotOkMsg) :
711 - sprintf(__('Form is available %s', 'bit-form'), $dayNotOkMsg, $timeNotOkMsg);
712 - } elseif (!$isdateOk) {
713 - $restrictionMessage[] = !empty($timeNotOkMsg) ? sprintf(__('Form is available %s From %s', 'bit-form'), $dateNotOkMsg, $timeNotOkMsg) :
714 - sprintf(__('Form is available %s', 'bit-form'), $dateNotOkMsg, $timeNotOkMsg);
715 - } elseif (!$istimeOk) {
716 - $restrictionMessage[] = sprintf(__('Form is available on %s', 'bit-form'), $timeNotOkMsg);
717 - }
1014 + $onePerIp = apply_filters(
1015 + 'bitform_filter_restriction_one_per_ip_message',
1016 + $onePerIp,
1017 + $this->form_id
1018 + );
1019 +
1020 + $restrictionMessage[] = $onePerIp;
718 1021 }
719 1022 }
720 - if ('blocked_ip' === $restrictionKey && isset($fromRestrictionSetitings->{$restrictionKey})) {
721 - $isIpBlocked = false;
722 - foreach ($fromRestrictionSetitings->{$restrictionKey} as $ipIndex => $ipDetails) {
723 - if (!empty($ipDetails->status) && $ipDetails->status && !empty($ipDetails->ip) && $ipDetails->ip === $ipAddress) {
724 - $isIpBlocked = true;
725 - break;
726 - }
727 - }
728 - if ($isIpBlocked) {
729 - $restrictionMessage[] = sprintf(__('Sorry!! Your IP address is %s, Blocked from submitting the form', 'bit-form'), $ipAddress);
730 - }
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;
731 1033 }
732 - if ('private_ip' === $restrictionKey && isset($fromRestrictionSetitings->{$restrictionKey})) {
733 - $isIpWhiteListed = false;
734 - foreach ($fromRestrictionSetitings->{$restrictionKey} as $ipIndex => $ipDetails) {
735 - if (!empty($ipDetails->status) && $ipDetails->status && !empty($ipDetails->ip) && $ipDetails->ip === $ipAddress) {
736 - $isIpWhiteListed = true;
737 - break;
738 - }
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;
739 1046 }
740 - if (!$isIpWhiteListed) {
741 - $restrictionMessage[] = sprintf(__('Sorry!! Your IP address is %s, Blocked from submitting the form', 'bit-form'), $ipAddress);
742 - }
743 1047 }
744 1048 }
745 1049 }
746 1050 return $restrictionMessage;
@@ -746,51 +1050,21 @@
746 1050 return $restrictionMessage;
747 1051 }
748 1052
749 1053 /**
750 - * Will check if form is submitted by a bot
751 - *
752 - * @return Boolean true - if submitted by bot else false
753 - */
1054 + * Will check if form is submitted by a bot
1055 + *
1056 + * @return Boolean true - if submitted by bot else false
1057 + */
754 1058 public function isTrappedInHoneypot()
755 1059 {
756 - $isHoneyPot = false;
757 -
758 - if (!$this->isHoneypotActive()) {
759 - return false;
760 - }
761 -
762 - $token = $_POST['b_h_t'];
763 - $pattern = '/^([a-zA-Z0-9]*_[a-zA-Z0-9]*){4}$/';
764 - $decryptedToken = base64_decode(base64_decode($token));
765 -
766 - preg_match($pattern, $decryptedToken, $validToken);
767 -
768 - if ($validToken) {
769 - if (isset($_POST[$token]) && empty($_POST[$token])) {
770 - $isHoneyPot = false;
771 - } else {
772 - $isHoneyPot = true;
773 - }
774 - } else {
775 - $isHoneyPot = true;
776 - }
777 -
778 - if (isset($_POST[$token])) {
779 - unset($_POST[$token]);
780 - }
781 - unset($_POST['b_h_t']);
782 - return $isHoneyPot;
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));
783 1062 }
784 1063
785 1064 public function isHoneypotActive()
786 1065 {
787 - $formContents = $this->getFormContent();
788 - $enabled = empty($formContents->additional->enabled) ? null : $formContents->additional->enabled;
789 - if (!empty($enabled->honeypot) && $enabled->honeypot) {
790 - return true;
791 - }
792 - return false;
1066 + return (bool) apply_filters('bitform_is_honeypot_active', false, $this->_form_id, $this->getFormContent());
793 1067 }
794 1068
795 1069 public function checkPaymentFields()
796 1070 {
@@ -798,11 +1072,14 @@
798 1072 $fields = $formContents->fields;
799 1073
800 1074 $payments = [];
801 1075 foreach ($fields as $fldData) {
1076 + if (!is_object($fldData)) {
1077 + continue;
1078 + }
802 1079 if ('paypal' === $fldData->typ && property_exists($fldData, 'payIntegID')) {
803 1080 $payments['paypalKey'] = $this->getClientKey($fldData->payIntegID, 'clientID');
804 - } elseif ('razorpay' === $fldData->typ && property_exists($fldData->options, 'payIntegID')) {
1081 + } elseif ('razorpay' === $fldData->typ && isset($fldData->options) && is_object($fldData->options) && property_exists($fldData->options, 'payIntegID')) {
805 1082 $payments['razorpayKey'] = $this->getClientKey($fldData->options->payIntegID, 'apiKey');
806 1083 }
807 1084 }
808 1085
@@ -815,10 +1092,13 @@
815 1092 if (!empty($integID)) {
816 1093 $integrationHandler = new IntegrationHandler(0);
817 1094 $integration = $integrationHandler->getAIntegration($integID, 'app', 'payments');
818 1095 if (!is_wp_error($integration)) {
819 - $integration_details = json_decode($integration[0]->integration_details);
820 - $client = base64_encode($integration_details->{$keyName});
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 + }
821 1101 }
822 1102 }
823 1103 return $client;
824 1104 }
@@ -824,38 +1104,11 @@
824 1104 }
825 1105
826 1106 public function getSuccessMessageMarkups()
827 1107 {
828 - if (is_null($this->_work_flows)) {
829 - $workFlowManager = new WorkFlowHandler($this->form_id);
830 - $this->_work_flows = $workFlowManager->getAllworkFlow();
831 - }
832 -
833 - $ids = [];
834 - foreach ($this->_work_flows as $msgItem) {
835 - foreach ($msgItem['conditions'] as $condition) {
836 - if (isset($condition->actions->success)) {
837 - foreach ($condition->actions->success as $msg) {
838 - if ('successMsg' === $msg->type && isset($msg->details->id)) {
839 - $idObj = json_decode(stripslashes($msg->details->id));
840 - if (is_object($idObj) && !empty($idObj->id)) {
841 - array_push($ids, $idObj->id);
842 - }
843 - }
844 - }
845 - }
846 - if (isset($condition->actions->failure)) {
847 - $idObj = json_decode(stripslashes($condition->actions->failure));
848 - if (is_object($idObj) && !empty($idObj->id)) {
849 - array_push($ids, $idObj->id);
850 - }
851 - }
852 - }
853 - }
854 - $ids = array_unique($ids);
855 1108 if (is_null($this->_conf_messages)) {
856 1109 $successMsgHandler = new SuccessMessageHandler($this->form_id);
857 - $this->_conf_messages = $successMsgHandler->getMessages($ids);
1110 + $this->_conf_messages = $successMsgHandler->getAllMessage();
858 1111 }
859 1112
860 1113 $messageMarkups = '';
861 1114 if (is_wp_error($this->_conf_messages)) {
@@ -862,8 +1115,12 @@
862 1115 return $messageMarkups;
863 1116 }
864 1117
865 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 + }
866 1123 $messageMarkups .= $this->messageMarkup($msgItem);
867 1124 }
868 1125
869 1126 return $messageMarkups;
@@ -870,38 +1127,47 @@
870 1127 }
871 1128
872 1129 public function getFormAbandonmentMessage()
873 1130 {
874 - if (class_exists('\BitCode\BitFormPro\Admin\FormSettings\FormAbandonment')) {
875 - $formAbandonmentSettings = FormAbandonment::getFormAbandonmentSettings($this->form_id);
876 - $msg = '';
877 - if (isset($formAbandonmentSettings->showWarningMsg) && $formAbandonmentSettings->showWarningMsg && !empty($formAbandonmentSettings->warningMsg)) {
878 - $msg = $formAbandonmentSettings->warningMsg;
879 - $msg = '<div class="bf-form-msg active warning">' . wp_kses_post($msg) . '</div>';
880 - }
881 - return $msg;
882 - }
1131 + $msg = apply_filters('bitform_form_abandonment_warning_markup', '', $this->form_id);
1132 + return is_string($msg) ? $msg : '';
883 1133 }
884 1134
1135 + public function getFormAbandonmentSettings()
1136 + {
1137 + return apply_filters('bitform_form_abandonment_settings', null, $this->form_id);
1138 + }
1139 +
885 1140 private function messageMarkup($msg)
886 1141 {
887 1142 $msgId = $msg->id;
888 1143 $msgConfig = json_decode($msg->message_config);
889 - $scrollClass = 'below' === $msgConfig->msgType ? 'scroll' : '';
1144 + $msgType = (is_object($msgConfig) && isset($msgConfig->msgType)) ? $msgConfig->msgType : 'below';
1145 + $scrollClass = 'below' === $msgType ? 'scroll' : '';
890 1146
891 - return <<<SUCCESSMSG
892 - <div role="dialog" aria-hidden="true" data-modal-backdrop="true" class="{$this->getAtomicCls("msg-container-{$msgId}")} deactive {$scrollClass}">
893 - <div data-contentid="{$this->getFormIdentifier()}" data-msgid="{$msgId}" role="button" class="{$this->getAtomicCls("msg-background-{$msgId}")} msg-backdrop">
894 - <div class="bf-msg-content {$this->getAtomicCls("msg-content-{$msgId}")}">
895 - <button data-contentid="{$this->getFormIdentifier()}" data-msgid="{$msgId}" class="{$this->getAtomicCls("close-{$msgId}")} bf-msg-close" type="button">
896 - <svg class="{$this->getAtomicCls("close-icn-{$msgId}")}" viewBox="0 0 30 30">
897 - <line fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" x1="4" y1="3.88" x2="26" y2="26.12"></line>
898 - <line fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" x1="26" y1="3.88" x2="4" y2="26.12"></line>
899 - </svg>
900 - </button>
901 - <div class="msg-content"></div>
902 - </div>
903 - </div>
904 - </div>
905 -SUCCESSMSG;
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>';
906 1172 }
907 1173 }