Issues (3627)

app/bundles/FormBundle/Model/SubmissionModel.php (1 issue)

1
<?php
2
3
/*
4
 * @copyright   2014 Mautic Contributors. All rights reserved
5
 * @author      Mautic
6
 *
7
 * @link        http://mautic.org
8
 *
9
 * @license     GNU/GPLv3 http://www.gnu.org/licenses/gpl-3.0.html
10
 */
11
12
namespace Mautic\FormBundle\Model;
13
14
use Doctrine\ORM\ORMException;
15
use Mautic\CampaignBundle\Membership\MembershipManager;
16
use Mautic\CampaignBundle\Model\CampaignModel;
17
use Mautic\CoreBundle\Exception\FileUploadException;
18
use Mautic\CoreBundle\Helper\Chart\ChartQuery;
19
use Mautic\CoreBundle\Helper\Chart\LineChart;
20
use Mautic\CoreBundle\Helper\DateTimeHelper;
21
use Mautic\CoreBundle\Helper\InputHelper;
22
use Mautic\CoreBundle\Helper\IpLookupHelper;
23
use Mautic\CoreBundle\Helper\TemplatingHelper;
24
use Mautic\CoreBundle\Model\FormModel as CommonFormModel;
25
use Mautic\CoreBundle\Templating\Helper\DateHelper;
26
use Mautic\FormBundle\Crate\UploadFileCrate;
27
use Mautic\FormBundle\Entity\Action;
28
use Mautic\FormBundle\Entity\Field;
29
use Mautic\FormBundle\Entity\Form;
30
use Mautic\FormBundle\Entity\Submission;
31
use Mautic\FormBundle\Entity\SubmissionRepository;
32
use Mautic\FormBundle\Event\Service\FieldValueTransformer;
33
use Mautic\FormBundle\Event\SubmissionEvent;
34
use Mautic\FormBundle\Event\ValidationEvent;
35
use Mautic\FormBundle\Exception\FileValidationException;
36
use Mautic\FormBundle\Exception\NoFileGivenException;
37
use Mautic\FormBundle\Exception\ValidationException;
38
use Mautic\FormBundle\FormEvents;
39
use Mautic\FormBundle\Helper\FormFieldHelper;
40
use Mautic\FormBundle\Helper\FormUploader;
41
use Mautic\FormBundle\Validator\UploadFieldValidator;
42
use Mautic\LeadBundle\DataObject\LeadManipulator;
43
use Mautic\LeadBundle\Entity\Company;
44
use Mautic\LeadBundle\Entity\CompanyChangeLog;
45
use Mautic\LeadBundle\Entity\Lead;
46
use Mautic\LeadBundle\Helper\IdentifyCompanyHelper;
47
use Mautic\LeadBundle\Model\CompanyModel;
48
use Mautic\LeadBundle\Model\FieldModel as LeadFieldModel;
49
use Mautic\LeadBundle\Model\LeadModel;
50
use Mautic\LeadBundle\Tracker\ContactTracker;
51
use Mautic\LeadBundle\Tracker\Service\DeviceTrackingService\DeviceTrackingServiceInterface;
52
use Mautic\PageBundle\Model\PageModel;
53
use PhpOffice\PhpSpreadsheet\IOFactory;
54
use PhpOffice\PhpSpreadsheet\Spreadsheet;
55
use Symfony\Component\HttpFoundation\Request;
56
use Symfony\Component\HttpFoundation\Response;
57
use Symfony\Component\HttpFoundation\StreamedResponse;
58
59
class SubmissionModel extends CommonFormModel
60
{
61
    /**
62
     * @var IpLookupHelper
63
     */
64
    protected $ipLookupHelper;
65
66
    /**
67
     * @var TemplatingHelper
68
     */
69
    protected $templatingHelper;
70
71
    /**
72
     * @var FormModel
73
     */
74
    protected $formModel;
75
76
    /**
77
     * @var PageModel
78
     */
79
    protected $pageModel;
80
81
    /**
82
     * @var LeadModel
83
     */
84
    protected $leadModel;
85
86
    /**
87
     * @var CampaignModel
88
     */
89
    protected $campaignModel;
90
91
    /**
92
     * @var MembershipManager
93
     */
94
    protected $membershipManager;
95
96
    /**
97
     * @var LeadFieldModel
98
     */
99
    protected $leadFieldModel;
100
101
    /**
102
     * @var CompanyModel
103
     */
104
    protected $companyModel;
105
106
    /**
107
     * @var FormFieldHelper
108
     */
109
    protected $fieldHelper;
110
111
    /**
112
     * @var UploadFieldValidator
113
     */
114
    private $uploadFieldValidator;
115
116
    /**
117
     * @var FormUploader
118
     */
119
    private $formUploader;
120
121
    /**
122
     * @var DeviceTrackingServiceInterface
123
     */
124
    private $deviceTrackingService;
125
126
    /**
127
     * @var FieldValueTransformer
128
     */
129
    private $fieldValueTransformer;
130
131
    /**
132
     * @var DateHelper
133
     */
134
    private $dateHelper;
135
136
    /**
137
     * @var ContactTracker
138
     */
139
    private $contactTracker;
140
141
    public function __construct(
142
        IpLookupHelper $ipLookupHelper,
143
        TemplatingHelper $templatingHelper,
144
        FormModel $formModel,
145
        PageModel $pageModel,
146
        LeadModel $leadModel,
147
        CampaignModel $campaignModel,
148
        MembershipManager $membershipManager,
149
        LeadFieldModel $leadFieldModel,
150
        CompanyModel $companyModel,
151
        FormFieldHelper $fieldHelper,
152
        UploadFieldValidator $uploadFieldValidator,
153
        FormUploader $formUploader,
154
        DeviceTrackingServiceInterface $deviceTrackingService,
155
        FieldValueTransformer $fieldValueTransformer,
156
        DateHelper $dateHelper,
157
        ContactTracker $contactTracker
158
    ) {
159
        $this->ipLookupHelper         = $ipLookupHelper;
160
        $this->templatingHelper       = $templatingHelper;
161
        $this->formModel              = $formModel;
162
        $this->pageModel              = $pageModel;
163
        $this->leadModel              = $leadModel;
164
        $this->campaignModel          = $campaignModel;
165
        $this->membershipManager      = $membershipManager;
166
        $this->leadFieldModel         = $leadFieldModel;
167
        $this->companyModel           = $companyModel;
168
        $this->fieldHelper            = $fieldHelper;
169
        $this->uploadFieldValidator   = $uploadFieldValidator;
170
        $this->formUploader           = $formUploader;
171
        $this->deviceTrackingService  = $deviceTrackingService;
172
        $this->fieldValueTransformer  = $fieldValueTransformer;
173
        $this->dateHelper             = $dateHelper;
174
        $this->contactTracker         = $contactTracker;
175
    }
176
177
    /**
178
     * {@inheritdoc}
179
     *
180
     * @return SubmissionRepository
181
     */
182
    public function getRepository()
183
    {
184
        return $this->em->getRepository('MauticFormBundle:Submission');
185
    }
186
187
    /**
188
     * @param      $post
189
     * @param      $server
190
     * @param bool $returnEvent
191
     *
192
     * @return bool|array
193
     *
194
     * @throws ORMException
195
     */
196
    public function saveSubmission($post, $server, Form $form, Request $request, $returnEvent = false)
197
    {
198
        $leadFields = $this->leadFieldModel->getFieldListWithProperties(false);
199
200
        //everything matches up so let's save the results
201
        $submission = new Submission();
202
        $submission->setDateSubmitted(new \DateTime());
203
        $submission->setForm($form);
204
205
        //set the landing page the form was submitted from if applicable
206
        if (!empty($post['mauticpage'])) {
207
            $page = $this->pageModel->getEntity((int) $post['mauticpage']);
208
            if (null != $page) {
209
                $submission->setPage($page);
210
            }
211
        }
212
213
        $ipAddress = $this->ipLookupHelper->getIpAddress();
214
        $submission->setIpAddress($ipAddress);
215
216
        if (!empty($post['return'])) {
217
            $referer = $post['return'];
218
        } elseif (!empty($server['HTTP_REFERER'])) {
219
            $referer = $server['HTTP_REFERER'];
220
        } else {
221
            $referer = '';
222
        }
223
224
        //clean the referer by removing mauticError and mauticMessage
225
        $referer = InputHelper::url($referer, null, null, ['mauticError', 'mauticMessage']);
226
        $submission->setReferer($referer);
227
228
        // Create an event to be dispatched through the processes
229
        $submissionEvent = new SubmissionEvent($submission, $post, $server, $request);
230
231
        // Get a list of components to build custom fields from
232
        $components = $this->formModel->getCustomComponents();
233
234
        $fields           = $form->getFields();
235
        $fieldArray       = [];
236
        $results          = [];
237
        $tokens           = [];
238
        $leadFieldMatches = [];
239
        $validationErrors = [];
240
        $filesToUpload    = new UploadFileCrate();
241
242
        /** @var Field $f */
243
        foreach ($fields as $f) {
244
            $id    = $f->getId();
245
            $type  = $f->getType();
246
            $alias = $f->getAlias();
247
            $value = (isset($post[$alias])) ? $post[$alias] : '';
248
249
            $fieldArray[$id] = [
250
                'id'    => $id,
251
                'type'  => $type,
252
                'alias' => $alias,
253
            ];
254
255
            if ($f->isCaptchaType()) {
256
                $captcha = $this->fieldHelper->validateFieldValue($type, $value, $f);
257
                if (!empty($captcha)) {
258
                    $props = $f->getProperties();
259
                    //check for a custom message
260
                    $validationErrors[$alias] = (!empty($props['errorMessage'])) ? $props['errorMessage'] : implode('<br />', $captcha);
261
                }
262
                continue;
263
            } elseif ($f->isFileType()) {
264
                try {
265
                    $file  = $this->uploadFieldValidator->processFileValidation($f, $request);
266
                    $value = $file->getClientOriginalName();
267
                    $filesToUpload->addFile($file, $f);
268
                } catch (NoFileGivenException $e) { //No error here, we just move to another validation, eg. if a field is required
269
                } catch (FileValidationException $e) {
270
                    $validationErrors[$alias] = $e->getMessage();
271
                }
272
            }
273
274
            if ('' === $value && $f->isRequired()) {
275
                //field is required, but hidden from form because of 'ShowWhenValueExists'
276
                if (false === $f->getShowWhenValueExists() && !isset($post[$alias])) {
277
                    continue;
278
                }
279
280
                //somehow the user got passed the JS validation
281
                $msg = $f->getValidationMessage();
282
                if (empty($msg)) {
283
                    $msg = $this->translator->trans(
284
                        'mautic.form.field.generic.validationfailed',
285
                        [
286
                            '%label%' => $f->getLabel(),
287
                        ],
288
                        'validators'
289
                    );
290
                }
291
292
                $validationErrors[$alias] = $msg;
293
294
                continue;
295
            }
296
297
            if (isset($components['viewOnlyFields']) && in_array($type, $components['viewOnlyFields'])) {
298
                //don't save items that don't have a value associated with it
299
                continue;
300
            }
301
302
            //clean and validate the input
303
            if ($f->isCustom()) {
304
                if (!isset($components['fields'][$f->getType()])) {
305
                    continue;
306
                }
307
308
                $params = $components['fields'][$f->getType()];
309
                if (!empty($value)) {
310
                    if (isset($params['valueFilter'])) {
311
                        if (is_string($params['valueFilter']) && is_callable(['\Mautic\CoreBundle\Helper\InputHelper', $params['valueFilter']])) {
312
                            $value = InputHelper::_($value, $params['valueFilter']);
313
                        } elseif (is_callable($params['valueFilter'])) {
314
                            $value = call_user_func_array($params['valueFilter'], [$f, $value]);
315
                        } else {
316
                            $value = InputHelper::_($value, 'clean');
317
                        }
318
                    } else {
319
                        $value = InputHelper::_($value, 'clean');
320
                    }
321
                }
322
            } elseif (!empty($value)) {
323
                $filter = $this->fieldHelper->getFieldFilter($type);
324
                $value  = InputHelper::_($value, $filter);
325
326
                $isValid = $this->validateFieldValue($f, $value);
327
                if (true !== $isValid) {
328
                    $validationErrors[$alias] = is_array($isValid) ? implode('<br />', $isValid) : $isValid;
329
                }
330
            }
331
332
            // Check for custom validators
333
            $isValid = $this->validateFieldValue($f, $value);
334
            if (true !== $isValid) {
335
                $validationErrors[$alias] = $isValid;
336
            }
337
338
            $leadField = $f->getLeadField();
339
            if (!empty($leadField)) {
340
                $leadValue = $value;
341
342
                $leadFieldMatches[$leadField] = $leadValue;
343
            }
344
345
            //convert array from checkbox groups and multiple selects
346
            if (is_array($value)) {
347
                $value = implode(', ', $value);
348
            }
349
350
            $tokens["{formfield={$alias}}"] = $value;
351
352
            //save the result
353
            if (false !== $f->getSaveResult()) {
354
                $results[$alias] = $value;
355
            }
356
        }
357
358
        // Set the results
359
        $submission->setResults($results);
360
361
        // Update the event
362
        $submissionEvent->setFields($fieldArray)
363
            ->setTokens($tokens)
364
            ->setResults($results)
365
            ->setContactFieldMatches($leadFieldMatches);
366
367
        $lead = $this->contactTracker->getContact();
368
369
        // Remove validation errors if the field is not visible
370
        if ($lead && $form->usesProgressiveProfiling()) {
371
            $leadSubmissions = $this->formModel->getLeadSubmissions($form, $lead->getId());
372
373
            foreach ($fields as $field) {
374
                if (isset($validationErrors[$field->getAlias()]) && !$field->showForContact($leadSubmissions, $lead, $form)) {
375
                    unset($validationErrors[$field->getAlias()]);
376
                }
377
            }
378
        }
379
380
        //return errors if there any
381
        if (!empty($validationErrors)) {
382
            return ['errors' => $validationErrors];
383
        }
384
385
        // Create/update lead
386
        if (!empty($leadFieldMatches)) {
387
            $lead = $this->createLeadFromSubmit($form, $leadFieldMatches, $leadFields);
388
        }
389
390
        $trackedDevice = $this->deviceTrackingService->getTrackedDevice();
391
        $trackingId    = (null === $trackedDevice ? null : $trackedDevice->getTrackingId());
392
393
        //set tracking ID for stats purposes to determine unique hits
394
        $submission->setTrackingId($trackingId)
395
            ->setLead($lead);
396
397
        /*
398
         * Process File upload and save the result to the entity
399
         * Upload is here to minimize a need for deleting file if there is a validation error
400
         * The action can still be invalidated below - deleteEntity takes care for File deletion
401
         *
402
         * @todo Refactor form validation to execute this code only if Submission is valid
403
         */
404
        try {
405
            $this->formUploader->uploadFiles($filesToUpload, $submission);
406
        } catch (FileUploadException $e) {
407
            $msg                                = $this->translator->trans('mautic.form.submission.error.file.uploadFailed', [], 'validators');
408
            $validationErrors[$e->getMessage()] = $msg;
409
410
            return ['errors' => $validationErrors];
411
        }
412
413
        // set results after uploader what can change file name if file name exists
414
        $submissionEvent->setResults($submission->getResults());
415
416
        // Save the submission
417
        $this->saveEntity($submission);
418
        $this->fieldValueTransformer->transformValuesAfterSubmit($submissionEvent);
419
        // Now handle post submission actions
420
        try {
421
            $this->executeFormActions($submissionEvent);
422
        } catch (ValidationException $exception) {
423
            // The action invalidated the form for whatever reason
424
            $this->deleteEntity($submission);
425
426
            if ($validationErrors = $exception->getViolations()) {
427
                return ['errors' => $validationErrors];
428
            }
429
430
            return ['errors' => [$exception->getMessage()]];
431
        }
432
433
        // update contact fields with transform values
434
        if (!empty($this->fieldValueTransformer->getContactFieldsToUpdate())) {
435
            $this->leadModel->setFieldValues($lead, $this->fieldValueTransformer->getContactFieldsToUpdate());
436
            $this->leadModel->saveEntity($lead, false);
437
        }
438
439
        if (!$form->isStandalone()) {
440
            // Find and add the lead to the associated campaigns
441
            $campaigns = $this->campaignModel->getCampaignsByForm($form);
442
            if (!empty($campaigns)) {
443
                foreach ($campaigns as $campaign) {
444
                    $this->membershipManager->addContact($lead, $campaign);
445
                }
446
            }
447
        }
448
449
        if ($this->dispatcher->hasListeners(FormEvents::FORM_ON_SUBMIT)) {
450
            // Reset action config from executeFormActions()
451
            $submissionEvent->setAction(null);
452
453
            // Dispatch to on submit listeners
454
            $this->dispatcher->dispatch(FormEvents::FORM_ON_SUBMIT, $submissionEvent);
455
        }
456
457
        //get callback commands from the submit action
458
        if ($submissionEvent->hasPostSubmitCallbacks()) {
459
            return ['callback' => $submissionEvent];
460
        }
461
462
        // made it to the end so return the submission event to give the calling method access to tokens, results, etc
463
        // otherwise return false that no errors were encountered (to keep BC really)
464
        return ($returnEvent) ? ['submission' => $submissionEvent] : false;
465
    }
466
467
    /**
468
     * @param Submission $submission
469
     */
470
    public function deleteEntity($submission)
471
    {
472
        $this->formUploader->deleteUploadedFiles($submission);
473
474
        parent::deleteEntity($submission);
475
    }
476
477
    /**
478
     * {@inheritdoc}
479
     */
480
    public function getEntities(array $args = [])
481
    {
482
        return $this->getRepository()->getEntities($args);
483
    }
484
485
    /**
486
     * @param $format
487
     * @param $form
488
     * @param $queryArgs
489
     *
490
     * @return StreamedResponse|Response
491
     *
492
     * @throws \Exception
493
     */
494
    public function exportResults($format, $form, $queryArgs)
495
    {
496
        $viewOnlyFields              = $this->formModel->getCustomComponents()['viewOnlyFields'];
497
        $queryArgs['viewOnlyFields'] = $viewOnlyFields;
498
        $queryArgs['simpleResults']  = true;
499
        $results                     = $this->getEntities($queryArgs);
500
        $translator                  = $this->translator;
501
502
        $date = (new DateTimeHelper())->toLocalString();
503
        $name = str_replace(' ', '_', $date).'_'.$form->getAlias();
504
505
        switch ($format) {
506
            case 'csv':
507
                $response = new StreamedResponse(
508
                    function () use ($results, $form, $translator, $viewOnlyFields) {
509
                        $handle = fopen('php://output', 'r+');
510
511
                        //build the header row
512
                        $fields = $form->getFields();
513
                        $header = [
514
                            $translator->trans('mautic.core.id'),
515
                            $translator->trans('mautic.form.result.thead.date'),
516
                            $translator->trans('mautic.core.ipaddress'),
517
                            $translator->trans('mautic.form.result.thead.referrer'),
518
                        ];
519
                        foreach ($fields as $f) {
520
                            if (in_array($f->getType(), $viewOnlyFields) || false === $f->getSaveResult()) {
521
                                continue;
522
                            }
523
                            $header[] = $f->getLabel();
524
                        }
525
                        //free memory
526
                        unset($fields);
527
528
                        //write the row
529
                        fputcsv($handle, $header);
530
531
                        //build the data rows
532
                        foreach ($results as $k => $s) {
533
                            $row = [
534
                                $s['id'],
535
                                $this->dateHelper->toFull($s['dateSubmitted'], 'UTC'),
536
                                $s['ipAddress'],
537
                                $s['referer'],
538
                            ];
539
                            foreach ($s['results'] as $k2 => $r) {
540
                                if (in_array($r['type'], $viewOnlyFields)) {
541
                                    continue;
542
                                }
543
                                $row[] = htmlspecialchars_decode($r['value'], ENT_QUOTES);
544
                                //free memory
545
                                unset($s['results'][$k2]);
546
                            }
547
548
                            fputcsv($handle, $row);
549
550
                            //free memory
551
                            unset($row, $results[$k]);
552
                        }
553
554
                        fclose($handle);
555
                    }
556
                );
557
558
                $response->headers->set('Content-Type', 'application/force-download');
559
                $response->headers->set('Content-Type', 'application/octet-stream');
560
                $response->headers->set('Content-Disposition', 'attachment; filename="'.$name.'.csv"');
561
                $response->headers->set('Expires', 0);
562
                $response->headers->set('Cache-Control', 'must-revalidate');
563
                $response->headers->set('Pragma', 'public');
564
565
                return $response;
566
            case 'html':
567
                $content = $this->templatingHelper->getTemplating()->renderResponse(
568
                    'MauticFormBundle:Result:export.html.php',
569
                    [
570
                        'form'           => $form,
571
                        'results'        => $results,
572
                        'pageTitle'      => $name,
573
                        'viewOnlyFields' => $viewOnlyFields,
574
                    ]
575
                )->getContent();
576
577
                return new Response($content);
578
            case 'xlsx':
579
                if (class_exists(Spreadsheet::class)) {
580
                    $response = new StreamedResponse(
581
                        function () use ($results, $form, $translator, $name, $viewOnlyFields) {
582
                            $objPHPExcel = new Spreadsheet();
583
                            $objPHPExcel->getProperties()->setTitle($name);
584
585
                            $objPHPExcel->createSheet();
586
587
                            //build the header row
588
                            $fields = $form->getFields();
589
                            $header = [
590
                                $translator->trans('mautic.core.id'),
591
                                $translator->trans('mautic.form.result.thead.date'),
592
                                $translator->trans('mautic.core.ipaddress'),
593
                                $translator->trans('mautic.form.result.thead.referrer'),
594
                            ];
595
                            foreach ($fields as $f) {
596
                                if (in_array($f->getType(), $viewOnlyFields) || false === $f->getSaveResult()) {
597
                                    continue;
598
                                }
599
                                $header[] = $f->getLabel();
600
                            }
601
                            //free memory
602
                            unset($fields);
603
604
                            //write the row
605
                            $objPHPExcel->getActiveSheet()->fromArray($header, null, 'A1');
606
607
                            //build the data rows
608
                            $count = 2;
609
                            foreach ($results as $k => $s) {
610
                                $row = [
611
                                    $s['id'],
612
                                    $this->dateHelper->toFull($s['dateSubmitted'], 'UTC'),
613
                                    $s['ipAddress'],
614
                                    $s['referer'],
615
                                ];
616
                                foreach ($s['results'] as $k2 => $r) {
617
                                    if (in_array($r['type'], $viewOnlyFields)) {
618
                                        continue;
619
                                    }
620
                                    $row[] = htmlspecialchars_decode($r['value'], ENT_QUOTES);
621
                                    //free memory
622
                                    unset($s['results'][$k2]);
623
                                }
624
625
                                $objPHPExcel->getActiveSheet()->fromArray($row, null, "A{$count}");
626
627
                                //free memory
628
                                unset($row, $results[$k]);
629
630
                                //increment letter
631
                                ++$count;
632
                            }
633
634
                            $objWriter = IOFactory::createWriter($objPHPExcel, 'Xlsx');
635
                            $objWriter->setPreCalculateFormulas(false);
636
637
                            $objWriter->save('php://output');
638
                        }
639
                    );
640
                    $response->headers->set('Content-Type', 'application/force-download');
641
                    $response->headers->set('Content-Type', 'application/octet-stream');
642
                    $response->headers->set('Content-Disposition', 'attachment; filename="'.$name.'.xlsx"');
643
                    $response->headers->set('Expires', 0);
644
                    $response->headers->set('Cache-Control', 'must-revalidate');
645
                    $response->headers->set('Pragma', 'public');
646
647
                    return $response;
648
                }
649
                throw new \Exception('PHPSpreadsheet is required to export to Excel spreadsheets');
650
            default:
651
                return new Response();
652
        }
653
    }
654
655
    /**
656
     * Get line chart data of submissions.
657
     *
658
     * @param string $unit          {@link php.net/manual/en/function.date.php#refsect1-function.date-parameters}
659
     * @param string $dateFormat
660
     * @param array  $filter
661
     * @param bool   $canViewOthers
662
     *
663
     * @return array
664
     */
665
    public function getSubmissionsLineChartData(
666
        $unit,
667
        \DateTime $dateFrom,
668
        \DateTime $dateTo,
669
        $dateFormat = null,
670
        $filter = [],
671
        $canViewOthers = true
672
    ) {
673
        $chart = new LineChart($unit, $dateFrom, $dateTo, $dateFormat);
674
        $query = new ChartQuery($this->em->getConnection(), $dateFrom, $dateTo);
675
        $q     = $query->prepareTimeDataQuery('form_submissions', 'date_submitted', $filter);
676
677
        if (!$canViewOthers) {
678
            $q->join('t', MAUTIC_TABLE_PREFIX.'forms', 'f', 'f.id = t.form_id')
679
                ->andWhere('f.created_by = :userId')
680
                ->setParameter('userId', $this->userHelper->getUser()->getId());
681
        }
682
683
        $data = $query->loadAndBuildTimeData($q);
684
        $chart->setDataset($this->translator->trans('mautic.form.submission.count'), $data);
685
686
        return $chart->render();
687
    }
688
689
    /**
690
     * Get a list of top submission referrers.
691
     *
692
     * @param int    $limit
693
     * @param string $dateFrom
694
     * @param string $dateTo
695
     * @param array  $filters
696
     * @param bool   $canViewOthers
697
     *
698
     * @return array
699
     */
700
    public function getTopSubmissionReferrers($limit = 10, $dateFrom = null, $dateTo = null, $filters = [], $canViewOthers = true)
701
    {
702
        $q = $this->em->getConnection()->createQueryBuilder();
703
        $q->select('COUNT(DISTINCT t.id) AS submissions, t.referer')
704
            ->from(MAUTIC_TABLE_PREFIX.'form_submissions', 't')
705
            ->orderBy('submissions', 'DESC')
706
            ->groupBy('t.referer')
707
            ->setMaxResults($limit);
708
709
        if (!$canViewOthers) {
710
            $q->join('t', MAUTIC_TABLE_PREFIX.'forms', 'f', 'f.id = t.form_id')
711
                ->andWhere('f.created_by = :userId')
712
                ->setParameter('userId', $this->userHelper->getUser()->getId());
713
        }
714
715
        $chartQuery = new ChartQuery($this->em->getConnection(), $dateFrom, $dateTo);
716
        $chartQuery->applyFilters($q, $filters);
717
        $chartQuery->applyDateFilters($q, 'date_submitted');
718
719
        return $q->execute()->fetchAll();
720
    }
721
722
    /**
723
     * Get a list of the most submisions per lead.
724
     *
725
     * @param int    $limit
726
     * @param string $dateFrom
727
     * @param string $dateTo
728
     * @param array  $filters
729
     * @param bool   $canViewOthers
730
     *
731
     * @return array
732
     */
733
    public function getTopSubmitters($limit = 10, $dateFrom = null, $dateTo = null, $filters = [], $canViewOthers = true)
734
    {
735
        $q = $this->em->getConnection()->createQueryBuilder();
736
        $q->select('COUNT(DISTINCT t.id) AS submissions, t.lead_id, l.firstname, l.lastname, l.email')
737
            ->from(MAUTIC_TABLE_PREFIX.'form_submissions', 't')
738
            ->join('t', MAUTIC_TABLE_PREFIX.'leads', 'l', 'l.id = t.lead_id')
739
            ->orderBy('submissions', 'DESC')
740
            ->groupBy('t.lead_id, l.firstname, l.lastname, l.email')
741
            ->setMaxResults($limit);
742
743
        if (!$canViewOthers) {
744
            $q->join('t', MAUTIC_TABLE_PREFIX.'forms', 'f', 'f.id = t.form_id')
745
                ->andWhere('f.created_by = :userId')
746
                ->setParameter('userId', $this->userHelper->getUser()->getId());
747
        }
748
749
        $chartQuery = new ChartQuery($this->em->getConnection(), $dateFrom, $dateTo);
750
        $chartQuery->applyFilters($q, $filters);
751
        $chartQuery->applyDateFilters($q, 'date_submitted');
752
753
        return $q->execute()->fetchAll();
754
    }
755
756
    /**
757
     * Execute a form submit action.
758
     *
759
     * @throws ValidationException
760
     */
761
    protected function executeFormActions(SubmissionEvent $event): void
762
    {
763
        $actions          = $event->getSubmission()->getForm()->getActions();
764
        $customComponents = $this->formModel->getCustomComponents();
765
        $availableActions = $customComponents['actions'] ?? [];
766
767
        $actions->filter(function (Action $action) use ($availableActions) {
768
            return array_key_exists($action->getType(), $availableActions);
769
        })->map(function (Action $action) use ($event, $availableActions) {
770
            $event->setAction($action);
771
            $this->dispatcher->dispatch($availableActions[$action->getType()]['eventName'], $event);
772
        });
773
    }
774
775
    /**
776
     * Create/update lead from form submit.
777
     *
778
     * @return Lead
779
     *
780
     * @throws ORMException
781
     */
782
    protected function createLeadFromSubmit(Form $form, array $leadFieldMatches, $leadFields)
783
    {
784
        //set the mapped data
785
        $inKioskMode   = $form->isInKioskMode();
786
        $leadId        = null;
787
        $lead          = new Lead();
788
        $currentFields = $leadFieldMatches;
789
        $companyFields = $this->leadFieldModel->getFieldListWithProperties('company');
790
791
        if (!$inKioskMode) {
792
            // Default to currently tracked lead
793
            if ($currentLead = $this->contactTracker->getContact()) {
794
                $lead          = $currentLead;
795
                $leadId        = $lead->getId();
796
                $currentFields = $lead->getProfileFields();
797
            }
798
799
            $this->logger->debug('FORM: Not in kiosk mode so using current contact ID #'.$leadId);
800
        } else {
801
            // Default to a new lead in kiosk mode
802
            $lead->setNewlyCreated(true);
803
804
            $this->logger->debug('FORM: In kiosk mode so assuming a new contact');
805
        }
806
807
        $uniqueLeadFields = $this->leadFieldModel->getUniqueIdentifierFields();
808
809
        // Closure to get data and unique fields
810
        $getData = function ($currentFields, $uniqueOnly = false) use ($leadFields, $uniqueLeadFields) {
811
            $uniqueFieldsWithData = $data = [];
812
            foreach ($leadFields as $alias => $properties) {
813
                if (isset($currentFields[$alias])) {
814
                    $value        = $currentFields[$alias];
815
                    $data[$alias] = $value;
816
817
                    // make sure the value is actually there and the field is one of our uniques
818
                    if (!empty($value) && array_key_exists($alias, $uniqueLeadFields)) {
819
                        $uniqueFieldsWithData[$alias] = $value;
820
                    }
821
                }
822
            }
823
824
            return ($uniqueOnly) ? $uniqueFieldsWithData : [$data, $uniqueFieldsWithData];
825
        };
826
827
        // Closure to get data and unique fields
828
        $getCompanyData = function ($currentFields) use ($companyFields) {
829
            $companyData = [];
830
            // force add company contact field to company fields check
831
            $companyFields = array_merge($companyFields, ['company'=> 'company']);
832
            foreach ($companyFields as $alias => $properties) {
833
                if (isset($currentFields[$alias])) {
834
                    $value               = $currentFields[$alias];
835
                    $companyData[$alias] = $value;
836
                }
837
            }
838
839
            return $companyData;
840
        };
841
842
        // Closure to help search for a conflict
843
        $checkForIdentifierConflict = function ($fieldSet1, $fieldSet2) {
844
            // Find fields in both sets
845
            $potentialConflicts = array_keys(
846
                array_intersect_key($fieldSet1, $fieldSet2)
847
            );
848
849
            $this->logger->debug(
850
                'FORM: Potential conflicts '.implode(', ', array_keys($potentialConflicts)).' = '.implode(', ', $potentialConflicts)
851
            );
852
853
            $conflicts = [];
854
            foreach ($potentialConflicts as $field) {
855
                if (!empty($fieldSet1[$field]) && !empty($fieldSet2[$field])) {
856
                    if (strtolower($fieldSet1[$field]) !== strtolower($fieldSet2[$field])) {
857
                        $conflicts[] = $field;
858
                    }
859
                }
860
            }
861
862
            return [count($conflicts), $conflicts];
863
        };
864
865
        // Get data for the form submission
866
        list($data, $uniqueFieldsWithData) = $getData($leadFieldMatches);
867
        $this->logger->debug('FORM: Unique fields submitted include '.implode(', ', $uniqueFieldsWithData));
868
869
        // Check for duplicate lead
870
        /** @var \Mautic\LeadBundle\Entity\Lead[] $leads */
871
        $leads = (!empty($uniqueFieldsWithData)) ? $this->em->getRepository('MauticLeadBundle:Lead')->getLeadsByUniqueFields(
872
            $uniqueFieldsWithData,
873
            $leadId
874
        ) : [];
875
876
        $uniqueFieldsCurrent = $getData($currentFields, true);
877
        if (count($leads)) {
878
            $this->logger->debug(count($leads).' found based on unique identifiers');
879
880
            /** @var \Mautic\LeadBundle\Entity\Lead $foundLead */
881
            $foundLead = $leads[0];
882
883
            $this->logger->debug('FORM: Testing contact ID# '.$foundLead->getId().' for conflicts');
884
885
            // Check for a conflict with the currently tracked lead
886
            $foundLeadFields = $foundLead->getProfileFields();
887
888
            // Get unique identifier fields for the found lead then compare with the lead currently tracked
889
            $uniqueFieldsFound             = $getData($foundLeadFields, true);
890
            list($hasConflict, $conflicts) = $checkForIdentifierConflict($uniqueFieldsFound, $uniqueFieldsCurrent);
891
892
            if ($inKioskMode || $hasConflict || !$lead->getId()) {
893
                // Use the found lead without merging because there is some sort of conflict with unique identifiers or in kiosk mode and thus should not merge
894
                $lead = $foundLead;
895
896
                if ($hasConflict) {
897
                    $this->logger->debug('FORM: Conflicts found in '.implode(', ', $conflicts).' so not merging');
898
                } else {
899
                    $this->logger->debug('FORM: In kiosk mode so not merging');
900
                }
901
            } else {
902
                $this->logger->debug('FORM: Merging contacts '.$lead->getId().' and '.$foundLead->getId());
903
904
                // Merge the found lead with currently tracked lead
905
                $lead = $this->leadModel->mergeLeads($lead, $foundLead);
0 ignored issues
show
Deprecated Code introduced by
The function Mautic\LeadBundle\Model\LeadModel::mergeLeads() has been deprecated: 2.13.0; to be removed in 3.0. Use \Mautic\LeadBundle\Deduplicate\ContactMerger instead ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-deprecated  annotation

905
                $lead = /** @scrutinizer ignore-deprecated */ $this->leadModel->mergeLeads($lead, $foundLead);

This function has been deprecated. The supplier of the function has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the function will be removed and what other function to use instead.

Loading history...
906
            }
907
908
            // Update unique fields data for comparison with submitted data
909
            $currentFields       = $lead->getProfileFields();
910
            $uniqueFieldsCurrent = $getData($currentFields, true);
911
        }
912
913
        if (!$inKioskMode) {
914
            // Check for conflicts with the submitted data and the currently tracked lead
915
            list($hasConflict, $conflicts) = $checkForIdentifierConflict($uniqueFieldsWithData, $uniqueFieldsCurrent);
916
917
            $this->logger->debug(
918
                'FORM: Current unique contact fields '.implode(', ', array_keys($uniqueFieldsCurrent)).' = '.implode(', ', $uniqueFieldsCurrent)
919
            );
920
921
            $this->logger->debug(
922
                'FORM: Submitted unique contact fields '.implode(', ', array_keys($uniqueFieldsWithData)).' = '.implode(', ', $uniqueFieldsWithData)
923
            );
924
            if ($hasConflict) {
925
                // There's a conflict so create a new lead
926
                $lead = new Lead();
927
                $lead->setNewlyCreated(true);
928
929
                $this->logger->debug(
930
                    'FORM: Conflicts found in '.implode(', ', $conflicts)
931
                    .' between current tracked contact and submitted data so assuming a new contact'
932
                );
933
            }
934
        }
935
936
        //check for existing IP address
937
        $ipAddress = $this->ipLookupHelper->getIpAddress();
938
939
        //no lead was found by a mapped email field so create a new one
940
        if ($lead->isNewlyCreated()) {
941
            if (!$inKioskMode) {
942
                $lead->addIpAddress($ipAddress);
943
                $this->logger->debug('FORM: Associating '.$ipAddress->getIpAddress().' to contact');
944
            }
945
        } elseif (!$inKioskMode) {
946
            $leadIpAddresses = $lead->getIpAddresses();
947
            if (!$leadIpAddresses->contains($ipAddress)) {
948
                $lead->addIpAddress($ipAddress);
949
950
                $this->logger->debug('FORM: Associating '.$ipAddress->getIpAddress().' to contact');
951
            }
952
        }
953
954
        //set the mapped fields
955
        $this->leadModel->setFieldValues($lead, $data, false, true, true);
956
957
        // last active time
958
        $lead->setLastActive(new \DateTime());
959
960
        //create a new lead
961
        $lead->setManipulator(new LeadManipulator(
962
            'form',
963
            'submission',
964
            $form->getId(),
965
            $form->getName()
966
        ));
967
        $this->leadModel->saveEntity($lead, false);
968
969
        if (!$inKioskMode) {
970
            // Set the current lead which will generate tracking cookies
971
            $this->contactTracker->setTrackedContact($lead);
972
        } else {
973
            // Set system current lead which will still allow execution of events without generating tracking cookies
974
            $this->contactTracker->setSystemContact($lead);
975
        }
976
977
        $companyFieldMatches = $getCompanyData($leadFieldMatches);
978
        if (!empty($companyFieldMatches)) {
979
            list($company, $leadAdded, $companyEntity) = IdentifyCompanyHelper::identifyLeadsCompany($companyFieldMatches, $lead, $this->companyModel);
980
            if ($leadAdded) {
981
                $lead->addCompanyChangeLogEntry('form', 'Identify Company', 'Lead added to the company, '.$company['companyname'], $company['id']);
982
            } elseif ($companyEntity instanceof Company) {
983
                $this->companyModel->setFieldValues($companyEntity, $companyFieldMatches);
984
                $this->companyModel->saveEntity($companyEntity);
985
            }
986
987
            if (!empty($company) and $companyEntity instanceof Company) {
988
                // Save after the lead in for new leads created through the API and maybe other places
989
                $this->companyModel->addLeadToCompany($companyEntity, $lead);
990
                $this->leadModel->setPrimaryCompany($companyEntity->getId(), $lead->getId());
991
            }
992
            $this->em->clear(CompanyChangeLog::class);
993
        }
994
995
        return $lead;
996
    }
997
998
    /**
999
     * Validates a field value.
1000
     *
1001
     * @param $value
1002
     *
1003
     * @return bool|string True if valid; otherwise string with invalid reason
1004
     */
1005
    protected function validateFieldValue(Field $field, $value)
1006
    {
1007
        $standardValidation = $this->fieldHelper->validateFieldValue($field->getType(), $value, $field);
1008
        if (!empty($standardValidation)) {
1009
            return $standardValidation;
1010
        }
1011
1012
        $components = $this->formModel->getCustomComponents();
1013
        foreach ([$field->getType(), 'form'] as $type) {
1014
            if (isset($components['validators'][$type])) {
1015
                if (!is_array($components['validators'][$type])) {
1016
                    $components['validators'][$type] = [$components['validators'][$type]];
1017
                }
1018
                foreach ($components['validators'][$type] as $validator) {
1019
                    if (!is_array($validator)) {
1020
                        $validator = ['eventName' => $validator];
1021
                    }
1022
                    $event = $this->dispatcher->dispatch($validator['eventName'], new ValidationEvent($field, $value));
1023
                    if (!$event->isValid()) {
1024
                        return $event->getInvalidReason();
1025
                    }
1026
                }
1027
            }
1028
        }
1029
1030
        return true;
1031
    }
1032
}
1033