PluginProbe
Fluent Forms – Customizable Contact Forms, Survey, Quiz, & Conversational Form Builder / 6.2.10
Fluent Forms – Customizable Contact Forms, Survey, Quiz, & Conversational Form Builder v6.2.10
6.2.14 6.2.13 6.2.12 6.2.10 6.2.11 6.2.9 6.2.8 6.2.7 6.2.6 6.2.5 6.2.4 6.2.3 6.2.2 3.6.22 3.6.31 3.6.40 3.6.41 3.6.42 3.6.50 3.6.51 3.6.60 3.6.61 3.6.62 3.6.64 3.6.65 All 196 releases
fluentform / app / Services / Form / FormValidationService.php

FormValidationService.php in Fluent Forms – Customizable Contact Forms, Survey, Quiz, & Conversational Form Builder 6.2.10, at app/Services/Form/FormValidationService.php

916 lines 34.0 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 namespace FluentForm\App\Services\Form;
4
5 use FluentForm\App\Helpers\Helper;
6 use FluentForm\App\Models\FormMeta;
7 use FluentForm\App\Modules\Form\AkismetHandler;
8 use FluentForm\App\Modules\Form\CleanTalkHandler;
9 use FluentForm\App\Modules\Form\FormDataParser;
10 use FluentForm\App\Modules\Form\FormFieldsParser;
11 use FluentForm\App\Modules\HCaptcha\HCaptcha;
12 use FluentForm\App\Modules\ReCaptcha\ReCaptcha;
13 use FluentForm\App\Modules\Turnstile\Turnstile;
14 use FluentForm\App\Services\FormBuilder\Components\SelectCountry;
15 use FluentForm\Framework\Foundation\App;
16 use FluentForm\Framework\Helpers\ArrayHelper as Arr;
17 use FluentForm\Framework\Validator\ValidationException;
18
19 class FormValidationService
20 {
21 protected $app;
22 protected $form;
23 protected $formData;
24
25 public function __construct()
26 {
27 $this->app = App::getInstance();
28 }
29
30 public function setForm($form)
31 {
32 $this->form = $form;
33 }
34
35 public function setFormData($formData)
36 {
37 $this->formData = $formData;
38 }
39
40 /**
41 * @param $fields
42 * @param $formData
43 * @return bool
44 * @throws ValidationException
45 */
46 public function validateSubmission(&$fields, &$formData)
47 {
48 do_action('fluentform/before_form_validation', $fields, $formData);
49
50 $this->preventMaliciousAttacks();
51
52 $this->validateRestrictions($fields);
53
54 $this->validateNonce();
55
56 $this->validateReCaptcha();
57 $this->validateHCaptcha();
58 $this->validateTurnstile();
59
60 foreach ($fields as $fieldName => $field) {
61 if (isset($formData[$fieldName])) {
62 $element = $field['element'];
63
64 $formData[$fieldName] = apply_filters_deprecated('fluentform_input_data_' . $element, [
65 $formData[$fieldName],
66 $field,
67 $formData,
68 $this->form
69 ],
70 FLUENTFORM_FRAMEWORK_UPGRADE,
71 'fluentform/input_data_' . $element,
72 'Use fluentform/input_data_' . $element . ' instead of fluentform_input_data_' . $element
73 );
74 $formData[$fieldName] = apply_filters('fluentform/input_data_' . $element, $formData[$fieldName], $field, $formData, $this->form);
75 }
76 }
77
78 $originalValidations = FormFieldsParser::getValidations($this->form, $formData, $fields);
79
80 // Fire an event so that one can hook into it to work with the rules & messages.
81 $originalValidations = apply_filters_deprecated('fluentform_validations', [
82 $originalValidations,
83 $this->form,
84 $formData
85 ],
86 FLUENTFORM_FRAMEWORK_UPGRADE,
87 'fluentform/validations',
88 'Use fluentform/validations instead of fluentform_validations.'
89 );
90 $validations = apply_filters('fluentform/validations', $originalValidations, $this->form, $formData);
91
92 /*
93 * Clean talk fix for now
94 * They should not hook fluentform_validations and return nothing!
95 * We will remove this extra check once it's done
96 */
97 if ($originalValidations && (!$validations || !array_filter($validations))) {
98 $validations = $originalValidations;
99 }
100
101 $validator = wpFluentForm('validator')->make($formData, $validations[0], $validations[1]);
102
103 $errors = [];
104 if ($validator->validate()->fails()) {
105 foreach ($validator->errors() as $attribute => $rules) {
106 $position = strpos($attribute, ']');
107
108 if ($position) {
109 $attribute = substr($attribute, 0, strpos($attribute, ']') + 1);
110 }
111
112 $errors[$attribute] = $rules;
113 }
114 // Fire an event so that one can hook into it to work with the errors.
115 $errors = apply_filters_deprecated('fluentform_validation_error', [
116 $errors,
117 $this->form,
118 $fields,
119 $formData
120 ],
121 FLUENTFORM_FRAMEWORK_UPGRADE,
122 'fluentform/validation_error',
123 'Use fluentform/validation_error instead of fluentform_validation_error.'
124 );
125
126 $errors = $this->app->applyFilters('fluentform/validation_error', $errors, $this->form, $fields, $formData);
127 }
128
129 foreach ($fields as $fieldKey => $field) {
130 $field['data_key'] = $fieldKey;
131 $inputName = Arr::get($field, 'raw.attributes.name');
132 $field['name'] = $inputName;
133 $error = $this->validateInput($field, $formData, $this->form);
134 $error = apply_filters_deprecated('fluentform_validate_input_item_' . $field['element'], [
135 $error,
136 $field,
137 $formData,
138 $fields,
139 $this->form,
140 $errors
141 ],
142 FLUENTFORM_FRAMEWORK_UPGRADE,
143 'fluentform/validate_input_item_' . $field['element'],
144 'Use fluentform/validate_input_item_' . $field['element'] . ' instead of fluentform_validate_input_item_' . $field['element']
145 );
146
147 $error = apply_filters('fluentform/validate_input_item_' . $field['element'], $error, $field, $formData, $fields, $this->form, $errors);
148 if ($error) {
149 if (empty($errors[$inputName])) {
150 $errors[$inputName] = [];
151 }
152 if (is_string($error)) {
153 $error = [fluentform_sanitize_html($error)];
154 } else {
155 if (is_array($error)) {
156 foreach ($error as $rule => $message) {
157 $error[$rule] = fluentform_sanitize_html($message);
158 }
159 }
160 }
161 $errors[$inputName] = array_merge($error, $errors[$inputName]);
162 }
163 }
164
165 $errors = apply_filters_deprecated('fluentform_validation_errors', [
166 $errors,
167 $formData,
168 $this->form,
169 $fields
170 ],
171 FLUENTFORM_FRAMEWORK_UPGRADE,
172 'fluentform/validation_errors',
173 'Use fluentform/validation_errors instead of fluentform_validation_errors.'
174 );
175
176 $errors = apply_filters('fluentform/validation_errors', $errors, $formData, $this->form, $fields);
177
178 if ('yes' == Helper::getFormMeta($this->form->id, '_has_user_registration') && !get_current_user_id()) {
179 $errors = apply_filters_deprecated('fluentform_validation_user_registration_errors', [
180 $errors,
181 $formData,
182 $this->form,
183 $fields
184 ],
185 FLUENTFORM_FRAMEWORK_UPGRADE,
186 'fluentform/validation_user_registration_errors',
187 'Use fluentform/validation_user_registration_errors instead of fluentform_validation_user_registration_errors.'
188 );
189
190 $errors = apply_filters('fluentform/validation_user_registration_errors', $errors, $formData, $this->form, $fields);
191 }
192
193 if ('yes' == Helper::getFormMeta($this->form->id, '_has_user_update') && get_current_user_id()) {
194 $errors = apply_filters_deprecated('fluentform_validation_user_update_errors', [
195 $errors,
196 $formData,
197 $this->form,
198 $fields
199 ],
200 FLUENTFORM_FRAMEWORK_UPGRADE,
201 'fluentform/validation_user_update_errors',
202 'Use fluentform/validation_user_update_errors instead of fluentform_validation_user_update_errors.'
203 );
204 $errors = apply_filters('fluentform/validation_user_update_errors', $errors, $formData, $this->form, $fields);
205 }
206
207 if ('update' == Arr::get(Helper::getFormMeta($this->form->id, 'postFeeds'), 'post_form_type')) {
208 $errors = apply_filters('fluentform/validation_post_update_errors', $errors, $formData, $this->form, $fields);
209 }
210
211 if ($errors) {
212 // phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped -- Exception message, not output
213 throw new ValidationException('', 423, null, ['errors' => $errors]);
214 }
215
216 return true;
217 }
218
219 protected function validateInput($field, $formData, $form, $fieldName = '', $inputValue = [])
220 {
221 return Helper::validateInput($field, $formData, $form, $fieldName, $inputValue);
222 }
223
224 /**
225 * Prevents malicious attacks when the submission
226 * count exceeds in an allowed interval.
227 * @throws ValidationException
228 */
229 public function preventMaliciousAttacks()
230 {
231 $prevent = apply_filters('fluentform/prevent_malicious_attacks', true, $this->form->id);
232
233 if ($prevent) {
234 $maxSubmissionCount = apply_filters('fluentform/max_submission_count', 5, $this->form->id);
235 $minSubmissionInterval = apply_filters('fluentform/min_submission_interval', 30, $this->form->id);
236
237 $interval = date('Y-m-d H:i:s', strtotime(current_time('mysql')) - $minSubmissionInterval);
238
239 $clientIp = sanitize_text_field($this->app->request->getIp());
240 $submissionCount = wpFluent()->table('fluentform_submissions')
241 ->where('status', '!=', 'trashed')
242 ->where('ip', $clientIp ?: '0.0.0.0')
243 ->where('created_at', '>=', $interval)
244 ->count();
245
246 if ($submissionCount >= $maxSubmissionCount) {
247 throw new ValidationException('', 429, null, [
248 'errors' => [
249 'restricted' => [
250 // phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped -- Sanitized by fluentform_sanitize_html
251 fluentform_sanitize_html(apply_filters(
252 'fluentform/too_many_requests',
253 __('Too Many Requests.', 'fluentform'),
254 $this->form->id
255 )),
256 ],
257 ]
258 ]);
259 }
260 }
261 }
262
263 /**
264 * Validate form data based on the form restrictions settings.
265 *
266 * @param $fields
267 * @throws ValidationException
268 */
269 private function validateRestrictions(&$fields)
270 {
271 $formSettings = FormMeta::retrieve('formSettings', $this->form->id);
272
273 $this->form->settings = is_array($formSettings) ? $formSettings : [];
274
275 $isAllowed = [
276 'status' => true,
277 'message' => '',
278 ];
279
280 // This will check the following restriction settings.
281 // 1. limitNumberOfEntries
282 // 2. scheduleForm
283 // 3. requireLogin
284 // 4. restricted submission based on ip, country and keywords
285
286 /* This filter is deprecated and will be removed soon */
287 $isAllowed = apply_filters('fluentform_is_form_renderable', $isAllowed, $this->form);
288
289 $isAllowed = apply_filters('fluentform/is_form_renderable', $isAllowed, $this->form);
290
291 if (!$isAllowed['status']) {
292 throw new ValidationException('', 422, null, [
293 'errors' => [
294 'restricted' => [
295 // phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped -- Sanitized by fluentform_sanitize_html
296 fluentform_sanitize_html($isAllowed['message']),
297 ],
298 ],
299 ]);
300 }
301
302 // Since we are here, we should now handle if the form should be allowed to submit empty.
303 $restrictions = Arr::get($this->form->settings, 'restrictions.denyEmptySubmission', []);
304
305 $this->handleDenyEmptySubmission($restrictions, $fields);
306
307 $formRestrictions = Arr::get($this->form->settings, 'restrictions.restrictForm', []);
308
309 $this->handleRestrictedSubmission($formRestrictions, $fields);
310 }
311
312 /**
313 * Handle response when empty form submission is not allowed.
314 *
315 * @param array $settings
316 * @param $fields
317 * @throws ValidationException
318 */
319 private function handleDenyEmptySubmission($settings, &$fields)
320 {
321 // Determine whether empty form submission is allowed or not.
322 if (Arr::isTrue($settings, 'enabled')) {
323 // confirm this form has no required fields.
324 if (!FormFieldsParser::hasRequiredFields($this->form, $fields)) {
325 // Filter out the form data which doesn't have values.
326 $filteredFormData = array_filter(
327 // Filter out the other meta fields that aren't actual inputs.
328 array_intersect_key($this->formData, $fields)
329 );
330 if (!count(Helper::arrayFilterRecursive($filteredFormData))) {
331 $defaultMessage = esc_html(__('Sorry! You can\'t submit an empty form.','fluentform'));
332 $customMessage = Arr::get($settings, 'message');
333 $customMessage = fluentform_sanitize_html(apply_filters('fluentform/deny_empty_submission_message', $customMessage, $this->form));
334
335 throw new ValidationException('', 422, null, [
336 'errors' => [
337 'restricted' => [
338 // phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped -- Sanitized by fluentform_sanitize_html
339 !empty($customMessage) ? fluentform_sanitize_html($customMessage) : fluentform_sanitize_html($defaultMessage),
340 ],
341 ],
342 ]);
343 }
344 }
345 }
346 }
347
348 /**
349 * Handle response when form submission is restricted based on ip, country or keywords.
350 *
351 * @param array $settings
352 * @param $fields
353 * @throws ValidationException
354 */
355 protected function handleRestrictedSubmission($settings, &$fields)
356 {
357 // Determine this restriction is enabled ot not
358 if (!Arr::isTrue($settings, 'enabled')) {
359 return;
360 }
361
362 $rawIp = $this->app->request->getIp();
363 if (is_array($rawIp)) {
364 $rawIp = Arr::get($rawIp, '0');
365 }
366 $ip = sanitize_text_field($rawIp);
367 if ($ip) {
368 $this->checkIpRestriction($settings, $ip);
369 }
370
371 $isCountryRestrictionEnabled = Arr::isTrue($settings, 'fields.country.status');
372 if ($isCountryRestrictionEnabled) {
373 if ($ipInfo = $this->getIpInfo($ip)) {
374 $country = Arr::get($ipInfo, 'country');
375 } else {
376 $country = $this->getIpBasedOnCountry($ip);
377 }
378 $this->checkCountryRestriction($settings, $country);
379 }
380
381 $this->checkKeyWordRestriction($settings);
382 }
383
384
385 /**
386 * Validate nonce.
387 * @throws ValidationException
388 */
389 protected function validateNonce()
390 {
391 $formId = $this->form->id;
392 $shouldVerifyNonce = false;
393 /* This filter is deprecated and will be removed soon. */
394 $shouldVerifyNonce = $this->app->applyFilters('fluentform_nonce_verify', $shouldVerifyNonce, $formId);
395
396 $shouldVerifyNonce = $this->app->applyFilters('fluentform/nonce_verify', $shouldVerifyNonce, $formId);
397
398 if ($shouldVerifyNonce) {
399 $nonce = Arr::get($this->formData, '_fluentform_' . $formId . '_fluentformnonce');
400 if (!wp_verify_nonce($nonce, 'fluentform-submit-form')) {
401 $errors = apply_filters_deprecated(
402 'fluentForm_nonce_error',
403 [
404 '_fluentformnonce' => [
405 __('Nonce verification failed, please try again.', 'fluentform'),
406 ],
407 ],
408 FLUENTFORM_FRAMEWORK_UPGRADE,
409 'fluentForm/nonce_error',
410 'Use fluentForm/nonce_error instead of fluentForm_nonce_error.'
411 );
412
413 $errors = $this->app->applyFilters('fluentForm/nonce_error', $errors);
414 // phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped -- Exception message, not output
415 throw new ValidationException('', 422, null, ['errors' => $errors]);
416 }
417 }
418 }
419
420 /** Validate Akismet Spam
421 * @throws ValidationException
422 */
423 public function handleAkismetSpamError()
424 {
425 $settings = get_option('_fluentform_global_form_settings');
426 if (!$settings || 'validation_failed' != Arr::get($settings, 'misc.akismet_validation')) {
427 return;
428 }
429
430 $errors = [
431 '_fluentformakismet' => __('Submission marked as spammed. Please try again', 'fluentform'),
432 ];
433 // phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped -- Exception message, not output
434 throw new ValidationException('', 422, null, ['errors' => $errors]);
435 }
436
437 /** Validate CleanTalk Spam
438 * @throws ValidationException
439 */
440 public function handleCleanTalkSpamError()
441 {
442 $settings = get_option('_fluentform_global_form_settings');
443 if (!$settings || 'validation_failed' != Arr::get($settings, 'misc.cleantalk_validation')) {
444 return;
445 }
446
447 $errors = [
448 '_fluentformcleantalk' => __('Submission marked as spammed. Please try again', 'fluentform'),
449 ];
450 // phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped -- Exception message, not output
451 throw new ValidationException('', 422, null, ['errors' => $errors]);
452 }
453
454 /** Validate CleanTalk Spam While Using API
455 * @throws ValidationException
456 */
457 public function handleCleanTalkSpamErrorUsingAPi()
458 {
459 $cleantalkSettings = get_option('_fluentform_cleantalk_details');
460
461 if (
462 !$cleantalkSettings ||
463 'validation_failed' != Arr::get($cleantalkSettings, 'validation')
464 ) {
465 return;
466 }
467
468 $errors = [
469 '_fluentformcleantalk' => __('Submission marked as spammed. Please try again', 'fluentform'),
470 ];
471 // phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped -- Exception message, not output
472 throw new ValidationException('', 422, null, ['errors' => $errors]);
473 }
474
475 public function isAkismetSpam($formData, $form)
476 {
477 if (!AkismetHandler::isEnabled()) {
478 return false;
479 }
480 $isSpamCheck = apply_filters_deprecated(
481 'fluentform_akismet_check_spam',
482 [
483 true,
484 $form->id,
485 $formData
486 ],
487 FLUENTFORM_FRAMEWORK_UPGRADE,
488 'fluentform/akismet_check_spam',
489 'Use fluentform/akismet_check_spam instead of fluentform_akismet_check_spam.'
490 );
491
492 $isSpamCheck = apply_filters('fluentform/akismet_check_spam', $isSpamCheck, $form->id, $formData);
493
494 if (!$isSpamCheck) {
495 return false;
496 }
497 // Let's validate now
498 $isSpam = AkismetHandler::isSpamSubmission($formData, $form);
499
500 $isSpam = apply_filters_deprecated(
501 'fluentform_akismet_spam_result',
502 [
503 $isSpam,
504 $form->id,
505 $formData
506 ],
507 FLUENTFORM_FRAMEWORK_UPGRADE,
508 'fluentform/akismet_spam_result',
509 'Use fluentform/akismet_spam_result instead of fluentform_akismet_spam_result.'
510 );
511 return apply_filters('fluentform/akismet_spam_result', $isSpam, $form->id, $formData);
512 }
513
514 public function isCleanTalkSpam($formData, $form)
515 {
516 if (!CleanTalkHandler::isEnabled()) {
517 return false;
518 }
519 $isSpamCheck = apply_filters('fluentform/cleantalk_check_spam', true, $form->id, $formData);
520
521 if (!$isSpamCheck) {
522 return false;
523 }
524 $isSpam = CleanTalkHandler::isSpamSubmission($formData, $form);
525
526 return apply_filters('fluentform/cleantalk_spam_result', $isSpam, $form->id, $formData);
527 }
528
529 public function isCleanTalkSpamUsingApi($formData, $form)
530 {
531 if (!CleanTalkHandler::isCleantalkActivated()) {
532 return false;
533 }
534
535 $isSpamCheck = apply_filters('fluentform/cleantalk_check_spam', true, $form->id, $formData);
536
537 if (!$isSpamCheck) {
538 return false;
539 }
540
541 $isSpam = CleanTalkHandler::spamSubmissionCheckWithApi($formData, $form);
542
543 return apply_filters('fluentform/cleantalk_spam_result', $isSpam, $form->id, $formData);
544 }
545
546 /**
547 * Validate reCaptcha.
548 * Uses 'fluentform/disable_captcha' filter with 'recaptcha' as the captcha type since 6.0.3
549 * @throws ValidationException
550 */
551 private function validateReCaptcha()
552 {
553 // Check if autoload_captcha is enabled and if it's not recaptcha, skip validation
554 if ($this->shouldSkipCaptchaValidation('recaptcha')) {
555 return;
556 }
557
558 $hasAutoRecap = apply_filters_deprecated(
559 'ff_has_auto_recaptcha',
560 [
561 false
562 ],
563 FLUENTFORM_FRAMEWORK_UPGRADE,
564 'fluentform/has_recaptcha',
565 'Use fluentform/has_recaptcha instead of ff_has_auto_recaptcha.'
566 );
567 $autoInclude = apply_filters('fluentform/has_recaptcha', $hasAutoRecap);
568 $disableReCaptcha = apply_filters('fluentform/disable_captcha', false, $this->form, 'recaptcha');
569
570 if (!$disableReCaptcha && (FormFieldsParser::hasElement($this->form, 'recaptcha') || $autoInclude)) {
571 $keys = get_option('_fluentform_reCaptcha_details');
572 $token = Arr::get($this->formData, 'g-recaptcha-response');
573 $version = 'v2_visible';
574 if (!empty($keys['api_version'])) {
575 $version = $keys['api_version'];
576 }
577 $isValid = ReCaptcha::validate($token, $keys['secretKey'], $version);
578
579 if (!$isValid) {
580 // phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped -- Exception message, not output
581 throw new ValidationException('', 422, null, [
582 'errors' => [
583 'g-recaptcha-response' => [
584 esc_html(__('reCaptcha verification failed, please try again.', 'fluentform')),
585 ],
586 ],
587 ]);
588 }
589 }
590 }
591
592 /**
593 * Validate hCaptcha.
594 *
595 * @throws ValidationException
596 */
597 private function validateHCaptcha()
598 {
599 // Check if autoload_captcha is enabled and if it's not hcaptcha, skip validation
600 if ($this->shouldSkipCaptchaValidation('hcaptcha')) {
601 return;
602 }
603 $hasAutoHcap = apply_filters_deprecated(
604 'ff_has_auto_hcaptcha',
605 [
606 false
607 ],
608 FLUENTFORM_FRAMEWORK_UPGRADE,
609 'fluentform/has_hcaptcha',
610 'Use fluentform/has_hcaptcha instead of ff_has_auto_hcaptcha.'
611 );
612 $autoInclude = apply_filters('fluentform/has_hcaptcha', $hasAutoHcap);
613 $disableHCaptcha = apply_filters('fluentform/disable_captcha', false, $this->form, 'hcaptcha');
614
615 FormFieldsParser::resetData();
616 if (!$disableHCaptcha && (FormFieldsParser::hasElement($this->form, 'hcaptcha') || $autoInclude)) {
617 $keys = get_option('_fluentform_hCaptcha_details');
618 $token = Arr::get($this->formData, 'h-captcha-response');
619 $isValid = HCaptcha::validate($token, $keys['secretKey']);
620
621 if (!$isValid) {
622 throw new ValidationException('', 422, null, [
623 'errors' => [
624 'h-captcha-response' => [
625 esc_html(__('hCaptcha verification failed, please try again.', 'fluentform')),
626 ],
627 ],
628 ]);
629 }
630 }
631 }
632
633 /**
634 * Validate turnstile.
635 *
636 * @throws ValidationException
637 */
638 private function validateTurnstile()
639 {
640 // Check if autoload_captcha is enabled and if it's not turnstile, skip validation
641 if ($this->shouldSkipCaptchaValidation('turnstile')) {
642 return;
643 }
644
645 $hasAutoTurnsTile = apply_filters_deprecated(
646 'ff_has_auto_turnstile',
647 [
648 false
649 ],
650 FLUENTFORM_FRAMEWORK_UPGRADE,
651 'fluentform/has_turnstile',
652 'Use fluentform/has_turnstile instead of ff_has_auto_turnstile.'
653 );
654 $autoInclude = apply_filters('fluentform/has_turnstile', $hasAutoTurnsTile);
655 $disableTurnsTile = apply_filters('fluentform/disable_captcha', false, $this->form, 'turnstile');
656
657 if (!$disableTurnsTile && (FormFieldsParser::hasElement($this->form, 'turnstile') || $autoInclude)) {
658 $keys = get_option('_fluentform_turnstile_details');
659 $token = Arr::get($this->formData, 'cf-turnstile-response');
660
661 $isValid = Turnstile::validate($token, $keys['secretKey']);
662
663 if (!$isValid) {
664 throw new ValidationException('', 422, null, [
665 'errors' => [
666 'cf-turnstile-response' => [
667 esc_html(__('Turnstile verification failed, please try again.', 'fluentform')),
668 ],
669 ],
670 ]);
671 }
672 }
673 }
674
675
676 /**
677 * Delegate the validation rules & messages to the
678 * ones that the validation library recognizes.
679 *
680 * @param $rules
681 * @param $messages
682 * @param array $search
683 * @param array $replace
684 * @return array
685 */
686 protected function delegateValidations($rules, $messages, $search = [], $replace = [])
687 {
688 $search = $search ?: ['max_file_size', 'allowed_file_types'];
689 $replace = $replace ?: ['max', 'mimes'];
690
691 foreach ($rules as &$rule) {
692 $rule = str_replace($search, $replace, $rule);
693 }
694
695 foreach ($messages as $key => $message) {
696 $newKey = str_replace($search, $replace, $key);
697 $messages[$newKey] = $message;
698 unset($messages[$key]);
699 }
700
701 return [$rules, $messages];
702 }
703
704 /**
705 * Get IP info from ipinfo.io
706 *
707 * @throws ValidationException
708 */
709 private function getIpInfo($ip) {
710 $token = Helper::getIpinfo();
711
712 if (!$token) {
713 return false;
714 }
715
716 $url = 'https://ipinfo.io/' . $ip . '?token=' . $token;
717 $data = wp_remote_get($url);
718 $code = wp_remote_retrieve_response_code($data);
719 $body = wp_remote_retrieve_body($data);
720 $result = \json_decode($body, true);
721 if ($code === 200) {
722 return $result;
723 } else {
724 $message = __('Sorry! There is an error in your geocode IP address settings. Please check the token', 'fluentform');
725 self::throwValidationException($message);
726 }
727 }
728
729 /**
730 * Get IP and Country from geoplugin
731 *
732 * @throws ValidationException
733 */
734 private function getIpBasedOnCountry($ip) {
735 $request = wp_remote_get("https://apip.cc/api-json/{$ip}");
736 $code = wp_remote_retrieve_response_code($request);
737
738 $message = __('Sorry! There is an error occurred in getting Country using ip-api.com. Please check form settings and try again.', 'fluentform');
739
740 if ($code === 200) {
741 $body = wp_remote_retrieve_body($request);
742 $body = \json_decode($body, true);
743 $status = Arr::get($body, 'status', false) === 'success';
744
745 if (!$status) {
746 return Helper::getCountryCodeFromHeaders();
747 }
748
749 if ($country = Arr::get($body,'CountryCode')) {
750 return $country;
751 } else {
752 self::throwValidationException($message);
753 }
754 } else {
755 if ($country = Helper::getCountryCodeFromHeaders()) {
756 return $country;
757 }
758 self::throwValidationException($message);
759 }
760 }
761
762 /**
763 * @param $value
764 * @param $providedKeywords
765 * @return bool
766 */
767 public static function containsRestrictedKeywords($value, $providedKeywords) {
768 preg_match_all('/\b[\p{L}\d\s]+\b/u', $value, $matches);
769 $words = $matches[0] ?? [];
770
771 foreach ($providedKeywords as $keyword) {
772 foreach ($words as $word) {
773 if (
774 strtoupper($word) === strtoupper($keyword) ||
775 preg_match('/\b' . strtoupper($keyword) . '\b/', strtoupper($word))
776 ) {
777 return true;
778 }
779 }
780 }
781
782 return false;
783 }
784
785
786 /**
787 * @throws ValidationException
788 */
789 private function checkIpRestriction($settings, $ip)
790 {
791 if (Arr::isTrue($settings, 'fields.ip.status') && $ip) {
792 $providedIp = array_map('trim', explode(',', (string) Arr::get($settings, 'fields.ip.values', '')));
793
794 $isFailed = Arr::get($settings, 'fields.ip.validation_type') === 'fail_on_condition_met';
795
796 $failedSubmissionIfExists = $isFailed && in_array($ip, $providedIp);
797 $allowSubmissionIfNotExists = !$isFailed && !in_array($ip, $providedIp);
798
799 if ($failedSubmissionIfExists || $allowSubmissionIfNotExists) {
800 $defaultMessage = __('Sorry! You can\'t submit a form from your IP address.', 'fluentform');
801 $message = apply_filters('fluentform/ip_restriction_message', Arr::get($settings, 'fields.ip.message', $defaultMessage), $this->form);
802 self::throwValidationException($message);
803 }
804 }
805 }
806
807 /**
808 * @throws ValidationException
809 */
810 private function checkCountryRestriction($settings, $country)
811 {
812 if (Arr::isTrue($settings, 'fields.country.status') && $country) {
813 $providedCountry = (array) Arr::get($settings, 'fields.country.values', []);
814
815 $isFailed = Arr::get($settings, 'fields.country.validation_type') === 'fail_on_condition_met';
816
817 $failedSubmissionIfExists = $isFailed && in_array($country, $providedCountry);
818 $allowSubmissionIfNotExists = !$isFailed && !in_array($country, $providedCountry);
819
820 if ($failedSubmissionIfExists || $allowSubmissionIfNotExists) {
821 $defaultMessage = __('Sorry! You can\'t submit this form from the country you are residing.', 'fluentform');
822 $message = apply_filters('fluentform/country_restriction_message', Arr::get($settings, 'fields.country.message', $defaultMessage), $this->form);
823 self::throwValidationException($message);
824 }
825 }
826 }
827
828 private function checkKeyWordRestriction($settings)
829 {
830 if (!Arr::isTrue($settings, 'fields.keywords.status')) {
831 return;
832 }
833
834 $keywords = Arr::get($settings, 'fields.keywords.values');
835 if (!$keywords || !is_string($keywords)) {
836 return;
837 }
838 $providedKeywords = explode(',', $keywords);
839 $providedKeywords = array_filter(array_map('trim', $providedKeywords));
840 if (!$providedKeywords) {
841 return;
842 }
843 $inputSubmission = array_intersect_key(
844 $this->formData,
845 array_flip(
846 array_keys(
847 FormFieldsParser::getInputs($this->form)
848 )
849 )
850 );
851 $defaultMessage = __('Sorry! Your submission contains some restricted keywords.', 'fluentform');
852 $message = apply_filters('fluentform/keyword_restriction_message', Arr::get($settings, 'fields.keywords.message', $defaultMessage), $this->form);
853
854 self::checkKeywordsMatching($inputSubmission, $message, $providedKeywords);
855 }
856
857 private static function checkKeywordsMatching($inputSubmission, $message, $providedKeywords)
858 {
859 foreach ($inputSubmission as $value) {
860 if (!empty($value)) {
861 if (is_array($value)) {
862 self::checkKeywordsMatching($value, $message, $providedKeywords);
863 } else {
864 if (self::containsRestrictedKeywords($value, $providedKeywords)) {
865 self::throwValidationException($message);
866 }
867 }
868 }
869 }
870 }
871
872 /**
873 * @throws ValidationException
874 */
875 public static function throwValidationException($message) {
876 throw new ValidationException('', 422, null, [
877 'errors' => [
878 'restricted' => [
879 // phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped -- Sanitized by fluentform_sanitize_html
880 fluentform_sanitize_html($message)
881 ],
882 ],
883 ]);
884 }
885
886 /**
887 * Check if captcha validation should be skipped based on autoload captcha settings
888 *
889 * When autoload captcha is enabled, only the selected captcha type should be validated.
890 * This method returns true if the current captcha type is NOT the selected autoload type,
891 * preventing unnecessary validation of multiple captcha types on the same form.
892 *
893 * @param string $captchaType The captcha type to check ('recaptcha', 'hcaptcha', 'turnstile')
894 * @return bool True if validation should be skipped, false otherwise
895 */
896 private function shouldSkipCaptchaValidation($captchaType)
897 {
898 $globalSettings = get_option('_fluentform_global_form_settings');
899 $autoloadEnabled = Arr::get($globalSettings, 'misc.autoload_captcha');
900
901 // If autoload captcha is not enabled, don't skip any validation
902 if (!$autoloadEnabled) {
903 return false;
904 }
905
906 $selectedCaptchaType = Arr::get($globalSettings, 'misc.captcha_type');
907
908 // If the current captcha type matches the selected autoload type, proceed with validation
909 if ($captchaType === $selectedCaptchaType) {
910 return false;
911 }
912
913 return true; // Skip validation for non-selected captcha types
914 }
915 }
916