PluginProbe
Fluent Forms – Customizable Contact Forms, Survey, Quiz, & Conversational Form Builder / 6.2.12
Fluent Forms – Customizable Contact Forms, Survey, Quiz, & Conversational Form Builder v6.2.12
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 / SubmissionHandlerService.php

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

632 lines 23.7 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\Form;
7 use FluentForm\App\Models\FormMeta;
8 use FluentForm\App\Models\Submission;
9 use FluentForm\App\Modules\Form\FormFieldsParser;
10 use FluentForm\App\Services\Browser\Browser;
11 use FluentForm\App\Services\FormBuilder\ShortCodeParser;
12 use FluentForm\App\Services\Submission\SubmissionService;
13 use FluentForm\Database\Migrations\SubmissionDetails;
14 use FluentForm\Framework\Foundation\App;
15 use FluentForm\Framework\Helpers\ArrayHelper as Arr;
16 use FluentForm\Framework\Validator\ValidationException;
17
18
19 class SubmissionHandlerService
20 {
21 protected $app;
22 protected $form;
23 protected $fields;
24 protected $formData;
25 protected $validationService;
26 protected $submissionService;
27 protected $alreadyInsertedId;
28
29 public function __construct()
30 {
31 $this->app = App::getInstance();
32 $this->validationService = new FormValidationService();
33 $this->submissionService = new SubmissionService();
34 }
35
36 /**
37 * Form Submission
38 *
39 * @param $formDataRaw
40 * @param $formId
41 * @return array
42 * @throws \FluentForm\Framework\Validator\ValidationException
43 */
44 public function handleSubmission($formDataRaw, $formId)
45 {
46 $this->prepareHandler($formId, $formDataRaw);
47 $insertData = $this->handleValidation();
48 if ($returnData = $this->isSpamAndSkipProcessing($insertData)) {
49 return $returnData;
50 }
51 $insertId = $this->insertSubmission($insertData, $formDataRaw, $formId);
52
53 return $this->processSubmissionData($insertId, $this->formData, $this->form);
54 }
55
56 /**
57 * Prepare the form and validated form data for submission handling.
58 *
59 * @throws ValidationException
60 */
61 protected function prepareHandler($formId, $formDataRaw)
62 {
63 $this->form = Form::find($formId);
64
65 if (!$this->form) {
66 throw new ValidationException('', 422, null, ['errors' => 'Sorry, No corresponding form found']);
67 }
68
69 /**
70 * Filtering empty array inputs to normalize
71 *
72 * For unchecked checkable type, the name filled with empty value
73 * by serialized on the client-side JavaScript. This adjustment ensures filter empty array inputs to normalize- Ex: [''] -> []
74 */
75 foreach ($formDataRaw as $name => $input) {
76 if (is_array($input)) {
77 $formDataRaw[$name] = array_filter($input, function ($value) {
78 return null !== $value && false !== $value && '' !== $value;
79 });
80 }
81
82 // Process "Other" options for checkboxes and radio fields
83 if (strpos($name, '__ff_other_input__') !== false && !empty($input)) {
84 $fieldName = str_replace('__ff_other_input__', '', $name);
85 // Store with the field's own (translated) "Other" label as prefix
86 $rawField = Arr::get(FormFieldsParser::getInputs($this->form, ['raw']), $fieldName . '.raw', []);
87 $otherPrefix = Helper::getOtherOptionValuePrefix($rawField, $this->form);
88
89 // Handle checkbox fields (array values)
90 if (isset($formDataRaw[$fieldName]) && is_array($formDataRaw[$fieldName])) {
91 $selectedValues = $formDataRaw[$fieldName];
92
93 // Handle field-specific "Other" values
94 $otherValue = '__ff_other_' . $fieldName . '__';
95
96 $key = array_search($otherValue, $selectedValues);
97
98 if (false !== $key) {
99 $selectedValues[$key] = $otherPrefix . sanitize_text_field($input);
100 $formDataRaw[$fieldName] = $selectedValues;
101 }
102 } elseif (isset($formDataRaw[$fieldName]) && '__ff_other_' . $fieldName . '__' === $formDataRaw[$fieldName]) {
103 // Handle radio fields (single value)
104 $formDataRaw[$fieldName] = $otherPrefix . sanitize_text_field($input);
105 }
106
107 unset($formDataRaw[$name]);
108 }
109 }
110
111 // Parse the form and get the flat inputs with validations.
112 $this->fields = FormFieldsParser::getEssentialInputs($this->form, $formDataRaw, ['rules', 'raw']);
113
114 // @todo Remove this after few version as we are doing it during conversation now
115 // Removing left out fields during conversation which causes validation issues
116 $isConversationalForm = Helper::isConversionForm($formId);
117 if ($isConversationalForm) {
118 $conversationalForm = $this->form;
119 $conversationalForm->form_fields = \FluentForm\App\Services\FluentConversational\Classes\Converter\Converter::convertExistingForm($this->form);
120 $conversationalFields = FormFieldsParser::getInputs($conversationalForm);
121 $this->fields = array_intersect_key($this->fields, $conversationalFields);
122 }
123 $formData = fluentFormSanitizer($formDataRaw, null, $this->fields);
124
125 $acceptedFieldKeys = array_merge($this->fields, array_flip(Helper::getWhiteListedFields($formId)));
126
127 $this->formData = array_intersect_key($formData, $acceptedFieldKeys);
128 }
129
130
131 /**
132 * Prepare the data to be inserted to the database.
133 *
134 * @param boolean $formData
135 * @return array
136 */
137 public function prepareInsertData($formData = false)
138 {
139 $formId = $this->form->id;
140 if (!$formData) {
141 $formData = $this->formData;
142 }
143 $previousItem = Submission::select('serial_number')->where('form_id', $formId)->orderBy('id', 'DESC')->first();
144 $serialNumber = 1;
145 if ($previousItem) {
146 $serialNumber = $previousItem->serial_number + 1;
147 }
148 $browser = new Browser();
149 $inputConfigs = FormFieldsParser::getEntryInputs($this->form, ['admin_label', 'raw']);
150
151 $formData = apply_filters_deprecated(
152 'fluentform_insert_response_data',
153 [
154 $formData,
155 $formId,
156 $inputConfigs,
157 ],
158 FLUENTFORM_FRAMEWORK_UPGRADE,
159 'fluentform/insert_response_data',
160 'Use fluentform/insert_response_data instead of fluentform_insert_response_data.'
161 );
162 $this->formData = apply_filters('fluentform/insert_response_data', $formData, $formId, $inputConfigs);
163
164 $ipAddress = sanitize_text_field($this->app->request->getIp());
165
166 $disableIpLog = apply_filters_deprecated(
167 'fluentform_disable_ip_logging',
168 [
169 false,
170 $formId,
171 ],
172 FLUENTFORM_FRAMEWORK_UPGRADE,
173 'fluentform/disable_ip_logging',
174 'Use fluentform/disable_ip_logging instead of fluentform_disable_ip_logging.'
175 );
176
177 if ((defined('FLUENTFROM_DISABLE_IP_LOGGING') && FLUENTFROM_DISABLE_IP_LOGGING) || apply_filters('fluentform/disable_ip_logging',
178 $disableIpLog, $formId)) {
179 $ipAddress = false;
180 }
181
182 $response = [
183 'form_id' => $formId,
184 'serial_number' => $serialNumber,
185 'response' => wp_json_encode($this->formData, JSON_UNESCAPED_UNICODE),
186 'source_url' => site_url(Arr::get($formData, '_wp_http_referer')),
187 'user_id' => get_current_user_id(),
188 'browser' => $browser->getBrowser(),
189 'device' => $browser->getPlatform(),
190 'country' => apply_filters('fluentform/disable_submission_country_detection', false, $formId) ? null : Helper::getCountryCodeFromHeaders(),
191 'ip' => $ipAddress,
192 'created_at' => current_time('mysql'),
193 'updated_at' => current_time('mysql'),
194 ];
195
196 $response = apply_filters_deprecated(
197 'fluentform_filter_insert_data',
198 [
199 $response,
200 ],
201 FLUENTFORM_FRAMEWORK_UPGRADE,
202 'fluentform/filter_insert_data',
203 'Use fluentform/filter_insert_data instead of fluentform_filter_insert_data.'
204 );
205
206 return apply_filters('fluentform/filter_insert_data', $response);
207 }
208
209 public function processSubmissionData($insertId, $formData, $form)
210 {
211 $form = isset($this->form) ? $this->form : $form;
212 $formData = isset($this->formData) ? $this->formData : $formData;
213 do_action_deprecated(
214 'fluentform_before_form_actions_processing', [
215 $insertId,
216 $this->formData,
217 $form,
218 ],
219 FLUENTFORM_FRAMEWORK_UPGRADE,
220 'fluentform/before_form_actions_processing',
221 'Use fluentform/before_form_actions_processing instead of fluentform_before_form_actions_processing.'
222 );
223
224 do_action('fluentform/before_form_actions_processing', $insertId, $formData, $form);
225
226 if ($insertId) {
227 ob_start();
228 $formData = apply_filters('fluentform/submission_form_data', $formData, $insertId, $form);
229 $this->submissionService->recordEntryDetails($insertId, $form->id, $formData);
230 $isError = ob_get_clean();
231 if ($isError) {
232 SubmissionDetails::migrate();
233 }
234 }
235 $error = '';
236 try {
237 $formData = apply_filters('fluentform/submission_form_data', $formData, $insertId, $form);
238
239 do_action('fluentform_submission_inserted', $insertId, $formData, $form);
240
241 do_action('fluentform/submission_inserted', $insertId, $formData, $form);
242
243 Helper::setSubmissionMeta($insertId, 'is_form_action_fired', 'yes');
244
245 do_action_deprecated(
246 'fluentform_submission_inserted_' . $form->type . '_form', [
247 $insertId,
248 $formData,
249 $form,
250 ],
251 FLUENTFORM_FRAMEWORK_UPGRADE,
252 'fluentform/submission_inserted_' . $form->type . '_form',
253 'Use fluentform/submission_inserted_' . $form->type . '_form instead of fluentform_submission_inserted_' . $form->type . '_form'
254 );
255
256 $this->app->doAction(
257 'fluentform/submission_inserted_' . $form->type . '_form',
258 $insertId,
259 $formData,
260 $form
261 );
262
263 } catch (\Exception $e) {
264 if (defined('WP_DEBUG') && WP_DEBUG) {
265 $error = $e->getMessage();
266 }
267 }
268
269 do_action_deprecated(
270 'fluentform_before_submission_confirmation', [
271 $insertId,
272 $formData,
273 $form,
274 ],
275 FLUENTFORM_FRAMEWORK_UPGRADE,
276 'fluentform/before_submission_confirmation',
277 'Use fluentform/before_submission_confirmation instead of fluentform_before_submission_confirmation.'
278 );
279
280 do_action('fluentform/before_submission_confirmation', $insertId, $formData, $form);
281
282 return [
283 'insert_id' => $insertId,
284 'result' => $this->getReturnData($insertId, $form, $formData),
285 'error' => $error,
286 ];
287 }
288
289 /**
290 * Return Formatted Response Data
291 *
292 * @param $insertId
293 * @param $form
294 * @param $formData
295 * @return mixed
296 */
297 public function getReturnData($insertId, $form, $formData)
298 {
299 if (empty($form->settings)) {
300 $formSettings = FormMeta::retrieve('formSettings', $form->id);
301 $form->settings = is_array($formSettings) ? $formSettings : [];
302 }
303 $confirmation = $form->settings['confirmation'];
304 $confirmation = apply_filters_deprecated(
305 'fluentform_form_submission_confirmation',
306 [
307 $confirmation,
308 $formData,
309 $form,
310 ],
311 FLUENTFORM_FRAMEWORK_UPGRADE,
312 'fluentform/form_submission_confirmation',
313 'Use fluentform/form_submission_confirmation instead of fluentform_form_submission_confirmation.'
314 );
315
316 $confirmation = apply_filters(
317 'fluentform/form_submission_confirmation',
318 $confirmation,
319 $formData,
320 $form
321 );
322 if ('samePage' == Arr::get($confirmation, 'redirectTo')) {
323
324 $confirmation['messageToShow'] = fluentform_sanitize_html($confirmation['messageToShow']);
325
326 $confirmation['messageToShow'] = apply_filters_deprecated(
327 'fluentform_submission_message_parse',
328 [
329 $confirmation['messageToShow'],
330 $insertId,
331 $formData,
332 $form,
333 ],
334 FLUENTFORM_FRAMEWORK_UPGRADE,
335 'fluentform/submission_message_parse',
336 'Use fluentform/submission_message_parse instead of fluentform_submission_message_parse.'
337 );
338
339 $confirmation['messageToShow'] = apply_filters('fluentform/submission_message_parse', $confirmation['messageToShow'], $insertId, $formData, $form);
340
341 $confirmation['messageToShow'] = do_shortcode($confirmation['messageToShow']);
342
343 $message = ShortCodeParser::parse(
344 $confirmation['messageToShow'],
345 $insertId,
346 $formData,
347 $form,
348 false,
349 true,
350 true
351 );
352 $message = $message ? $message : __('The form has been successfully submitted.', 'fluentform');
353
354 $returnData = [
355 'message' => $message,
356 'action' => $confirmation['samePageFormBehavior'],
357 ];
358 } else {
359 $redirectUrl = Arr::get($confirmation, 'customUrl');
360 if ('customPage' == $confirmation['redirectTo']) {
361 $redirectUrl = get_permalink($confirmation['customPage']);
362 }
363 $enableQueryString = Arr::get($confirmation, 'enable_query_string') === 'yes';
364 $queryStrings = Arr::get($confirmation, 'query_strings');
365
366 if ($enableQueryString && $queryStrings) {
367 $separator = strpos($redirectUrl, '?') !== false ? '&' : '?';
368 $redirectUrl .= $separator . $queryStrings;
369 }
370 $parseUrl = apply_filters_deprecated('fluentform_will_parse_url_value', [
371 true,
372 $form,
373 ],
374 FLUENTFORM_FRAMEWORK_UPGRADE,
375 'fluentform/will_parse_url_value',
376 'Use fluentform/will_parse_url_value instead of fluentform_will_parse_url_value.'
377 );
378
379 $isUrlParser = apply_filters('fluentform/will_parse_url_value', $parseUrl, $form);
380 $redirectUrl = ShortCodeParser::parse(
381 $redirectUrl,
382 $insertId,
383 $formData,
384 $form,
385 $isUrlParser
386 );
387 if ($isUrlParser) {
388 /*
389 * Encode Redirect Value
390 */
391 $encodeUrl = apply_filters('fluentform/will_encode_url_value', false, $redirectUrl, $insertId, $form, $formData);
392 if (strpos($redirectUrl, '&') || '=' == substr($redirectUrl, -1) || $encodeUrl) {
393 $urlArray = explode('?', $redirectUrl);
394 $baseUrl = array_shift($urlArray);
395
396 $parsedUrl = wp_parse_url($redirectUrl);
397 $query = Arr::get($parsedUrl, 'query', '');
398 $queryParams = explode('&', $query);
399
400 $params = [];
401 foreach ($queryParams as $queryParam) {
402 $paramArray = explode('=', $queryParam);
403 if (!empty($paramArray[1])) {
404 if (strpos($paramArray[1], '%') === false) {
405 $params[$paramArray[0]] = rawurlencode($paramArray[1]);
406 } else {
407 // Param string is URL-encoded
408 $params[$paramArray[0]] = $paramArray[1];
409 }
410 }
411 }
412 if ($params) {
413 $redirectUrl = add_query_arg($params, $baseUrl);
414 if ($fragment = Arr::get($parsedUrl, 'fragment')) {
415 $redirectUrl .= '#' . $fragment;
416 }
417 }
418 }
419 }
420
421 $message = ShortCodeParser::parse(
422 Arr::get($confirmation, 'redirectMessage', ''),
423 $insertId,
424 $formData,
425 $form,
426 false,
427 true
428 );
429
430 $redirectUrl = apply_filters('fluentform/redirect_url_value', wp_sanitize_redirect($redirectUrl), $insertId, $form, $formData);
431 $returnData = [
432 'redirectUrl' => esc_url_raw($redirectUrl),
433 'message' => fluentform_sanitize_html($message),
434 ];
435 }
436
437 $returnData = apply_filters_deprecated('fluentform_submission_confirmation', [
438 $returnData,
439 $form,
440 $confirmation,
441 $insertId,
442 $formData,
443 ],
444 FLUENTFORM_FRAMEWORK_UPGRADE,
445 'fluentform/submission_confirmation',
446 'Use fluentform/submission_confirmation instead of fluentform_submission_confirmation.'
447 );
448
449 return $this->app->applyFilters(
450 'fluentform/submission_confirmation',
451 $returnData,
452 $form,
453 $confirmation,
454 $insertId,
455 $formData
456 );
457 }
458
459 private function isSpamAndSkipProcessing(&$insertData)
460 {
461 $spamSources = Arr::get($insertData, 'spam_from', []);
462
463 if ($spamSources) {
464 unset($insertData['spam_from']);
465 }
466
467 if (Arr::get($insertData, 'status') !== 'spam') {
468 return false;
469 }
470
471 $insertId = Submission::insertGetId($insertData);
472
473 if (!$insertId) {
474 return false;
475 }
476
477 $shouldSkip = false;
478
479 foreach ($spamSources as $source) {
480 if ($this->shouldSkipProcessingForSource($source)) {
481 $this->processSpamSubmission($insertId, $source);
482 $shouldSkip = true;
483 }
484 }
485
486 if ($shouldSkip) {
487 return [
488 'insert_id' => $insertId,
489 'result' => $this->getReturnData($insertId, $this->form, $this->formData),
490 ];
491 }
492
493 // Set a property for already inserted data for spam
494 $this->alreadyInsertedId = $insertId;
495
496 return false;
497 }
498
499 private function shouldSkipProcessingForSource($source)
500 {
501 $settings = get_option('_fluentform_global_form_settings');
502 $cleanTalkSettings = get_option('_fluentform_cleantalk_details');
503
504 switch ($source) {
505 case 'Akismet':
506 return $settings &&
507 'yes' == Arr::get($settings, 'misc.akismet_status') &&
508 'mark_as_spam_and_skip_processing' == Arr::get($settings, 'misc.akismet_validation');
509 case 'CleanTalk':
510 return $settings &&
511 'yes' == Arr::get($settings, 'misc.cleantalk_status') &&
512 'mark_as_spam_and_skip_processing' == Arr::get($settings, 'misc.cleantalk_validation');
513 case 'CleanTalk API':
514 return Arr::get($cleanTalkSettings, 'status') &&
515 'mark_as_spam_and_skip_processing' == Arr::get($cleanTalkSettings, 'validation');
516 default:
517 return false;
518 }
519 }
520
521 /**
522 * Validates Submission
523 *
524 * @throws ValidationException
525 */
526 private function handleValidation()
527 {
528 /* Now validate the data using the previous validations. */
529 $this->validationService->setForm($this->form);
530 $this->validationService->setFormData($this->formData);
531
532 $this->validationService->validateSubmission($this->fields, $this->formData);
533 $hasSpam = false;
534 $spamFrom = [];
535
536 if ($this->validationService->isAkismetSpam($this->formData, $this->form)) {
537 $hasSpam = true;
538 $this->validationService->handleAkismetSpamError();
539 $spamFrom[] = 'Akismet';
540 }
541
542 if ($this->validationService->isCleanTalkSpam($this->formData, $this->form)) {
543 $hasSpam = true;
544 $this->validationService->handleCleanTalkSpamError();
545 $spamFrom[] = 'CleanTalk';
546 }
547
548 if ($this->validationService->isCleanTalkSpamUsingApi($this->formData, $this->form)) {
549 $hasSpam = true;
550 $this->validationService->handleCleanTalkSpamErrorUsingAPi();
551 $spamFrom[] = 'CleanTalk API';
552 unset($this->formData['ff_ct_form_load_time'], $this->formData['ct_bot_detector_event_token']);
553 }
554
555 $insertData = $this->prepareInsertData();
556 if ($hasSpam) {
557 $insertData['status'] = 'spam';
558 $insertData['spam_from'] = $spamFrom;
559 }
560
561 return $insertData;
562 }
563
564 protected function insertSubmission($insertData, $formDataRaw, $formId)
565 {
566 do_action_deprecated(
567 'fluentform_before_insert_submission',
568 [
569 $insertData,
570 $formDataRaw,
571 $this->form,
572 ],
573 FLUENTFORM_FRAMEWORK_UPGRADE,
574 'fluentform/before_insert_submission',
575 'Use fluentform/before_insert_submission instead of fluentform_before_insert_submission.'
576 );
577
578 do_action('fluentform/before_insert_submission', $insertData, $formDataRaw, $this->form);
579
580 if ($this->form->has_payment) {
581 do_action_deprecated(
582 'fluentform_before_insert_payment_form',
583 [
584 $insertData,
585 $formDataRaw,
586 $this->form,
587 ],
588 FLUENTFORM_FRAMEWORK_UPGRADE,
589 'fluentform/before_insert_payment_form',
590 'Use fluentform/before_insert_payment_form instead of fluentform_before_insert_payment_form.'
591 );
592 do_action('fluentform/before_insert_payment_form', $insertData, $formDataRaw, $this->form);
593 }
594
595 // Check if we already have an inserted ID from spam processing
596 if (isset($this->alreadyInsertedId) && $this->alreadyInsertedId) {
597 return $this->alreadyInsertedId;
598 }
599
600 $insertId = Submission::insertGetId($insertData);
601
602 do_action('fluentform/notify_on_form_submit', $insertId, $this->formData, $this->form);
603
604 $uidHash = md5(wp_generate_uuid4() . $insertId);
605 Helper::setSubmissionMeta($insertId, '_entry_uid_hash', $uidHash, $formId);
606
607 return $insertId;
608 }
609
610 private function processSpamSubmission($insertId, $type)
611 {
612 $uidHash = md5(wp_generate_uuid4() . $insertId);
613 Helper::setSubmissionMeta($insertId, '_entry_uid_hash', $uidHash, $this->form->id);
614 ob_start();
615 $this->submissionService->recordEntryDetails($insertId, $this->form->id, $this->formData);
616 $isError = ob_get_clean();
617 if ($isError) {
618 SubmissionDetails::migrate();
619 }
620 Helper::setSubmissionMeta($insertId, 'is_form_action_fired', 'yes');
621 do_action('fluentform/log_data', [
622 'parent_source_id' => $this->form->id,
623 'source_type' => 'submission_item',
624 'source_id' => $insertId,
625 'component' => $type . ' Integration',
626 'status' => 'info',
627 'title' => __('Skip Submission Processing', 'fluentform'),
628 'description' => __('Submission marked as spammed. And skip all actions processing', 'fluentform'),
629 ]);
630 }
631 }
632