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

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