PluginProbe
Fluent Forms – Customizable Contact Forms, Survey, Quiz, & Conversational Form Builder / 4.3.13
Fluent Forms – Customizable Contact Forms, Survey, Quiz, & Conversational Form Builder v4.3.13
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 3.6.66 All 195 releases
fluentform / app / Modules / Form / FormHandler.php

FormHandler.php in Fluent Forms – Customizable Contact Forms, Survey, Quiz, & Conversational Form Builder 4.3.13, at app/Modules/Form/FormHandler.php

708 lines 22.9 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\Modules\Form;
4
5 use FluentForm\App\Databases\Migrations\FormSubmissionDetails;
6 use FluentForm\App\Helpers\Helper;
7 use FluentForm\App\Modules\Activator;
8 use FluentForm\App\Modules\Entries\Entries;
9 use FluentForm\App\Modules\ReCaptcha\ReCaptcha;
10 use FluentForm\App\Modules\HCaptcha\HCaptcha;
11 use FluentForm\App\Modules\Turnstile\Turnstile;
12 use FluentForm\App\Services\Browser\Browser;
13 use FluentForm\App\Services\FormBuilder\ShortCodeParser;
14 use FluentForm\Framework\Foundation\Application;
15 use FluentForm\Framework\Helpers\ArrayHelper as Arr;
16 use FluentForm\Framework\Helpers\ArrayHelper;
17
18 class FormHandler
19 {
20 /**
21 * @var \FluentForm\Framework\Foundation\Application
22 */
23 protected $app;
24
25 /**
26 * @var \FluentForm\Framework\Request\Request
27 */
28 protected $request;
29
30 /**
31 * @var array $formData
32 */
33 protected $formData;
34
35 /**
36 * The Fluent Forms object.
37 *
38 * @var \stdClass
39 */
40 protected $form;
41
42 /**
43 * Form Handler constructor.
44 *
45 * @param \FluentForm\Framework\Foundation\Application $app
46 */
47 public function __construct(Application $app)
48 {
49 $this->app = $app;
50 $this->request = $app->request;
51 }
52
53 /**
54 * Set the form using it's ID.
55 *
56 * @param $formId
57 * @return $this
58 */
59 public function setForm($formId)
60 {
61 $this->form = wpFluent()->table('fluentform_forms')->find($formId);
62 return $this;
63 }
64
65 /**
66 * Handle form submition
67 */
68 public function onSubmit()
69 {
70 // Parse the url encoded data from the request object.
71 parse_str($this->app->request->get('data'), $data);
72
73 $data['_wp_http_referer'] = urldecode( $data['_wp_http_referer'] );
74
75 // Merge it back again to the request object.
76 $this->app->request->merge(['data' => $data]);
77
78 $formId = intval($this->app->request->get('form_id'));
79
80 $this->setForm($formId);
81
82 if (!$this->form) {
83 wp_send_json([
84 'errors' => [],
85 'message' => 'Sorry, No corresponding form found'
86 ], 423);
87 }
88
89 // Parse the form and get the flat inputs with validations.
90 $fields = FormFieldsParser::getInputs($this->form, ['rules', 'raw']);
91
92 // Sanitize the data properly.
93 $this->formData = fluentFormSanitizer($data, null, $fields);
94
95 // Now validate the data using the previous validations.
96 $this->validate($fields);
97
98 // Prepare the data to be inserted to the DB.
99 $insertData = $this->prepareInsertData();
100
101 if ($this->isSpam($this->formData, $this->form)) {
102 $insertData['status'] = 'spam';
103 $this->handleSpamError();
104 }
105
106 do_action('fluentform_before_insert_submission', $insertData, $data, $this->form);
107
108 if ($this->form->has_payment) {
109 do_action('fluentform_before_insert_payment_form', $insertData, $data, $this->form);
110 }
111
112 $insertId = wpFluent()->table('fluentform_submissions')->insert($insertData);
113
114 $uidHash = md5(wp_generate_uuid4() . $insertId);
115 Helper::setSubmissionMeta($insertId, '_entry_uid_hash', $uidHash, $formId);
116
117 do_action('fluentform_before_form_actions_processing', $insertId, $this->formData, $this->form);
118
119 $result = $this->processFormSubmissionData($insertId, $this->formData, $this->form);
120
121 wp_send_json_success($result, 200);
122 }
123
124 public function processFormSubmissionData($insertId, $formData, $form)
125 {
126 if ($insertId) {
127 ob_start();
128 $entries = new Entries();
129 $entries->recordEntryDetails($insertId, $form->id, $formData);
130 $isError = ob_get_clean();
131 if ($isError) {
132 FormSubmissionDetails::migrate();
133 }
134 }
135
136 $returnData = $this->getReturnData($insertId, $form, $formData);
137
138 $error = '';
139 try {
140 $this->app->doAction(
141 'fluentform_submission_inserted',
142 $insertId,
143 $formData,
144 $form
145 );
146
147 Helper::setSubmissionMeta($insertId, 'is_form_action_fired', 'yes');
148
149 $this->app->doAction(
150 'fluentform_submission_inserted_' . $form->type . '_form',
151 $insertId,
152 $formData,
153 $form
154 );
155 } catch (\Exception $e) {
156 if (defined('WP_DEBUG') && WP_DEBUG) {
157 $error = $e->getMessage();
158 }
159 }
160
161 do_action('fluentform_before_submission_confirmation', $insertId, $formData, $form);
162
163 // that was a typo. We will remove that after september
164 // @todo: Remove this action after september 2021
165 do_action('fluenform_before_submission_confirmation', $insertId, $formData, $form);
166
167 return [
168 'insert_id' => $insertId,
169 'result' => $returnData,
170 'error' => $error
171 ];
172 }
173
174 public function getReturnData($insertId, $form, $formData)
175 {
176 if (empty($form->settings)) {
177 $formSettings = wpFluent()->table('fluentform_form_meta')
178 ->where('form_id', $form->id)
179 ->where('meta_key', 'formSettings')
180 ->first();
181
182 $form->settings = $formSettings ? json_decode($formSettings->value, true) : [];
183 }
184
185
186 $confirmation = apply_filters(
187 'fluentform_form_submission_confirmation',
188 $form->settings['confirmation'],
189 $formData,
190 $form
191 );
192
193 if ($confirmation['redirectTo'] == 'samePage') {
194
195 $confirmation['messageToShow'] = apply_filters('fluentform_submission_message_parse', $confirmation['messageToShow'], $insertId, $formData, $form);
196
197
198 $message = ShortCodeParser::parse(
199 $confirmation['messageToShow'],
200 $insertId,
201 $formData,
202 $form,
203 false,
204 true
205 );
206
207
208 $message = $message ? $message : 'The form has been successfully submitted.';
209
210 $returnData = [
211 'message' => do_shortcode($message),
212 'action' => $confirmation['samePageFormBehavior'],
213 ];
214
215 } else {
216 $redirectUrl = Arr::get($confirmation, 'customUrl');
217
218 if ($confirmation['redirectTo'] == 'customPage') {
219 $redirectUrl = get_permalink($confirmation['customPage']);
220 }
221
222 if (
223 (Arr::get($confirmation, 'enable_query_string') == 'yes') &&
224 Arr::get($confirmation, 'query_strings')
225 ) {
226 if (strpos($redirectUrl, '?')) {
227 $redirectUrl .= '&' . Arr::get($confirmation, 'query_strings');
228 } else {
229 $redirectUrl .= '?' . Arr::get($confirmation, 'query_strings');
230 }
231 }
232
233 $isUrlParser = apply_filters('fluentform_will_parse_url_value', true, $form);
234
235 $redirectUrl = ShortCodeParser::parse(
236 $redirectUrl,
237 $insertId,
238 $formData,
239 $form,
240 $isUrlParser
241 );
242
243 if($isUrlParser) {
244 /*
245 * For Empty Redirect Value
246 */
247 if(strpos($redirectUrl, '=&') || substr($redirectUrl, -1) == '=') {
248 $urlArray = explode('?', $redirectUrl);
249 $baseUrl = array_shift($urlArray);
250
251 $query = wp_parse_url($redirectUrl)['query'];
252
253 $queryParams = explode('&', $query);
254
255 $params = [];
256 foreach ($queryParams as $queryParam) {
257 $paramArray = explode('=', $queryParam);
258 if(!empty($paramArray[1])) {
259 $params[$paramArray[0]] = $paramArray[1];
260 }
261 }
262
263 $redirectUrl = add_query_arg($params, $baseUrl);
264 }
265 }
266
267 $message = ShortCodeParser::parse(
268 ArrayHelper::get($confirmation, 'redirectMessage', ''),
269 $insertId,
270 $formData,
271 $form,
272 false,
273 true
274 );
275
276 $returnData = [
277 'redirectUrl' => wp_sanitize_redirect(urldecode($redirectUrl)),
278 'message' => $message
279 ];
280 }
281
282 return $this->app->applyFilters(
283 'fluentform_submission_confirmation',
284 $returnData,
285 $form,
286 $confirmation
287 );
288 }
289
290 /**
291 * Validate form data.
292 *
293 * @param $fields
294 * @return bool
295 */
296 private function validate(&$fields)
297 {
298 $this->preventMaliciousAttacks();
299
300 $this->validateRestrictions($fields);
301
302 $this->validateNonce();
303
304 $this->validateReCaptcha();
305 $this->validateHCaptcha();
306 $this->validateTurnstile();
307
308 foreach ($fields as $fieldName => $field) {
309 if(isset($this->formData[$fieldName])) {
310 $element = $field['element'];
311 $this->formData[$fieldName] = apply_filters('fluentform_input_data_'.$element, $this->formData[$fieldName], $field, $this->formData, $this->form);
312 }
313 }
314
315 $originalValidations = FormFieldsParser::getValidations($this->form, $this->formData, $fields);
316
317 // Fire an event so that one can hook into it to work with the rules & messages.
318 $validations = apply_filters('fluentform_validations', $originalValidations, $this->form, $this->formData);
319
320 /*
321 * Clean talk fix for now
322 * They should not hook fluentform_validations and return nothing!
323 * We will remove this extra check once it's done
324 */
325 if ($originalValidations && (!$validations || !array_filter($validations))) {
326 $validations = $originalValidations;
327 }
328
329 $validator = \FluentValidator\Validator::make($this->formData, $validations[0], $validations[1]);
330
331 $errors = [];
332 if ($validator->validate()->fails()) {
333 foreach ($validator->errors() as $attribute => $rules) {
334 $position = strpos($attribute, ']');
335
336 if ($position) {
337 $attribute = substr($attribute, 0, strpos($attribute, ']') + 1);
338 }
339
340 $errors[$attribute] = $rules;
341 }
342 // Fire an event so that one can hook into it to work with the errors.
343 $errors = $this->app->applyFilters('fluentform_validation_error', $errors, $this->form, $fields, $this->formData);
344 }
345
346 foreach ($fields as $fieldKey => $field) {
347 $field['data_key'] = $fieldKey;
348 $inputName = \FluentForm\Framework\Helpers\ArrayHelper::get($field, 'raw.attributes.name');
349 $field['name'] = $inputName;
350 $error = apply_filters('fluentform_validate_input_item_' . $field['element'], '', $field, $this->formData, $fields, $this->form, $errors);
351 if ($error) {
352 if (empty($errors[$inputName])) {
353 $errors[$inputName] = [];
354 }
355
356 if(is_string($error)) {
357 $error = [$error];
358 }
359
360 $errors[$inputName] = array_merge($error, $errors[$inputName]);
361 }
362 }
363
364 $errors = apply_filters('fluentform_validation_errors', $errors, $this->formData, $this->form, $fields);
365
366 if (Helper::getFormMeta($this->form->id, '_has_user_registration') == 'yes' && !get_current_user_id()) {
367 $errors = apply_filters('fluentform_validation_user_registration_errors', $errors, $this->formData, $this->form, $fields);
368 }
369
370 if (Helper::getFormMeta($this->form->id, '_has_user_update') == 'yes' && get_current_user_id()) {
371 $errors = apply_filters('fluentform_validation_user_update_errors', $errors, $this->formData, $this->form, $fields);
372 }
373
374 if ($errors) {
375 wp_send_json(['errors' => $errors], 423);
376 }
377
378 return true;
379 }
380
381 /**
382 * Validate nonce.
383 */
384 protected function validateNonce()
385 {
386 $formId = $this->form->id;
387
388 $shouldVerifyNonce = $this->app->applyFilters('fluentform_nonce_verify', false, $formId);
389
390 if ($shouldVerifyNonce) {
391 $nonce = Arr::get($this->formData, '_fluentform_' . $formId . '_fluentformnonce');
392 if (!wp_verify_nonce($nonce, 'fluentform-submit-form')) {
393 $errors = $this->app->applyFilters('fluentForm_nonce_error', [
394 '_fluentformnonce' => [
395 __('Nonce verification failed, please try again.', 'fluentform')
396 ]
397 ]);
398 wp_send_json(['errors' => $errors], 422);
399 }
400 }
401 }
402
403 protected function handleSpamError()
404 {
405 $settings = get_option('_fluentform_global_form_settings');
406 if (!$settings || ArrayHelper::get($settings, 'misc.akismet_validation') != 'validation_failed') {
407 return;
408 }
409
410 $errors = [
411 '_fluentformakismet' => __('Submission marked as spammed. Please try again', 'fluentform')
412 ];
413
414 wp_send_json(['errors' => $errors], 422);
415 }
416
417 protected function isSpam($formData, $form)
418 {
419 if (!AkismetHandler::isEnabled()) {
420 return false;
421 }
422
423 $isSpamCheck = apply_filters('fluentform_akismet_check_spam', true, $form->id, $formData);
424 if (!$isSpamCheck) {
425 return false;
426 }
427 // Let's validate now
428 $isSpam = AkismetHandler::isSpamSubmission($formData, $form);
429
430 return apply_filters('fluentform_akismet_spam_result', $isSpam, $form->id, $formData);
431 }
432
433 /**
434 * Validate reCaptcha.
435 */
436 private function validateReCaptcha()
437 {
438 $autoInclude = apply_filters('ff_has_auto_recaptcha', false);
439 if (FormFieldsParser::hasElement($this->form, 'recaptcha') || $autoInclude) {
440 $keys = get_option('_fluentform_reCaptcha_details');
441 $token = Arr::get($this->formData, 'g-recaptcha-response');
442 $version = 'v2_visible';
443 if(!empty($keys['api_version'])) {
444 $version = $keys['api_version'];
445 }
446 $isValid = ReCaptcha::validate($token, $keys['secretKey'], $version);
447
448 if (!$isValid) {
449 wp_send_json([
450 'errors' => [
451 'g-recaptcha-response' => [
452 __('reCaptcha verification failed, please try again.', 'fluentform')
453 ]
454 ]
455 ], 422);
456 }
457 }
458 }
459
460 /**
461 * Validate hCaptcha.
462 */
463 private function validateHCaptcha()
464 {
465 $autoInclude = apply_filters('ff_has_auto_hcaptcha', false);
466 FormFieldsParser::resetData();
467 if (FormFieldsParser::hasElement($this->form, 'hcaptcha') || $autoInclude) {
468 $keys = get_option('_fluentform_hCaptcha_details');
469 $token = Arr::get($this->formData, 'h-captcha-response');
470 $isValid = HCaptcha::validate($token, $keys['secretKey']);
471
472 if (!$isValid) {
473 wp_send_json([
474 'errors' => [
475 'h-captcha-response' => [
476 __('hCaptcha verification failed, please try again.', 'fluentform')
477 ]
478 ]
479 ], 422);
480 }
481 }
482 }
483
484 /**
485 * Validate turnstile.
486 */
487 private function validateTurnstile()
488 {
489 $autoInclude = apply_filters('ff_has_auto_turnstile', false);
490 if (FormFieldsParser::hasElement($this->form, 'turnstile') || $autoInclude) {
491 $keys = get_option('_fluentform_turnstile_details');
492 $token = Arr::get($this->formData, 'cf-turnstile-response');
493
494 $isValid = Turnstile::validate($token, $keys['secretKey']);
495
496 if (!$isValid) {
497 wp_send_json([
498 'errors' => [
499 'cf-turnstile-response' => [
500 __('Turnstile verification failed, please try again.', 'fluentform')
501 ]
502 ]
503 ], 422);
504 }
505 }
506 }
507
508 /**
509 * Validate form data based on the form restrictions settings.
510 *
511 * @param $fields
512 */
513 private function validateRestrictions(&$fields)
514 {
515 $formSettings = wpFluent()->table('fluentform_form_meta')
516 ->where('form_id', $this->form->id)
517 ->where('meta_key', 'formSettings')
518 ->first();
519
520 $this->form->settings = $formSettings ? json_decode($formSettings->value, true) : [];
521
522 $isAllowed = [
523 'status' => true,
524 'message' => ''
525 ];
526
527 // This will check the following restriction settings.
528 // 1. limitNumberOfEntries
529 // 2. scheduleForm
530 // 3. requireLogin
531 $isAllowed = apply_filters('fluentform_is_form_renderable', $isAllowed, $this->form);
532
533 if (!$isAllowed['status']) {
534 wp_send_json([
535 'errors' => [
536 'restricted' => [
537 $isAllowed['message']
538 ]
539 ]
540 ], 422);
541 }
542
543 // Since we are here, we should now handle if the form should be allowed to submit empty.
544 $restrictions = Arr::get($this->form->settings, 'restrictions.denyEmptySubmission', []);
545
546 $this->handleDenyEmptySubmission($restrictions, $fields);
547 }
548
549 /**
550 * Handle response when empty form submission is not allowed.
551 *
552 * @param array $settings
553 * @param $fields
554 */
555 private function handleDenyEmptySubmission($settings, &$fields)
556 {
557 // Determine whether empty form submission is allowed or not.
558 if (Arr::get($settings, 'enabled')) {
559 // confirm this form has no required fields.
560 if (!FormFieldsParser::hasRequiredFields($this->form, $fields)) {
561 // Filter out the form data which doesn't have values.
562 $filteredFormData = array_filter(
563 // Filter out the other meta fields that aren't actual inputs.
564 array_intersect_key($this->formData, $fields)
565 );
566
567 // TODO: Extract this function into global functions file...
568 $arrayFilterRecursive = function ($array) use (&$arrayFilterRecursive) {
569 foreach ($array as $key => $item) {
570 is_array($item) && $array[$key] = $arrayFilterRecursive($item);
571 if (empty($array[$key])) {
572 unset($array[$key]);
573 }
574 }
575 return $array;
576 };
577
578 if (!count($arrayFilterRecursive($filteredFormData))) {
579 wp_send_json([
580 'errors' => [
581 'restricted' => [
582 __(
583 !($m = Arr::get($settings, 'message'))
584 ? 'Sorry! You can\'t submit an empty form.'
585 : $m,
586 'fluentform'
587 )
588 ]
589 ]
590 ], 422);
591 }
592 }
593 }
594 }
595
596 /**
597 * Prepare the data to be inserted to the database.
598 *
599 * @param boolean $formData
600 * @return array
601 */
602 public function prepareInsertData($formData = false)
603 {
604 $formId = $this->form->id;
605
606 if (!$formData) {
607 $formData = $this->formData;
608 }
609
610 $previousItem = wpFluent()->table('fluentform_submissions')
611 ->where('form_id', $formId)
612 ->orderBy('id', 'DESC')
613 ->first();
614
615 $serialNumber = 1;
616
617 if ($previousItem) {
618 $serialNumber = $previousItem->serial_number + 1;
619 }
620
621 $browser = new Browser;
622
623 $inputConfigs = FormFieldsParser::getEntryInputs($this->form, array('admin_label', 'raw'));
624
625 $this->formData = apply_filters('fluentform_insert_response_data', $formData, $formId, $inputConfigs);
626
627 $ipAddress = $this->app->request->getIp();
628
629 if ((defined('FLUENTFROM_DISABLE_IP_LOGGING') && FLUENTFROM_DISABLE_IP_LOGGING) || apply_filters('fluentform_disable_ip_logging', false, $formId)) {
630 $ipAddress = false;
631 }
632
633 $response = [
634 'form_id' => $formId,
635 'serial_number' => $serialNumber,
636 'response' => json_encode($this->formData, JSON_UNESCAPED_UNICODE),
637 'source_url' => site_url(Arr::get($formData, '_wp_http_referer')),
638 'user_id' => get_current_user_id(),
639 'browser' => $browser->getBrowser(),
640 'device' => $browser->getPlatform(),
641 'ip' => $ipAddress,
642 'created_at' => current_time('mysql'),
643 'updated_at' => current_time('mysql')
644 ];
645
646
647 return apply_filters('fluentform_filter_insert_data', $response);
648 }
649
650 /**
651 * Delegate the validation rules & messages to the
652 * ones that the validation library recognizes.
653 *
654 * @param $rules
655 * @param $messages
656 * @return array
657 */
658 protected function delegateValidations($rules, $messages, $search = [], $replace = [])
659 {
660 $search = $search ?: ['max_file_size', 'allowed_file_types'];
661 $replace = $replace ?: ['max', 'mimes'];
662
663 foreach ($rules as &$rule) {
664 $rule = str_replace($search, $replace, $rule);
665 }
666
667 foreach ($messages as $key => $message) {
668 $newKey = str_replace($search, $replace, $key);
669 $messages[$newKey] = $message;
670 unset($messages[$key]);
671 }
672
673 return [$rules, $messages];
674 }
675
676 /**
677 * Prevents malicious attacks when the submission
678 * count exceeds in an allowed interval.
679 */
680 public function preventMaliciousAttacks()
681 {
682 $prevent = apply_filters('fluentform/prevent_malicious_attacks', true, $this->form->id);
683
684 if ($prevent) {
685 $maxSubmissionCount = apply_filters('fluentform/max_submission_count', 5, $this->form->id);
686 $minSubmissionInterval = apply_filters('fluentform/min_submission_interval', 30, $this->form->id);
687
688 $interval = date('Y-m-d H:i:s', strtotime(current_time('mysql')) - $minSubmissionInterval);
689
690 $submissionCount = wpFluent()->table('fluentform_submissions')
691 ->where('status', '!=', 'trashed')
692 ->where('ip', $this->app->request->getIp())
693 ->where('created_at', '>=', $interval)
694 ->count();
695
696 if ($submissionCount >= $maxSubmissionCount) {
697 wp_send_json([
698 'errors' => [
699 'restricted' => [
700 __(apply_filters('fluentform/too_many_requests', 'Too Many Requests.', $this->form->id), 'fluentform')
701 ]
702 ]
703 ], 429);
704 }
705 }
706 }
707 }
708