Issues (3627)

app/bundles/LeadBundle/Entity/LeadRepository.php (1 issue)

1
<?php
2
3
/*
4
 * @copyright   2014 Mautic Contributors. All rights reserved
5
 * @author      Mautic
6
 *
7
 * @link        http://mautic.org
8
 *
9
 * @license     GNU/GPLv3 http://www.gnu.org/licenses/gpl-3.0.html
10
 */
11
12
namespace Mautic\LeadBundle\Entity;
13
14
use Doctrine\Common\Collections\ArrayCollection;
15
use Doctrine\DBAL\Connection;
16
use Doctrine\DBAL\Exception\DriverException;
17
use Doctrine\DBAL\Query\QueryBuilder;
18
use Mautic\CoreBundle\Entity\CommonRepository;
19
use Mautic\CoreBundle\Helper\DateTimeHelper;
20
use Mautic\CoreBundle\Helper\SearchStringHelper;
21
use Mautic\LeadBundle\Event\LeadBuildSearchEvent;
22
use Mautic\LeadBundle\LeadEvents;
23
use Mautic\PointBundle\Model\TriggerModel;
24
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
25
26
class LeadRepository extends CommonRepository implements CustomFieldRepositoryInterface
27
{
28
    use CustomFieldRepositoryTrait {
29
        prepareDbalFieldsForSave as defaultPrepareDbalFieldsForSave;
30
    }
31
32
    use ExpressionHelperTrait;
33
    use OperatorListTrait;
34
35
    /**
36
     * @var EventDispatcherInterface
37
     */
38
    protected $dispatcher;
39
40
    /**
41
     * @var array
42
     */
43
    private $availableSocialFields = [];
44
45
    /**
46
     * @var array
47
     */
48
    private $availableSearchFields = [];
49
50
    /**
51
     * Required to get the color based on a lead's points.
52
     *
53
     * @var TriggerModel
54
     */
55
    private $triggerModel;
56
57
    /**
58
     * Used by search functions to search social profiles.
59
     */
60
    public function setAvailableSocialFields(array $fields)
61
    {
62
        $this->availableSocialFields = $fields;
63
    }
64
65
    /**
66
     * Used by search functions to search using aliases as commands.
67
     */
68
    public function setAvailableSearchFields(array $fields)
69
    {
70
        $this->availableSearchFields = $fields;
71
    }
72
73
    /**
74
     * Sets trigger model.
75
     */
76
    public function setTriggerModel(TriggerModel $triggerModel)
77
    {
78
        $this->triggerModel = $triggerModel;
79
    }
80
81
    public function setDispatcher(EventDispatcherInterface $dispatcher)
82
    {
83
        $this->dispatcher = $dispatcher;
84
    }
85
86
    /**
87
     * Get a list of leads based on field value.
88
     *
89
     * @param string $field
90
     * @param string $value
91
     * @param ?int   $ignoreId
92
     * @param bool   $indexByColumn
93
     *
94
     * @return array
95
     */
96
    public function getLeadsByFieldValue($field, $value, $ignoreId = null, $indexByColumn = false)
97
    {
98
        $results = $this->getEntities([
99
            'qb'               => $this->buildQueryForGetLeadsByFieldValue($field, $value, $ignoreId),
100
            'ignore_paginator' => true,
101
        ]);
102
103
        if (!$indexByColumn) {
104
            return $results;
105
        }
106
107
        return array_combine(array_map(function (Lead $lead) use ($field) {
108
            return $lead->getFieldValue($field);
109
        }, $results), $results);
110
    }
111
112
    /**
113
     * Builds the query for the getLeadsByFieldValue method.
114
     *
115
     * @internal
116
     *
117
     * @param string $field
118
     * @param string $value
119
     * @param ?int   $ignoreId
120
     *
121
     * @return QueryBuilder
122
     */
123
    protected function buildQueryForGetLeadsByFieldValue($field, $value, $ignoreId = null)
124
    {
125
        $col = 'l.'.$field;
126
127
        $q = $this->getEntityManager()->getConnection()->createQueryBuilder()
128
            ->select('l.id')
129
            ->from(MAUTIC_TABLE_PREFIX.'leads', 'l');
130
131
        if ($ignoreId) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $ignoreId 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...
132
            $q->where('l.id != :ignoreId')
133
                ->setParameter('ignoreId', $ignoreId);
134
        }
135
136
        if (is_array($value)) {
137
            /**
138
             * Bind each value to specific named parameters.
139
             *
140
             * @see https://www.doctrine-project.org/projects/doctrine-orm/en/2.6/reference/query-builder.html#line-number-0a267d5a2c69797a7656aae33fcc140d16b0a566-72
141
             */
142
            $valueParams = [];
143
            for ($i = 0; $i < count($value); ++$i) {
144
                $valueParams[':'.$this->generateRandomParameterName()] = $value[$i];
145
            }
146
147
            $q->andWhere(
148
                $q->expr()->in($col, array_keys($valueParams))
149
            );
150
151
            foreach ($valueParams as $param => $value) {
152
                $q->setParameter(ltrim($param, ':'), $value);
153
            }
154
155
            return $q;
156
        }
157
158
        $q->andWhere("$col = :search")->setParameter('search', $value);
159
160
        return $q;
161
    }
162
163
    /**
164
     * @param $email
165
     *
166
     * @return Lead[]
167
     */
168
    public function getContactsByEmail($email)
169
    {
170
        $contacts = $this->getLeadsByFieldValue('email', $email);
171
172
        // Attempt to search for contacts without a + suffix
173
        if (empty($contacts) && preg_match('#^(.*?)\+(.*?)@(.*?)$#', $email, $parts)) {
174
            $email    = $parts[1].'@'.$parts[3];
175
            $contacts = $this->getLeadsByFieldValue('email', $email);
176
        }
177
178
        return $contacts;
179
    }
180
181
    /**
182
     * @param string[] $emails
183
     *
184
     * @return int[]|array
185
     */
186
    public function getContactIdsByEmails(array $emails): array
187
    {
188
        $result = $this->getEntityManager()
189
            ->createQuery("
190
                SELECT c.id 
191
                FROM Mautic\LeadBundle\Entity\Lead c
192
                WHERE c.email IN (:emails)
193
            ")
194
            ->setParameter(':emails', $emails, Connection::PARAM_STR_ARRAY)
195
            ->getArrayResult();
196
197
        return array_map(
198
            function ($row) {
199
                return (int) $row['id'];
200
            },
201
            $result
202
        );
203
    }
204
205
    /**
206
     * Get a list of lead entities.
207
     *
208
     * @param     $uniqueFieldsWithData
209
     * @param int $leadId
210
     * @param int $limit
211
     *
212
     * @return array
213
     */
214
    public function getLeadsByUniqueFields($uniqueFieldsWithData, $leadId = null, $limit = null)
215
    {
216
        $q = $this->getEntityManager()->getConnection()->createQueryBuilder()
217
            ->select('l.*')
218
            ->from(MAUTIC_TABLE_PREFIX.'leads', 'l');
219
220
        // loop through the fields and
221
        foreach ($uniqueFieldsWithData as $col => $val) {
222
            $q->orWhere("l.$col = :".$col)
223
                ->setParameter($col, $val);
224
        }
225
226
        // if we have a lead ID lets use it
227
        if (!empty($leadId)) {
228
            // make sure that its not the id we already have
229
            $q->andWhere('l.id != :leadId')
230
                ->setParameter('leadId', $leadId);
231
        }
232
233
        if ($limit) {
234
            $q->setMaxResults($limit);
235
        }
236
237
        $results = $q->execute()->fetchAll();
238
239
        // Collect the IDs
240
        $leads = [];
241
        foreach ($results as $r) {
242
            $leads[$r['id']] = $r;
243
        }
244
245
        // Get entities
246
        $q = $this->getEntityManager()->createQueryBuilder()
247
            ->select('l')
248
            ->from(Lead::class, 'l');
249
250
        $q->where(
251
            $q->expr()->in('l.id', ':ids')
252
        )
253
            ->setParameter('ids', array_keys($leads))
254
            ->orderBy('l.dateAdded', 'DESC')
255
            ->addOrderBy('l.id', 'DESC');
256
        $entities = $q->getQuery()
257
            ->getResult();
258
259
        /** @var Lead $lead */
260
        foreach ($entities as $lead) {
261
            $lead->setAvailableSocialFields($this->availableSocialFields);
262
            if (!empty($this->triggerModel)) {
263
                $lead->setColor($this->triggerModel->getColorForLeadPoints($lead->getPoints()));
264
            }
265
266
            $lead->setFields(
267
                $this->formatFieldValues($leads[$lead->getId()])
268
            );
269
        }
270
271
        return $entities;
272
    }
273
274
    /**
275
     * Get list of lead Ids by unique field data.
276
     *
277
     * @param $uniqueFieldsWithData is an array of columns & values to filter by
278
     * @param int $leadId is the current lead id. Added to query to skip and find other leads
279
     *
280
     * @return array
281
     */
282
    public function getLeadIdsByUniqueFields($uniqueFieldsWithData, $leadId = null)
283
    {
284
        $q = $this->getEntityManager()->getConnection()->createQueryBuilder()
285
            ->select('l.id')
286
            ->from(MAUTIC_TABLE_PREFIX.'leads', 'l');
287
288
        // loop through the fields and
289
        foreach ($uniqueFieldsWithData as $col => $val) {
290
            $q->orWhere("l.$col = :".$col)
291
                ->setParameter($col, $val);
292
        }
293
294
        // if we have a lead ID lets use it
295
        if (!empty($leadId)) {
296
            // make sure that its not the id we already have
297
            $q->andWhere('l.id != '.$leadId);
298
        }
299
300
        return $q->execute()->fetchAll();
301
    }
302
303
    /**
304
     * @param string $email
305
     * @param bool   $all   Set to true to return all matching lead id's
306
     *
307
     * @return array|null
308
     */
309
    public function getLeadByEmail($email, $all = false)
310
    {
311
        $q = $this->getEntityManager()->getConnection()->createQueryBuilder()
312
            ->select('l.id')
313
            ->from(MAUTIC_TABLE_PREFIX.'leads', 'l')
314
            ->where('email = :search')
315
            ->setParameter('search', $email);
316
317
        $result = $q->execute()->fetchAll();
318
319
        if (count($result)) {
320
            return $all ? $result : $result[0];
321
        } else {
322
            return;
323
        }
324
    }
325
326
    /**
327
     * Get leads by IP address.
328
     *
329
     * @param      $ip
330
     * @param bool $byId
331
     *
332
     * @return array
333
     */
334
    public function getLeadsByIp($ip, $byId = false)
335
    {
336
        $q = $this->createQueryBuilder('l')
337
            ->leftJoin('l.ipAddresses', 'i');
338
        $col = ($byId) ? 'i.id' : 'i.ipAddress';
339
        $q->where($col.' = :ip')
340
            ->setParameter('ip', $ip)
341
            ->orderBy('l.dateAdded', 'DESC');
342
        $results = $q->getQuery()->getResult();
343
344
        /** @var Lead $lead */
345
        foreach ($results as $lead) {
346
            $lead->setAvailableSocialFields($this->availableSocialFields);
347
        }
348
349
        return $results;
350
    }
351
352
    /**
353
     * @param $id
354
     *
355
     * @return array
356
     */
357
    public function getLead($id)
358
    {
359
        $fq = $this->getEntityManager()->getConnection()->createQueryBuilder();
360
        $fq->select('l.*')
361
            ->from(MAUTIC_TABLE_PREFIX.'leads', 'l')
362
            ->where('l.id = '.$id);
363
        $results = $fq->execute()->fetchAll();
364
365
        return (isset($results[0])) ? $results[0] : [];
366
    }
367
368
    /**
369
     * {@inheritdoc}
370
     *
371
     * @param int $id
372
     *
373
     * @return mixed|null
374
     */
375
    public function getEntity($id = 0)
376
    {
377
        try {
378
            $q = $this->createQueryBuilder($this->getTableAlias());
379
            if (is_array($id)) {
380
                $this->buildSelectClause($q, $id);
381
                $contactId = (int) $id['id'];
382
            } else {
383
                $q->select('l, u, i')
384
                    ->leftJoin('l.ipAddresses', 'i')
385
                    ->leftJoin('l.owner', 'u');
386
                $contactId = $id;
387
            }
388
            $q->andWhere($this->getTableAlias().'.id = :id')
389
                ->setParameter('id', (int) $contactId);
390
            $entity = $q->getQuery()->getSingleResult();
391
        } catch (\Exception $e) {
392
            $entity = null;
393
        }
394
395
        if (null === $entity) {
396
            return $entity;
397
        }
398
399
        if ($entity->getFields()) {
400
            // Pulled from Doctrine memory so don't make unnecessary queries as this has already happened
401
            return $entity;
402
        }
403
404
        if (!empty($this->triggerModel)) {
405
            $entity->setColor($this->triggerModel->getColorForLeadPoints($entity->getPoints()));
406
        }
407
408
        $fieldValues = $this->getFieldValues($id);
409
        $entity->setFields($fieldValues);
410
411
        $entity->setAvailableSocialFields($this->availableSocialFields);
412
413
        return $entity;
414
    }
415
416
    /**
417
     * Get a contact entity with the primary company data populated.
418
     *
419
     * The primary company data will be a flat array on the entity
420
     * with a key of `primaryCompany`
421
     *
422
     * @param mixed $entity
423
     *
424
     * @return mixed|null
425
     */
426
    public function getEntityWithPrimaryCompany($entity)
427
    {
428
        if (is_int($entity)) {
429
            $entity = $this->getEntity($entity);
430
        }
431
432
        if ($entity instanceof Lead) {
433
            $id        = $entity->getId();
434
            $companies = $this->getEntityManager()->getRepository(Company::class)->getCompaniesForContacts([$id]);
435
436
            if (!empty($companies[$id])) {
437
                $primary = null;
438
439
                foreach ($companies as $company) {
440
                    if (isset($company['is_primary']) && 1 == $company['is_primary']) {
441
                        $primary = $company;
442
                    }
443
                }
444
445
                if (empty($primary)) {
446
                    $primary = $companies[$id][0];
447
                }
448
449
                $entity->setPrimaryCompany($primary);
450
            }
451
        }
452
453
        return $entity;
454
    }
455
456
    /**
457
     * Get a list of leads.
458
     *
459
     * @return array
460
     */
461
    public function getEntities(array $args = [])
462
    {
463
        $contacts = $this->getEntitiesWithCustomFields(
464
            'lead',
465
            $args,
466
            function ($r) {
467
                if (!empty($this->triggerModel)) {
468
                    $r->setColor($this->triggerModel->getColorForLeadPoints($r->getPoints()));
469
                }
470
                $r->setAvailableSocialFields($this->availableSocialFields);
471
            }
472
        );
473
474
        $contactCount = isset($contacts['results']) ? count($contacts['results']) : count($contacts);
475
        if ($contactCount && (!empty($args['withPrimaryCompany']) || !empty($args['withChannelRules']))) {
476
            $withTotalCount = (array_key_exists('withTotalCount', $args) && $args['withTotalCount']);
477
            /** @var Lead[] $tmpContacts */
478
            $tmpContacts = ($withTotalCount) ? $contacts['results'] : $contacts;
479
480
            $withCompanies   = !empty($args['withPrimaryCompany']);
481
            $withPreferences = !empty($args['withChannelRules']);
482
            $contactIds      = array_keys($tmpContacts);
483
484
            if ($withCompanies) {
485
                $companies = $this->getEntityManager()->getRepository(Company::class)->getCompaniesForContacts($contactIds);
486
            }
487
488
            if ($withPreferences) {
489
                /** @var FrequencyRuleRepository $frequencyRepo */
490
                $frequencyRepo  = $this->getEntityManager()->getRepository(FrequencyRule::class);
491
                $frequencyRules = $frequencyRepo->getFrequencyRules(null, $contactIds);
492
493
                /** @var DoNotContactRepository $dncRepository */
494
                $dncRepository = $this->getEntityManager()->getRepository(DoNotContact::class);
495
                $dncRules      = $dncRepository->getChannelList(null, $contactIds);
496
            }
497
498
            foreach ($contactIds as $id) {
499
                if ($withCompanies && isset($companies[$id]) && !empty($companies[$id])) {
500
                    $primary = null;
501
502
                    // Try to find the primary company
503
                    foreach ($companies[$id] as $company) {
504
                        if (1 == $company['is_primary']) {
505
                            $primary = $company;
506
                        }
507
                    }
508
509
                    // If no primary was found, just grab the first
510
                    if (empty($primary)) {
511
                        $primary = $companies[$id][0];
512
                    }
513
514
                    if (is_array($tmpContacts[$id])) {
515
                        $tmpContacts[$id]['primaryCompany'] = $primary;
516
                    } elseif ($tmpContacts[$id] instanceof Lead) {
517
                        $tmpContacts[$id]->setPrimaryCompany($primary);
518
                    }
519
                }
520
521
                if ($withPreferences) {
522
                    $contactFrequencyRules = (isset($frequencyRules[$id])) ? $frequencyRules[$id] : [];
523
                    $contactDncRules       = (isset($dncRules[$id])) ? $dncRules[$id] : [];
524
525
                    $channelRules = Lead::generateChannelRules($contactFrequencyRules, $contactDncRules);
526
                    if (is_array($tmpContacts[$id])) {
527
                        $tmpContacts[$id]['channelRules'] = $channelRules;
528
                    } elseif ($tmpContacts[$id] instanceof Lead) {
529
                        $tmpContacts[$id]->setChannelRules($channelRules);
530
                    }
531
                }
532
            }
533
534
            if ($withTotalCount) {
535
                $contacts['results'] = $tmpContacts;
536
            } else {
537
                $contacts = $tmpContacts;
538
            }
539
        }
540
541
        return $contacts;
542
    }
543
544
    /**
545
     * @return array
546
     */
547
    public function getFieldGroups()
548
    {
549
        return ['core', 'social', 'personal', 'professional'];
550
    }
551
552
    /**
553
     * @return \Doctrine\DBAL\Query\QueryBuilder
554
     */
555
    public function getEntitiesDbalQueryBuilder()
556
    {
557
        $alias = $this->getTableAlias();
558
559
        return $this->getEntityManager()->getConnection()->createQueryBuilder()
560
            ->from(MAUTIC_TABLE_PREFIX.'leads', $alias)
561
            ->leftJoin($alias, MAUTIC_TABLE_PREFIX.'users', 'u', 'u.id = '.$alias.'.owner_id');
562
    }
563
564
    /**
565
     * @param $order
566
     *
567
     * @return \Doctrine\ORM\QueryBuilder
568
     */
569
    public function getEntitiesOrmQueryBuilder($order)
570
    {
571
        $alias = $this->getTableAlias();
572
        $q     = $this->getEntityManager()->createQueryBuilder();
573
        $q->select($alias.', u, i,'.$order)
574
            ->from('MauticLeadBundle:Lead', $alias, $alias.'.id')
575
            ->leftJoin($alias.'.ipAddresses', 'i')
576
            ->leftJoin($alias.'.owner', 'u')
577
            ->indexBy($alias, $alias.'.id');
578
579
        return $q;
580
    }
581
582
    /**
583
     * Get contacts for a specific channel entity.
584
     *
585
     * @param $args - same as getEntity/getEntities
586
     * @param        $joinTable
587
     * @param        $entityId
588
     * @param array  $filters
589
     * @param string $entityColumnName
590
     * @param array  $additionalJoins  [ ['type' => 'join|leftJoin', 'from_alias' => '', 'table' => '', 'condition' => ''], ... ]
591
     *
592
     * @return array
593
     */
594
    public function getEntityContacts($args, $joinTable, $entityId, $filters = [], $entityColumnName = 'id', array $additionalJoins = null, $contactColumnName = 'lead_id')
595
    {
596
        $qb = $this->getEntitiesDbalQueryBuilder();
597
598
        if (empty($contactColumnName)) {
599
            $contactColumnName = 'lead_id';
600
        }
601
602
        $joinCondition = $qb->expr()->andX(
603
            $qb->expr()->eq($this->getTableAlias().'.id', 'entity.'.$contactColumnName)
604
        );
605
606
        if ($entityId && $entityColumnName) {
607
            $joinCondition->add(
608
                $qb->expr()->eq("entity.{$entityColumnName}", (int) $entityId)
609
            );
610
        }
611
612
        if (!empty($joinTable)) {
613
            $qb->join(
614
                $this->getTableAlias(),
615
                MAUTIC_TABLE_PREFIX.$joinTable,
616
                'entity',
617
                $joinCondition
618
            );
619
        }
620
621
        if (is_array($additionalJoins)) {
622
            foreach ($additionalJoins as $t) {
623
                $qb->{$t['type']}(
624
                    $t['from_alias'],
625
                    MAUTIC_TABLE_PREFIX.$t['table'],
626
                    $t['alias'],
627
                    $t['condition']
628
                );
629
            }
630
        }
631
632
        if ($filters) {
633
            $expr = $qb->expr()->andX();
634
            foreach ($filters as $column => $value) {
635
                if (is_array($value)) {
636
                    $this->buildWhereClauseFromArray($qb, [$value]);
637
                } else {
638
                    if (false === strpos($column, '.')) {
639
                        $column = "entity.$column";
640
                    }
641
642
                    $expr->add(
643
                        $qb->expr()->eq($column, $qb->createNamedParameter($value))
644
                    );
645
                    $qb->andWhere($expr);
646
                }
647
            }
648
        }
649
650
        $args['qb'] = $qb;
651
652
        return $this->getEntities($args);
653
    }
654
655
    /**
656
     * Adds the "catch all" where clause to the QueryBuilder.
657
     *
658
     * @param \Doctrine\ORM\QueryBuilder|\Doctrine\DBAL\Query\QueryBuilder $q
659
     * @param                                                              $filter
660
     *
661
     * @return array
662
     */
663
    protected function addCatchAllWhereClause($q, $filter)
664
    {
665
        $columns = array_merge(
666
            [
667
                'l.firstname',
668
                'l.lastname',
669
                'l.email',
670
                'l.company',
671
                'l.city',
672
                'l.state',
673
                'l.zipcode',
674
                'l.country',
675
            ],
676
            $this->availableSocialFields
677
        );
678
679
        return $this->addStandardCatchAllWhereClause($q, $filter, $columns);
680
    }
681
682
    /**
683
     * Adds the command where clause to the QueryBuilder.
684
     *
685
     * @param \Doctrine\ORM\QueryBuilder|\Doctrine\DBAL\Query\QueryBuilder $q
686
     * @param                                                              $filter
687
     *
688
     * @return array
689
     */
690
    protected function addSearchCommandWhereClause($q, $filter)
691
    {
692
        $command                 = $filter->command;
693
        $string                  = $filter->string;
694
        $unique                  = $this->generateRandomParameterName();
695
        $returnParameter         = false; //returning a parameter that is not used will lead to a Doctrine error
696
        list($expr, $parameters) = parent::addSearchCommandWhereClause($q, $filter);
697
698
        //DBAL QueryBuilder does not have an expr()->not() function; boo!!
699
700
        // This will be switched by some commands that use join tables as NOT EXISTS queries will be used
701
        $exprType = ($filter->not) ? 'negate_expr' : 'expr';
702
703
        $operators = $this->getFilterExpressionFunctions();
704
        $operators = array_merge($operators, [
705
            'x' => [
706
                'expr'        => 'andX',
707
                'negate_expr' => 'orX',
708
            ],
709
            'null' => [
710
                'expr'        => 'isNull',
711
                'negate_expr' => 'isNotNull',
712
            ],
713
        ]);
714
715
        $innerJoinTables = (isset($this->advancedFilterCommands[$command])
716
            && SearchStringHelper::COMMAND_NEGATE !== $this->advancedFilterCommands[$command]);
717
        $likeExpr = $operators['like'][$exprType];
718
        $eqExpr   = $operators['='][$exprType];
719
        $nullExpr = $operators['null'][$exprType];
720
        $inExpr   = $operators['in'][$exprType];
721
        $xExpr    = $operators['x'][$exprType];
722
723
        switch ($command) {
724
            case $this->translator->trans('mautic.lead.lead.searchcommand.isanonymous'):
725
            case $this->translator->trans('mautic.lead.lead.searchcommand.isanonymous', [], null, 'en_US'):
726
                $expr = $q->expr()->$nullExpr('l.date_identified');
727
                break;
728
            case $this->translator->trans('mautic.core.searchcommand.ismine'):
729
            case $this->translator->trans('mautic.core.searchcommand.ismine', [], null, 'en_US'):
730
                $expr = $q->expr()->$eqExpr('l.owner_id', $this->currentUser->getId());
731
                break;
732
            case $this->translator->trans('mautic.lead.lead.searchcommand.isunowned'):
733
            case $this->translator->trans('mautic.lead.lead.searchcommand.isunowned', [], null, 'en_US'):
734
                $expr = $q->expr()->orX(
735
                    $q->expr()->$eqExpr('l.owner_id', 0),
736
                    $q->expr()->$nullExpr('l.owner_id')
737
                );
738
                break;
739
            case $this->translator->trans('mautic.lead.lead.searchcommand.owner'):
740
            case $this->translator->trans('mautic.lead.lead.searchcommand.owner', [], null, 'en_US'):
741
                $expr = $q->expr()->orX(
742
                    $q->expr()->$likeExpr('u.first_name', ':'.$unique),
743
                    $q->expr()->$likeExpr('u.last_name', ':'.$unique)
744
                );
745
                $returnParameter = true;
746
                break;
747
            case $this->translator->trans('mautic.core.searchcommand.name'):
748
            case $this->translator->trans('mautic.core.searchcommand.name', [], null, 'en_US'):
749
                $expr = $q->expr()->orX(
750
                    $q->expr()->$likeExpr('l.firstname', ":$unique"),
751
                    $q->expr()->$likeExpr('l.lastname', ":$unique")
752
                );
753
                $returnParameter = true;
754
                break;
755
            case $this->translator->trans('mautic.core.searchcommand.email'):
756
            case $this->translator->trans('mautic.core.searchcommand.email', [], null, 'en_US'):
757
                $expr            = $q->expr()->$likeExpr('l.email', ":$unique");
758
                $returnParameter = true;
759
                break;
760
            case $this->translator->trans('mautic.lead.lead.searchcommand.list'):
761
            case $this->translator->trans('mautic.lead.lead.searchcommand.list', [], null, 'en_US'):
762
                $this->applySearchQueryRelationship(
763
                    $q,
764
                    [
765
                        [
766
                            'from_alias' => 'l',
767
                            'table'      => 'lead_lists_leads',
768
                            'alias'      => 'list_lead',
769
                            'condition'  => 'l.id = list_lead.lead_id',
770
                        ],
771
                        [
772
                            'from_alias' => 'list_lead',
773
                            'table'      => 'lead_lists',
774
                            'alias'      => 'list',
775
                            'condition'  => 'list_lead.leadlist_id = list.id',
776
                        ],
777
                    ],
778
                    $innerJoinTables,
779
                    $this->generateFilterExpression($q, 'list.alias', $eqExpr, $unique, ($filter->not) ? true : null,
780
                        // orX for filter->not either manuall removed or is null
781
                        $q->expr()->$xExpr(
782
                            $q->expr()->$eqExpr('list_lead.manually_removed', 0)
783
                        )
784
                    )
785
                );
786
                $filter->strict  = true;
787
                $returnParameter = true;
788
789
                break;
790
            case $this->translator->trans('mautic.core.searchcommand.ip'):
791
            case $this->translator->trans('mautic.core.searchcommand.ip', [], null, 'en_US'):
792
                $this->applySearchQueryRelationship(
793
                    $q,
794
                    [
795
                        [
796
                            'from_alias' => 'l',
797
                            'table'      => 'lead_ips_xref',
798
                            'alias'      => 'ip_lead',
799
                            'condition'  => 'l.id = ip_lead.lead_id',
800
                        ],
801
                        [
802
                            'from_alias' => 'ip_lead',
803
                            'table'      => 'ip_addresses',
804
                            'alias'      => 'ip',
805
                            'condition'  => 'ip_lead.ip_id = ip.id',
806
                        ],
807
                    ],
808
                    $innerJoinTables,
809
                    $this->generateFilterExpression($q, 'ip.ip_address', $likeExpr, $unique, null)
810
                );
811
                $returnParameter = true;
812
813
                break;
814
            case $this->translator->trans('mautic.lead.lead.searchcommand.duplicate'):
815
            case $this->translator->trans('mautic.lead.lead.searchcommand.duplicate', [], null, 'en_US'):
816
                $prateek  = explode('+', $string);
817
                $imploder = [];
818
819
                foreach ($prateek as $value) {
820
                    $list       = $this->getEntityManager()->getRepository(LeadList::class)->findOneByAlias($value);
821
                    $imploder[] = ((!empty($list)) ? (int) $list->getId() : 0);
822
                }
823
824
                //logic. In query, Sum(manually_removed) should be less than the current)
825
                $pluck    = count($imploder);
826
                $imploder = (string) (implode(',', $imploder));
827
828
                $sq = $this->getEntityManager()->getConnection()->createQueryBuilder();
829
                $sq->select('duplicate.lead_id')
830
                    ->from(MAUTIC_TABLE_PREFIX.'lead_lists_leads', 'duplicate')
831
                    ->where(
832
                        $q->expr()->andX(
833
                            $q->expr()->in('duplicate.leadlist_id', $imploder),
834
                            $q->expr()->eq('duplicate.manually_removed', 0)
835
                        )
836
                    )
837
                    ->groupBy('duplicate.lead_id')
838
                    ->having("COUNT(duplicate.lead_id) = $pluck");
839
840
                $expr            = $q->expr()->$inExpr('l.id', sprintf('(%s)', $sq->getSQL()));
841
                $returnParameter = true;
842
843
                break;
844
            case $this->translator->trans('mautic.lead.lead.searchcommand.tag'):
845
            case $this->translator->trans('mautic.lead.lead.searchcommand.tag', [], null, 'en_US'):
846
                $this->applySearchQueryRelationship(
847
                    $q,
848
                    [
849
                        [
850
                            'from_alias' => 'l',
851
                            'table'      => 'lead_tags_xref',
852
                            'alias'      => 'xtag',
853
                            'condition'  => 'l.id = xtag.lead_id',
854
                        ],
855
                        [
856
                            'from_alias' => 'xtag',
857
                            'table'      => 'lead_tags',
858
                            'alias'      => 'tag',
859
                            'condition'  => 'xtag.tag_id = tag.id',
860
                        ],
861
                    ],
862
                    $innerJoinTables,
863
                    $this->generateFilterExpression($q, 'tag.tag', $likeExpr, $unique, null)
864
                );
865
                $returnParameter = true;
866
                break;
867
            case $this->translator->trans('mautic.lead.lead.searchcommand.company'):
868
            case $this->translator->trans('mautic.lead.lead.searchcommand.company', [], null, 'en_US'):
869
                $this->applySearchQueryRelationship(
870
                    $q,
871
                    [
872
                        [
873
                            'from_alias' => 'l',
874
                            'table'      => 'companies_leads',
875
                            'alias'      => 'comp_lead',
876
                            'condition'  => 'l.id = comp_lead.lead_id',
877
                        ],
878
                        [
879
                            'from_alias' => 'comp_lead',
880
                            'table'      => 'companies',
881
                            'alias'      => 'comp',
882
                            'condition'  => 'comp_lead.company_id = comp.id',
883
                        ],
884
                    ],
885
                    $innerJoinTables,
886
                    $this->generateFilterExpression($q, 'comp.companyname', $likeExpr, $unique, null)
887
                );
888
                $returnParameter = true;
889
                break;
890
            case $this->translator->trans('mautic.lead.lead.searchcommand.stage'):
891
            case $this->translator->trans('mautic.lead.lead.searchcommand.stage', [], null, 'en_US'):
892
                $this->applySearchQueryRelationship(
893
                    $q,
894
                    [
895
                        [
896
                            'from_alias' => 'l',
897
                            'table'      => 'stages',
898
                            'alias'      => 's',
899
                            'condition'  => 'l.stage_id = s.id',
900
                        ],
901
                    ],
902
                    $innerJoinTables,
903
                    $this->generateFilterExpression($q, 's.name', $likeExpr, $unique, null)
904
                );
905
                $returnParameter = true;
906
                break;
907
            default:
908
                if (in_array($command, $this->availableSearchFields)) {
909
                    $expr = $q->expr()->$likeExpr("l.$command", ":$unique");
910
                }
911
                $returnParameter = true;
912
                break;
913
        }
914
915
        if ($this->dispatcher) {
916
            $event = new LeadBuildSearchEvent($filter->string, $filter->command, $unique, $filter->not, $q);
917
            $this->dispatcher->dispatch(LeadEvents::LEAD_BUILD_SEARCH_COMMANDS, $event);
918
            if ($event->isSearchDone()) {
919
                $returnParameter = $event->getReturnParameters();
920
                $filter->strict  = $event->getStrict();
921
                $expr            = $event->getSubQuery();
922
                $parameters      = array_merge($parameters, $event->getParameters());
923
            }
924
        }
925
926
        if ($returnParameter) {
927
            $string              = ($filter->strict) ? $filter->string : "{$filter->string}%";
928
            $parameters[$unique] = $string;
929
        }
930
931
        return [
932
            $expr,
933
            $parameters,
934
        ];
935
    }
936
937
    /**
938
     * Returns the array of search commands.
939
     *
940
     * @return array
941
     */
942
    public function getSearchCommands()
943
    {
944
        $commands = [
945
            'mautic.lead.lead.searchcommand.isanonymous',
946
            'mautic.core.searchcommand.ismine',
947
            'mautic.lead.lead.searchcommand.isunowned',
948
            'mautic.lead.lead.searchcommand.list',
949
            'mautic.core.searchcommand.name',
950
            'mautic.lead.lead.searchcommand.company',
951
            'mautic.core.searchcommand.email',
952
            'mautic.lead.lead.searchcommand.owner',
953
            'mautic.core.searchcommand.ip',
954
            'mautic.lead.lead.searchcommand.tag',
955
            'mautic.lead.lead.searchcommand.stage',
956
            'mautic.lead.lead.searchcommand.duplicate',
957
            'mautic.lead.lead.searchcommand.email_sent',
958
            'mautic.lead.lead.searchcommand.email_read',
959
            'mautic.lead.lead.searchcommand.email_queued',
960
            'mautic.lead.lead.searchcommand.email_pending',
961
            'mautic.lead.lead.searchcommand.page_source',
962
            'mautic.lead.lead.searchcommand.page_source_id',
963
            'mautic.lead.lead.searchcommand.page_id',
964
            'mautic.lead.lead.searchcommand.sms_sent',
965
            'mautic.lead.lead.searchcommand.web_sent',
966
            'mautic.lead.lead.searchcommand.mobile_sent',
967
        ];
968
969
        if (!empty($this->availableSearchFields)) {
970
            $commands = array_merge($commands, $this->availableSearchFields);
971
        }
972
973
        return array_merge($commands, parent::getSearchCommands());
974
    }
975
976
    /**
977
     * Returns the array of columns with the default order.
978
     *
979
     * @return array
980
     */
981
    protected function getDefaultOrder()
982
    {
983
        return [
984
            ['l.last_active', 'DESC'],
985
        ];
986
    }
987
988
    /**
989
     * Updates lead's lastActive with now date/time.
990
     *
991
     * @param int $leadId
992
     */
993
    public function updateLastActive($leadId)
994
    {
995
        $dt     = new DateTimeHelper();
996
        $fields = ['last_active' => $dt->toUtcString()];
997
998
        $this->getEntityManager()->getConnection()->update(MAUTIC_TABLE_PREFIX.'leads', $fields, ['id' => $leadId]);
999
    }
1000
1001
    /**
1002
     * Gets the ID of the latest ID.
1003
     *
1004
     * @return int
1005
     */
1006
    public function getMaxLeadId()
1007
    {
1008
        $result = $this->getEntityManager()->getConnection()->createQueryBuilder()
1009
            ->select('max(id) as max_lead_id')
1010
            ->from(MAUTIC_TABLE_PREFIX.'leads', 'l')
1011
            ->execute()->fetchAll();
1012
1013
        return $result[0]['max_lead_id'];
1014
    }
1015
1016
    /**
1017
     * Gets names, signature and email of the user(lead owner).
1018
     *
1019
     * @param int $ownerId
1020
     *
1021
     * @return array|false
1022
     */
1023
    public function getLeadOwner($ownerId)
1024
    {
1025
        if (!$ownerId) {
1026
            return false;
1027
        }
1028
1029
        $q = $this->getEntityManager()->getConnection()->createQueryBuilder()
1030
            ->select('u.id, u.first_name, u.last_name, u.email, u.position, u.signature')
1031
            ->from(MAUTIC_TABLE_PREFIX.'users', 'u')
1032
            ->where('u.id = :ownerId')
1033
            ->setParameter('ownerId', (int) $ownerId);
1034
1035
        $result = $q->execute()->fetch();
1036
1037
        // Fix the HTML markup
1038
        if (is_array($result)) {
1039
            foreach ($result as &$field) {
1040
                $field = html_entity_decode($field);
1041
            }
1042
        }
1043
1044
        return $result;
1045
    }
1046
1047
    /**
1048
     * Check lead owner.
1049
     *
1050
     * @param array $ownerIds
1051
     *
1052
     * @return array|false
1053
     */
1054
    public function checkLeadOwner(Lead $lead, $ownerIds = [])
1055
    {
1056
        if (empty($ownerIds)) {
1057
            return false;
1058
        }
1059
1060
        $q = $this->getEntityManager()->getConnection()->createQueryBuilder();
1061
        $q->select('u.id')
1062
            ->from(MAUTIC_TABLE_PREFIX.'users', 'u')
1063
            ->join('u', MAUTIC_TABLE_PREFIX.'leads', 'l', 'l.owner_id = u.id')
1064
            ->where(
1065
                $q->expr()->andX(
1066
                    $q->expr()->in('u.id', ':ownerIds'),
1067
                    $q->expr()->eq('l.id', ':leadId')
1068
                )
1069
            )
1070
            ->setParameter('ownerIds', implode(',', $ownerIds))
1071
            ->setParameter('leadId', $lead->getId());
1072
1073
        return (bool) $q->execute()->fetchColumn();
1074
    }
1075
1076
    /**
1077
     * @return array
1078
     */
1079
    public function getContacts(array $contactIds)
1080
    {
1081
        $qb = $this->getEntityManager()->getConnection()->createQueryBuilder();
1082
1083
        $qb->select('l.*')->from(MAUTIC_TABLE_PREFIX.'leads', 'l')
1084
            ->where(
1085
                $qb->expr()->in('l.id', $contactIds)
1086
            );
1087
1088
        $results = $qb->execute()->fetchAll();
1089
1090
        if ($results) {
1091
            $contacts = [];
1092
            foreach ($results as $result) {
1093
                $contacts[$result['id']] = $result;
1094
            }
1095
1096
            return $contacts;
1097
        }
1098
1099
        return [];
1100
    }
1101
1102
    /**
1103
     * @return ArrayCollection
1104
     */
1105
    public function getContactCollection(array $ids)
1106
    {
1107
        if (empty($ids)) {
1108
            return new ArrayCollection();
1109
        }
1110
1111
        $contacts = $this->getEntities(
1112
            [
1113
                'filter'             => [
1114
                    'force' => [
1115
                        [
1116
                            'column' => 'l.id',
1117
                            'expr'   => 'in',
1118
                            'value'  => $ids,
1119
                        ],
1120
                    ],
1121
                ],
1122
                'orderBy'            => 'l.id',
1123
                'orderByDir'         => 'asc',
1124
                'withPrimaryCompany' => true,
1125
                'withChannelRules'   => true,
1126
            ]
1127
        );
1128
1129
        return new ArrayCollection($contacts);
1130
    }
1131
1132
    /**
1133
     * @return string
1134
     */
1135
    public function getTableAlias()
1136
    {
1137
        return 'l';
1138
    }
1139
1140
    /**
1141
     * Get the count of identified contacts.
1142
     *
1143
     * @return int
1144
     */
1145
    public function getIdentifiedContactCount()
1146
    {
1147
        $qb = $this->getEntityManager()->getConnection()->createQueryBuilder()
1148
            ->select('count(*)')
1149
            ->from($this->getTableName(), $this->getTableAlias());
1150
1151
        $qb->where(
1152
            $qb->expr()->isNotNull($this->getTableAlias().'.date_identified')
1153
        );
1154
1155
        return (int) $qb->execute()->fetchColumn();
1156
    }
1157
1158
    /**
1159
     * Get the next contact after an specific ID; mainly used in deduplication.
1160
     *
1161
     * @return Lead
1162
     */
1163
    public function getNextIdentifiedContact($lastId)
1164
    {
1165
        $alias = $this->getTableAlias();
1166
        $qb    = $this->getEntityManager()->getConnection()->createQueryBuilder()
1167
            ->select("$alias.id")
1168
            ->from($this->getTableName(), $this->getTableAlias());
1169
1170
        $qb->where(
1171
            $qb->expr()->andX(
1172
                $qb->expr()->gt("$alias.id", (int) $lastId),
1173
                $qb->expr()->isNotNull("$alias.date_identified")
1174
            )
1175
        )
1176
            ->orderBy("$alias.id")
1177
            ->setMaxResults(1);
1178
1179
        $next = $qb->execute()->fetchColumn();
1180
1181
        return ($next) ? $this->getEntity($next) : null;
1182
    }
1183
1184
    /**
1185
     * @param array $tables          $tables[0] should be primary table
1186
     * @param bool  $innerJoinTables
1187
     * @param null  $whereExpression
1188
     * @param null  $having
1189
     */
1190
    public function applySearchQueryRelationship(QueryBuilder $q, array $tables, $innerJoinTables, $whereExpression = null, $having = null)
1191
    {
1192
        $primaryTable = $tables[0];
1193
        unset($tables[0]);
1194
1195
        $joinType = ($innerJoinTables) ? 'join' : 'leftJoin';
1196
1197
        $this->useDistinctCount = true;
1198
        $joins                  = $q->getQueryPart('join');
1199
        if (!preg_match('/"'.preg_quote($primaryTable['alias'], '/').'"/i', json_encode($joins))) {
1200
            $q->$joinType(
1201
                $primaryTable['from_alias'],
1202
                MAUTIC_TABLE_PREFIX.$primaryTable['table'],
1203
                $primaryTable['alias'],
1204
                $primaryTable['condition']
1205
            );
1206
        }
1207
        foreach ($tables as $table) {
1208
            $q->$joinType($table['from_alias'], MAUTIC_TABLE_PREFIX.$table['table'], $table['alias'], $table['condition']);
1209
        }
1210
1211
        if ($whereExpression) {
1212
            $q->andWhere($whereExpression);
1213
        }
1214
1215
        if ($having) {
1216
            $q->andHaving($having);
1217
        }
1218
        $q->groupBy('l.id');
1219
    }
1220
1221
    /**
1222
     * @param     $id
1223
     * @param int $tries
1224
     */
1225
    protected function updateContactPoints(array $changes, $id, $tries = 1)
1226
    {
1227
        $qb = $this->getEntityManager()->getConnection()->createQueryBuilder()
1228
            ->update(MAUTIC_TABLE_PREFIX.'leads')
1229
            ->where('id = '.$id);
1230
1231
        $ph = 0;
1232
        // Keep operator in same order as was used in Lead::adjustPoints() in order to be congruent with what was calculated in PHP
1233
        // Again ignoring Aunt Sally here (PEMDAS)
1234
        foreach ($changes as $operator => $points) {
1235
            $qb->set('points', 'points '.$operator.' :points'.$ph)
1236
                ->setParameter('points'.$ph, $points, \PDO::PARAM_INT);
1237
1238
            ++$ph;
1239
        }
1240
1241
        try {
1242
            $qb->execute();
1243
        } catch (DriverException $exception) {
1244
            $message = $exception->getMessage();
1245
1246
            if (false !== strpos($message, 'Deadlock') && $tries <= 3) {
1247
                ++$tries;
1248
1249
                $this->updateContactPoints($changes, $id, $tries);
1250
            }
1251
        }
1252
1253
        // Query new points
1254
        return (int) $this->getEntityManager()->getConnection()->createQueryBuilder()
1255
            ->select('l.points')
1256
            ->from(MAUTIC_TABLE_PREFIX.'leads', 'l')
1257
            ->where('l.id = '.$id)
1258
            ->execute()
1259
            ->fetchColumn();
1260
    }
1261
1262
    /**
1263
     * @param Lead $entity
1264
     */
1265
    protected function postSaveEntity($entity)
1266
    {
1267
        // Check if points need to be appended
1268
        if ($entity->getPointChanges()) {
1269
            $newPoints = $this->updateContactPoints($entity->getPointChanges(), $entity->getId());
1270
1271
            // Set actual points so that code using getPoints knows the true value
1272
            $entity->setActualPoints($newPoints);
1273
1274
            $changes = $entity->getChanges();
1275
1276
            if (isset($changes['points'])) {
1277
                // Let's adjust the points to be more accurate in the change log
1278
                $changes['points'][1] = $newPoints;
1279
                $entity->setChanges($changes);
1280
            }
1281
        }
1282
    }
1283
1284
    /**
1285
     * @param $fields
1286
     */
1287
    protected function prepareDbalFieldsForSave(&$fields)
1288
    {
1289
        // Do not save points as they are handled by postSaveEntity
1290
        unset($fields['points']);
1291
1292
        $this->defaultPrepareDbalFieldsForSave($fields);
1293
    }
1294
}
1295