PluginProbe
Fluent Forms – Customizable Contact Forms, Survey, Quiz, & Conversational Form Builder / 4.3.17
Fluent Forms – Customizable Contact Forms, Survey, Quiz, & Conversational Form Builder v4.3.17
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.17, at app/Modules/Form/FormHandler.php

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