Issues (3627)

app/bundles/LeadBundle/Model/LeadModel.php (2 issues)

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\LeadBundle\Model;
13
14
use Doctrine\ORM\NonUniqueResultException;
15
use Doctrine\ORM\Tools\Pagination\Paginator;
16
use Mautic\CategoryBundle\Entity\Category;
17
use Mautic\CategoryBundle\Model\CategoryModel;
18
use Mautic\ChannelBundle\Helper\ChannelListHelper;
19
use Mautic\CoreBundle\Entity\IpAddress;
20
use Mautic\CoreBundle\Form\RequestTrait;
21
use Mautic\CoreBundle\Helper\Chart\ChartQuery;
22
use Mautic\CoreBundle\Helper\Chart\LineChart;
23
use Mautic\CoreBundle\Helper\Chart\PieChart;
24
use Mautic\CoreBundle\Helper\CookieHelper;
25
use Mautic\CoreBundle\Helper\CoreParametersHelper;
26
use Mautic\CoreBundle\Helper\DateTimeHelper;
27
use Mautic\CoreBundle\Helper\InputHelper;
28
use Mautic\CoreBundle\Helper\IpLookupHelper;
29
use Mautic\CoreBundle\Helper\PathsHelper;
30
use Mautic\CoreBundle\Model\FormModel;
31
use Mautic\EmailBundle\Helper\EmailValidator;
32
use Mautic\LeadBundle\DataObject\LeadManipulator;
33
use Mautic\LeadBundle\Entity\Company;
34
use Mautic\LeadBundle\Entity\CompanyChangeLog;
35
use Mautic\LeadBundle\Entity\CompanyLead;
36
use Mautic\LeadBundle\Entity\DoNotContact as DNC;
37
use Mautic\LeadBundle\Entity\FrequencyRule;
38
use Mautic\LeadBundle\Entity\Lead;
39
use Mautic\LeadBundle\Entity\LeadCategory;
40
use Mautic\LeadBundle\Entity\LeadEventLog;
41
use Mautic\LeadBundle\Entity\LeadField;
42
use Mautic\LeadBundle\Entity\LeadList;
43
use Mautic\LeadBundle\Entity\LeadRepository;
44
use Mautic\LeadBundle\Entity\OperatorListTrait;
45
use Mautic\LeadBundle\Entity\PointsChangeLog;
46
use Mautic\LeadBundle\Entity\StagesChangeLog;
47
use Mautic\LeadBundle\Entity\Tag;
48
use Mautic\LeadBundle\Entity\UtmTag;
49
use Mautic\LeadBundle\Event\CategoryChangeEvent;
50
use Mautic\LeadBundle\Event\DoNotContactAddEvent;
51
use Mautic\LeadBundle\Event\DoNotContactRemoveEvent;
52
use Mautic\LeadBundle\Event\LeadEvent;
53
use Mautic\LeadBundle\Event\LeadTimelineEvent;
54
use Mautic\LeadBundle\Exception\ImportFailedException;
55
use Mautic\LeadBundle\Form\Type\LeadType;
56
use Mautic\LeadBundle\Helper\ContactRequestHelper;
57
use Mautic\LeadBundle\Helper\IdentifyCompanyHelper;
58
use Mautic\LeadBundle\LeadEvents;
59
use Mautic\LeadBundle\Tracker\ContactTracker;
60
use Mautic\LeadBundle\Tracker\DeviceTracker;
61
use Mautic\PluginBundle\Helper\IntegrationHelper;
62
use Mautic\StageBundle\Entity\Stage;
63
use Mautic\UserBundle\Entity\User;
64
use Mautic\UserBundle\Security\Provider\UserProvider;
65
use Symfony\Component\EventDispatcher\Event;
66
use Symfony\Component\Form\FormFactory;
67
use Symfony\Component\HttpFoundation\RequestStack;
68
use Symfony\Component\HttpKernel\Exception\MethodNotAllowedHttpException;
69
use Symfony\Component\Intl\Intl;
70
71
/**
72
 * Class LeadModel
73
 * {@inheritdoc}
74
 */
75
class LeadModel extends FormModel
76
{
77
    use DefaultValueTrait;
78
    use OperatorListTrait;
79
    use RequestTrait;
80
81
    const CHANNEL_FEATURE = 'contact_preference';
82
83
    /**
84
     * @var \Symfony\Component\HttpFoundation\Request|null
85
     */
86
    protected $request;
87
88
    /**
89
     * @var CookieHelper
90
     */
91
    protected $cookieHelper;
92
93
    /**
94
     * @var IpLookupHelper
95
     */
96
    protected $ipLookupHelper;
97
98
    /**
99
     * @var PathsHelper
100
     */
101
    protected $pathsHelper;
102
103
    /**
104
     * @var IntegrationHelper
105
     */
106
    protected $integrationHelper;
107
108
    /**
109
     * @var FieldModel
110
     */
111
    protected $leadFieldModel;
112
113
    /**
114
     * @var array
115
     */
116
    protected $leadFields = [];
117
118
    /**
119
     * @var ListModel
120
     */
121
    protected $leadListModel;
122
123
    /**
124
     * @var CompanyModel
125
     */
126
    protected $companyModel;
127
128
    /**
129
     * @var CategoryModel
130
     */
131
    protected $categoryModel;
132
133
    /**
134
     * @var FormFactory
135
     */
136
    protected $formFactory;
137
138
    /**
139
     * @var ChannelListHelper
140
     */
141
    protected $channelListHelper;
142
143
    /**
144
     * @var CoreParametersHelper
145
     */
146
    protected $coreParametersHelper;
147
148
    /**
149
     * @var UserProvider
150
     */
151
    protected $userProvider;
152
153
    protected $leadTrackingId;
154
155
    /**
156
     * @var bool
157
     */
158
    protected $leadTrackingCookieGenerated = false;
159
160
    /**
161
     * @var array
162
     */
163
    protected $availableLeadFields = [];
164
165
    /**
166
     * @var EmailValidator
167
     */
168
    protected $emailValidator;
169
170
    /**
171
     * @var ContactTracker
172
     */
173
    private $contactTracker;
174
175
    /**
176
     * @var DeviceTracker
177
     */
178
    private $deviceTracker;
179
180
    /**
181
     * @var LegacyLeadModel
182
     */
183
    private $legacyLeadModel;
184
185
    /**
186
     * @var IpAddressModel
187
     */
188
    private $ipAddressModel;
189
190
    /**
191
     * @var bool
192
     */
193
    private $repoSetup = false;
194
195
    /**
196
     * @var array
197
     */
198
    private $flattenedFields = [];
199
200
    /**
201
     * @var array
202
     */
203
    private $fieldsByGroup = [];
204
205
    public function __construct(
206
        RequestStack $requestStack,
207
        CookieHelper $cookieHelper,
208
        IpLookupHelper $ipLookupHelper,
209
        PathsHelper $pathsHelper,
210
        IntegrationHelper $integrationHelper,
211
        FieldModel $leadFieldModel,
212
        ListModel $leadListModel,
213
        FormFactory $formFactory,
214
        CompanyModel $companyModel,
215
        CategoryModel $categoryModel,
216
        ChannelListHelper $channelListHelper,
217
        CoreParametersHelper $coreParametersHelper,
218
        EmailValidator $emailValidator,
219
        UserProvider $userProvider,
220
        ContactTracker $contactTracker,
221
        DeviceTracker $deviceTracker,
222
        LegacyLeadModel $legacyLeadModel,
223
        IpAddressModel $ipAddressModel
224
    ) {
225
        $this->request              = $requestStack->getCurrentRequest();
226
        $this->cookieHelper         = $cookieHelper;
227
        $this->ipLookupHelper       = $ipLookupHelper;
228
        $this->pathsHelper          = $pathsHelper;
229
        $this->integrationHelper    = $integrationHelper;
230
        $this->leadFieldModel       = $leadFieldModel;
231
        $this->leadListModel        = $leadListModel;
232
        $this->companyModel         = $companyModel;
233
        $this->formFactory          = $formFactory;
234
        $this->categoryModel        = $categoryModel;
235
        $this->channelListHelper    = $channelListHelper;
236
        $this->coreParametersHelper = $coreParametersHelper;
237
        $this->emailValidator       = $emailValidator;
238
        $this->userProvider         = $userProvider;
239
        $this->contactTracker       = $contactTracker;
240
        $this->deviceTracker        = $deviceTracker;
241
        $this->legacyLeadModel      = $legacyLeadModel;
242
        $this->ipAddressModel       = $ipAddressModel;
243
    }
244
245
    /**
246
     * @return LeadRepository
247
     */
248
    public function getRepository()
249
    {
250
        /** @var LeadRepository $repo */
251
        $repo = $this->em->getRepository(Lead::class);
252
        $repo->setDispatcher($this->dispatcher);
253
254
        if (!$this->repoSetup) {
255
            $this->repoSetup = true;
256
257
            //set the point trigger model in order to get the color code for the lead
258
            $fields = $this->leadFieldModel->getFieldList(true, false);
259
260
            $socialFields = (!empty($fields['social'])) ? array_keys($fields['social']) : [];
261
            $repo->setAvailableSocialFields($socialFields);
262
263
            $searchFields = [];
264
            foreach ($fields as $groupFields) {
265
                $searchFields = array_merge($searchFields, array_keys($groupFields));
266
            }
267
            $repo->setAvailableSearchFields($searchFields);
268
        }
269
270
        return $repo;
271
    }
272
273
    /**
274
     * Get the tags repository.
275
     *
276
     * @return \Mautic\LeadBundle\Entity\TagRepository
277
     */
278
    public function getTagRepository()
279
    {
280
        return $this->em->getRepository('MauticLeadBundle:Tag');
281
    }
282
283
    /**
284
     * @return \Mautic\LeadBundle\Entity\PointsChangeLogRepository
285
     */
286
    public function getPointLogRepository()
287
    {
288
        return $this->em->getRepository('MauticLeadBundle:PointsChangeLog');
289
    }
290
291
    /**
292
     * Get the tags repository.
293
     *
294
     * @return \Mautic\LeadBundle\Entity\UtmTagRepository
295
     */
296
    public function getUtmTagRepository()
297
    {
298
        return $this->em->getRepository('MauticLeadBundle:UtmTag');
299
    }
300
301
    /**
302
     * Get the tags repository.
303
     *
304
     * @return \Mautic\LeadBundle\Entity\LeadDeviceRepository
305
     */
306
    public function getDeviceRepository()
307
    {
308
        return $this->em->getRepository('MauticLeadBundle:LeadDevice');
309
    }
310
311
    /**
312
     * Get the lead event log repository.
313
     *
314
     * @return \Mautic\LeadBundle\Entity\LeadEventLogRepository
315
     */
316
    public function getEventLogRepository()
317
    {
318
        return $this->em->getRepository('MauticLeadBundle:LeadEventLog');
319
    }
320
321
    /**
322
     * Get the frequency rules repository.
323
     *
324
     * @return \Mautic\LeadBundle\Entity\FrequencyRuleRepository
325
     */
326
    public function getFrequencyRuleRepository()
327
    {
328
        return $this->em->getRepository('MauticLeadBundle:FrequencyRule');
329
    }
330
331
    /**
332
     * Get the Stages change log repository.
333
     *
334
     * @return \Mautic\LeadBundle\Entity\StagesChangeLogRepository
335
     */
336
    public function getStagesChangeLogRepository()
337
    {
338
        return $this->em->getRepository('MauticLeadBundle:StagesChangeLog');
339
    }
340
341
    /**
342
     * Get the lead categories repository.
343
     *
344
     * @return \Mautic\LeadBundle\Entity\LeadCategoryRepository
345
     */
346
    public function getLeadCategoryRepository()
347
    {
348
        return $this->em->getRepository('MauticLeadBundle:LeadCategory');
349
    }
350
351
    /**
352
     * @return \Mautic\LeadBundle\Entity\MergeRecordRepository
353
     */
354
    public function getMergeRecordRepository()
355
    {
356
        return $this->em->getRepository('MauticLeadBundle:MergeRecord');
357
    }
358
359
    /**
360
     * @return LeadListRepository
361
     */
362
    public function getLeadListRepository()
363
    {
364
        return $this->em->getRepository('MauticLeadBundle:LeadList');
365
    }
366
367
    /**
368
     * {@inheritdoc}
369
     *
370
     * @return string
371
     */
372
    public function getPermissionBase()
373
    {
374
        return 'lead:leads';
375
    }
376
377
    /**
378
     * {@inheritdoc}
379
     *
380
     * @return string
381
     */
382
    public function getNameGetter()
383
    {
384
        return 'getPrimaryIdentifier';
385
    }
386
387
    /**
388
     * {@inheritdoc}
389
     *
390
     * @param Lead                                $entity
391
     * @param \Symfony\Component\Form\FormFactory $formFactory
392
     * @param string|null                         $action
393
     * @param array                               $options
394
     *
395
     * @return \Symfony\Component\Form\Form
396
     *
397
     * @throws \Symfony\Component\HttpKernel\Exception\MethodNotAllowedHttpException
398
     */
399
    public function createForm($entity, $formFactory, $action = null, $options = [])
400
    {
401
        if (!$entity instanceof Lead) {
402
            throw new MethodNotAllowedHttpException(['Lead'], 'Entity must be of class Lead()');
403
        }
404
        if (!empty($action)) {
405
            $options['action'] = $action;
406
        }
407
408
        return $formFactory->create(LeadType::class, $entity, $options);
409
    }
410
411
    /**
412
     * Get a specific entity or generate a new one if id is empty.
413
     *
414
     * @param $id
415
     *
416
     * @return Lead|null
417
     */
418
    public function getEntity($id = null)
419
    {
420
        if (null === $id) {
421
            return new Lead();
422
        }
423
424
        $entity = parent::getEntity($id);
425
426
        if (null === $entity) {
427
            // Check if this contact was merged into another and if so, return the new contact
428
            if ($entity = $this->getMergeRecordRepository()->findMergedContact($id)) {
429
                // Hydrate fields with custom field data
430
                $fields = $this->getRepository()->getFieldValues($entity->getId());
431
                $entity->setFields($fields);
432
            }
433
        }
434
435
        return $entity;
436
    }
437
438
    /**
439
     * {@inheritdoc}
440
     *
441
     * @param $action
442
     * @param $event
443
     * @param $entity
444
     * @param $isNew
445
     *
446
     * @throws \Symfony\Component\HttpKernel\Exception\MethodNotAllowedHttpException
447
     */
448
    protected function dispatchEvent($action, &$entity, $isNew = false, Event $event = null)
449
    {
450
        if (!$entity instanceof Lead) {
451
            throw new MethodNotAllowedHttpException(['Lead'], 'Entity must be of class Lead()');
452
        }
453
454
        switch ($action) {
455
            case 'pre_save':
456
                $name = LeadEvents::LEAD_PRE_SAVE;
457
                break;
458
            case 'post_save':
459
                $name = LeadEvents::LEAD_POST_SAVE;
460
                break;
461
            case 'pre_delete':
462
                $name = LeadEvents::LEAD_PRE_DELETE;
463
                break;
464
            case 'post_delete':
465
                $name = LeadEvents::LEAD_POST_DELETE;
466
                break;
467
            default:
468
                return null;
469
        }
470
471
        if ($this->dispatcher->hasListeners($name)) {
472
            if (empty($event)) {
473
                $event = new LeadEvent($entity, $isNew);
474
                $event->setEntityManager($this->em);
475
            }
476
            $this->dispatcher->dispatch($name, $event);
477
478
            return $event;
479
        } else {
480
            return null;
481
        }
482
    }
483
484
    /**
485
     * {@inheritdoc}
486
     *
487
     * @param Lead $entity
488
     * @param bool $unlock
489
     */
490
    public function saveEntity($entity, $unlock = true)
491
    {
492
        $companyFieldMatches = [];
493
        $fields              = $entity->getFields();
494
        $company             = null;
495
496
        //check to see if we can glean information from ip address
497
        if (!$entity->imported && count($ips = $entity->getIpAddresses())) {
498
            $details = $ips->first()->getIpDetails();
499
            // Only update with IP details if none of the following are set to prevent wrong combinations
500
            if (empty($fields['core']['city']['value']) && empty($fields['core']['state']['value']) && empty($fields['core']['country']['value']) && empty($fields['core']['zipcode']['value'])) {
501
                if ($this->coreParametersHelper->get('anonymize_ip') && $this->ipLookupHelper->getRealIp()) {
502
                    $details = $this->ipLookupHelper->getIpDetails($this->ipLookupHelper->getRealIp());
503
                }
504
505
                if (!empty($details['city'])) {
506
                    $entity->addUpdatedField('city', $details['city']);
507
                    $companyFieldMatches['city'] = $details['city'];
508
                }
509
510
                if (!empty($details['region'])) {
511
                    $entity->addUpdatedField('state', $details['region']);
512
                    $companyFieldMatches['state'] = $details['region'];
513
                }
514
515
                if (!empty($details['country'])) {
516
                    $entity->addUpdatedField('country', $details['country']);
517
                    $companyFieldMatches['country'] = $details['country'];
518
                }
519
520
                if (!empty($details['zipcode'])) {
521
                    $entity->addUpdatedField('zipcode', $details['zipcode']);
522
                }
523
            }
524
525
            if (!$entity->getCompany() && !empty($details['organization']) && $this->coreParametersHelper->get('ip_lookup_create_organization', false)) {
526
                $entity->addUpdatedField('company', $details['organization']);
527
            }
528
        }
529
530
        $updatedFields = $entity->getUpdatedFields();
531
        if (isset($updatedFields['company'])) {
532
            $companyFieldMatches['company']            = $updatedFields['company'];
533
            [$company, $leadAdded, $companyEntity]     = IdentifyCompanyHelper::identifyLeadsCompany($companyFieldMatches, $entity, $this->companyModel);
534
            if ($leadAdded) {
535
                $entity->addCompanyChangeLogEntry('form', 'Identify Company', 'Lead added to the company, '.$company['companyname'], $company['id']);
536
            }
537
        }
538
539
        $this->processManipulator($entity);
540
541
        $this->setEntityDefaultValues($entity);
542
543
        $this->ipAddressModel->saveIpAddressesReferencesForContact($entity);
544
545
        parent::saveEntity($entity, $unlock);
546
547
        if (!empty($company)) {
548
            // Save after the lead in for new leads created through the API and maybe other places
549
            $this->companyModel->addLeadToCompany($companyEntity, $entity);
550
            $this->setPrimaryCompany($companyEntity->getId(), $entity->getId());
551
        }
552
553
        $this->em->clear(CompanyChangeLog::class);
554
    }
555
556
    /**
557
     * @param object $entity
558
     */
559
    public function deleteEntity($entity)
560
    {
561
        // Delete custom avatar if one exists
562
        $imageDir = $this->pathsHelper->getSystemPath('images', true);
563
        $avatar   = $imageDir.'/lead_avatars/avatar'.$entity->getId();
564
565
        if (file_exists($avatar)) {
566
            unlink($avatar);
567
        }
568
569
        parent::deleteEntity($entity);
570
    }
571
572
    /**
573
     * Clear all Lead entities.
574
     */
575
    public function clearEntities()
576
    {
577
        $this->getRepository()->clear();
578
    }
579
580
    /**
581
     * Populates custom field values for updating the lead. Also retrieves social media data.
582
     *
583
     * @param bool|false $overwriteWithBlank
584
     * @param bool|true  $fetchSocialProfiles
585
     * @param bool|false $bindWithForm        Send $data through the Lead form and only use valid data (should be used with request data)
586
     *
587
     * @return array
588
     *
589
     * @throws ImportFailedException
590
     */
591
    public function setFieldValues(Lead $lead, array $data, $overwriteWithBlank = false, $fetchSocialProfiles = true, $bindWithForm = false)
592
    {
593
        if ($fetchSocialProfiles) {
594
            //@todo - add a catch to NOT do social gleaning if a lead is created via a form, etc as we do not want the user to experience the wait
595
            //generate the social cache
596
            [$socialCache, $socialFeatureSettings] = $this->integrationHelper->getUserProfiles(
597
                $lead,
598
                $data,
599
                true,
600
                null,
601
                false,
602
                true
603
            );
604
605
            //set the social cache while we have it
606
            if (!empty($socialCache)) {
607
                $lead->setSocialCache($socialCache);
608
            }
609
        }
610
611
        if (isset($data['stage'])) {
612
            $stagesChangeLogRepo  = $this->getStagesChangeLogRepository();
613
            $currentLeadStageId   = $stagesChangeLogRepo->getCurrentLeadStage($lead->getId());
614
            $currentLeadStageName = null;
615
            if ($currentLeadStageId) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $currentLeadStageId of type integer|null is loosely compared to true; this is ambiguous if the integer can be 0. You might want to explicitly use !== null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For integer values, zero is a special case, in particular the following results might be unexpected:

0   == false // true
0   == null  // true
123 == false // false
123 == null  // false

// It is often better to use strict comparison
0 === false // false
0 === null  // false
Loading history...
616
                $currentStage = $this->em->getRepository(Stage::class)->findByIdOrName($currentLeadStageId);
0 ignored issues
show
The method findByIdOrName() does not exist on Doctrine\Common\Persistence\ObjectRepository. Did you maybe mean findBy()? ( Ignorable by Annotation )

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

616
                $currentStage = $this->em->getRepository(Stage::class)->/** @scrutinizer ignore-call */ findByIdOrName($currentLeadStageId);

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
617
                if ($currentStage) {
618
                    $currentLeadStageName = $currentStage->getName();
619
                }
620
            }
621
622
            $newLeadStageIdOrName = is_object($data['stage']) ? $data['stage']->getId() : $data['stage'];
623
            if ((int) $newLeadStageIdOrName !== $currentLeadStageId && $newLeadStageIdOrName !== $currentLeadStageName) {
624
                $newStage = $this->em->getRepository(Stage::class)->findByIdOrName($newLeadStageIdOrName);
625
                if ($newStage) {
626
                    $lead->stageChangeLogEntry(
627
                        $newStage,
628
                        $newStage->getId().':'.$newStage->getName(),
629
                        $this->translator->trans('mautic.stage.event.changed')
630
                    );
631
                } else {
632
                    throw new ImportFailedException($this->translator->trans('mautic.lead.import.stage.not.exists', ['id' => $newLeadStageIdOrName]));
633
                }
634
            }
635
        }
636
637
        //save the field values
638
        $fieldValues = $lead->getFields();
639
640
        if (empty($fieldValues) || $bindWithForm) {
641
            // Lead is new or they haven't been populated so let's build the fields now
642
            if (empty($this->flattenedFields)) {
643
                $this->flattenedFields = $this->leadFieldModel->getEntities(
644
                    [
645
                        'filter'         => ['isPublished' => true, 'object' => 'lead'],
646
                        'hydration_mode' => 'HYDRATE_ARRAY',
647
                    ]
648
                );
649
                $this->fieldsByGroup = $this->organizeFieldsByGroup($this->flattenedFields);
650
            }
651
652
            if (empty($fieldValues)) {
653
                $fieldValues = $this->fieldsByGroup;
654
            }
655
        }
656
657
        if ($bindWithForm) {
658
            // Cleanup the field values
659
            $form = $this->createForm(
660
                new Lead(), // use empty lead to prevent binding errors
661
                $this->formFactory,
662
                null,
663
                ['fields' => $this->flattenedFields, 'csrf_protection' => false, 'allow_extra_fields' => true]
664
            );
665
666
            // Unset stage and owner from the form because it's already been handled
667
            unset($data['stage'], $data['owner'], $data['tags']);
668
            // Prepare special fields
669
            $this->prepareParametersFromRequest($form, $data, $lead, [], $this->fieldsByGroup);
670
            // Submit the data
671
            $form->submit($data);
672
673
            if ($form->getErrors()->count()) {
674
                $this->logger->addDebug('LEAD: form validation failed with an error of '.$form->getErrors());
675
            }
676
            foreach ($form as $field => $formField) {
677
                if (isset($data[$field])) {
678
                    if ($formField->getErrors()->count()) {
679
                        $this->logger->addDebug('LEAD: '.$field.' failed form validation with an error of '.$formField->getErrors());
680
                        // Don't save bad data
681
                        unset($data[$field]);
682
                    } else {
683
                        $data[$field] = $formField->getData();
684
                    }
685
                }
686
            }
687
        }
688
689
        //update existing values
690
        foreach ($fieldValues as $group => &$groupFields) {
691
            if ('all' === $group) {
692
                continue;
693
            }
694
695
            foreach ($groupFields as $alias => &$field) {
696
                if (!isset($field['value'])) {
697
                    $field['value'] = null;
698
                }
699
700
                // Only update fields that are part of the passed $data array
701
                if (array_key_exists($alias, $data)) {
702
                    if (!$bindWithForm) {
703
                        $this->cleanFields($data, $field);
704
                    }
705
                    $curValue = $field['value'];
706
                    $newValue = isset($data[$alias]) ? $data[$alias] : '';
707
708
                    if (is_array($newValue)) {
709
                        $newValue = implode('|', $newValue);
710
                    }
711
712
                    $isEmpty = (null === $newValue || '' === $newValue);
713
                    if ($curValue !== $newValue && (!$isEmpty || ($isEmpty && $overwriteWithBlank))) {
714
                        $field['value'] = $newValue;
715
                        $lead->addUpdatedField($alias, $newValue, $curValue);
716
                    }
717
718
                    //if empty, check for social media data to plug the hole
719
                    if (empty($newValue) && !empty($socialCache)) {
720
                        foreach ($socialCache as $service => $details) {
721
                            //check to see if a field has been assigned
722
723
                            if (!empty($socialFeatureSettings[$service]['leadFields'])
724
                                && in_array($field['alias'], $socialFeatureSettings[$service]['leadFields'])
725
                            ) {
726
                                //check to see if the data is available
727
                                $key = array_search($field['alias'], $socialFeatureSettings[$service]['leadFields']);
728
                                if (isset($details['profile'][$key])) {
729
                                    //Found!!
730
                                    $field['value'] = $details['profile'][$key];
731
                                    $lead->addUpdatedField($alias, $details['profile'][$key]);
732
                                    break;
733
                                }
734
                            }
735
                        }
736
                    }
737
                }
738
            }
739
        }
740
741
        $lead->setFields($fieldValues);
742
    }
743
744
    /**
745
     * Disassociates a user from leads.
746
     *
747
     * @param $userId
748
     */
749
    public function disassociateOwner($userId)
750
    {
751
        $leads = $this->getRepository()->findByOwner($userId);
752
        foreach ($leads as $lead) {
753
            $lead->setOwner(null);
754
            $this->saveEntity($lead);
755
        }
756
    }
757
758
    /**
759
     * Get list of entities for autopopulate fields.
760
     *
761
     * @param $type
762
     * @param $filter
763
     * @param $limit
764
     * @param $start
765
     *
766
     * @return array
767
     */
768
    public function getLookupResults($type, $filter = '', $limit = 10, $start = 0)
769
    {
770
        $results = [];
771
        switch ($type) {
772
            case 'user':
773
                $results = $this->em->getRepository('MauticUserBundle:User')->getUserList($filter, $limit, $start, ['lead' => 'leads']);
774
                break;
775
        }
776
777
        return $results;
778
    }
779
780
    /**
781
     * Obtain an array of users for api lead edits.
782
     *
783
     * @return mixed
784
     */
785
    public function getOwnerList()
786
    {
787
        return $this->em->getRepository('MauticUserBundle:User')->getUserList('', 0);
788
    }
789
790
    /**
791
     * Obtains a list of leads based off IP.
792
     *
793
     * @param $ip
794
     *
795
     * @return mixed
796
     */
797
    public function getLeadsByIp($ip)
798
    {
799
        return $this->getRepository()->getLeadsByIp($ip);
800
    }
801
802
    /**
803
     * Obtains a list of leads based a list of IDs.
804
     *
805
     * @return Paginator
806
     */
807
    public function getLeadsByIds(array $ids)
808
    {
809
        return $this->getEntities([
810
            'filter' => [
811
                'force' => [
812
                    [
813
                        'column' => 'l.id',
814
                        'expr'   => 'in',
815
                        'value'  => $ids,
816
                    ],
817
                ],
818
            ],
819
        ]);
820
    }
821
822
    /**
823
     * @return bool
824
     */
825
    public function canEditContact(Lead $contact)
826
    {
827
        return $this->security->hasEntityAccess('lead:leads:editown', 'lead:leads:editother', $contact->getPermissionUser());
828
    }
829
830
    /**
831
     * Gets the details of a lead if not already set.
832
     *
833
     * @param $lead
834
     *
835
     * @return mixed
836
     */
837
    public function getLeadDetails($lead)
838
    {
839
        if ($lead instanceof Lead) {
840
            $fields = $lead->getFields();
841
            if (!empty($fields)) {
842
                return $fields;
843
            }
844
        }
845
846
        $leadId = ($lead instanceof Lead) ? $lead->getId() : (int) $lead;
847
848
        return $this->getRepository()->getFieldValues($leadId);
849
    }
850
851
    /**
852
     * Reorganizes a field list to be keyed by field's group then alias.
853
     *
854
     * @param $fields
855
     *
856
     * @return array
857
     */
858
    public function organizeFieldsByGroup($fields)
859
    {
860
        $array = [];
861
862
        foreach ($fields as $field) {
863
            if ($field instanceof LeadField) {
864
                $alias = $field->getAlias();
865
                if ($field->isPublished() and 'Lead' === $field->getObject()) {
866
                    $group                                = $field->getGroup();
867
                    $array[$group][$alias]['id']          = $field->getId();
868
                    $array[$group][$alias]['group']       = $group;
869
                    $array[$group][$alias]['label']       = $field->getLabel();
870
                    $array[$group][$alias]['alias']       = $alias;
871
                    $array[$group][$alias]['type']        = $field->getType();
872
                    $array[$group][$alias]['properties']  = $field->getProperties();
873
                }
874
            } else {
875
                $alias = $field['alias'];
876
                if ($field['isPublished'] and 'lead' === $field['object']) {
877
                    $group                                = $field['group'];
878
                    $array[$group][$alias]['id']          = $field['id'];
879
                    $array[$group][$alias]['group']       = $group;
880
                    $array[$group][$alias]['label']       = $field['label'];
881
                    $array[$group][$alias]['alias']       = $alias;
882
                    $array[$group][$alias]['type']        = $field['type'];
883
                    $array[$group][$alias]['properties']  = $field['properties'] ?? [];
884
                }
885
            }
886
        }
887
888
        //make sure each group key is present
889
        $groups = ['core', 'social', 'personal', 'professional'];
890
        foreach ($groups as $g) {
891
            if (!isset($array[$g])) {
892
                $array[$g] = [];
893
            }
894
        }
895
896
        return $array;
897
    }
898
899
    /**
900
     * Returns flat array for single lead.
901
     *
902
     * @param $leadId
903
     *
904
     * @return array
905
     */
906
    public function getLead($leadId)
907
    {
908
        return $this->getRepository()->getLead($leadId);
909
    }
910
911
    /**
912
     * Get the contat from request (ct/clickthrough) and handles auto merging of contact data from request parameters.
913
     *
914
     * @param array $queryFields
915
     *
916
     * @return array|Lead|null
917
     */
918
    public function getContactFromRequest($queryFields = [])
919
    {
920
        // @todo Instantiate here until we can remove circular dependency on LeadModel in order to make it a service
921
        $requestStack = new RequestStack();
922
        $requestStack->push($this->request);
923
        $contactRequestHelper = new ContactRequestHelper(
924
            $this,
925
            $this->contactTracker,
926
            $this->coreParametersHelper,
927
            $this->ipLookupHelper,
928
            $requestStack,
929
            $this->logger,
930
            $this->dispatcher
931
        );
932
933
        return $contactRequestHelper->getContactFromQuery($queryFields);
934
    }
935
936
    /**
937
     * @param bool $returnWithQueryFields
938
     *
939
     * @return array|Lead
940
     */
941
    public function checkForDuplicateContact(array $queryFields, Lead $lead = null, $returnWithQueryFields = false, $onlyPubliclyUpdateable = false)
942
    {
943
        // Search for lead by request and/or update lead fields if some data were sent in the URL query
944
        if (empty($this->availableLeadFields)) {
945
            $filter = ['isPublished' => true, 'object' => 'lead'];
946
947
            if ($onlyPubliclyUpdateable) {
948
                $filter['isPubliclyUpdatable'] = true;
949
            }
950
951
            $this->availableLeadFields = $this->leadFieldModel->getFieldList(
952
                false,
953
                false,
954
                $filter
955
            );
956
        }
957
958
        if (is_null($lead)) {
959
            $lead = new Lead();
960
        }
961
962
        $uniqueFields    = $this->leadFieldModel->getUniqueIdentifierFields();
963
        $uniqueFieldData = [];
964
        $inQuery         = array_intersect_key($queryFields, $this->availableLeadFields);
965
        $values          = $onlyPubliclyUpdateable ? $inQuery : $queryFields;
966
967
        // Run values through setFieldValues to clean them first
968
        $this->setFieldValues($lead, $values, false, false);
969
        $cleanFields = $lead->getFields();
970
971
        foreach ($inQuery as $k => $v) {
972
            if (empty($queryFields[$k])) {
973
                unset($inQuery[$k]);
974
            }
975
        }
976
977
        foreach ($cleanFields as $group) {
978
            foreach ($group as $key => $field) {
979
                if (array_key_exists($key, $uniqueFields) && !empty($field['value'])) {
980
                    $uniqueFieldData[$key] = $field['value'];
981
                }
982
            }
983
        }
984
985
        // Check for leads using unique identifier
986
        if (count($uniqueFieldData)) {
987
            $existingLeads = $this->getRepository()->getLeadsByUniqueFields($uniqueFieldData, ($lead) ? $lead->getId() : null);
988
989
            if (!empty($existingLeads)) {
990
                $this->logger->addDebug("LEAD: Existing contact ID# {$existingLeads[0]->getId()} found through query identifiers.");
991
                // Merge with existing lead or use the one found
992
                $lead = ($lead->getId()) ? $this->mergeLeads($lead, $existingLeads[0]) : $existingLeads[0];
993
            }
994
        }
995
996
        return $returnWithQueryFields ? [$lead, $inQuery] : $lead;
997
    }
998
999
    /**
1000
     * Get a list of segments this lead belongs to.
1001
     *
1002
     * @param bool $forLists
1003
     * @param bool $arrayHydration
1004
     * @param bool $isPublic
1005
     *
1006
     * @return mixed
1007
     */
1008
    public function getLists(Lead $lead, $forLists = false, $arrayHydration = false, $isPublic = false, $isPreferenceCenter = false)
1009
    {
1010
        $repo = $this->em->getRepository(LeadList::class);
1011
1012
        return $repo->getLeadLists($lead->getId(), $forLists, $arrayHydration, $isPublic, $isPreferenceCenter);
1013
    }
1014
1015
    /**
1016
     * Get a list of companies this contact belongs to.
1017
     *
1018
     * @return mixed
1019
     */
1020
    public function getCompanies(Lead $lead)
1021
    {
1022
        $repo = $this->em->getRepository('MauticLeadBundle:CompanyLead');
1023
1024
        return $repo->getCompaniesByLeadId($lead->getId());
1025
    }
1026
1027
    /**
1028
     * Add lead to lists.
1029
     *
1030
     * @param array|Lead     $lead
1031
     * @param array|LeadList $lists
1032
     * @param bool           $manuallyAdded
1033
     */
1034
    public function addToLists($lead, $lists, $manuallyAdded = true)
1035
    {
1036
        $this->leadListModel->addLead($lead, $lists, $manuallyAdded);
1037
    }
1038
1039
    /**
1040
     * Remove lead from lists.
1041
     *
1042
     * @param      $lead
1043
     * @param      $lists
1044
     * @param bool $manuallyRemoved
1045
     */
1046
    public function removeFromLists($lead, $lists, $manuallyRemoved = true)
1047
    {
1048
        $this->leadListModel->removeLead($lead, $lists, $manuallyRemoved);
1049
    }
1050
1051
    /**
1052
     * Add lead to Stage.
1053
     *
1054
     * @param array|Lead  $lead
1055
     * @param array|Stage $stage
1056
     * @param bool        $manuallyAdded
1057
     *
1058
     * @return $this
1059
     */
1060
    public function addToStages($lead, $stage, $manuallyAdded = true)
1061
    {
1062
        if (!$lead instanceof Lead) {
1063
            $leadId = (is_array($lead) && isset($lead['id'])) ? $lead['id'] : $lead;
1064
            $lead   = $this->em->getReference('MauticLeadBundle:Lead', $leadId);
1065
        }
1066
        $lead->setStage($stage);
1067
        $lead->stageChangeLogEntry(
1068
            $stage,
1069
            $stage->getId().': '.$stage->getName(),
1070
            $this->translator->trans('mautic.stage.event.added.batch')
1071
        );
1072
1073
        return $this;
1074
    }
1075
1076
    /**
1077
     * Remove lead from Stage.
1078
     *
1079
     * @param      $lead
1080
     * @param      $stage
1081
     * @param bool $manuallyRemoved
1082
     *
1083
     * @return $this
1084
     */
1085
    public function removeFromStages($lead, $stage, $manuallyRemoved = true)
1086
    {
1087
        $lead->setStage(null);
1088
        $lead->stageChangeLogEntry(
1089
            $stage,
1090
            $stage->getId().': '.$stage->getName(),
1091
            $this->translator->trans('mautic.stage.event.removed.batch')
1092
        );
1093
1094
        return $this;
1095
    }
1096
1097
    /**
1098
     * @param string $channel
1099
     *
1100
     * @return mixed
1101
     */
1102
    public function getFrequencyRules(Lead $lead, $channel = null)
1103
    {
1104
        if (is_array($channel)) {
1105
            $channel = key($channel);
1106
        }
1107
1108
        /** @var \Mautic\LeadBundle\Entity\FrequencyRuleRepository $frequencyRuleRepo */
1109
        $frequencyRuleRepo = $this->em->getRepository('MauticLeadBundle:FrequencyRule');
1110
        $frequencyRules    = $frequencyRuleRepo->getFrequencyRules($channel, $lead->getId());
1111
1112
        if (empty($frequencyRules)) {
1113
            return [];
1114
        }
1115
1116
        return $frequencyRules;
1117
    }
1118
1119
    /**
1120
     * Set frequency rules for lead per channel.
1121
     *
1122
     * @param null $data
1123
     * @param null $leadLists
1124
     *
1125
     * @return bool Returns true
1126
     */
1127
    public function setFrequencyRules(Lead $lead, $data = null, $leadLists = null, $persist = true)
1128
    {
1129
        // One query to get all the lead's current frequency rules and go ahead and create entities for them
1130
        $frequencyRules = $lead->getFrequencyRules()->toArray();
1131
        $entities       = [];
1132
        $channels       = $this->getPreferenceChannels();
1133
1134
        foreach ($channels as $ch) {
1135
            if (empty($data['lead_channels']['preferred_channel'])) {
1136
                $data['lead_channels']['preferred_channel'] = $ch;
1137
            }
1138
1139
            $frequencyRule = (isset($frequencyRules[$ch])) ? $frequencyRules[$ch] : new FrequencyRule();
1140
            $frequencyRule->setChannel($ch);
1141
            $frequencyRule->setLead($lead);
1142
            $frequencyRule->setDateAdded(new \DateTime());
1143
1144
            if (!empty($data['lead_channels']['frequency_number_'.$ch]) && !empty($data['lead_channels']['frequency_time_'.$ch])) {
1145
                $frequencyRule->setFrequencyNumber($data['lead_channels']['frequency_number_'.$ch]);
1146
                $frequencyRule->setFrequencyTime($data['lead_channels']['frequency_time_'.$ch]);
1147
            } else {
1148
                $frequencyRule->setFrequencyNumber(null);
1149
                $frequencyRule->setFrequencyTime(null);
1150
            }
1151
1152
            $frequencyRule->setPauseFromDate(!empty($data['lead_channels']['contact_pause_start_date_'.$ch]) ? $data['lead_channels']['contact_pause_start_date_'.$ch] : null);
1153
            $frequencyRule->setPauseToDate(!empty($data['lead_channels']['contact_pause_end_date_'.$ch]) ? $data['lead_channels']['contact_pause_end_date_'.$ch] : null);
1154
1155
            $frequencyRule->setLead($lead);
1156
            $frequencyRule->setPreferredChannel($data['lead_channels']['preferred_channel'] === $ch);
1157
1158
            if ($persist) {
1159
                $entities[$ch] = $frequencyRule;
1160
            } else {
1161
                $lead->addFrequencyRule($frequencyRule);
1162
            }
1163
        }
1164
1165
        if (!empty($entities)) {
1166
            $this->em->getRepository('MauticLeadBundle:FrequencyRule')->saveEntities($entities);
1167
        }
1168
1169
        foreach ($data['lead_lists'] as $leadList) {
1170
            if (!isset($leadLists[$leadList])) {
1171
                $this->addToLists($lead, [$leadList]);
1172
            }
1173
        }
1174
        // Delete lists that were removed
1175
        $deletedLists = array_diff(array_keys($leadLists), $data['lead_lists']);
1176
        if (!empty($deletedLists)) {
1177
            $this->removeFromLists($lead, $deletedLists);
1178
        }
1179
1180
        if (!empty($data['global_categories'])) {
1181
            $this->addToCategory($lead, $data['global_categories']);
1182
        }
1183
        $leadCategories = $this->getLeadCategories($lead);
1184
        // Delete categories that were removed
1185
        $deletedCategories = array_diff($leadCategories, $data['global_categories']);
1186
1187
        if (!empty($deletedCategories)) {
1188
            $this->removeFromCategories($deletedCategories);
1189
        }
1190
1191
        // Delete channels that were removed
1192
        $deleted = array_diff_key($frequencyRules, $entities);
1193
        if (!empty($deleted)) {
1194
            $this->em->getRepository('MauticLeadBundle:FrequencyRule')->deleteEntities($deleted);
1195
        }
1196
1197
        return true;
1198
    }
1199
1200
    /**
1201
     * @param $categories
1202
     * @param bool $manuallyAdded
1203
     *
1204
     * @return array
1205
     */
1206
    public function addToCategory(Lead $lead, $categories, $manuallyAdded = true)
1207
    {
1208
        $leadCategories = $this->getLeadCategoryRepository()->getLeadCategories($lead);
1209
1210
        $results = [];
1211
        foreach ($categories as $category) {
1212
            if (!isset($leadCategories[$category])) {
1213
                $newLeadCategory = new LeadCategory();
1214
                $newLeadCategory->setLead($lead);
1215
                if (!$category instanceof Category) {
1216
                    $category = $this->categoryModel->getEntity($category);
1217
                }
1218
                $newLeadCategory->setCategory($category);
1219
                $newLeadCategory->setDateAdded(new \DateTime());
1220
                $newLeadCategory->setManuallyAdded($manuallyAdded);
1221
                $results[$category->getId()] = $newLeadCategory;
1222
1223
                if ($this->dispatcher->hasListeners(LeadEvents::LEAD_CATEGORY_CHANGE)) {
1224
                    $this->dispatcher->dispatch(LeadEvents::LEAD_CATEGORY_CHANGE, new CategoryChangeEvent($lead, $category));
1225
                }
1226
            }
1227
        }
1228
        if (!empty($results)) {
1229
            $this->getLeadCategoryRepository()->saveEntities($results);
1230
        }
1231
1232
        return $results;
1233
    }
1234
1235
    /**
1236
     * @param $categories
1237
     */
1238
    public function removeFromCategories($categories)
1239
    {
1240
        $deleteCats = [];
1241
        if (is_array($categories)) {
1242
            foreach ($categories as $key => $category) {
1243
                /** @var LeadCategory $category */
1244
                $category     = $this->getLeadCategoryRepository()->getEntity($key);
1245
                $deleteCats[] = $category;
1246
1247
                if ($this->dispatcher->hasListeners(LeadEvents::LEAD_CATEGORY_CHANGE)) {
1248
                    $this->dispatcher->dispatch(LeadEvents::LEAD_CATEGORY_CHANGE, new CategoryChangeEvent($category->getLead(), $category->getCategory(), false));
1249
                }
1250
            }
1251
        } elseif ($categories instanceof LeadCategory) {
1252
            $deleteCats[] = $categories;
1253
1254
            if ($this->dispatcher->hasListeners(LeadEvents::LEAD_CATEGORY_CHANGE)) {
1255
                $this->dispatcher->dispatch(LeadEvents::LEAD_CATEGORY_CHANGE, new CategoryChangeEvent($categories->getLead(), $categories->getCategory(), false));
1256
            }
1257
        }
1258
1259
        if (!empty($deleteCats)) {
1260
            $this->getLeadCategoryRepository()->deleteEntities($deleteCats);
1261
        }
1262
    }
1263
1264
    /**
1265
     * @return array
1266
     */
1267
    public function getLeadCategories(Lead $lead)
1268
    {
1269
        $leadCategories   = $this->getLeadCategoryRepository()->getLeadCategories($lead);
1270
        $leadCategoryList = [];
1271
        foreach ($leadCategories as $category) {
1272
            $leadCategoryList[$category['id']] = $category['category_id'];
1273
        }
1274
1275
        return $leadCategoryList;
1276
    }
1277
1278
    /**
1279
     * @param array        $fields
1280
     * @param array        $data
1281
     * @param null         $owner
1282
     * @param null         $list
1283
     * @param null         $tags
1284
     * @param bool         $persist
1285
     * @param LeadEventLog $eventLog
1286
     *
1287
     * @return bool|null
1288
     *
1289
     * @throws \Exception
1290
     */
1291
    public function import($fields, $data, $owner = null, $list = null, $tags = null, $persist = true, LeadEventLog $eventLog = null, $importId = null)
1292
    {
1293
        $fields    = array_flip($fields);
1294
        $fieldData = [];
1295
1296
        // Extract company data and import separately
1297
        // Modifies the data array
1298
        $company                           = null;
1299
        [$companyFields, $companyData]     = $this->companyModel->extractCompanyDataFromImport($fields, $data);
1300
1301
        if (!empty($companyData)) {
1302
            $companyFields = array_flip($companyFields);
1303
            $this->companyModel->import($companyFields, $companyData, $owner, $list, $tags, $persist, $eventLog);
1304
            $companyFields = array_flip($companyFields);
1305
1306
            $companyName    = isset($companyFields['companyname']) ? $companyData[$companyFields['companyname']] : null;
1307
            $companyCity    = isset($companyFields['companycity']) ? $companyData[$companyFields['companycity']] : null;
1308
            $companyCountry = isset($companyFields['companycountry']) ? $companyData[$companyFields['companycountry']] : null;
1309
            $companyState   = isset($companyFields['companystate']) ? $companyData[$companyFields['companystate']] : null;
1310
1311
            $company = $this->companyModel->getRepository()->identifyCompany($companyName, $companyCity, $companyCountry, $companyState);
1312
        }
1313
1314
        foreach ($fields as $leadField => $importField) {
1315
            // Prevent overwriting existing data with empty data
1316
            if (array_key_exists($importField, $data) && !is_null($data[$importField]) && '' != $data[$importField]) {
1317
                $fieldData[$leadField] = InputHelper::_($data[$importField], 'string');
1318
            }
1319
        }
1320
1321
        $lead   = $this->checkForDuplicateContact($fieldData);
1322
        $merged = ($lead->getId());
1323
1324
        if (!empty($fields['dateAdded']) && !empty($data[$fields['dateAdded']])) {
1325
            $dateAdded = new DateTimeHelper($data[$fields['dateAdded']]);
1326
            $lead->setDateAdded($dateAdded->getUtcDateTime());
1327
        }
1328
        unset($fieldData['dateAdded']);
1329
1330
        if (!empty($fields['dateModified']) && !empty($data[$fields['dateModified']])) {
1331
            $dateModified = new DateTimeHelper($data[$fields['dateModified']]);
1332
            $lead->setDateModified($dateModified->getUtcDateTime());
1333
        }
1334
        unset($fieldData['dateModified']);
1335
1336
        if (!empty($fields['lastActive']) && !empty($data[$fields['lastActive']])) {
1337
            $lastActive = new DateTimeHelper($data[$fields['lastActive']]);
1338
            $lead->setLastActive($lastActive->getUtcDateTime());
1339
        }
1340
        unset($fieldData['lastActive']);
1341
1342
        if (!empty($fields['dateIdentified']) && !empty($data[$fields['dateIdentified']])) {
1343
            $dateIdentified = new DateTimeHelper($data[$fields['dateIdentified']]);
1344
            $lead->setDateIdentified($dateIdentified->getUtcDateTime());
1345
        }
1346
        unset($fieldData['dateIdentified']);
1347
1348
        if (!empty($fields['createdByUser']) && !empty($data[$fields['createdByUser']])) {
1349
            $userRepo      = $this->em->getRepository('MauticUserBundle:User');
1350
            $createdByUser = $userRepo->findByIdentifier($data[$fields['createdByUser']]);
1351
            if (null !== $createdByUser) {
1352
                $lead->setCreatedBy($createdByUser);
1353
            }
1354
        }
1355
        unset($fieldData['createdByUser']);
1356
1357
        if (!empty($fields['modifiedByUser']) && !empty($data[$fields['modifiedByUser']])) {
1358
            $userRepo       = $this->em->getRepository('MauticUserBundle:User');
1359
            $modifiedByUser = $userRepo->findByIdentifier($data[$fields['modifiedByUser']]);
1360
            if (null !== $modifiedByUser) {
1361
                $lead->setModifiedBy($modifiedByUser);
1362
            }
1363
        }
1364
        unset($fieldData['modifiedByUser']);
1365
1366
        if (!empty($fields['ip']) && !empty($data[$fields['ip']])) {
1367
            $addresses = explode(',', $data[$fields['ip']]);
1368
            foreach ($addresses as $address) {
1369
                $address = trim($address);
1370
                if (!$ipAddress = $this->ipAddressModel->findOneByIpAddress($address)) {
1371
                    $ipAddress = new IpAddress();
1372
                    $ipAddress->setIpAddress($address);
1373
                }
1374
                $lead->addIpAddress($ipAddress);
1375
            }
1376
        }
1377
        unset($fieldData['ip']);
1378
1379
        if (!empty($fields['points']) && !empty($data[$fields['points']]) && null === $lead->getId()) {
1380
            // Add points only for new leads
1381
            $lead->setPoints($data[$fields['points']]);
1382
1383
            //add a lead point change log
1384
            $log = new PointsChangeLog();
1385
            $log->setDelta($data[$fields['points']]);
1386
            $log->setLead($lead);
1387
            $log->setType('lead');
1388
            $log->setEventName($this->translator->trans('mautic.lead.import.event.name'));
1389
            $log->setActionName($this->translator->trans('mautic.lead.import.action.name', [
1390
                '%name%' => $this->userHelper->getUser()->getUsername(),
1391
            ]));
1392
            $log->setIpAddress($this->ipLookupHelper->getIpAddress());
1393
            $log->setDateAdded(new \DateTime());
1394
            $lead->addPointsChangeLog($log);
1395
        }
1396
1397
        if (!empty($fields['stage']) && !empty($data[$fields['stage']])) {
1398
            static $stages = [];
1399
            $stageName     = $data[$fields['stage']];
1400
            if (!array_key_exists($stageName, $stages)) {
1401
                // Set stage for contact
1402
                $stage = $this->em->getRepository('MauticStageBundle:Stage')->getStageByName($stageName);
1403
1404
                if (empty($stage)) {
1405
                    $stage = new Stage();
1406
                    $stage->setName($stageName);
1407
                    $stages[$stageName] = $stage;
1408
                }
1409
            } else {
1410
                $stage = $stages[$stageName];
1411
            }
1412
1413
            $lead->setStage($stage);
1414
1415
            //add a contact stage change log
1416
            $log = new StagesChangeLog();
1417
            $log->setStage($stage);
1418
            $log->setEventName($stage->getId().':'.$stage->getName());
1419
            $log->setLead($lead);
1420
            $log->setActionName(
1421
                $this->translator->trans(
1422
                    'mautic.stage.import.action.name',
1423
                    [
1424
                        '%name%' => $this->userHelper->getUser()->getUsername(),
1425
                    ]
1426
                )
1427
            );
1428
            $log->setDateAdded(new \DateTime());
1429
            $lead->stageChangeLog($log);
1430
        }
1431
        unset($fieldData['stage']);
1432
1433
        // Set unsubscribe status
1434
        if (!empty($fields['doNotEmail']) && isset($data[$fields['doNotEmail']]) && (!empty($fields['email']) && !empty($data[$fields['email']]))) {
1435
            $doNotEmail = filter_var($data[$fields['doNotEmail']], FILTER_VALIDATE_BOOLEAN, FILTER_NULL_ON_FAILURE);
1436
            if (null !== $doNotEmail) {
1437
                $reason = $this->translator->trans('mautic.lead.import.by.user', [
1438
                    '%user%' => $this->userHelper->getUser()->getUsername(),
1439
                ]);
1440
1441
                // The email must be set for successful unsubscribtion
1442
                $lead->addUpdatedField('email', $data[$fields['email']]);
1443
                if ($doNotEmail) {
1444
                    $event = new DoNotContactAddEvent($lead, 'email', $reason, DNC::MANUAL);
1445
                    $this->dispatcher->dispatch(DoNotContactAddEvent::ADD_DONOT_CONTACT, $event);
1446
                } else {
1447
                    $event = new DoNotContactRemoveEvent($lead, 'email');
1448
                    $this->dispatcher->dispatch(DoNotContactRemoveEvent::REMOVE_DONOT_CONTACT, $event);
1449
                }
1450
            }
1451
        }
1452
1453
        unset($fieldData['doNotEmail']);
1454
1455
        if (!empty($fields['ownerusername']) && !empty($data[$fields['ownerusername']])) {
1456
            try {
1457
                $newOwner = $this->userProvider->loadUserByUsername($data[$fields['ownerusername']]);
1458
                $lead->setOwner($newOwner);
1459
                //reset default import owner if exists owner for contact
1460
                $owner = null;
1461
            } catch (NonUniqueResultException $exception) {
1462
                // user not found
1463
            }
1464
        }
1465
        unset($fieldData['ownerusername']);
1466
1467
        if (null !== $owner) {
1468
            $lead->setOwner($this->em->getReference('MauticUserBundle:User', $owner));
1469
        }
1470
1471
        if (null !== $tags) {
1472
            $this->modifyTags($lead, $tags, null, false);
1473
        }
1474
1475
        if (empty($this->leadFields)) {
1476
            $this->leadFields = $this->leadFieldModel->getEntities(
1477
                [
1478
                    'filter' => [
1479
                        'force' => [
1480
                            [
1481
                                'column' => 'f.isPublished',
1482
                                'expr'   => 'eq',
1483
                                'value'  => true,
1484
                            ],
1485
                            [
1486
                                'column' => 'f.object',
1487
                                'expr'   => 'eq',
1488
                                'value'  => 'lead',
1489
                            ],
1490
                        ],
1491
                    ],
1492
                    'hydration_mode' => 'HYDRATE_ARRAY',
1493
                ]
1494
            );
1495
        }
1496
1497
        $fieldErrors = [];
1498
1499
        foreach ($this->leadFields as $leadField) {
1500
            if (isset($fieldData[$leadField['alias']])) {
1501
                if ('NULL' === $fieldData[$leadField['alias']]) {
1502
                    $fieldData[$leadField['alias']] = null;
1503
1504
                    continue;
1505
                }
1506
1507
                try {
1508
                    $this->cleanFields($fieldData, $leadField);
1509
                } catch (\Exception $exception) {
1510
                    $fieldErrors[] = $leadField['alias'].': '.$exception->getMessage();
1511
                }
1512
1513
                if ('email' === $leadField['type'] && !empty($fieldData[$leadField['alias']])) {
1514
                    try {
1515
                        $this->emailValidator->validate($fieldData[$leadField['alias']], false);
1516
                    } catch (\Exception $exception) {
1517
                        $fieldErrors[] = $leadField['alias'].': '.$exception->getMessage();
1518
                    }
1519
                }
1520
1521
                // Skip if the value is in the CSV row
1522
                continue;
1523
            } elseif ($lead->isNew() && $leadField['defaultValue']) {
1524
                // Fill in the default value if any
1525
                $fieldData[$leadField['alias']] = ('multiselect' === $leadField['type']) ? [$leadField['defaultValue']] : $leadField['defaultValue'];
1526
            }
1527
        }
1528
1529
        if ($fieldErrors) {
1530
            $fieldErrors = implode("\n", $fieldErrors);
1531
1532
            throw new \Exception($fieldErrors);
1533
        }
1534
1535
        // All clear
1536
        foreach ($fieldData as $field => $value) {
1537
            $lead->addUpdatedField($field, $value);
1538
        }
1539
1540
        $lead->imported = true;
1541
1542
        if ($eventLog) {
1543
            $action = $merged ? 'updated' : 'inserted';
1544
            $eventLog->setAction($action);
1545
        }
1546
1547
        if ($persist) {
1548
            $lead->setManipulator(new LeadManipulator(
1549
                'lead',
1550
                'import',
1551
                $importId,
1552
                $this->userHelper->getUser()->getName()
1553
            ));
1554
            $this->saveEntity($lead);
1555
1556
            if (null !== $list) {
1557
                $this->addToLists($lead, [$list]);
1558
            }
1559
1560
            if (null !== $company) {
1561
                $this->companyModel->addLeadToCompany($company, $lead);
1562
            }
1563
1564
            if ($eventLog) {
1565
                $lead->addEventLog($eventLog);
1566
            }
1567
        }
1568
1569
        return $merged;
1570
    }
1571
1572
    /**
1573
     * Update a leads tags.
1574
     *
1575
     * @param bool|false $removeOrphans
1576
     */
1577
    public function setTags(Lead $lead, array $tags, $removeOrphans = false)
1578
    {
1579
        /** @var Tag[] $currentTags */
1580
        $currentTags  = $lead->getTags();
1581
        $leadModified = $tagsDeleted = false;
1582
1583
        foreach ($currentTags as $tag) {
1584
            if (!in_array($tag->getId(), $tags)) {
1585
                // Tag has been removed
1586
                $lead->removeTag($tag);
1587
                $leadModified = $tagsDeleted = true;
1588
            } else {
1589
                // Remove tag so that what's left are new tags
1590
                $key = array_search($tag->getId(), $tags);
1591
                unset($tags[$key]);
1592
            }
1593
        }
1594
1595
        if (!empty($tags)) {
1596
            foreach ($tags as $tag) {
1597
                if (is_numeric($tag)) {
1598
                    // Existing tag being added to this lead
1599
                    $lead->addTag(
1600
                        $this->em->getReference('MauticLeadBundle:Tag', $tag)
1601
                    );
1602
                } else {
1603
                    $lead->addTag(
1604
                        $this->getTagRepository()->getTagByNameOrCreateNewOne($tag)
1605
                    );
1606
                }
1607
            }
1608
            $leadModified = true;
1609
        }
1610
1611
        if ($leadModified) {
1612
            $this->saveEntity($lead);
1613
1614
            // Delete orphaned tags
1615
            if ($tagsDeleted && $removeOrphans) {
1616
                $this->getTagRepository()->deleteOrphans();
1617
            }
1618
        }
1619
    }
1620
1621
    /**
1622
     * Update a leads UTM tags.
1623
     */
1624
    public function setUtmTags(Lead $lead, UtmTag $utmTags)
1625
    {
1626
        $lead->setUtmTags($utmTags);
1627
1628
        $this->saveEntity($lead);
1629
    }
1630
1631
    /**
1632
     * Add leads UTM tags via API.
1633
     *
1634
     * @param array $params
1635
     */
1636
    public function addUTMTags(Lead $lead, $params)
1637
    {
1638
        // known "synonym" fields expected
1639
        $synonyms = ['useragent'  => 'user_agent',
1640
                     'remotehost' => 'remote_host', ];
1641
1642
        // convert 'query' option to an array if necessary
1643
        if (isset($params['query']) && !is_array($params['query'])) {
1644
            // assume it's a query string; convert it to array
1645
            parse_str($params['query'], $queryResult);
1646
            if (!empty($queryResult)) {
1647
                $params['query'] = $queryResult;
1648
            } else {
1649
                // Something wrong with, remove it
1650
                unset($params['query']);
1651
            }
1652
        }
1653
1654
        // Fix up known synonym/mismatch field names
1655
        foreach ($synonyms as $expected => $replace) {
1656
            if (array_key_exists($expected, $params) && !isset($params[$replace])) {
1657
                // add expected key name
1658
                $params[$replace] = $params[$expected];
1659
            }
1660
        }
1661
1662
        // see if active date set, so we can use it
1663
        $updateLastActive = false;
1664
        $lastActive       = new \DateTime();
1665
        // should be: yyyy-mm-ddT00:00:00+00:00
1666
        if (isset($params['lastActive'])) {
1667
            $lastActive       = new \DateTime($params['lastActive']);
1668
            $updateLastActive = true;
1669
        }
1670
        $params['date_added'] = $lastActive;
1671
1672
        // New utmTag
1673
        $utmTags = new UtmTag();
1674
1675
        // get available fields and their setter.
1676
        $fields = $utmTags->getFieldSetterList();
1677
1678
        // cycle through calling appropriate setter
1679
        foreach ($fields as $q => $setter) {
1680
            if (isset($params[$q])) {
1681
                $utmTags->$setter($params[$q]);
1682
            }
1683
        }
1684
1685
        // create device
1686
        if (!empty($params['useragent'])) {
1687
            $this->deviceTracker->createDeviceFromUserAgent($lead, $params['useragent']);
1688
        }
1689
1690
        // add the lead
1691
        $utmTags->setLead($lead);
1692
        if ($updateLastActive) {
1693
            $lead->setLastActive($lastActive);
1694
        }
1695
1696
        $this->setUtmTags($lead, $utmTags);
1697
    }
1698
1699
    /**
1700
     * Removes a UtmTag set from a Lead.
1701
     *
1702
     * @param int $utmId
1703
     */
1704
    public function removeUtmTags(Lead $lead, $utmId)
1705
    {
1706
        /** @var UtmTag $utmTag */
1707
        foreach ($lead->getUtmTags() as $utmTag) {
1708
            if ($utmTag->getId() === $utmId) {
1709
                $lead->removeUtmTagEntry($utmTag);
1710
                $this->saveEntity($lead);
1711
1712
                return true;
1713
            }
1714
        }
1715
1716
        return false;
1717
    }
1718
1719
    /**
1720
     * Modify tags with support to remove via a prefixed minus sign.
1721
     *
1722
     * @param $tags
1723
     * @param $removeTags
1724
     * @param $persist
1725
     * @param bool True if tags modified
1726
     *
1727
     * @return bool
1728
     */
1729
    public function modifyTags(Lead $lead, $tags, array $removeTags = null, $persist = true)
1730
    {
1731
        $tagsModified = false;
1732
        $leadTags     = $lead->getTags();
1733
1734
        if (!$leadTags->isEmpty()) {
1735
            $this->logger->debug('CONTACT: Contact currently has tags '.implode(', ', $leadTags->getKeys()));
1736
        } else {
1737
            $this->logger->debug('CONTACT: Contact currently does not have any tags');
1738
        }
1739
1740
        if (!is_array($tags)) {
1741
            $tags = explode(',', $tags);
1742
        }
1743
1744
        if (empty($tags) && empty($removeTags)) {
1745
            return false;
1746
        }
1747
1748
        $this->logger->debug('CONTACT: Adding '.implode(', ', $tags).' to contact ID# '.$lead->getId());
1749
1750
        array_walk($tags, function (&$val) {
1751
            $val = html_entity_decode(trim($val), ENT_QUOTES);
1752
            $val = InputHelper::clean($val);
1753
        });
1754
1755
        // See which tags already exist
1756
        $foundTags = $this->getTagRepository()->getTagsByName($tags);
1757
        foreach ($tags as $tag) {
1758
            if (0 === strpos($tag, '-')) {
1759
                // Tag to be removed
1760
                $tag = substr($tag, 1);
1761
1762
                if (array_key_exists($tag, $foundTags) && $leadTags->contains($foundTags[$tag])) {
1763
                    $tagsModified = true;
1764
                    $lead->removeTag($foundTags[$tag]);
1765
1766
                    $this->logger->debug('CONTACT: Removed '.$tag);
1767
                }
1768
            } else {
1769
                $tagToBeAdded = null;
1770
1771
                if (!array_key_exists($tag, $foundTags)) {
1772
                    $tagToBeAdded = new Tag($tag, false);
1773
                } elseif (!$leadTags->contains($foundTags[$tag])) {
1774
                    $tagToBeAdded = $foundTags[$tag];
1775
                }
1776
1777
                if ($tagToBeAdded) {
1778
                    $lead->addTag($tagToBeAdded);
1779
                    $tagsModified = true;
1780
                    $this->logger->debug('CONTACT: Added '.$tag);
1781
                }
1782
            }
1783
        }
1784
1785
        if (!empty($removeTags)) {
1786
            $this->logger->debug('CONTACT: Removing '.implode(', ', $removeTags).' for contact ID# '.$lead->getId());
1787
1788
            array_walk($removeTags, function (&$val) {
1789
                $val = html_entity_decode(trim($val), ENT_QUOTES);
1790
                $val = InputHelper::clean($val);
1791
            });
1792
1793
            // See which tags really exist
1794
            $foundRemoveTags = $this->getTagRepository()->getTagsByName($removeTags);
1795
1796
            foreach ($removeTags as $tag) {
1797
                // Tag to be removed
1798
                if (array_key_exists($tag, $foundRemoveTags) && $leadTags->contains($foundRemoveTags[$tag])) {
1799
                    $lead->removeTag($foundRemoveTags[$tag]);
1800
                    $tagsModified = true;
1801
1802
                    $this->logger->debug('CONTACT: Removed '.$tag);
1803
                }
1804
            }
1805
        }
1806
1807
        if ($persist) {
1808
            $this->saveEntity($lead);
1809
        }
1810
1811
        return $tagsModified;
1812
    }
1813
1814
    /**
1815
     * Modify companies for lead.
1816
     *
1817
     * @param $companies
1818
     */
1819
    public function modifyCompanies(Lead $lead, $companies)
1820
    {
1821
        // See which companies belong to the lead already
1822
        $leadCompanies = $this->companyModel->getCompanyLeadRepository()->getCompaniesByLeadId($lead->getId());
1823
1824
        foreach ($leadCompanies as $leadCompany) {
1825
            if (false === array_search($leadCompany['company_id'], $companies)) {
1826
                $this->companyModel->removeLeadFromCompany([$leadCompany['company_id']], $lead);
1827
            }
1828
        }
1829
1830
        if (count($companies)) {
1831
            $this->companyModel->addLeadToCompany($companies, $lead);
1832
        } else {
1833
            // update the lead's company name to nothing
1834
            $lead->addUpdatedField('company', '');
1835
            $this->getRepository()->saveEntity($lead);
1836
        }
1837
    }
1838
1839
    /**
1840
     * Get array of available lead tags.
1841
     */
1842
    public function getTagList()
1843
    {
1844
        return $this->getTagRepository()->getSimpleList(null, [], 'tag', 'id');
1845
    }
1846
1847
    /**
1848
     * Get bar chart data of contacts.
1849
     *
1850
     * @param string    $unit          {@link php.net/manual/en/function.date.php#refsect1-function.date-parameters}
1851
     * @param \DateTime $dateFrom
1852
     * @param \DateTime $dateTo
1853
     * @param string    $dateFormat
1854
     * @param array     $filter
1855
     * @param bool      $canViewOthers
1856
     *
1857
     * @return array
1858
     */
1859
    public function getLeadsLineChartData($unit, $dateFrom, $dateTo, $dateFormat = null, $filter = [], $canViewOthers = true)
1860
    {
1861
        $flag        = null;
1862
        $topLists    = null;
1863
        $allLeadsT   = $this->translator->trans('mautic.lead.all.leads');
1864
        $identifiedT = $this->translator->trans('mautic.lead.identified');
1865
        $anonymousT  = $this->translator->trans('mautic.lead.lead.anonymous');
1866
1867
        if (isset($filter['flag'])) {
1868
            $flag = $filter['flag'];
1869
            unset($filter['flag']);
1870
        }
1871
1872
        if (!$canViewOthers) {
1873
            $filter['owner_id'] = $this->userHelper->getUser()->getId();
1874
        }
1875
1876
        $chart                              = new LineChart($unit, $dateFrom, $dateTo, $dateFormat);
1877
        $query                              = new ChartQuery($this->em->getConnection(), $dateFrom, $dateTo);
1878
        $anonymousFilter                    = $filter;
1879
        $anonymousFilter['date_identified'] = [
1880
            'expression' => 'isNull',
1881
        ];
1882
        $identifiedFilter                    = $filter;
1883
        $identifiedFilter['date_identified'] = [
1884
            'expression' => 'isNotNull',
1885
        ];
1886
1887
        if ('top' == $flag) {
1888
            $topLists = $this->leadListModel->getTopLists(6, $dateFrom, $dateTo);
1889
            if ($topLists) {
1890
                foreach ($topLists as $list) {
1891
                    $filter['leadlist_id'] = [
1892
                        'value'            => $list['id'],
1893
                        'list_column_name' => 't.id',
1894
                    ];
1895
                    $all = $query->fetchTimeData('leads', 'date_added', $filter);
1896
                    $chart->setDataset($list['name'].': '.$allLeadsT, $all);
1897
                }
1898
            }
1899
        } elseif ('topIdentifiedVsAnonymous' == $flag) {
1900
            $topLists = $this->leadListModel->getTopLists(3, $dateFrom, $dateTo);
1901
            if ($topLists) {
1902
                foreach ($topLists as $list) {
1903
                    $anonymousFilter['leadlist_id'] = [
1904
                        'value'            => $list['id'],
1905
                        'list_column_name' => 't.id',
1906
                    ];
1907
                    $identifiedFilter['leadlist_id'] = [
1908
                        'value'            => $list['id'],
1909
                        'list_column_name' => 't.id',
1910
                    ];
1911
                    $identified = $query->fetchTimeData('leads', 'date_added', $identifiedFilter);
1912
                    $anonymous  = $query->fetchTimeData('leads', 'date_added', $anonymousFilter);
1913
                    $chart->setDataset($list['name'].': '.$identifiedT, $identified);
1914
                    $chart->setDataset($list['name'].': '.$anonymousT, $anonymous);
1915
                }
1916
            }
1917
        } elseif ('identified' == $flag) {
1918
            $identified = $query->fetchTimeData('leads', 'date_added', $identifiedFilter);
1919
            $chart->setDataset($identifiedT, $identified);
1920
        } elseif ('anonymous' == $flag) {
1921
            $anonymous = $query->fetchTimeData('leads', 'date_added', $anonymousFilter);
1922
            $chart->setDataset($anonymousT, $anonymous);
1923
        } elseif ('identifiedVsAnonymous' == $flag) {
1924
            $identified = $query->fetchTimeData('leads', 'date_added', $identifiedFilter);
1925
            $anonymous  = $query->fetchTimeData('leads', 'date_added', $anonymousFilter);
1926
            $chart->setDataset($identifiedT, $identified);
1927
            $chart->setDataset($anonymousT, $anonymous);
1928
        } else {
1929
            $all = $query->fetchTimeData('leads', 'date_added', $filter);
1930
            $chart->setDataset($allLeadsT, $all);
1931
        }
1932
1933
        return $chart->render();
1934
    }
1935
1936
    /**
1937
     * Get pie chart data of dwell times.
1938
     *
1939
     * @param string $dateFrom
1940
     * @param string $dateTo
1941
     * @param array  $filters
1942
     * @param bool   $canViewOthers
1943
     *
1944
     * @return array
1945
     */
1946
    public function getAnonymousVsIdentifiedPieChartData($dateFrom, $dateTo, $filters = [], $canViewOthers = true)
1947
    {
1948
        $chart = new PieChart();
1949
        $query = new ChartQuery($this->em->getConnection(), $dateFrom, $dateTo);
1950
1951
        if (!$canViewOthers) {
1952
            $filter['owner_id'] = $this->userHelper->getUser()->getId();
1953
        }
1954
1955
        $identified = $query->count('leads', 'date_identified', 'date_added', $filters);
1956
        $all        = $query->count('leads', 'id', 'date_added', $filters);
1957
        $chart->setDataset($this->translator->trans('mautic.lead.identified'), $identified);
1958
        $chart->setDataset($this->translator->trans('mautic.lead.lead.anonymous'), ($all - $identified));
1959
1960
        return $chart->render();
1961
    }
1962
1963
    /**
1964
     * Get leads count per country name.
1965
     * Can't use entity, because country is a custom field.
1966
     *
1967
     * @param string $dateFrom
1968
     * @param string $dateTo
1969
     * @param array  $filters
1970
     * @param bool   $canViewOthers
1971
     *
1972
     * @return array
1973
     */
1974
    public function getLeadMapData($dateFrom, $dateTo, $filters = [], $canViewOthers = true)
1975
    {
1976
        if (!$canViewOthers) {
1977
            $filter['owner_id'] = $this->userHelper->getUser()->getId();
1978
        }
1979
1980
        $q = $this->em->getConnection()->createQueryBuilder();
1981
        $q->select('COUNT(t.id) as quantity, t.country')
1982
            ->from(MAUTIC_TABLE_PREFIX.'leads', 't')
1983
            ->groupBy('t.country')
1984
            ->where($q->expr()->isNotNull('t.country'));
1985
1986
        $chartQuery = new ChartQuery($this->em->getConnection(), $dateFrom, $dateTo);
1987
        $chartQuery->applyFilters($q, $filters);
1988
        $chartQuery->applyDateFilters($q, 'date_added');
1989
1990
        $results = $q->execute()->fetchAll();
1991
1992
        $countries = array_flip(Intl::getRegionBundle()->getCountryNames('en'));
1993
        $mapData   = [];
1994
1995
        // Convert country names to 2-char code
1996
        if ($results) {
1997
            foreach ($results as $leadCountry) {
1998
                if (isset($countries[$leadCountry['country']])) {
1999
                    $mapData[$countries[$leadCountry['country']]] = $leadCountry['quantity'];
2000
                }
2001
            }
2002
        }
2003
2004
        return $mapData;
2005
    }
2006
2007
    /**
2008
     * Get a list of top (by leads owned) users.
2009
     *
2010
     * @param int    $limit
2011
     * @param string $dateFrom
2012
     * @param string $dateTo
2013
     * @param array  $filters
2014
     *
2015
     * @return array
2016
     */
2017
    public function getTopOwners($limit = 10, $dateFrom = null, $dateTo = null, $filters = [])
2018
    {
2019
        $q = $this->em->getConnection()->createQueryBuilder();
2020
        $q->select('COUNT(t.id) AS leads, t.owner_id, u.first_name, u.last_name')
2021
            ->from(MAUTIC_TABLE_PREFIX.'leads', 't')
2022
            ->join('t', MAUTIC_TABLE_PREFIX.'users', 'u', 'u.id = t.owner_id')
2023
            ->where($q->expr()->isNotNull('t.owner_id'))
2024
            ->orderBy('leads', 'DESC')
2025
            ->groupBy('t.owner_id, u.first_name, u.last_name')
2026
            ->setMaxResults($limit);
2027
2028
        $chartQuery = new ChartQuery($this->em->getConnection(), $dateFrom, $dateTo);
2029
        $chartQuery->applyFilters($q, $filters);
2030
        $chartQuery->applyDateFilters($q, 'date_added');
2031
2032
        return $q->execute()->fetchAll();
2033
    }
2034
2035
    /**
2036
     * Get a list of top (by leads owned) users.
2037
     *
2038
     * @param int    $limit
2039
     * @param string $dateFrom
2040
     * @param string $dateTo
2041
     * @param array  $filters
2042
     *
2043
     * @return array
2044
     */
2045
    public function getTopCreators($limit = 10, $dateFrom = null, $dateTo = null, $filters = [])
2046
    {
2047
        $q = $this->em->getConnection()->createQueryBuilder();
2048
        $q->select('COUNT(t.id) AS leads, t.created_by, t.created_by_user')
2049
            ->from(MAUTIC_TABLE_PREFIX.'leads', 't')
2050
            ->where($q->expr()->isNotNull('t.created_by'))
2051
            ->andWhere($q->expr()->isNotNull('t.created_by_user'))
2052
            ->orderBy('leads', 'DESC')
2053
            ->groupBy('t.created_by, t.created_by_user')
2054
            ->setMaxResults($limit);
2055
2056
        $chartQuery = new ChartQuery($this->em->getConnection(), $dateFrom, $dateTo);
2057
        $chartQuery->applyFilters($q, $filters);
2058
        $chartQuery->applyDateFilters($q, 'date_added');
2059
2060
        return $q->execute()->fetchAll();
2061
    }
2062
2063
    /**
2064
     * Get a list of leads in a date range.
2065
     *
2066
     * @param int       $limit
2067
     * @param \DateTime $dateFrom
2068
     * @param \DateTime $dateTo
2069
     * @param array     $filters
2070
     * @param array     $options
2071
     *
2072
     * @return array
2073
     */
2074
    public function getLeadList($limit = 10, \DateTime $dateFrom = null, \DateTime $dateTo = null, $filters = [], $options = [])
2075
    {
2076
        if (!empty($options['canViewOthers'])) {
2077
            $filter['owner_id'] = $this->userHelper->getUser()->getId();
2078
        }
2079
2080
        $q = $this->em->getConnection()->createQueryBuilder();
2081
        $q->select('t.id, t.firstname, t.lastname, t.email, t.date_added, t.date_modified')
2082
            ->from(MAUTIC_TABLE_PREFIX.'leads', 't')
2083
            ->setMaxResults($limit);
2084
2085
        $chartQuery = new ChartQuery($this->em->getConnection(), $dateFrom, $dateTo);
2086
        $chartQuery->applyFilters($q, $filters);
2087
        $chartQuery->applyDateFilters($q, 'date_added');
2088
2089
        if (empty($options['includeAnonymous'])) {
2090
            $q->andWhere($q->expr()->isNotNull('t.date_identified'));
2091
        }
2092
        $results = $q->execute()->fetchAll();
2093
2094
        if ($results) {
2095
            foreach ($results as &$result) {
2096
                if ($result['firstname'] || $result['lastname']) {
2097
                    $result['name'] = trim($result['firstname'].' '.$result['lastname']);
2098
                } elseif ($result['email']) {
2099
                    $result['name'] = $result['email'];
2100
                } else {
2101
                    $result['name'] = 'anonymous';
2102
                }
2103
                unset($result['firstname']);
2104
                unset($result['lastname']);
2105
                unset($result['email']);
2106
            }
2107
        }
2108
2109
        return $results;
2110
    }
2111
2112
    /**
2113
     * Get timeline/engagement data.
2114
     *
2115
     * @param null $filters
2116
     * @param int  $page
2117
     * @param int  $limit
2118
     * @param bool $forTimeline
2119
     *
2120
     * @return array
2121
     */
2122
    public function getEngagements(Lead $lead = null, $filters = null, array $orderBy = null, $page = 1, $limit = 25, $forTimeline = true)
2123
    {
2124
        $event = $this->dispatcher->dispatch(
2125
            LeadEvents::TIMELINE_ON_GENERATE,
2126
            new LeadTimelineEvent($lead, $filters, $orderBy, $page, $limit, $forTimeline, $this->coreParametersHelper->get('site_url'))
2127
        );
2128
2129
        $payload = [
2130
            'events'   => $event->getEvents(),
2131
            'filters'  => $filters,
2132
            'order'    => $orderBy,
2133
            'types'    => $event->getEventTypes(),
2134
            'total'    => $event->getEventCounter()['total'],
2135
            'page'     => $page,
2136
            'limit'    => $limit,
2137
            'maxPages' => $event->getMaxPage(),
2138
        ];
2139
2140
        return ($forTimeline) ? $payload : [$payload, $event->getSerializerGroups()];
2141
    }
2142
2143
    /**
2144
     * @return array
2145
     */
2146
    public function getEngagementTypes()
2147
    {
2148
        $event = new LeadTimelineEvent();
2149
        $event->fetchTypesOnly();
2150
2151
        $this->dispatcher->dispatch(LeadEvents::TIMELINE_ON_GENERATE, $event);
2152
2153
        return $event->getEventTypes();
2154
    }
2155
2156
    /**
2157
     * Get engagement counts by time unit.
2158
     *
2159
     * @param string $unit
2160
     *
2161
     * @return array
2162
     */
2163
    public function getEngagementCount(Lead $lead, \DateTime $dateFrom = null, \DateTime $dateTo = null, $unit = 'm', ChartQuery $chartQuery = null)
2164
    {
2165
        $event = new LeadTimelineEvent($lead);
2166
        $event->setCountOnly($dateFrom, $dateTo, $unit, $chartQuery);
2167
2168
        $this->dispatcher->dispatch(LeadEvents::TIMELINE_ON_GENERATE, $event);
2169
2170
        return $event->getEventCounter();
2171
    }
2172
2173
    /**
2174
     * @param $company
2175
     *
2176
     * @return bool
2177
     */
2178
    public function addToCompany(Lead $lead, $company)
2179
    {
2180
        //check if lead is in company already
2181
        if (!$company instanceof Company) {
2182
            $company = $this->companyModel->getEntity($company);
2183
        }
2184
2185
        // company does not exist anymore
2186
        if (null === $company) {
2187
            return false;
2188
        }
2189
2190
        $companyLead = $this->companyModel->getCompanyLeadRepository()->getCompaniesByLeadId($lead->getId(), $company->getId());
2191
2192
        if (empty($companyLead)) {
2193
            $this->companyModel->addLeadToCompany($company, $lead);
2194
2195
            return true;
2196
        }
2197
2198
        return false;
2199
    }
2200
2201
    /**
2202
     * Get contact channels.
2203
     *
2204
     * @return array
2205
     */
2206
    public function getContactChannels(Lead $lead)
2207
    {
2208
        $allChannels = $this->getPreferenceChannels();
2209
2210
        $channels = [];
2211
        foreach ($allChannels as $channel) {
2212
            if (DNC::IS_CONTACTABLE === $this->isContactable($lead, $channel)) {
2213
                $channels[$channel] = $channel;
2214
            }
2215
        }
2216
2217
        return $channels;
2218
    }
2219
2220
    /**
2221
     * Get contact channels.
2222
     *
2223
     * @return array
2224
     */
2225
    public function getDoNotContactChannels(Lead $lead)
2226
    {
2227
        $allChannels = $this->getPreferenceChannels();
2228
2229
        $channels = [];
2230
        foreach ($allChannels as $channel) {
2231
            if (DNC::IS_CONTACTABLE !== $this->isContactable($lead, $channel)) {
2232
                $channels[$channel] = $channel;
2233
            }
2234
        }
2235
2236
        return $channels;
2237
    }
2238
2239
    /**
2240
     * @return array
2241
     */
2242
    public function getPreferenceChannels()
2243
    {
2244
        return $this->channelListHelper->getFeatureChannels(self::CHANNEL_FEATURE, true);
2245
    }
2246
2247
    /**
2248
     * @return array
2249
     */
2250
    public function getPreferredChannel(Lead $lead)
2251
    {
2252
        $preferredChannel = $this->getFrequencyRuleRepository()->getPreferredChannel($lead->getId());
2253
        if (!empty($preferredChannel)) {
2254
            return $preferredChannel[0];
2255
        }
2256
2257
        return [];
2258
    }
2259
2260
    /**
2261
     * @param $companyId
2262
     * @param $leadId
2263
     *
2264
     * @return array
2265
     */
2266
    public function setPrimaryCompany($companyId, $leadId)
2267
    {
2268
        $companyArray      = [];
2269
        $oldPrimaryCompany = $newPrimaryCompany = false;
2270
2271
        $lead = $this->getEntity($leadId);
2272
2273
        $companyLeads = $this->companyModel->getCompanyLeadRepository()->getEntitiesByLead($lead);
2274
2275
        /** @var CompanyLead $companyLead */
2276
        foreach ($companyLeads as $companyLead) {
2277
            $company = $companyLead->getCompany();
2278
2279
            if ($companyLead) {
2280
                if ($companyLead->getPrimary() && !$oldPrimaryCompany) {
2281
                    $oldPrimaryCompany = $companyLead->getCompany()->getId();
2282
                }
2283
                if ($company->getId() === (int) $companyId) {
2284
                    $companyLead->setPrimary(true);
2285
                    $newPrimaryCompany = $companyId;
2286
                    $lead->addUpdatedField('company', $company->getName());
2287
                } else {
2288
                    $companyLead->setPrimary(false);
2289
                }
2290
                $companyArray[] = $companyLead;
2291
            }
2292
        }
2293
2294
        if (!$newPrimaryCompany) {
2295
            $latestCompany = $this->companyModel->getCompanyLeadRepository()->getLatestCompanyForLead($leadId);
2296
            if (!empty($latestCompany)) {
2297
                $lead->addUpdatedField('company', $latestCompany['companyname'])
2298
                    ->setDateModified(new \DateTime());
2299
            }
2300
        }
2301
2302
        if (!empty($companyArray)) {
2303
            $this->em->getRepository('MauticLeadBundle:Lead')->saveEntity($lead);
2304
            $this->companyModel->getCompanyLeadRepository()->saveEntities($companyArray, false);
2305
        }
2306
2307
        // Clear CompanyLead entities from Doctrine memory
2308
        $this->em->clear(CompanyLead::class);
2309
2310
        return ['oldPrimary' => $oldPrimaryCompany, 'newPrimary' => $companyId];
2311
    }
2312
2313
    /**
2314
     * @param $score
2315
     *
2316
     * @return bool
2317
     */
2318
    public function scoreContactsCompany(Lead $lead, $score)
2319
    {
2320
        $success          = false;
2321
        $entities         = [];
2322
        $contactCompanies = $this->companyModel->getCompanyLeadRepository()->getCompaniesByLeadId($lead->getId());
2323
2324
        if (!empty($contactCompanies)) {
2325
            foreach ($contactCompanies as $contactCompany) {
2326
                $company  = $this->companyModel->getEntity($contactCompany['company_id']);
2327
                $oldScore = $company->getScore();
2328
                $newScore = $score + $oldScore;
2329
                $company->setScore($newScore);
2330
                $entities[] = $company;
2331
                $success    = true;
2332
            }
2333
        }
2334
2335
        if (!empty($entities)) {
2336
            $this->companyModel->getRepository()->saveEntities($entities);
2337
        }
2338
2339
        return $success;
2340
    }
2341
2342
    /**
2343
     * @param $ownerId
2344
     */
2345
    public function updateLeadOwner(Lead $lead, $ownerId)
2346
    {
2347
        $owner = $this->em->getReference(User::class, $ownerId);
2348
        $lead->setOwner($owner);
2349
2350
        parent::saveEntity($lead);
2351
    }
2352
2353
    private function processManipulator(Lead $lead)
2354
    {
2355
        if ($lead->isNewlyCreated() || $lead->wasAnonymous()) {
2356
            // Only store an entry once for created and once for identified, not every time the lead is saved
2357
            $manipulator = $lead->getManipulator();
2358
            if (null !== $manipulator && !$manipulator->wasLogged()) {
2359
                $manipulationLog = new LeadEventLog();
2360
                $manipulationLog->setLead($lead)
2361
                    ->setBundle($manipulator->getBundleName())
2362
                    ->setObject($manipulator->getObjectName())
2363
                    ->setObjectId($manipulator->getObjectId());
2364
                if ($lead->isAnonymous()) {
2365
                    $manipulationLog->setAction('created_contact');
2366
                } else {
2367
                    $manipulationLog->setAction('identified_contact');
2368
                }
2369
                $description = $manipulator->getObjectDescription();
2370
                $manipulationLog->setProperties(['object_description' => $description]);
2371
2372
                $lead->addEventLog($manipulationLog);
2373
                $manipulator->setAsLogged();
2374
            }
2375
        }
2376
    }
2377
2378
    /**
2379
     * @param bool $persist
2380
     *
2381
     * @return Lead
2382
     */
2383
    protected function createNewContact(IpAddress $ip, $persist = true)
2384
    {
2385
        //let's create a lead
2386
        $lead = new Lead();
2387
        $lead->addIpAddress($ip);
2388
        $lead->setNewlyCreated(true);
2389
2390
        if ($persist && !defined('MAUTIC_NON_TRACKABLE_REQUEST')) {
2391
            // Set to prevent loops
2392
            $this->contactTracker->setTrackedContact($lead);
2393
2394
            // Note ignoring a lead manipulator object here on purpose to not falsely record entries
2395
            $this->saveEntity($lead, false);
2396
2397
            $fields = $this->getLeadDetails($lead);
2398
            $lead->setFields($fields);
2399
        }
2400
2401
        if ($leadId = $lead->getId()) {
2402
            $this->logger->addDebug("LEAD: New lead created with ID# $leadId.");
2403
        }
2404
2405
        return $lead;
2406
    }
2407
2408
    /**
2409
     * @deprecated 2.12.0 to be removed in 3.0; use Mautic\LeadBundle\Model\DoNotContact instead
2410
     *
2411
     * @param string $channel
2412
     *
2413
     * @return int
2414
     *
2415
     * @see \Mautic\LeadBundle\Entity\DoNotContact This method can return boolean false, so be
2416
     *                                             sure to always compare the return value against
2417
     *                                             the class constants of DoNotContact
2418
     */
2419
    public function isContactable(Lead $lead, $channel)
2420
    {
2421
        if (is_array($channel)) {
2422
            $channel = key($channel);
2423
        }
2424
2425
        /** @var \Mautic\LeadBundle\Entity\DoNotContactRepository $dncRepo */
2426
        $dncRepo = $this->em->getRepository('MauticLeadBundle:DoNotContact');
2427
2428
        /** @var \Mautic\LeadBundle\Entity\DoNotContact[] $entries */
2429
        $dncEntries = $dncRepo->getEntriesByLeadAndChannel($lead, $channel);
2430
2431
        // If the lead has no entries in the DNC table, we're good to go
2432
        if (empty($dncEntries)) {
2433
            return DNC::IS_CONTACTABLE;
2434
        }
2435
2436
        foreach ($dncEntries as $dnc) {
2437
            if (DNC::IS_CONTACTABLE !== $dnc->getReason()) {
2438
                return $dnc->getReason();
2439
            }
2440
        }
2441
2442
        return DNC::IS_CONTACTABLE;
2443
    }
2444
2445
    /**
2446
     * Merge two leads; if a conflict of data occurs, the newest lead will get precedence.
2447
     *
2448
     * @deprecated 2.13.0; to be removed in 3.0. Use \Mautic\LeadBundle\Deduplicate\ContactMerger instead
2449
     *
2450
     * @param bool $autoMode If true, the newest lead will be merged into the oldes then deleted; otherwise, $lead will be merged into $lead2 then deleted
2451
     *
2452
     * @return Lead
2453
     */
2454
    public function mergeLeads(Lead $lead, Lead $lead2, $autoMode = true)
2455
    {
2456
        return $this->legacyLeadModel->mergeLeads($lead, $lead2, $autoMode);
2457
    }
2458
2459
    public function getAvailableLeadFields(): array
2460
    {
2461
        return $this->availableLeadFields;
2462
    }
2463
}
2464