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

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