PluginProbe
Bit Form – Contact Form, Payment Forms, Multi Step Forms, Calculator & Custom Form Builder / V3.3.2
Bit Form – Contact Form, Payment Forms, Multi Step Forms, Calculator & Custom Form Builder vV3.3.2
V3.3.2 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 All 139 releases
← All changes | includes/Frontend/Form/FrontendFormManager.php +536 -266 3.1.4 → V3.3.2 View file →
@@ -17,12 +17,16 @@
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\Log;
27 +use BitCode\BitForm\Core\Util\Utilities;
23 28 use BitCode\BitForm\Core\WorkFlow\WorkFlow;
24 -use BitCode\BitForm\Core\WorkFlow\WorkFlowHandler;
25 29 use BitCode\BitForm\Frontend\Form\View\FormViewer;
26 30 use BitCode\BitForm\GlobalHelper;
27 31 use WP_Error;
28 32
@@ -28,11 +32,13 @@
28 32
29 33 final class FrontendFormManager extends FormManager
30 34 {
31 35 private $_form_identifier;
36 +
37 + /** @var array|null reCAPTCHA v3 verdict, reused for the log */
38 + private $recaptchaV3Verification;
32 39 private $_form_token;
33 40 private $_form_id;
34 - private $_work_flows;
35 41 private $_conf_messages;
36 42 private static $_instance = [];
37 43
38 44 // private $_has_upload = false;
@@ -78,9 +84,9 @@
78 84 // unset($submitted_data['bit-form-submit-btn']);
79 85 return array_keys($submitted_data);
80 86 }
81 87
82 - public function formView($fields = null, $hasFile = false, $errorMessages = null, $previousValue = null)
88 + public function formView($fields = null, $hasFile = false, $errorMessages = null, $previousValue = null, $isEntryEdit = false)
83 89 {
84 90 $formContents = $this->getFormContent();
85 91 $formAtomicClsMap = $this->getAtomicClsMap();
86 92 if (!empty($fields)) {
@@ -93,14 +99,14 @@
93 99 );
94 100 $formContents->fields = empty($workFlowreturnedOnLoad['fields']) ? $formContents->fields : $workFlowreturnedOnLoad['fields'];
95 101 }
96 102 $formViewer = new FormViewer($this, $formContents, $formAtomicClsMap, $errorMessages, $previousValue);
97 - $isRestricted = $this->checkSubmissionRestriction(false);
103 + $isRestricted = $this->checkSubmissionRestriction(false, $isEntryEdit);
98 104 $msg = !empty($isRestricted) ? $isRestricted[0] : '';
99 105 return $formViewer->getView($hasFile, $msg);
100 106 }
101 107
102 - public function conversationalFormView($fields = null, $hasFile = false, $errorMessages = null, $previousValue = null)
108 + public function conversationalFormView($fields = null, $hasFile = false, $errorMessages = null, $previousValue = null, $isEntryEdit = false)
103 109 {
104 110 $formContents = $this->getFormContent();
105 111 $formAtomicClsMap = $this->getAtomicClsMap();
106 112 if (!empty($fields)) {
@@ -113,14 +119,14 @@
113 119 );
114 120 $formContents->fields = empty($workFlowreturnedOnLoad['fields']) ? $formContents->fields : $workFlowreturnedOnLoad['fields'];
115 121 }
116 122 $formViewer = new FormViewer($this, $formContents, $formAtomicClsMap, $errorMessages, $previousValue);
117 - $isRestricted = $this->checkSubmissionRestriction(false);
123 + $isRestricted = $this->checkSubmissionRestriction(false, $isEntryEdit);
118 124 $msg = !empty($isRestricted) ? $isRestricted[0] : '';
119 125 return $formViewer->getConversationalView($hasFile, $msg);
120 126 }
121 127
122 - public function checkEmptySubmission($data, $file)
128 + public function checkEmptySubmission($data, $file, $isEntryEdit = false)
123 129 {
124 130 $formFields = $this->getFields();
125 131 foreach ($formFields as $key => $field) {
126 132 $fieldType = $field['type'];
@@ -131,8 +137,16 @@
131 137 if ('decision-box' === $fieldType || 'gdpr' === $fieldType) {
132 138 continue;
133 139 }
134 140 $isFileType = in_array($fieldType, $fileUploadFieldTypes);
141 + // An edit keeps an untouched file/signature as `<fieldKey>_old`, not as an upload.
142 + if (
143 + $isEntryEdit
144 + && ($isFileType || 'signature' === $fieldType)
145 + && !empty(FieldValueHandler::retainedOldValues($data, $key))
146 + ) {
147 + return false;
148 + }
135 149 if ($this->isRepeatedField($key)) {
136 150 $fileData = !empty($file[$key]) ? $file[$key] : [];
137 151 $dataVal = !empty($data[$key]) ? $data[$key] : [];
138 152 if (!$this->checkRepeatedFieldEmptySubmission($isFileType, $dataVal, $fileData)) {
@@ -179,13 +193,10 @@
179 193 {
180 194 $url = wp_parse_url(wp_get_referer());
181 195 $parameter = [];
182 196 if (isset($url['query'])) {
183 - $queries = explode('&', $url['query']);
184 - foreach ($queries as $query) {
185 - list($field, $value) = explode('=', $query);
186 - $parameter[$field] = $value;
187 - }
197 + // a bare key (?preview) made the old explode() split warn; this also URL-decodes values
198 + wp_parse_str($url['query'], $parameter);
188 199 }
189 200 return $parameter;
190 201 }
191 202
@@ -220,8 +231,119 @@
220 231
221 232 return $post;
222 233 }
223 234
235 + /**
236 + * WP auth errors carry markup and the confirmation box paints them with innerHTML,
237 + * so esc_html() would show the tags as text. kses keeps only the safe markup.
238 + *
239 + * @param mixed $message
240 + *
241 + * @return string
242 + */
243 + private static function authErrorMessage($message)
244 + {
245 + return wp_kses(is_string($message) ? $message : '', EscapingHelper::getAllowedHtmlTags());
246 + }
247 +
248 + /**
249 + * A confirm-enabled email/password field posts as one composite and the validator collapses it
250 + * to the primary value, so the confirm child's own field key never reaches $_POST. WP auth
251 + * integrations map fields by key, so fill those child keys on a copy for the auth filter.
252 + *
253 + * @param mixed $postData
254 + *
255 + * @return mixed
256 + */
257 + private function resolveConfirmChildValues($postData)
258 + {
259 + if (!is_array($postData)) {
260 + return $postData;
261 + }
262 + $fields = $this->getFields();
263 + $postData = $this->resolveCompositeChildValues($postData, $fields);
264 + foreach ($fields as $fieldKey => $fieldData) {
265 + if (
266 + empty($fieldData['childFields'])
267 + || !isset($fieldData['type'])
268 + || !in_array($fieldData['type'], ['email', 'password'], true)
269 + || !empty($fieldData['repeated'])
270 + || !isset($postData[$fieldKey])
271 + ) {
272 + continue;
273 + }
274 + $parentValue = $postData[$fieldKey];
275 + foreach ((array) $fieldData['childFields'] as $childFieldRef) {
276 + $childKey = is_object($childFieldRef) && isset($childFieldRef->fldKey) ? $childFieldRef->fldKey : '';
277 + if (
278 + empty($childKey)
279 + || !isset($fields[$childKey])
280 + || !empty($fields[$childKey]['isDeactive'])
281 + || isset($postData[$childKey])
282 + ) {
283 + continue;
284 + }
285 + if (is_array($parentValue)) {
286 + if (array_key_exists('confirm', $parentValue)) {
287 + $postData[$childKey] = $parentValue['confirm'];
288 + }
289 + continue;
290 + }
291 + // Validation matched primary against confirm before collapsing, so this is that value.
292 + $postData[$childKey] = $parentValue;
293 + }
294 + if (is_array($parentValue) && array_key_exists('primary', $parentValue)) {
295 + $postData[$fieldKey] = $parentValue['primary'];
296 + }
297 + }
298 +
299 + return $postData;
300 + }
301 +
302 + /**
303 + * Name/Address parts post as one array under the parent key ("parent[first_name]"), so a WP
304 + * auth mapping that points at a part's own key (First Name, Last Name) found nothing. Copy each
305 + * part's value to its key; the parent value is left as it was.
306 + *
307 + * @param array $postData
308 + * @param array $fields getFields() output
309 + *
310 + * @return array
311 + */
312 + private function resolveCompositeChildValues($postData, $fields)
313 + {
314 + foreach ($fields as $fieldKey => $fieldData) {
315 + if (
316 + empty($fieldData['childFields'])
317 + || !isset($fieldData['type'])
318 + || !in_array($fieldData['type'], ['name', 'address'], true)
319 + || !empty($fieldData['repeated'])
320 + || !isset($postData[$fieldKey])
321 + || !is_array($postData[$fieldKey])
322 + ) {
323 + continue;
324 + }
325 + foreach ((array) $fieldData['childFields'] as $childFieldRef) {
326 + $childKey = is_object($childFieldRef) && isset($childFieldRef->fldKey) ? $childFieldRef->fldKey : '';
327 + if (
328 + empty($childKey)
329 + || !isset($fields[$childKey])
330 + || !empty($fields[$childKey]['isDeactive'])
331 + || isset($postData[$childKey])
332 + || !preg_match('/\[([^\]]+)\]$/', (string) ($fields[$childKey]['name'] ?? ''), $matches)
333 + ) {
334 + continue;
335 + }
336 + $value = FieldValueHandler::extractChildValueFromParentValue($postData[$fieldKey], $matches[1], $childKey);
337 + if (null !== $value) {
338 + $postData[$childKey] = $value;
339 + }
340 + }
341 + }
342 +
343 + return $postData;
344 + }
345 +
224 346 public function handleSubmission()
225 347 {
226 348 // CSRF verified via verifySubmissionNonce() before this method is called. All $_POST reads below occur after that verification.
227 349 $this->fieldNameReplaceOfPost();
@@ -231,9 +353,9 @@
231 353 $validated = apply_filters('bitform_filter_form_validation', $validated, $this->_form_id);
232 354
233 355 if (true === $validated) {
234 356 do_action('bitform_validation_success', $this->_form_id);
235 - unset($_POST['hidden_fields']);
357 + $this->discardHiddenFieldValues();
236 358
237 359 $redirectPage = '';
238 360 $regSuccMsg = '';
239 361
@@ -243,17 +365,18 @@
243 365 $parameter = $this->getParams();
244 366 $existAuthFilter = has_filter('bitform_wp_user_auth');
245 367
246 368 if (true === $existAuthFilter) {
247 - $result = apply_filters('bitform_wp_user_auth', $existAuth[0], $unslashed_post, $parameter);
369 + $authPostData = $this->resolveConfirmChildValues($unslashed_post);
370 + $result = apply_filters('bitform_wp_user_auth', $existAuth[0], $authPostData, $parameter);
248 371
249 - $result = apply_filters('bitform_filter_wp_user_auth_response', $result, $this->_form_id, $unslashed_post, $parameter);
372 + $result = apply_filters('bitform_filter_wp_user_auth_response', $result, $this->_form_id, $authPostData, $parameter);
250 373
251 - do_action('bitform_wp_user_auth_response', $result, $this->_form_id, $unslashed_post, $parameter);
374 + do_action('bitform_wp_user_auth_response', $result, $this->_form_id, $authPostData, $parameter);
252 375
253 376 if (isset($result['auth_type']) && 'register' === $result['auth_type']) {
254 377 if (!$result['success']) {
255 - return new WP_Error('errors', esc_html($result['message']));
378 + return new WP_Error('errors', self::authErrorMessage($result['message']));
256 379 } elseif (isset($result['success'])) {
257 380 $redirectPage = $result['redirectPage'];
258 381 $regSuccMsg = $result['message'];
259 382 }
@@ -258,9 +381,9 @@
258 381 $regSuccMsg = $result['message'];
259 382 }
260 383 } else {
261 384 if (!$result['success']) {
262 - return new WP_Error('errors', esc_html($result['message']));
385 + return new WP_Error('errors', self::authErrorMessage($result['message']));
263 386 } else {
264 387 return $result;
265 388 }
266 389 }
@@ -278,36 +401,9 @@
278 401 $newPost = $this->transformDrpdwnValue($unslashed_post);
279 402 $filesData = GlobalHelper::sanitize_files_input($_FILES);
280 403 do_action('bitform_submit_success', $this->_form_id, $entryID, $newPost, $filesData);
281 404
282 - $captchaV3Settings = $this->getCaptchaV3Settings();
283 - if ($captchaV3Settings) {
284 - $token = isset($_POST['g-recaptcha-response']) ? sanitize_text_field(wp_unslash($_POST['g-recaptcha-response'])) : '';
285 - $integrationHandler = new IntegrationHandler(0);
286 - $allFormIntegrations = $integrationHandler->getAllIntegration('app', 'gReCaptchaV3');
287 - if (!is_wp_error($allFormIntegrations)) {
288 - foreach ($allFormIntegrations as $integration) {
289 - 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;
293 - }
294 - }
295 - }
296 - if (!empty($reCAPTCHA->secretKey)) {
297 - $gRecaptchaResponse = HttpHelper::post(
298 - 'https://www.google.com/recaptcha/api/siteverify',
299 - ['secret' => $reCAPTCHA->secretKey, 'response' => $token]
300 - );
301 - if ($captchaV3Settings && !empty($saveResponse['triggerData'])) {
302 - $logID = $saveResponse['triggerData']['logID'];
303 - $integId = $reCAPTCHA->id;
304 - $saveApiResponse = new UtilApiResponse();
305 - $saveApiResponse->apiResponse($logID, $integId, ['type_name' => 'ReCaptcha', 'type' => 'v3'], 'success', $gRecaptchaResponse);
306 - }
307 - }
308 - unset($_POST['g-recaptcha-response']);
309 - }
405 + $this->logRecaptchaV3Verification($saveResponse);
310 406 if (!empty($redirectPage) && empty($saveResponse['redirectPage']) || null === $saveResponse['redirectPage']) {
311 407 $saveResponse['redirectPage'] = $redirectPage;
312 408 }
313 409 if (!empty($regSuccMsg) && isset($saveResponse['dflt_message'])) {
@@ -330,9 +426,9 @@
330 426 public function handleUpdateEntry()
331 427 {
332 428 // Entry token or capability verified by caller (FrontendAjax::update_entry). All $_POST reads occur after that check.
333 429 $this->fieldNameReplaceOfPost();
334 - $validated = $this->beforeSubmittedValidate();
430 + $validated = $this->beforeSubmittedValidate(true, true);
335 431 $validated = apply_filters('bitform_filter_form_validation', $validated, $this->_form_id);
336 432
337 433 $entryID = isset($_REQUEST['entryID']) ? sanitize_text_field(wp_unslash($_REQUEST['entryID'])) : null;
338 434 $GLOBALS['bitform_entry_id'] = $entryID;
@@ -340,9 +436,10 @@
340 436 return new WP_Error('empty_form', __('Entries id is invalid', 'bit-form'));
341 437 }
342 438 if (true === $validated) {
343 439 do_action('bitform_validation_success', $this->_form_id);
344 - unset($_POST['hidden_fields'], $_POST['entryID']);
440 + $this->discardHiddenFieldValues();
441 + unset($_POST['entryID']);
345 442
346 443 $redirectPage = '';
347 444 $regSuccMsg = '';
348 445 $postData = wp_unslash($_POST);
@@ -352,13 +449,14 @@
352 449 $parameter = $this->getParams();
353 450 $existAuthFilter = has_filter('bitform_wp_user_auth');
354 451
355 452 if (true === $existAuthFilter) {
356 - $result = apply_filters('bitform_wp_user_auth', $existAuth[0], $postData, $parameter);
453 + $authPostData = $this->resolveConfirmChildValues($postData);
454 + $result = apply_filters('bitform_wp_user_auth', $existAuth[0], $authPostData, $parameter);
357 455
358 456 if (isset($result['auth_type']) && 'register' === $result['auth_type']) {
359 457 if (!$result['success']) {
360 - return new WP_Error('errors', esc_html($result['message']));
458 + return new WP_Error('errors', self::authErrorMessage($result['message']));
361 459 } elseif (isset($result['success'])) {
362 460 $redirectPage = $result['redirectPage'];
363 461 $regSuccMsg = $result['message'];
364 462 }
@@ -363,9 +461,9 @@
363 461 $regSuccMsg = $result['message'];
364 462 }
365 463 } else {
366 464 if (!$result['success']) {
367 - return new WP_Error('errors', esc_html($result['message']));
465 + return new WP_Error('errors', self::authErrorMessage($result['message']));
368 466 } else {
369 467 return $result;
370 468 }
371 469 }
@@ -384,36 +482,9 @@
384 482 //TO DO:: submit success action temporarily added for solution of a issue
385 483 do_action('bitform_submit_success', $this->_form_id, $entryID, $newPost, $filesData);
386 484 do_action('bitform_update_success', $this->_form_id, $entryID, $newPost, $filesData);
387 485
388 - $captchaV3Settings = $this->getCaptchaV3Settings();
389 - if ($captchaV3Settings) {
390 - $token = isset($_POST['g-recaptcha-response']) ? sanitize_text_field(wp_unslash($_POST['g-recaptcha-response'])) : '';
391 - $integrationHandler = new IntegrationHandler(0);
392 - $allFormIntegrations = $integrationHandler->getAllIntegration('app', 'gReCaptchaV3');
393 - if (!is_wp_error($allFormIntegrations)) {
394 - foreach ($allFormIntegrations as $integration) {
395 - 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;
399 - }
400 - }
401 - }
402 - if (!empty($reCAPTCHA->secretKey)) {
403 - $gRecaptchaResponse = HttpHelper::post(
404 - 'https://www.google.com/recaptcha/api/siteverify',
405 - ['secret' => $reCAPTCHA->secretKey, 'response' => $token]
406 - );
407 - if ($captchaV3Settings && !empty($updateResponse['triggerData'])) {
408 - $logID = $updateResponse['triggerData']['logID'];
409 - $integId = $reCAPTCHA->id;
410 - $saveApiResponse = new UtilApiResponse();
411 - $saveApiResponse->apiResponse($logID, $integId, ['type_name' => 'ReCaptcha', 'type' => 'v3'], 'success', $gRecaptchaResponse);
412 - }
413 - }
414 - unset($_POST['g-recaptcha-response']);
415 - }
486 + $this->logRecaptchaV3Verification($updateResponse);
416 487 if (!empty($redirectPage) && empty($updateResponse['redirectPage']) || null === $updateResponse['redirectPage']) {
417 488 $updateResponse['redirectPage'] = $redirectPage;
418 489 }
419 490 if (!empty($regSuccMsg) && isset($updateResponse['dflt_message'])) {
@@ -432,11 +503,96 @@
432 503 do_action('bitform_validation_error', $this->_form_id, $validated);
433 504 return $validated;
434 505 }
435 506
507 + /**
508 + * Drop the posted `hidden_fields` transport key and, when the form opts in, the values of
509 + * the fields it names.
510 + *
511 + * A hidden field keeps its typed value in the DOM, so the browser still submits it. Runs
512 + * here because it is the last point before entry, notifications and integrations are built
513 + * from $_POST.
514 + *
515 + * @return void
516 + */
517 + private function discardHiddenFieldValues()
518 + {
519 + // CSRF verified upstream via verifySubmissionNonce(); $_POST is only being narrowed here.
520 + $rawHiddenFields = isset($_POST['hidden_fields']) ? wp_unslash($_POST['hidden_fields']) : '';
521 + unset($_POST['hidden_fields']);
522 +
523 + if (!$this->shouldDiscardHiddenFieldValues()) {
524 + return;
525 + }
526 + $hiddenFieldKeys = FrontendHelpers::parseHiddenFieldKeys($rawHiddenFields);
527 + if (empty($hiddenFieldKeys)) {
528 + return;
529 + }
530 +
531 + $formFields = $this->getFields();
532 + foreach ($hiddenFieldKeys as $fieldKey) {
533 + if (!isset($formFields[$fieldKey])) {
534 + continue;
535 + }
536 + $field = $formFields[$fieldKey];
537 + // The posted list also names builder-hidden and hidden-type fields, which carry a value
538 + // on purpose. Only what conditional logic hid is discarded.
539 + if ('hidden' === $field['type'] || !empty($field['valid']['hide'])) {
540 + continue;
541 + }
542 + // Hiding flags a repeater child once, not per row, so discarding would wipe the column
543 + // in every row.
544 + if (!empty($field['repeated'])) {
545 + continue;
546 + }
547 + // Calculation and tracking fields opt out.
548 + if (!empty($field['valid']['keepValueWhenHidden'])) {
549 + continue;
550 + }
551 + // A composite child (name/address/confirm) posts nested under its parent key.
552 + if (!empty($field['parentFieldKey'])) {
553 + $this->discardCompositeChildValue($formFields, $field, $fieldKey);
554 + continue;
555 + }
556 + unset($_POST[$fieldKey], $_FILES[$fieldKey]);
557 + }
558 + }
559 +
560 + /**
561 + * @param array $formFields
562 + * @param array $field the child field's config
563 + * @param string $fieldKey the child field's key
564 + *
565 + * @return void
566 + */
567 + private function discardCompositeChildValue($formFields, $field, $fieldKey)
568 + {
569 + $parentKey = $field['parentFieldKey'];
570 + if (!isset($_POST[$parentKey]) || !is_array($_POST[$parentKey])) {
571 + return;
572 + }
573 + $parentName = isset($formFields[$parentKey]['name']) ? $formFields[$parentKey]['name'] : '';
574 + $childName = FieldValueHandler::deriveChildName(isset($field['name']) ? $field['name'] : '', $parentName);
575 + unset($_POST[$parentKey][$childName], $_POST[$parentKey][$fieldKey]);
576 + }
577 +
578 + /**
579 + * @return bool
580 + */
581 + private function shouldDiscardHiddenFieldValues()
582 + {
583 + $formInfo = $this->getFormInfo();
584 + if (!is_object($formInfo) || !isset($formInfo->submissionSettings)) {
585 + return false;
586 + }
587 + $submissionSettings = (object) $formInfo->submissionSettings;
588 +
589 + return !empty($submissionSettings->discardHiddenFieldValues);
590 + }
591 +
436 592 public function validateFormSubmission($submitted_data)
437 593 {
438 - $hidden_fields = isset($submitted_data['hidden_fields']) ? $submitted_data['hidden_fields'] : '';
594 + $hidden_fields = FrontendHelpers::parseHiddenFieldKeys(isset($submitted_data['hidden_fields']) ? $submitted_data['hidden_fields'] : '');
439 595 $submitted_fields = $this->getSubmittedFields($submitted_data);
440 596 $form_fields = $this->getFields();
441 597 $form_fields_names = array_keys($form_fields);
442 598 if ($this->isGCLIDEnabled()) {
@@ -442,9 +598,9 @@
442 598 if ($this->isGCLIDEnabled()) {
443 599 array_push($form_fields_names, 'GCLID');
444 600 }
445 601 foreach ($submitted_fields as $field) {
446 - if ('hidden_fields' !== $field && !in_array($field, $form_fields_names) || false !== strpos($hidden_fields, $field)) {
602 + if ('hidden_fields' !== $field && !in_array($field, $form_fields_names) || FrontendHelpers::isFieldHidden($hidden_fields, $field)) {
447 603 unset($submitted_data[$field]);
448 604 }
449 605 }
450 606 return $submitted_data;
@@ -449,13 +605,13 @@
449 605 }
450 606 return $submitted_data;
451 607 }
452 608
453 - public function beforeSubmittedValidate($verifyCaptcha = true)
609 + public function beforeSubmittedValidate($verifyCaptcha = true, $isEntryEdit = false)
454 610 {
455 611 if ($this->verifySubmissionNonce()) {
456 612 if ($this->isExist()) {
457 - $isRestricted = $this->checkSubmissionRestriction();
613 + $isRestricted = $this->checkSubmissionRestriction(true, $isEntryEdit);
458 614 if ($isRestricted && !empty($isRestricted)) {
459 615 return new WP_Error('spam_detection', $isRestricted[0]);
460 616 }
461 617 $postData = wp_unslash($_POST);
@@ -492,32 +648,73 @@
492 648 }
493 649 $validateForm = $this->validateFormSubmission($postData);
494 650 $validateFormFiles = $this->validateFormSubmission($filesData);
495 651 $validateForm = array_merge($validateForm, $validateFormFiles);
496 - $form_fields = $this->getFields();
652 + // Validate only provably-rendered fields: a field stranded in form_content->fields
653 + // with no layout entry (orphan) is never shown to the user and must not block
654 + // submission. getRenderedFields() unions ALL breakpoints × steps × nested layouts
655 + // + childFields of rendered parents, derives only from DB-stored form_content,
656 + // and fails closed (returns all fields) when the layout is unusable.
657 + $form_fields = $this->getRenderedFields();
497 658 // check if form-current-step is set and form is multi-step
498 659 $formCurrentStep = isset($_POST['form-current-step']) ? sanitize_text_field(wp_unslash($_POST['form-current-step'])) : null;
499 660 if (!is_null($formCurrentStep)) {
661 + // Narrow validation to the current step's fields. SECURITY: the step
662 + // key set unions ALL breakpoints (lg/md/sm) — an md/sm-only field was
663 + // previously null-skipped by the validator (silent bypass). A forged
664 + // step index or malformed layout skips the narrowing entirely so every
665 + // rendered field stays validated (fail closed).
500 666 $formContents = $this->getFormContent();
501 - $layout = $formContents->layout;
667 + $layout = isset($formContents->layout) ? $formContents->layout : null;
502 668 $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];
669 + if (is_array($layout) && isset($layout[$stepIndex]->layout) && is_object($layout[$stepIndex]->layout)) {
670 + $stepLayout = $layout[$stepIndex]->layout;
671 + $nestedLayout = isset($formContents->nestedLayout) && is_object($formContents->nestedLayout)
672 + ? $formContents->nestedLayout : null;
673 + $stepKeys = [];
674 + foreach (['lg', 'md', 'sm'] as $brkpnt) {
675 + if (!isset($stepLayout->{$brkpnt}) || !is_array($stepLayout->{$brkpnt})) {
676 + continue;
513 677 }
678 + foreach ($stepLayout->{$brkpnt} as $lay) {
679 + if (!is_object($lay) || !isset($lay->i)) {
680 + continue;
681 + }
682 + $fk = $lay->i;
683 + $stepKeys[$fk] = true;
684 + if (!is_null($nestedLayout) && isset($nestedLayout->{$fk})) {
685 + foreach (['lg', 'md', 'sm'] as $nBrkpnt) {
686 + if (!isset($nestedLayout->{$fk}->{$nBrkpnt}) || !is_array($nestedLayout->{$fk}->{$nBrkpnt})) {
687 + continue;
688 + }
689 + foreach ($nestedLayout->{$fk}->{$nBrkpnt} as $nestedLay) {
690 + if (is_object($nestedLay) && isset($nestedLay->i)) {
691 + $stepKeys[$nestedLay->i] = true;
692 + }
693 + }
694 + }
695 + }
696 + }
514 697 }
515 - $step_fields[$fk] = $form_fields[$fk];
698 + // Name/Address/Email/Password children live outside layouts; a child
699 + // is part of this step iff its parent is.
700 + self::expandChildFieldKeys($stepKeys, $form_fields);
701 + if (!empty($stepKeys)) {
702 + $step_fields = [];
703 + foreach (array_keys($stepKeys) as $fk) {
704 + if (isset($form_fields[$fk])) {
705 + $step_fields[$fk] = $form_fields[$fk];
706 + }
707 + }
708 + $form_fields = $step_fields;
709 + }
516 710 }
517 - $form_fields = $step_fields;
518 711 }
519 - $formFieldValidator = new FormFieldValidator($form_fields, $postData, $filesData);
712 + // Only an edit may satisfy a required upload/signature from a `_old` marker.
713 + $editedEntryID = $isEntryEdit && isset($_REQUEST['entryID'])
714 + ? sanitize_text_field(wp_unslash($_REQUEST['entryID']))
715 + : null;
716 + $formFieldValidator = new FormFieldValidator($form_fields, $postData, $filesData, $editedEntryID);
520 717 $validUniuqFields = [];
521 718 $existFilter = has_filter('bitform_check_duplicate_entry');
522 719 if (true === $existFilter) {
523 720 $validUniuqFields = apply_filters('bitform_check_duplicate_entry', $form_fields, $postData);
@@ -553,140 +750,236 @@
553 750 return new WP_Error('token_expired', __('Token expired', 'bit-form'));
554 751 }
555 752 }
556 753
754 + /**
755 + * Whether the IP already has a completed entry; drafts (status 9) don't count.
756 + *
757 + * @param string $ipAddress Visitor IP
758 + *
759 + * @return bool
760 + */
761 + private function hasCompletedEntryFromIp($ipAddress)
762 + {
763 + $storedIp = IpTool::storageValue($ipAddress);
764 + if ('' === $storedIp) {
765 + return false;
766 + }
767 +
768 + $entries = (new FormEntryModel())->get(
769 + ['status'],
770 + [
771 + 'form_id' => $this->form_id,
772 + 'user_ip' => $storedIp,
773 + ]
774 + );
775 +
776 + if (is_wp_error($entries) || !is_array($entries)) {
777 + return false;
778 + }
779 +
780 + foreach ($entries as $entry) {
781 + if (9 !== (int) $entry->status) {
782 + return true;
783 + }
784 + }
785 +
786 + return false;
787 + }
788 +
557 789 private function verifyGRecaptcha()
558 790 {
559 791 $captchaSettings = $this->getCaptchaSettings();
560 792 $captchaV3Settings = $this->getCaptchaV3Settings();
561 - if ($captchaSettings || $captchaV3Settings) {
562 - $token = isset($_POST['g-recaptcha-response']) ? sanitize_text_field(wp_unslash($_POST['g-recaptcha-response'])) : '';
563 - if (!isset($_POST['g-recaptcha-response'])) {
564 - return new WP_Error('spam_detection', __('Please recheck your reCaptcha Configuration', 'bit-form'));
793 + if (!$captchaSettings && !$captchaV3Settings) {
794 + return;
795 + }
796 +
797 + if (!isset($_POST['g-recaptcha-response'])) {
798 + return new WP_Error('spam_detection', __('Please recheck your reCaptcha Configuration', 'bit-form'));
799 + }
800 + $token = sanitize_text_field(wp_unslash($_POST['g-recaptcha-response']));
801 +
802 + $appType = $captchaSettings ? 'gReCaptcha' : 'gReCaptchaV3';
803 + $reCAPTCHA = $this->getCaptchaApp($appType);
804 +
805 + // Fail closed: no secret key must not skip verification.
806 + if (empty($reCAPTCHA->secretKey)) {
807 + Log::debug_log("reCAPTCHA ({$appType}) is enabled on form {$this->form_id}, but no secret key is set in App Settings.");
808 + return new WP_Error('spam_detection', __('reCAPTCHA is not configured. Please contact the site administrator.', 'bit-form'));
809 + }
810 +
811 + $gRecaptchaResponse = HttpHelper::post(
812 + 'https://www.google.com/recaptcha/api/siteverify',
813 + ['secret' => $reCAPTCHA->secretKey, 'response' => $token]
814 + );
815 + if (is_wp_error($gRecaptchaResponse)) {
816 + return new WP_Error('spam_detection', __('Please verify reCAPTCHA', 'bit-form'));
817 + }
818 +
819 + // Tokens are single-use; keep this verdict for the log.
820 + if ('gReCaptchaV3' === $appType) {
821 + $this->recaptchaV3Verification = ['integId' => $reCAPTCHA->id, 'response' => $gRecaptchaResponse];
822 + }
823 +
824 + if ($captchaV3Settings && self::isRecaptchaScoreTooLow($gRecaptchaResponse, $captchaV3Settings)) {
825 + wp_send_json_error(
826 + sanitize_text_field((string) $captchaV3Settings->message)
827 + );
828 + }
829 +
830 + if (!is_object($gRecaptchaResponse) || empty($gRecaptchaResponse->success)) {
831 + return new WP_Error('spam_detection', __('Please verify reCAPTCHA', 'bit-form'));
832 + }
833 + }
834 +
835 + /**
836 + * Connected captcha app from App Settings, with its integration id.
837 + *
838 + * @param string $appType gReCaptcha, gReCaptchaV3, hcaptcha or turnstileCaptcha
839 + *
840 + * @return object|null
841 + */
842 + private function getCaptchaApp($appType)
843 + {
844 + $apps = (new IntegrationHandler(0))->getAllIntegration('app', $appType);
845 + if (is_wp_error($apps) || !is_array($apps)) {
846 + return null;
847 + }
848 +
849 + $reCAPTCHA = null;
850 + foreach ($apps as $integration) {
851 + if ($appType !== $integration->integration_type) {
852 + continue;
565 853 }
566 - $integrationHandler = new IntegrationHandler(0);
567 - $allFormIntegrations = $integrationHandler->getAllIntegration('app', $captchaSettings ? 'gReCaptcha' : 'gReCaptchaV3');
568 - if (!is_wp_error($allFormIntegrations)) {
569 - foreach ($allFormIntegrations as $integration) {
570 - 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;
574 - }
575 - }
854 + $details = Utilities::jsonObj($integration->integration_details);
855 + if (is_object($details)) {
856 + $details->id = $integration->id;
857 + $reCAPTCHA = $details;
576 858 }
577 - if (!empty($reCAPTCHA->secretKey)) {
578 - $gRecaptchaResponse = HttpHelper::post(
579 - 'https://www.google.com/recaptcha/api/siteverify',
580 - ['secret' => $reCAPTCHA->secretKey, 'response' => $token]
581 - );
582 - $isgReCaptchaVerified = false;
583 - if (!is_wp_error($gRecaptchaResponse)) {
584 - if (
585 - $captchaV3Settings
586 - && !empty($gRecaptchaResponse->score)
587 - && ((float) $gRecaptchaResponse->score < (float) $captchaV3Settings->score)
588 - ) {
589 - wp_send_json_error(
590 - sanitize_text_field((string) $captchaV3Settings->message)
591 - );
592 - }
859 + }
593 860
594 - $isgReCaptchaVerified = $gRecaptchaResponse->success;
595 - }
596 - if (!$isgReCaptchaVerified) {
597 - return new WP_Error('spam_detection', __('Please verify reCAPTCHA', 'bit-form'));
598 - }
599 - }
861 + return $reCAPTCHA;
862 + }
863 +
864 + /**
865 + * Log the reCAPTCHA v3 verdict for this submission.
866 + *
867 + * @param array $saveResponse Entry save/update response
868 + *
869 + * @return void
870 + */
871 + private function logRecaptchaV3Verification($saveResponse)
872 + {
873 + if (!$this->getCaptchaV3Settings()) {
874 + return;
600 875 }
876 + unset($_POST['g-recaptcha-response']);
877 +
878 + if (empty($this->recaptchaV3Verification) || empty($saveResponse['triggerData']['logID'])) {
879 + return;
880 + }
881 +
882 + $response = $this->recaptchaV3Verification['response'];
883 + $status = is_object($response) && !empty($response->success) ? 'success' : 'error';
884 + (new UtilApiResponse())->apiResponse(
885 + $saveResponse['triggerData']['logID'],
886 + $this->recaptchaV3Verification['integId'],
887 + ['type_name' => 'ReCaptcha', 'type' => 'v3'],
888 + $status,
889 + $response
890 + );
601 891 }
602 892
603 - private function verifyHCaptcha()
893 + /**
894 + * Whether the v3 score is below tolerance; 0.0 counts, blank tolerance means 0.6.
895 + *
896 + * @param mixed $response Decoded siteverify response
897 + * @param mixed $settings Form's recaptchav3 settings
898 + *
899 + * @return bool
900 + */
901 + private static function isRecaptchaScoreTooLow($response, $settings)
604 902 {
605 - $hCaptchaExist = $this->isFieldTypeExist('hcaptcha'); // You can rename this to getHCaptchaSettings() if needed
606 - if ($hCaptchaExist) {
607 - if (!isset($_POST['h-captcha-response'])) {
608 - return new WP_Error('spam_detection', __('Please verify hCaptcha', 'bit-form'));
609 - }
903 + if (!is_object($response) || !isset($response->score) || !is_numeric($response->score)) {
904 + return false;
905 + }
610 906
611 - $token = sanitize_text_field(wp_unslash($_POST['h-captcha-response']));
907 + $threshold = is_object($settings) && isset($settings->score) && is_numeric($settings->score)
908 + ? (float) $settings->score
909 + : 0.6;
612 910
613 - $integrationHandler = new IntegrationHandler(0);
614 - $allFormIntegrations = $integrationHandler->getAllIntegration('app', 'hcaptcha');
911 + return (float) $response->score < $threshold;
912 + }
615 913
616 - if (!is_wp_error($allFormIntegrations)) {
617 - foreach ($allFormIntegrations as $integration) {
618 - 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;
622 - }
623 - }
624 - }
914 + private function verifyHCaptcha()
915 + {
916 + if (!$this->isFieldTypeExist('hcaptcha')) {
917 + return;
918 + }
625 919
626 - if (!empty($hCaptcha->secretKey)) {
627 - $hCaptchaResponse = HttpHelper::post(
628 - 'https://api.hcaptcha.com/siteverify',
629 - [
630 - 'secret' => $hCaptcha->secretKey,
631 - 'response' => $token,
632 - 'remoteip' => (isset($_SERVER['REMOTE_ADDR']) ? sanitize_text_field(wp_unslash($_SERVER['REMOTE_ADDR'])) : '')
633 - ]
634 - );
920 + if (!isset($_POST['h-captcha-response'])) {
921 + return new WP_Error('spam_detection', __('Please verify hCaptcha', 'bit-form'));
922 + }
923 + $token = sanitize_text_field(wp_unslash($_POST['h-captcha-response']));
924 + $hCaptcha = $this->getCaptchaApp('hcaptcha');
635 925
636 - $isVerified = false;
637 - if (!is_wp_error($hCaptchaResponse)) {
638 - $isVerified = $hCaptchaResponse->success;
639 - }
926 + // Fail closed: no secret key must not skip verification.
927 + if (empty($hCaptcha->secretKey)) {
928 + Log::debug_log("hCaptcha is enabled on form {$this->form_id}, but no secret key is set in App Settings.");
929 + return new WP_Error('spam_detection', __('hCaptcha is not configured. Please contact the site administrator.', 'bit-form'));
930 + }
640 931
641 - if (!$isVerified) {
642 - return new WP_Error('spam_detection', __('hCaptcha verification failed', 'bit-form'));
643 - }
644 - }
932 + $hCaptchaResponse = HttpHelper::post(
933 + 'https://api.hcaptcha.com/siteverify',
934 + [
935 + 'secret' => $hCaptcha->secretKey,
936 + 'response' => $token,
937 + 'remoteip' => (isset($_SERVER['REMOTE_ADDR']) ? sanitize_text_field(wp_unslash($_SERVER['REMOTE_ADDR'])) : '')
938 + ]
939 + );
940 +
941 + if (is_wp_error($hCaptchaResponse) || !is_object($hCaptchaResponse) || empty($hCaptchaResponse->success)) {
942 + return new WP_Error('spam_detection', __('hCaptcha verification failed', 'bit-form'));
645 943 }
646 944 }
647 945
648 946 private function verifyTurnstileCaptcha()
649 947 {
650 - $turnstileExist = $this->isFieldTypeExist('turnstile');
651 - if ($turnstileExist) {
652 - if (!isset($_POST['cf-turnstile-response'])) {
653 - return new WP_Error('spam_detection', __('Please verify Cloudflare Turnstile Captcha', 'bit-form'));
654 - }
655 - $token = sanitize_text_field(wp_unslash($_POST['cf-turnstile-response']));
656 - $turnstileCaptcha = null;
657 - $integrationHandler = new IntegrationHandler(0);
658 - $turnstileIntegration = $integrationHandler->getAllIntegration('app', 'turnstileCaptcha')[0];
659 - if (!is_wp_error($turnstileIntegration && !is_null($turnstileIntegration->integration_type))) {
660 - $turnstileCaptcha = json_decode($turnstileIntegration->integration_details);
661 - // $integrationDetails->id = $turnstileIntegration->id;
662 - // $turnstileCaptcha = $integrationDetails;
663 - }
664 - if (!is_null($turnstileCaptcha)) {
665 - $isTurnstileCaptchaVerified = false;
666 - $turnstileRecaptchaResponse = HttpHelper::post(
667 - 'https://challenges.cloudflare.com/turnstile/v0/siteverify',
668 - ['secret' => $turnstileCaptcha->secretKey, 'response' => $token]
669 - );
670 - if (!is_wp_error($turnstileRecaptchaResponse)) {
671 - if (!$turnstileRecaptchaResponse->success) {
672 - $errorCodes = implode(', ', (array) ($turnstileRecaptchaResponse->{'error-codes'} ?? []));
673 - wp_send_json_error(
674 - sprintf(
675 - /* translators: %s: dynamic value. */
676 - __('Cloudflare Turnstile Validation Error: %s', 'bit-form'),
677 - $errorCodes
678 - )
679 - );
680 - }
948 + if (!$this->isFieldTypeExist('turnstile')) {
949 + return;
950 + }
681 951
682 - $isTurnstileCaptchaVerified = $turnstileRecaptchaResponse->success;
683 - }
684 - if (!$isTurnstileCaptchaVerified) {
685 - return new WP_Error('spam_detection', __('Please verify Cloudflare Turnstile Captcha', 'bit-form'));
686 - }
687 - }
952 + if (!isset($_POST['cf-turnstile-response'])) {
953 + return new WP_Error('spam_detection', __('Please verify Cloudflare Turnstile Captcha', 'bit-form'));
688 954 }
955 + $token = sanitize_text_field(wp_unslash($_POST['cf-turnstile-response']));
956 + $turnstileCaptcha = $this->getCaptchaApp('turnstileCaptcha');
957 +
958 + // Fail closed: no secret key must not skip verification.
959 + if (empty($turnstileCaptcha->secretKey)) {
960 + Log::debug_log("Turnstile is enabled on form {$this->form_id}, but no secret key is set in App Settings.");
961 + return new WP_Error('spam_detection', __('Cloudflare Turnstile is not configured. Please contact the site administrator.', 'bit-form'));
962 + }
963 +
964 + $turnstileResponse = HttpHelper::post(
965 + 'https://challenges.cloudflare.com/turnstile/v0/siteverify',
966 + ['secret' => $turnstileCaptcha->secretKey, 'response' => $token]
967 + );
968 + if (is_wp_error($turnstileResponse) || !is_object($turnstileResponse)) {
969 + return new WP_Error('spam_detection', __('Please verify Cloudflare Turnstile Captcha', 'bit-form'));
970 + }
971 +
972 + if (empty($turnstileResponse->success)) {
973 + $errorCodes = implode(', ', (array) ($turnstileResponse->{'error-codes'} ?? []));
974 + wp_send_json_error(
975 + sprintf(
976 + /* translators: %s: dynamic value. */
977 + __('Cloudflare Turnstile Validation Error: %s', 'bit-form'),
978 + $errorCodes
979 + )
980 + );
981 + }
689 982 }
690 983
691 984 public function verifySubmissionNonce()
692 985 {
@@ -703,9 +996,9 @@
703 996 {
704 997 if (!current_user_can('manage_options')) {
705 998 $update_status = $this->formModel->update(
706 999 [
707 - 'views' => intval(static::$form[0]->views) + 1
1000 + 'views' => intval($this->form[0]->views) + 1
708 1001 ],
709 1002 [
710 1003 'id' => $this->form_id
711 1004 ]
@@ -712,9 +1005,13 @@
712 1005 );
713 1006 }
714 1007 }
715 1008
716 - public function checkSubmissionRestriction($checkedEmptySubmitted = true)
1009 + /**
1010 + * @param bool $checkedEmptySubmitted whether the empty-submission rule applies here
1011 + * @param bool $isEntryEdit true when an existing entry is being updated
1012 + */
1013 + public function checkSubmissionRestriction($checkedEmptySubmitted = true, $isEntryEdit = false)
717 1014 {
718 1015 $formContents = $this->getFormContent();
719 1016 $additionalSettings = isset($formContents->additional) ? $formContents->additional : null;
720 1017 $fromRestrictionSetitingsEnabled = empty($additionalSettings->enabled) ? [] : $additionalSettings->enabled;
@@ -730,8 +1027,21 @@
730 1027 $currentUserId = get_current_user_id();
731 1028
732 1029 foreach ($fromRestrictionSetitingsEnabled as $restrictionKey => $isEnabled) {
733 1030 if ($isEnabled) {
1031 + // Quota rules gate creating an entry, so an edit skips them; access-control keys stay.
1032 + $skippableOnEdit = ['onePerIp', 'entry_limit', 'entry_limit_by_user', 'restrict_form'];
1033 + if ($isEntryEdit && in_array($restrictionKey, $skippableOnEdit, true)) {
1034 + $skipOnEdit = apply_filters(
1035 + 'bitform_skip_restriction_on_entry_edit',
1036 + true,
1037 + $restrictionKey,
1038 + $this->form_id
1039 + );
1040 + if ($skipOnEdit) {
1041 + continue;
1042 + }
1043 + }
734 1044 /**
735 1045 * Allow add-ons to handle any restriction key (Pro-only restrictions
736 1046 * should be implemented in the add-on, not shipped in the free plugin).
737 1047 *
@@ -753,33 +1063,9 @@
753 1063 continue;
754 1064 }
755 1065
756 1066 if ('onePerIp' === $restrictionKey) {
757 - $formEntry = new FormEntryModel();
758 -
759 - $getResult = $formEntry->get(
760 - ['user_ip', 'status'],
761 - [
762 - 'form_id' => $this->form_id,
763 - 'user_ip' => (int) ip2long((string) $ipAddress)
764 - ],
765 - );
766 -
767 - $count = 0;
768 - $status = 0;
769 -
770 - if (!is_wp_error($getResult) && count($getResult) > 0) {
771 - $count = count($getResult);
772 -
773 - foreach ($getResult as $row) {
774 - if (9 === (int) $row->status) {
775 - $status = 9;
776 - break;
777 - }
778 - }
779 - }
780 -
781 - if ($count > 0 && 9 !== (int) $status) {
1067 + if ($this->hasCompletedEntryFromIp($ipAddress)) {
782 1068 $onePerIp = __('Sorry!! You have already submitted from this IP address', 'bit-form');
783 1069
784 1070 $onePerIp = apply_filters(
785 1071 'bitform_filter_restriction_one_per_ip_message',
@@ -801,9 +1087,9 @@
801 1087
802 1088 $restrictionMessage[] = $is_login_messages;
803 1089 }
804 1090 if ($checkedEmptySubmitted && 'empty_submission' === $restrictionKey) {
805 - $isEmpty = $this->checkEmptySubmission(wp_unslash($_POST), GlobalHelper::sanitize_files_input($_FILES));
1091 + $isEmpty = $this->checkEmptySubmission(wp_unslash($_POST), GlobalHelper::sanitize_files_input($_FILES), $isEntryEdit);
806 1092 if ($isEmpty) {
807 1093 $restriction = $fromRestrictionSetitings->empty_submission->message;
808 1094
809 1095 $restriction = apply_filters(
@@ -842,11 +1128,14 @@
842 1128 $fields = $formContents->fields;
843 1129
844 1130 $payments = [];
845 1131 foreach ($fields as $fldData) {
1132 + if (!is_object($fldData)) {
1133 + continue;
1134 + }
846 1135 if ('paypal' === $fldData->typ && property_exists($fldData, 'payIntegID')) {
847 1136 $payments['paypalKey'] = $this->getClientKey($fldData->payIntegID, 'clientID');
848 - } elseif ('razorpay' === $fldData->typ && property_exists($fldData->options, 'payIntegID')) {
1137 + } elseif ('razorpay' === $fldData->typ && isset($fldData->options) && is_object($fldData->options) && property_exists($fldData->options, 'payIntegID')) {
849 1138 $payments['razorpayKey'] = $this->getClientKey($fldData->options->payIntegID, 'apiKey');
850 1139 }
851 1140 }
852 1141
@@ -859,10 +1148,13 @@
859 1148 if (!empty($integID)) {
860 1149 $integrationHandler = new IntegrationHandler(0);
861 1150 $integration = $integrationHandler->getAIntegration($integID, 'app', 'payments');
862 1151 if (!is_wp_error($integration)) {
863 - $integration_details = json_decode($integration[0]->integration_details);
864 - $client = base64_encode($integration_details->{$keyName});
1152 + $integrationRow = Utilities::firstRow($integration);
1153 + $integration_details = Utilities::jsonObj($integrationRow->integration_details ?? '');
1154 + if ($integration_details && isset($integration_details->{$keyName})) {
1155 + $client = base64_encode($integration_details->{$keyName});
1156 + }
865 1157 }
866 1158 }
867 1159 return $client;
868 1160 }
@@ -868,38 +1160,11 @@
868 1160 }
869 1161
870 1162 public function getSuccessMessageMarkups()
871 1163 {
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 1164 if (is_null($this->_conf_messages)) {
900 1165 $successMsgHandler = new SuccessMessageHandler($this->form_id);
901 - $this->_conf_messages = $successMsgHandler->getMessages($ids);
1166 + $this->_conf_messages = $successMsgHandler->getAllMessage();
902 1167 }
903 1168
904 1169 $messageMarkups = '';
905 1170 if (is_wp_error($this->_conf_messages)) {
@@ -906,8 +1171,12 @@
906 1171 return $messageMarkups;
907 1172 }
908 1173
909 1174 foreach ($this->_conf_messages as $msgItem) {
1175 + $msgConfig = json_decode($msgItem->message_config);
1176 + if (is_object($msgConfig) && property_exists($msgConfig, 'status') && empty($msgConfig->status)) {
1177 + continue;
1178 + }
910 1179 $messageMarkups .= $this->messageMarkup($msgItem);
911 1180 }
912 1181
913 1182 return $messageMarkups;
@@ -927,9 +1196,10 @@
927 1196 private function messageMarkup($msg)
928 1197 {
929 1198 $msgId = $msg->id;
930 1199 $msgConfig = json_decode($msg->message_config);
931 - $scrollClass = 'below' === $msgConfig->msgType ? 'scroll' : '';
1200 + $msgType = (is_object($msgConfig) && isset($msgConfig->msgType)) ? $msgConfig->msgType : 'below';
1201 + $scrollClass = 'below' === $msgType ? 'scroll' : '';
932 1202
933 1203 return '<div
934 1204 role="dialog"
935 1205 aria-hidden="true"