PluginProbe
Fluent Forms – Customizable Contact Forms, Survey, Quiz, & Conversational Form Builder / 6.2.14
Fluent Forms – Customizable Contact Forms, Survey, Quiz, & Conversational Form Builder v6.2.14
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.14, at app/Services/Form/SubmissionHandlerService.php

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