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