Issues (3627)

app/bundles/EmailBundle/Entity/EmailRepository.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\EmailBundle\Entity;
13
14
use Doctrine\DBAL\DBALException;
15
use Doctrine\DBAL\Query\QueryBuilder;
16
use Doctrine\ORM\Query;
17
use Doctrine\ORM\Tools\Pagination\Paginator;
18
use Mautic\ChannelBundle\Entity\MessageQueue;
19
use Mautic\CoreBundle\Entity\CommonRepository;
20
use Mautic\LeadBundle\Entity\DoNotContact;
21
22
/**
23
 * Class EmailRepository.
24
 */
25
class EmailRepository extends CommonRepository
26
{
27
    /**
28
     * Get an array of do not email emails.
29
     *
30
     * @param array $leadIds
31
     *
32
     * @return array
33
     */
34
    public function getDoNotEmailList($leadIds = [])
35
    {
36
        $q = $this->getEntityManager()->getConnection()->createQueryBuilder();
37
        $q->select('l.id, l.email')
38
            ->from(MAUTIC_TABLE_PREFIX.'lead_donotcontact', 'dnc')
39
            ->leftJoin('dnc', MAUTIC_TABLE_PREFIX.'leads', 'l', 'l.id = dnc.lead_id')
40
            ->where('dnc.channel = "email"')
41
            ->andWhere($q->expr()->neq('l.email', $q->expr()->literal('')));
42
43
        if ($leadIds) {
44
            $q->andWhere(
45
                $q->expr()->in('l.id', $leadIds)
46
            );
47
        }
48
49
        $results = $q->execute()->fetchAll();
50
51
        $dnc = [];
52
        foreach ($results as $r) {
53
            $dnc[$r['id']] = strtolower($r['email']);
54
        }
55
56
        return $dnc;
57
    }
58
59
    /**
60
     * Check to see if an email is set as do not contact.
61
     *
62
     * @param $email
63
     *
64
     * @return bool
65
     */
66
    public function checkDoNotEmail($email)
67
    {
68
        $q = $this->getEntityManager()->getConnection()->createQueryBuilder();
69
        $q->select('dnc.*')
70
            ->from(MAUTIC_TABLE_PREFIX.'lead_donotcontact', 'dnc')
71
            ->leftJoin('dnc', MAUTIC_TABLE_PREFIX.'leads', 'l', 'l.id = dnc.lead_id')
72
            ->where('dnc.channel = "email"')
73
            ->andWhere('l.email = :email')
74
            ->setParameter('email', $email);
75
76
        $results = $q->execute()->fetchAll();
77
        $dnc     = count($results) ? $results[0] : null;
78
79
        if (null === $dnc) {
80
            return false;
81
        }
82
83
        $dnc['reason'] = (int) $dnc['reason'];
84
85
        return [
86
            'id'           => $dnc['id'],
87
            'unsubscribed' => (DoNotContact::UNSUBSCRIBED === $dnc['reason']),
88
            'bounced'      => (DoNotContact::BOUNCED === $dnc['reason']),
89
            'manual'       => (DoNotContact::MANUAL === $dnc['reason']),
90
            'comments'     => $dnc['comments'],
91
        ];
92
    }
93
94
    /**
95
     * Delete DNC row.
96
     *
97
     * @param $id
98
     */
99
    public function deleteDoNotEmailEntry($id)
100
    {
101
        $this->getEntityManager()->getConnection()->delete(MAUTIC_TABLE_PREFIX.'lead_donotcontact', ['id' => (int) $id]);
102
    }
103
104
    /**
105
     * Get a list of entities.
106
     *
107
     * @return Paginator
108
     */
109
    public function getEntities(array $args = [])
110
    {
111
        $q = $this->getEntityManager()
112
            ->createQueryBuilder()
113
            ->select('e')
114
            ->from('MauticEmailBundle:Email', 'e', 'e.id');
115
        if (empty($args['iterator_mode'])) {
116
            $q->leftJoin('e.category', 'c');
117
118
            if (empty($args['ignoreListJoin']) && (!isset($args['email_type']) || 'list' == $args['email_type'])) {
119
                $q->leftJoin('e.lists', 'l');
120
            }
121
        }
122
123
        $args['qb'] = $q;
124
125
        return parent::getEntities($args);
126
    }
127
128
    /**
129
     * Get amounts of sent and read emails.
130
     *
131
     * @return array
132
     */
133
    public function getSentReadCount()
134
    {
135
        $q = $this->getEntityManager()->createQueryBuilder();
136
        $q->select('SUM(e.sentCount) as sent_count, SUM(e.readCount) as read_count')
137
            ->from('MauticEmailBundle:Email', 'e');
138
        $results = $q->getQuery()->getSingleResult(Query::HYDRATE_ARRAY);
139
140
        if (!isset($results['sent_count'])) {
141
            $results['sent_count'] = 0;
142
        }
143
        if (!isset($results['read_count'])) {
144
            $results['read_count'] = 0;
145
        }
146
147
        return $results;
148
    }
149
150
    /**
151
     * @param      $emailId
152
     * @param null $variantIds
153
     * @param null $listIds
154
     * @param bool $countOnly
155
     * @param null $limit
156
     * @param int  $minContactId
157
     * @param int  $maxContactId
158
     * @param bool $countWithMaxMin
159
     *
160
     * @return QueryBuilder|int|array
161
     */
162
    public function getEmailPendingQuery(
163
        $emailId,
164
        $variantIds = null,
165
        $listIds = null,
166
        $countOnly = false,
167
        $limit = null,
168
        $minContactId = null,
169
        $maxContactId = null,
170
        $countWithMaxMin = false
171
    ) {
172
        // Do not include leads in the do not contact table
173
        $dncQb = $this->getEntityManager()->getConnection()->createQueryBuilder();
174
        $dncQb->select('null')
175
            ->from(MAUTIC_TABLE_PREFIX.'lead_donotcontact', 'dnc')
176
            ->where(
177
                $dncQb->expr()->andX(
178
                    $dncQb->expr()->eq('dnc.lead_id', 'l.id'),
179
                    $dncQb->expr()->eq('dnc.channel', $dncQb->expr()->literal('email'))
180
                )
181
            );
182
183
        // Do not include contacts where the message is pending in the message queue
184
        $mqQb = $this->getEntityManager()->getConnection()->createQueryBuilder();
185
        $mqQb->select('null')
186
            ->from(MAUTIC_TABLE_PREFIX.'message_queue', 'mq')
187
            ->where(
188
                $mqQb->expr()->andX(
189
                    $mqQb->expr()->eq('mq.lead_id', 'l.id'),
190
                    $mqQb->expr()->neq('mq.status', $mqQb->expr()->literal(MessageQueue::STATUS_SENT)),
191
                    $mqQb->expr()->eq('mq.channel', $mqQb->expr()->literal('email'))
192
                )
193
            );
194
195
        // Do not include leads that have already been emailed
196
        $statQb = $this->getEntityManager()->getConnection()->createQueryBuilder();
197
        $statQb->select('null')
198
            ->from(MAUTIC_TABLE_PREFIX.'email_stats', 'stat')
199
            ->where(
200
                $statQb->expr()->eq('stat.lead_id', 'l.id')
201
            );
202
203
        if ($variantIds) {
204
            if (!in_array($emailId, $variantIds)) {
205
                $variantIds[] = (int) $emailId;
206
            }
207
            $statQb->andWhere($statQb->expr()->in('stat.email_id', $variantIds));
208
            $mqQb->andWhere($mqQb->expr()->in('mq.channel_id', $variantIds));
209
        } else {
210
            $statQb->andWhere($statQb->expr()->eq('stat.email_id', (int) $emailId));
211
            $mqQb->andWhere($mqQb->expr()->eq('mq.channel_id', (int) $emailId));
212
        }
213
214
        // Only include those who belong to the associated lead lists
215
        if (is_null($listIds)) {
216
            // Get a list of lists associated with this email
217
            $lists = $this->getEntityManager()->getConnection()->createQueryBuilder()
218
                ->select('el.leadlist_id')
219
                ->from(MAUTIC_TABLE_PREFIX.'email_list_xref', 'el')
220
                ->where('el.email_id = '.(int) $emailId)
221
                ->execute()
222
                ->fetchAll();
223
224
            $listIds = [];
225
            foreach ($lists as $list) {
226
                $listIds[] = $list['leadlist_id'];
227
            }
228
229
            if (empty($listIds)) {
230
                // Prevent fatal error
231
                return ($countOnly) ? 0 : [];
232
            }
233
        } elseif (!is_array($listIds)) {
234
            $listIds = [$listIds];
235
        }
236
237
        // Only include those in associated segments
238
        $segmentQb = $this->getEntityManager()->getConnection()->createQueryBuilder();
239
        $segmentQb->select('null')
240
            ->from(MAUTIC_TABLE_PREFIX.'lead_lists_leads', 'll')
241
            ->where(
242
                $segmentQb->expr()->andX(
243
                    $segmentQb->expr()->eq('ll.lead_id', 'l.id'),
244
                    $segmentQb->expr()->in('ll.leadlist_id', $listIds),
245
                    $segmentQb->expr()->eq('ll.manually_removed', ':false')
246
                )
247
            );
248
249
        // Main query
250
        $q = $this->getEntityManager()->getConnection()->createQueryBuilder();
251
        if ($countOnly) {
252
            $q->select('count(*) as count');
253
            if ($countWithMaxMin) {
254
                $q->addSelect('MIN(l.id) as min_id, MAX(l.id) as max_id');
255
            }
256
        } else {
257
            $q->select('l.*');
258
        }
259
260
        $q->from(MAUTIC_TABLE_PREFIX.'leads', 'l')
261
            ->andWhere(sprintf('EXISTS (%s)', $segmentQb->getSQL()))
262
            ->andWhere(sprintf('NOT EXISTS (%s)', $dncQb->getSQL()))
263
            ->andWhere(sprintf('NOT EXISTS (%s)', $statQb->getSQL()))
264
            ->andWhere(sprintf('NOT EXISTS (%s)', $mqQb->getSQL()))
265
            ->setParameter('false', false, 'boolean');
266
267
        $q = $this->setMinMaxIds($q, 'l.id', $minContactId, $maxContactId);
268
269
        // Has an email
270
        $q->andWhere(
271
            $q->expr()->andX(
272
                $q->expr()->isNotNull('l.email'),
273
                $q->expr()->neq('l.email', $q->expr()->literal(''))
274
            )
275
        );
276
277
        if (!empty($limit)) {
278
            $q->setFirstResult(0)
279
                ->setMaxResults($limit);
280
        }
281
282
        return $q;
283
    }
284
285
    /**
286
     * @param      $emailId
287
     * @param null $variantIds
288
     * @param null $listIds
289
     * @param bool $countOnly
290
     * @param null $limit
291
     * @param int  $minContactId
292
     * @param int  $maxContactId
293
     * @param bool $countWithMaxMin
294
     *
295
     * @return array|int
296
     */
297
    public function getEmailPendingLeads(
298
        $emailId,
299
        $variantIds = null,
300
        $listIds = null,
301
        $countOnly = false,
302
        $limit = null,
303
        $minContactId = null,
304
        $maxContactId = null,
305
        $countWithMaxMin = false
306
    ) {
307
        $q = $this->getEmailPendingQuery(
308
            $emailId,
309
            $variantIds,
310
            $listIds,
311
            $countOnly,
312
            $limit,
313
            $minContactId,
314
            $maxContactId,
315
            $countWithMaxMin
316
        );
317
318
        if (!($q instanceof QueryBuilder)) {
0 ignored issues
show
$q is always a sub-type of Doctrine\DBAL\Query\QueryBuilder.
Loading history...
319
            return $q;
320
        }
321
322
        $results = $q->execute()->fetchAll();
323
324
        if ($countOnly && $countWithMaxMin) {
325
            // returns array in format ['count' => #, ['min_id' => #, 'max_id' => #]]
326
            return $results[0];
327
        } elseif ($countOnly) {
328
            return (isset($results[0])) ? $results[0]['count'] : 0;
329
        } else {
330
            $leads = [];
331
            foreach ($results as $r) {
332
                $leads[$r['id']] = $r;
333
            }
334
335
            return $leads;
336
        }
337
    }
338
339
    /**
340
     * @param string $search
341
     * @param int    $limit
342
     * @param int    $start
343
     * @param bool   $viewOther
344
     * @param bool   $topLevel
345
     * @param null   $emailType
346
     * @param null   $variantParentId
347
     *
348
     * @return array
349
     */
350
    public function getEmailList($search = '', $limit = 10, $start = 0, $viewOther = false, $topLevel = false, $emailType = null, array $ignoreIds = [], $variantParentId = null)
351
    {
352
        $q = $this->createQueryBuilder('e');
353
        $q->select('partial e.{id, subject, name, language}');
354
355
        if (!empty($search)) {
356
            if (is_array($search)) {
357
                $search = array_map('intval', $search);
358
                $q->andWhere($q->expr()->in('e.id', ':search'))
359
                    ->setParameter('search', $search);
360
            } else {
361
                $q->andWhere($q->expr()->like('e.name', ':search'))
362
                    ->setParameter('search', "%{$search}%");
363
            }
364
        }
365
366
        if (!$viewOther) {
367
            $q->andWhere($q->expr()->eq('e.createdBy', ':id'))
368
                ->setParameter('id', $this->currentUser->getId());
369
        }
370
371
        if ($topLevel) {
372
            if (true === $topLevel || 'variant' == $topLevel) {
373
                $q->andWhere($q->expr()->isNull('e.variantParent'));
374
            } elseif ('translation' == $topLevel) {
375
                $q->andWhere($q->expr()->isNull('e.translationParent'));
376
            }
377
        }
378
379
        if ($variantParentId) {
380
            $q->andWhere(
381
                $q->expr()->andX(
382
                    $q->expr()->eq('IDENTITY(e.variantParent)', (int) $variantParentId),
383
                    $q->expr()->eq('e.id', (int) $variantParentId)
384
                )
385
            );
386
        }
387
388
        if (!empty($ignoreIds)) {
389
            $q->andWhere($q->expr()->notIn('e.id', ':emailIds'))
390
                ->setParameter('emailIds', $ignoreIds);
391
        }
392
393
        if (!empty($emailType)) {
394
            $q->andWhere(
395
                $q->expr()->eq('e.emailType', $q->expr()->literal($emailType))
396
            );
397
        }
398
399
        $q->orderBy('e.name');
400
401
        if (!empty($limit)) {
402
            $q->setFirstResult($start)
403
                ->setMaxResults($limit);
404
        }
405
406
        return $q->getQuery()->getArrayResult();
407
    }
408
409
    /**
410
     * @param \Doctrine\ORM\QueryBuilder|QueryBuilder $q
411
     * @param                                         $filter
412
     *
413
     * @return array
414
     */
415
    protected function addCatchAllWhereClause($q, $filter)
416
    {
417
        return $this->addStandardCatchAllWhereClause($q, $filter, [
418
            'e.name',
419
            'e.subject',
420
        ]);
421
    }
422
423
    /**
424
     * @param \Doctrine\ORM\QueryBuilder|QueryBuilder $q
425
     * @param                                         $filter
426
     *
427
     * @return array
428
     */
429
    protected function addSearchCommandWhereClause($q, $filter)
430
    {
431
        list($expr, $parameters) = $this->addStandardSearchCommandWhereClause($q, $filter);
432
        if ($expr) {
433
            return [$expr, $parameters];
434
        }
435
436
        $command         = $filter->command;
437
        $unique          = $this->generateRandomParameterName();
438
        $returnParameter = false; //returning a parameter that is not used will lead to a Doctrine error
439
440
        switch ($command) {
441
            case $this->translator->trans('mautic.core.searchcommand.lang'):
442
                $langUnique      = $this->generateRandomParameterName();
443
                $langValue       = $filter->string.'_%';
444
                $forceParameters = [
445
                    $langUnique => $langValue,
446
                    $unique     => $filter->string,
447
                ];
448
                $expr = $q->expr()->orX(
449
                    $q->expr()->eq('e.language', ":$unique"),
450
                    $q->expr()->like('e.language', ":$langUnique")
451
                );
452
                $returnParameter = true;
453
                break;
454
        }
455
456
        if ($expr && $filter->not) {
457
            $expr = $q->expr()->not($expr);
458
        }
459
460
        if (!empty($forceParameters)) {
461
            $parameters = $forceParameters;
462
        } elseif ($returnParameter) {
463
            $string     = ($filter->strict) ? $filter->string : "%{$filter->string}%";
464
            $parameters = ["$unique" => $string];
465
        }
466
467
        return [$expr, $parameters];
468
    }
469
470
    /**
471
     * @return array
472
     */
473
    public function getSearchCommands()
474
    {
475
        $commands = [
476
            'mautic.core.searchcommand.ispublished',
477
            'mautic.core.searchcommand.isunpublished',
478
            'mautic.core.searchcommand.isuncategorized',
479
            'mautic.core.searchcommand.ismine',
480
            'mautic.core.searchcommand.category',
481
            'mautic.core.searchcommand.lang',
482
        ];
483
484
        return array_merge($commands, parent::getSearchCommands());
485
    }
486
487
    /**
488
     * @return string
489
     */
490
    protected function getDefaultOrder()
491
    {
492
        return [
493
            ['e.name', 'ASC'],
494
        ];
495
    }
496
497
    /**
498
     * @return string
499
     */
500
    public function getTableAlias()
501
    {
502
        return 'e';
503
    }
504
505
    /**
506
     * Resets variant_start_date, variant_read_count, variant_sent_count.
507
     *
508
     * @param $relatedIds
509
     * @param $date
510
     */
511
    public function resetVariants($relatedIds, $date)
512
    {
513
        if (!is_array($relatedIds)) {
514
            $relatedIds = [(int) $relatedIds];
515
        }
516
517
        $qb = $this->getEntityManager()->getConnection()->createQueryBuilder();
518
519
        $qb->update(MAUTIC_TABLE_PREFIX.'emails')
520
            ->set('variant_read_count', 0)
521
            ->set('variant_sent_count', 0)
522
            ->set('variant_start_date', ':date')
523
            ->setParameter('date', $date)
524
            ->where(
525
                $qb->expr()->in('id', $relatedIds)
526
            )
527
            ->execute();
528
    }
529
530
    /**
531
     * Up the read/sent counts.
532
     *
533
     * @param int        $id
534
     * @param string     $type
535
     * @param int        $increaseBy
536
     * @param bool|false $variant
537
     */
538
    public function upCount($id, $type = 'sent', $increaseBy = 1, $variant = false)
539
    {
540
        if (!$increaseBy) {
541
            return;
542
        }
543
544
        $q = $this->getEntityManager()->getConnection()->createQueryBuilder();
545
546
        $q->update(MAUTIC_TABLE_PREFIX.'emails');
547
        $q->set($type.'_count', $type.'_count + '.(int) $increaseBy);
548
        $q->where('id = '.(int) $id);
549
550
        if ($variant) {
551
            $q->set('variant_'.$type.'_count', 'variant_'.$type.'_count + '.(int) $increaseBy);
552
        }
553
554
        // Try to execute 3 times before throwing the exception
555
        // to increase the chance the update will do its stuff.
556
        $retrialLimit = 3;
557
        while ($retrialLimit >= 0) {
558
            try {
559
                $q->execute();
560
561
                return;
562
            } catch (DBALException $e) {
563
                --$retrialLimit;
564
                if (0 === $retrialLimit) {
565
                    throw $e;
566
                }
567
            }
568
        }
569
    }
570
571
    /**
572
     * @param null $id
573
     *
574
     * @return \Doctrine\ORM\Internal\Hydration\IterableResult
575
     */
576
    public function getPublishedBroadcasts($id = null)
577
    {
578
        $qb   = $this->createQueryBuilder($this->getTableAlias());
579
        $expr = $this->getPublishedByDateExpression($qb, null, true, true, false);
580
581
        $expr->add(
582
            $qb->expr()->eq($this->getTableAlias().'.emailType', $qb->expr()->literal('list'))
583
        );
584
585
        if (!empty($id)) {
586
            $expr->add(
587
                $qb->expr()->eq($this->getTableAlias().'.id', (int) $id)
588
            );
589
        }
590
        $qb->where($expr);
591
592
        return $qb->getQuery()->iterate();
593
    }
594
595
    /**
596
     * Set Max and/or Min ID where conditions to the query builder.
597
     *
598
     * @param string $column
599
     * @param int    $minContactId
600
     * @param int    $maxContactId
601
     *
602
     * @return QueryBuilder
603
     */
604
    private function setMinMaxIds(QueryBuilder $q, $column, $minContactId, $maxContactId)
605
    {
606
        if ($minContactId && is_numeric($minContactId)) {
607
            $q->andWhere($column.' >= :minContactId');
608
            $q->setParameter('minContactId', $minContactId);
609
        }
610
611
        if ($maxContactId && is_numeric($maxContactId)) {
612
            $q->andWhere($column.' <= :maxContactId');
613
            $q->setParameter('maxContactId', $maxContactId);
614
        }
615
616
        return $q;
617
    }
618
619
    /**
620
     * Is one of emails unpublished?
621
     *
622
     * @return bool
623
     */
624
    public function isOneUnpublished(array $ids)
625
    {
626
        $result = $this->getEntityManager()
627
            ->createQueryBuilder()
628
            ->select($this->getTableAlias().'.id')
629
            ->from('MauticEmailBundle:Email', $this->getTableAlias(), $this->getTableAlias().'.id')
630
            ->where($this->getTableAlias().'.id IN (:ids)')
631
            ->setParameter('ids', $ids)
632
            ->andWhere('e.isPublished = 0')
633
            ->setMaxResults(1)
634
            ->getQuery()
635
            ->getOneOrNullResult();
636
637
        return (bool) $result;
638
    }
639
}
640