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 +298 -84 3.1.33.3.1 View file →
@@ -17,12 +17,15 @@
17 17 use BitCode\BitForm\Core\Form\Validator\FormFieldValidator;
18 18 use BitCode\BitForm\Core\Integration\IntegrationHandler;
19 19 use BitCode\BitForm\Core\Messages\SuccessMessageHandler;
20 20 use BitCode\BitForm\Core\Util\ApiResponse as UtilApiResponse;
21 +use BitCode\BitForm\Core\Util\EscapingHelper;
22 +use BitCode\BitForm\Core\Util\FieldValueHandler;
23 +use BitCode\BitForm\Core\Util\FrontendHelpers;
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 29 use BitCode\BitForm\GlobalHelper;
27 30 use WP_Error;
28 31
@@ -30,9 +33,8 @@
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;
36 38 private static $_instance = [];
37 39
38 40 // private $_has_upload = false;
@@ -78,9 +80,9 @@
78 80 // unset($submitted_data['bit-form-submit-btn']);
79 81 return array_keys($submitted_data);
80 82 }
81 83
82 - 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)
83 85 {
84 86 $formContents = $this->getFormContent();
85 87 $formAtomicClsMap = $this->getAtomicClsMap();
86 88 if (!empty($fields)) {
@@ -93,14 +95,14 @@
93 95 );
94 96 $formContents->fields = empty($workFlowreturnedOnLoad['fields']) ? $formContents->fields : $workFlowreturnedOnLoad['fields'];
95 97 }
96 98 $formViewer = new FormViewer($this, $formContents, $formAtomicClsMap, $errorMessages, $previousValue);
97 - $isRestricted = $this->checkSubmissionRestriction(false);
99 + $isRestricted = $this->checkSubmissionRestriction(false, $isEntryEdit);
98 100 $msg = !empty($isRestricted) ? $isRestricted[0] : '';
99 101 return $formViewer->getView($hasFile, $msg);
100 102 }
101 103
102 - 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)
103 105 {
104 106 $formContents = $this->getFormContent();
105 107 $formAtomicClsMap = $this->getAtomicClsMap();
106 108 if (!empty($fields)) {
@@ -113,14 +115,14 @@
113 115 );
114 116 $formContents->fields = empty($workFlowreturnedOnLoad['fields']) ? $formContents->fields : $workFlowreturnedOnLoad['fields'];
115 117 }
116 118 $formViewer = new FormViewer($this, $formContents, $formAtomicClsMap, $errorMessages, $previousValue);
117 - $isRestricted = $this->checkSubmissionRestriction(false);
119 + $isRestricted = $this->checkSubmissionRestriction(false, $isEntryEdit);
118 120 $msg = !empty($isRestricted) ? $isRestricted[0] : '';
119 121 return $formViewer->getConversationalView($hasFile, $msg);
120 122 }
121 123
122 - public function checkEmptySubmission($data, $file)
124 + public function checkEmptySubmission($data, $file, $isEntryEdit = false)
123 125 {
124 126 $formFields = $this->getFields();
125 127 foreach ($formFields as $key => $field) {
126 128 $fieldType = $field['type'];
@@ -131,8 +133,16 @@
131 133 if ('decision-box' === $fieldType || 'gdpr' === $fieldType) {
132 134 continue;
133 135 }
134 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 + }
135 145 if ($this->isRepeatedField($key)) {
136 146 $fileData = !empty($file[$key]) ? $file[$key] : [];
137 147 $dataVal = !empty($data[$key]) ? $data[$key] : [];
138 148 if (!$this->checkRepeatedFieldEmptySubmission($isFileType, $dataVal, $fileData)) {
@@ -220,8 +230,74 @@
220 230
221 231 return $post;
222 232 }
223 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 +
224 300 public function handleSubmission()
225 301 {
226 302 // CSRF verified via verifySubmissionNonce() before this method is called. All $_POST reads below occur after that verification.
227 303 $this->fieldNameReplaceOfPost();
@@ -231,9 +307,9 @@
231 307 $validated = apply_filters('bitform_filter_form_validation', $validated, $this->_form_id);
232 308
233 309 if (true === $validated) {
234 310 do_action('bitform_validation_success', $this->_form_id);
235 - unset($_POST['hidden_fields']);
311 + $this->discardHiddenFieldValues();
236 312
237 313 $redirectPage = '';
238 314 $regSuccMsg = '';
239 315
@@ -243,17 +319,18 @@
243 319 $parameter = $this->getParams();
244 320 $existAuthFilter = has_filter('bitform_wp_user_auth');
245 321
246 322 if (true === $existAuthFilter) {
247 - $result = apply_filters('bitform_wp_user_auth', $existAuth[0], $unslashed_post, $parameter);
323 + $authPostData = $this->resolveConfirmChildValues($unslashed_post);
324 + $result = apply_filters('bitform_wp_user_auth', $existAuth[0], $authPostData, $parameter);
248 325
249 - $result = apply_filters('bitform_filter_wp_user_auth_response', $result, $this->_form_id, $unslashed_post, $parameter);
326 + $result = apply_filters('bitform_filter_wp_user_auth_response', $result, $this->_form_id, $authPostData, $parameter);
250 327
251 - do_action('bitform_wp_user_auth_response', $result, $this->_form_id, $unslashed_post, $parameter);
328 + do_action('bitform_wp_user_auth_response', $result, $this->_form_id, $authPostData, $parameter);
252 329
253 330 if (isset($result['auth_type']) && 'register' === $result['auth_type']) {
254 331 if (!$result['success']) {
255 - return new WP_Error('errors', esc_html($result['message']));
332 + return new WP_Error('errors', self::authErrorMessage($result['message']));
256 333 } elseif (isset($result['success'])) {
257 334 $redirectPage = $result['redirectPage'];
258 335 $regSuccMsg = $result['message'];
259 336 }
@@ -258,9 +335,9 @@
258 335 $regSuccMsg = $result['message'];
259 336 }
260 337 } else {
261 338 if (!$result['success']) {
262 - return new WP_Error('errors', esc_html($result['message']));
339 + return new WP_Error('errors', self::authErrorMessage($result['message']));
263 340 } else {
264 341 return $result;
265 342 }
266 343 }
@@ -286,11 +363,13 @@
286 363 $allFormIntegrations = $integrationHandler->getAllIntegration('app', 'gReCaptchaV3');
287 364 if (!is_wp_error($allFormIntegrations)) {
288 365 foreach ($allFormIntegrations as $integration) {
289 366 if (!is_null($integration->integration_type) && 'gReCaptchaV3' === $integration->integration_type) {
290 - $integrationDetails = json_decode($integration->integration_details);
291 - $integrationDetails->id = $integration->id;
292 - $reCAPTCHA = $integrationDetails;
367 + $integrationDetails = Utilities::jsonObj($integration->integration_details);
368 + if ($integrationDetails) {
369 + $integrationDetails->id = $integration->id;
370 + $reCAPTCHA = $integrationDetails;
371 + }
293 372 }
294 373 }
295 374 }
296 375 if (!empty($reCAPTCHA->secretKey)) {
@@ -330,9 +409,9 @@
330 409 public function handleUpdateEntry()
331 410 {
332 411 // Entry token or capability verified by caller (FrontendAjax::update_entry). All $_POST reads occur after that check.
333 412 $this->fieldNameReplaceOfPost();
334 - $validated = $this->beforeSubmittedValidate();
413 + $validated = $this->beforeSubmittedValidate(true, true);
335 414 $validated = apply_filters('bitform_filter_form_validation', $validated, $this->_form_id);
336 415
337 416 $entryID = isset($_REQUEST['entryID']) ? sanitize_text_field(wp_unslash($_REQUEST['entryID'])) : null;
338 417 $GLOBALS['bitform_entry_id'] = $entryID;
@@ -340,9 +419,10 @@
340 419 return new WP_Error('empty_form', __('Entries id is invalid', 'bit-form'));
341 420 }
342 421 if (true === $validated) {
343 422 do_action('bitform_validation_success', $this->_form_id);
344 - unset($_POST['hidden_fields'], $_POST['entryID']);
423 + $this->discardHiddenFieldValues();
424 + unset($_POST['entryID']);
345 425
346 426 $redirectPage = '';
347 427 $regSuccMsg = '';
348 428 $postData = wp_unslash($_POST);
@@ -352,13 +432,14 @@
352 432 $parameter = $this->getParams();
353 433 $existAuthFilter = has_filter('bitform_wp_user_auth');
354 434
355 435 if (true === $existAuthFilter) {
356 - $result = apply_filters('bitform_wp_user_auth', $existAuth[0], $postData, $parameter);
436 + $authPostData = $this->resolveConfirmChildValues($postData);
437 + $result = apply_filters('bitform_wp_user_auth', $existAuth[0], $authPostData, $parameter);
357 438
358 439 if (isset($result['auth_type']) && 'register' === $result['auth_type']) {
359 440 if (!$result['success']) {
360 - return new WP_Error('errors', esc_html($result['message']));
441 + return new WP_Error('errors', self::authErrorMessage($result['message']));
361 442 } elseif (isset($result['success'])) {
362 443 $redirectPage = $result['redirectPage'];
363 444 $regSuccMsg = $result['message'];
364 445 }
@@ -363,9 +444,9 @@
363 444 $regSuccMsg = $result['message'];
364 445 }
365 446 } else {
366 447 if (!$result['success']) {
367 - return new WP_Error('errors', esc_html($result['message']));
448 + return new WP_Error('errors', self::authErrorMessage($result['message']));
368 449 } else {
369 450 return $result;
370 451 }
371 452 }
@@ -392,11 +473,13 @@
392 473 $allFormIntegrations = $integrationHandler->getAllIntegration('app', 'gReCaptchaV3');
393 474 if (!is_wp_error($allFormIntegrations)) {
394 475 foreach ($allFormIntegrations as $integration) {
395 476 if (!is_null($integration->integration_type) && 'gReCaptchaV3' === $integration->integration_type) {
396 - $integrationDetails = json_decode($integration->integration_details);
397 - $integrationDetails->id = $integration->id;
398 - $reCAPTCHA = $integrationDetails;
477 + $integrationDetails = Utilities::jsonObj($integration->integration_details);
478 + if ($integrationDetails) {
479 + $integrationDetails->id = $integration->id;
480 + $reCAPTCHA = $integrationDetails;
481 + }
399 482 }
400 483 }
401 484 }
402 485 if (!empty($reCAPTCHA->secretKey)) {
@@ -432,11 +515,96 @@
432 515 do_action('bitform_validation_error', $this->_form_id, $validated);
433 516 return $validated;
434 517 }
435 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 +
436 604 public function validateFormSubmission($submitted_data)
437 605 {
438 - $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'] : '');
439 607 $submitted_fields = $this->getSubmittedFields($submitted_data);
440 608 $form_fields = $this->getFields();
441 609 $form_fields_names = array_keys($form_fields);
442 610 if ($this->isGCLIDEnabled()) {
@@ -442,9 +610,9 @@
442 610 if ($this->isGCLIDEnabled()) {
443 611 array_push($form_fields_names, 'GCLID');
444 612 }
445 613 foreach ($submitted_fields as $field) {
446 - 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)) {
447 615 unset($submitted_data[$field]);
448 616 }
449 617 }
450 618 return $submitted_data;
@@ -449,13 +617,13 @@
449 617 }
450 618 return $submitted_data;
451 619 }
452 620
453 - public function beforeSubmittedValidate($verifyCaptcha = true)
621 + public function beforeSubmittedValidate($verifyCaptcha = true, $isEntryEdit = false)
454 622 {
455 623 if ($this->verifySubmissionNonce()) {
456 624 if ($this->isExist()) {
457 - $isRestricted = $this->checkSubmissionRestriction();
625 + $isRestricted = $this->checkSubmissionRestriction(true, $isEntryEdit);
458 626 if ($isRestricted && !empty($isRestricted)) {
459 627 return new WP_Error('spam_detection', $isRestricted[0]);
460 628 }
461 629 $postData = wp_unslash($_POST);
@@ -492,32 +660,73 @@
492 660 }
493 661 $validateForm = $this->validateFormSubmission($postData);
494 662 $validateFormFiles = $this->validateFormSubmission($filesData);
495 663 $validateForm = array_merge($validateForm, $validateFormFiles);
496 - $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();
497 670 // check if form-current-step is set and form is multi-step
498 671 $formCurrentStep = isset($_POST['form-current-step']) ? sanitize_text_field(wp_unslash($_POST['form-current-step'])) : null;
499 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).
500 678 $formContents = $this->getFormContent();
501 - $layout = $formContents->layout;
679 + $layout = isset($formContents->layout) ? $formContents->layout : null;
502 680 $stepIndex = (int) $formCurrentStep - 1;
503 - $stepLayout = $layout[$stepIndex]->layout->lg;
504 - $nestedLayout = $formContents->nestedLayout;
505 - $step_fields = [];
506 - foreach ($stepLayout as $lay) {
507 - $fk = $lay->i;
508 - if (isset($nestedLayout->{$fk})) {
509 - $nestedLg = $nestedLayout->{$fk}->lg;
510 - foreach ($nestedLg as $nestedLay) {
511 - $nestedFk = $nestedLay->i;
512 - $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;
513 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 + }
514 709 }
515 - $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 + }
516 722 }
517 - $form_fields = $step_fields;
518 723 }
519 - $formFieldValidator = new FormFieldValidator($form_fields, $postData, $filesData);
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);
520 729 $validUniuqFields = [];
521 730 $existFilter = has_filter('bitform_check_duplicate_entry');
522 731 if (true === $existFilter) {
523 732 $validUniuqFields = apply_filters('bitform_check_duplicate_entry', $form_fields, $postData);
@@ -567,11 +776,13 @@
567 776 $allFormIntegrations = $integrationHandler->getAllIntegration('app', $captchaSettings ? 'gReCaptcha' : 'gReCaptchaV3');
568 777 if (!is_wp_error($allFormIntegrations)) {
569 778 foreach ($allFormIntegrations as $integration) {
570 779 if (!is_null($integration->integration_type) && $integration->integration_type === ($captchaSettings ? 'gReCaptcha' : 'gReCaptchaV3')) {
571 - $integrationDetails = json_decode($integration->integration_details);
572 - $integrationDetails->id = $integration->id;
573 - $reCAPTCHA = $integrationDetails;
780 + $integrationDetails = Utilities::jsonObj($integration->integration_details);
781 + if ($integrationDetails) {
782 + $integrationDetails->id = $integration->id;
783 + $reCAPTCHA = $integrationDetails;
784 + }
574 785 }
575 786 }
576 787 }
577 788 if (!empty($reCAPTCHA->secretKey)) {
@@ -615,11 +826,13 @@
615 826
616 827 if (!is_wp_error($allFormIntegrations)) {
617 828 foreach ($allFormIntegrations as $integration) {
618 829 if (!is_null($integration->integration_type) && 'hcaptcha' === $integration->integration_type) {
619 - $integrationDetails = json_decode($integration->integration_details);
620 - $integrationDetails->id = $integration->id;
621 - $hCaptcha = $integrationDetails;
830 + $integrationDetails = Utilities::jsonObj($integration->integration_details);
831 + if ($integrationDetails) {
832 + $integrationDetails->id = $integration->id;
833 + $hCaptcha = $integrationDetails;
834 + }
622 835 }
623 836 }
624 837 }
625 838
@@ -703,9 +916,9 @@
703 916 {
704 917 if (!current_user_can('manage_options')) {
705 918 $update_status = $this->formModel->update(
706 919 [
707 - 'views' => intval(static::$form[0]->views) + 1
920 + 'views' => intval($this->form[0]->views) + 1
708 921 ],
709 922 [
710 923 'id' => $this->form_id
711 924 ]
@@ -712,9 +925,13 @@
712 925 );
713 926 }
714 927 }
715 928
716 - 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)
717 934 {
718 935 $formContents = $this->getFormContent();
719 936 $additionalSettings = isset($formContents->additional) ? $formContents->additional : null;
720 937 $fromRestrictionSetitingsEnabled = empty($additionalSettings->enabled) ? [] : $additionalSettings->enabled;
@@ -730,8 +947,21 @@
730 947 $currentUserId = get_current_user_id();
731 948
732 949 foreach ($fromRestrictionSetitingsEnabled as $restrictionKey => $isEnabled) {
733 950 if ($isEnabled) {
951 + // Quota rules gate creating an entry, so an edit skips them; access-control keys stay.
952 + $skippableOnEdit = ['onePerIp', 'entry_limit', 'entry_limit_by_user', 'restrict_form'];
953 + if ($isEntryEdit && in_array($restrictionKey, $skippableOnEdit, true)) {
954 + $skipOnEdit = apply_filters(
955 + 'bitform_skip_restriction_on_entry_edit',
956 + true,
957 + $restrictionKey,
958 + $this->form_id
959 + );
960 + if ($skipOnEdit) {
961 + continue;
962 + }
963 + }
734 964 /**
735 965 * Allow add-ons to handle any restriction key (Pro-only restrictions
736 966 * should be implemented in the add-on, not shipped in the free plugin).
737 967 *
@@ -801,9 +1031,9 @@
801 1031
802 1032 $restrictionMessage[] = $is_login_messages;
803 1033 }
804 1034 if ($checkedEmptySubmitted && 'empty_submission' === $restrictionKey) {
805 - $isEmpty = $this->checkEmptySubmission(wp_unslash($_POST), GlobalHelper::sanitize_files_input($_FILES));
1035 + $isEmpty = $this->checkEmptySubmission(wp_unslash($_POST), GlobalHelper::sanitize_files_input($_FILES), $isEntryEdit);
806 1036 if ($isEmpty) {
807 1037 $restriction = $fromRestrictionSetitings->empty_submission->message;
808 1038
809 1039 $restriction = apply_filters(
@@ -842,11 +1072,14 @@
842 1072 $fields = $formContents->fields;
843 1073
844 1074 $payments = [];
845 1075 foreach ($fields as $fldData) {
1076 + if (!is_object($fldData)) {
1077 + continue;
1078 + }
846 1079 if ('paypal' === $fldData->typ && property_exists($fldData, 'payIntegID')) {
847 1080 $payments['paypalKey'] = $this->getClientKey($fldData->payIntegID, 'clientID');
848 - } 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')) {
849 1082 $payments['razorpayKey'] = $this->getClientKey($fldData->options->payIntegID, 'apiKey');
850 1083 }
851 1084 }
852 1085
@@ -859,10 +1092,13 @@
859 1092 if (!empty($integID)) {
860 1093 $integrationHandler = new IntegrationHandler(0);
861 1094 $integration = $integrationHandler->getAIntegration($integID, 'app', 'payments');
862 1095 if (!is_wp_error($integration)) {
863 - $integration_details = json_decode($integration[0]->integration_details);
864 - $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 + }
865 1101 }
866 1102 }
867 1103 return $client;
868 1104 }
@@ -868,38 +1104,11 @@
868 1104 }
869 1105
870 1106 public function getSuccessMessageMarkups()
871 1107 {
872 - if (is_null($this->_work_flows)) {
873 - $workFlowManager = new WorkFlowHandler($this->form_id);
874 - $this->_work_flows = $workFlowManager->getAllworkFlow();
875 - }
876 -
877 - $ids = [];
878 - foreach ($this->_work_flows as $msgItem) {
879 - foreach ($msgItem['conditions'] as $condition) {
880 - if (isset($condition->actions->success)) {
881 - foreach ($condition->actions->success as $msg) {
882 - if ('successMsg' === $msg->type && isset($msg->details->id)) {
883 - $idObj = json_decode(stripslashes($msg->details->id));
884 - if (is_object($idObj) && !empty($idObj->id)) {
885 - array_push($ids, $idObj->id);
886 - }
887 - }
888 - }
889 - }
890 - if (isset($condition->actions->failure)) {
891 - $idObj = json_decode(stripslashes($condition->actions->failure));
892 - if (is_object($idObj) && !empty($idObj->id)) {
893 - array_push($ids, $idObj->id);
894 - }
895 - }
896 - }
897 - }
898 - $ids = array_unique($ids);
899 1108 if (is_null($this->_conf_messages)) {
900 1109 $successMsgHandler = new SuccessMessageHandler($this->form_id);
901 - $this->_conf_messages = $successMsgHandler->getMessages($ids);
1110 + $this->_conf_messages = $successMsgHandler->getAllMessage();
902 1111 }
903 1112
904 1113 $messageMarkups = '';
905 1114 if (is_wp_error($this->_conf_messages)) {
@@ -906,8 +1115,12 @@
906 1115 return $messageMarkups;
907 1116 }
908 1117
909 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 + }
910 1123 $messageMarkups .= $this->messageMarkup($msgItem);
911 1124 }
912 1125
913 1126 return $messageMarkups;
@@ -927,9 +1140,10 @@
927 1140 private function messageMarkup($msg)
928 1141 {
929 1142 $msgId = $msg->id;
930 1143 $msgConfig = json_decode($msg->message_config);
931 - $scrollClass = 'below' === $msgConfig->msgType ? 'scroll' : '';
1144 + $msgType = (is_object($msgConfig) && isset($msgConfig->msgType)) ? $msgConfig->msgType : 'below';
1145 + $scrollClass = 'below' === $msgType ? 'scroll' : '';
932 1146
933 1147 return '<div
934 1148 role="dialog"
935 1149 aria-hidden="true"