Issues (3627)

app/bundles/SmsBundle/Model/SmsModel.php (1 issue)

1
<?php
2
3
/*
4
 * @copyright   2016 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\SmsBundle\Model;
13
14
use Doctrine\DBAL\Query\QueryBuilder;
15
use Mautic\ChannelBundle\Entity\MessageQueue;
16
use Mautic\ChannelBundle\Model\MessageQueueModel;
17
use Mautic\CoreBundle\Event\TokenReplacementEvent;
18
use Mautic\CoreBundle\Helper\CacheStorageHelper;
19
use Mautic\CoreBundle\Helper\Chart\ChartQuery;
20
use Mautic\CoreBundle\Helper\Chart\LineChart;
21
use Mautic\CoreBundle\Model\AjaxLookupModelInterface;
22
use Mautic\CoreBundle\Model\FormModel;
23
use Mautic\LeadBundle\Entity\DoNotContactRepository;
24
use Mautic\LeadBundle\Entity\Lead;
25
use Mautic\LeadBundle\Model\LeadModel;
26
use Mautic\PageBundle\Model\TrackableModel;
27
use Mautic\SmsBundle\Entity\Sms;
28
use Mautic\SmsBundle\Entity\Stat;
29
use Mautic\SmsBundle\Event\SmsEvent;
30
use Mautic\SmsBundle\Event\SmsSendEvent;
31
use Mautic\SmsBundle\Form\Type\SmsType;
32
use Mautic\SmsBundle\Sms\TransportChain;
33
use Mautic\SmsBundle\SmsEvents;
34
use Symfony\Component\EventDispatcher\Event;
35
use Symfony\Component\HttpKernel\Exception\MethodNotAllowedHttpException;
36
37
class SmsModel extends FormModel implements AjaxLookupModelInterface
38
{
39
    /**
40
     * @var TrackableModel
41
     */
42
    protected $pageTrackableModel;
43
44
    /**
45
     * @var LeadModel
46
     */
47
    protected $leadModel;
48
49
    /**
50
     * @var MessageQueueModel
51
     */
52
    protected $messageQueueModel;
53
54
    /**
55
     * @var TransportChain
56
     */
57
    protected $transport;
58
59
    /**
60
     * @var CacheStorageHelper
61
     */
62
    private $cacheStorageHelper;
63
64
    public function __construct(TrackableModel $pageTrackableModel, LeadModel $leadModel, MessageQueueModel $messageQueueModel, TransportChain $transport, CacheStorageHelper $cacheStorageHelper)
65
    {
66
        $this->pageTrackableModel = $pageTrackableModel;
67
        $this->leadModel          = $leadModel;
68
        $this->messageQueueModel  = $messageQueueModel;
69
        $this->transport          = $transport;
70
        $this->cacheStorageHelper = $cacheStorageHelper;
71
    }
72
73
    /**
74
     * {@inheritdoc}
75
     *
76
     * @return \Mautic\SmsBundle\Entity\SmsRepository
77
     */
78
    public function getRepository()
79
    {
80
        return $this->em->getRepository('MauticSmsBundle:Sms');
81
    }
82
83
    /**
84
     * @return \Mautic\SmsBundle\Entity\StatRepository
85
     */
86
    public function getStatRepository()
87
    {
88
        return $this->em->getRepository('MauticSmsBundle:Stat');
89
    }
90
91
    /**
92
     * {@inheritdoc}
93
     */
94
    public function getPermissionBase()
95
    {
96
        return 'sms:smses';
97
    }
98
99
    /**
100
     * Save an array of entities.
101
     *
102
     * @param  $entities
103
     * @param  $unlock
104
     *
105
     * @return array
106
     */
107
    public function saveEntities($entities, $unlock = true)
108
    {
109
        //iterate over the results so the events are dispatched on each delete
110
        $batchSize = 20;
111
        $i         = 0;
112
        foreach ($entities as $entity) {
113
            $isNew = ($entity->getId()) ? false : true;
114
115
            //set some defaults
116
            $this->setTimestamps($entity, $isNew, $unlock);
117
118
            if ($dispatchEvent = $entity instanceof Sms) {
119
                $event = $this->dispatchEvent('pre_save', $entity, $isNew);
120
            }
121
122
            $this->getRepository()->saveEntity($entity, false);
123
124
            if ($dispatchEvent) {
125
                $this->dispatchEvent('post_save', $entity, $isNew, $event);
126
            }
127
128
            if (0 === ++$i % $batchSize) {
129
                $this->em->flush();
130
            }
131
        }
132
        $this->em->flush();
133
    }
134
135
    /**
136
     * {@inheritdoc}
137
     *
138
     * @param       $entity
139
     * @param       $formFactory
140
     * @param null  $action
141
     * @param array $options
142
     *
143
     * @return mixed
144
     *
145
     * @throws \Symfony\Component\HttpKernel\Exception\NotFoundHttpException
146
     * @throws MethodNotAllowedHttpException
147
     */
148
    public function createForm($entity, $formFactory, $action = null, $options = [])
149
    {
150
        if (!$entity instanceof Sms) {
151
            throw new MethodNotAllowedHttpException(['Sms']);
152
        }
153
        if (!empty($action)) {
154
            $options['action'] = $action;
155
        }
156
157
        return $formFactory->create(SmsType::class, $entity, $options);
158
    }
159
160
    /**
161
     * Get a specific entity or generate a new one if id is empty.
162
     *
163
     * @param $id
164
     *
165
     * @return Sms|null
166
     */
167
    public function getEntity($id = null)
168
    {
169
        if (null === $id) {
170
            $entity = new Sms();
171
        } else {
172
            $entity = parent::getEntity($id);
173
        }
174
175
        return $entity;
176
    }
177
178
    /**
179
     * Return a list of entities.
180
     *
181
     * @param array $args [start, limit, filter, orderBy, orderByDir]
182
     *
183
     * @return \Doctrine\ORM\Tools\Pagination\Paginator|array
184
     */
185
    public function getEntities(array $args = [])
186
    {
187
        $entities = parent::getEntities($args);
188
189
        foreach ($entities as $entity) {
190
            $pending = $this->cacheStorageHelper->get(sprintf('%s|%s|%s', 'sms', $entity->getId(), 'pending'));
191
192
            if (false !== $pending) {
193
                $entity->setPendingCount($pending);
194
            }
195
        }
196
197
        return $entities;
198
    }
199
200
    /**
201
     * @param       $sendTo
202
     * @param array $options
203
     *
204
     * @return array
205
     */
206
    public function sendSms(Sms $sms, $sendTo, $options = [])
207
    {
208
        $channel = (isset($options['channel'])) ? $options['channel'] : null;
209
        $listId  = (isset($options['listId'])) ? $options['listId'] : null;
210
211
        if ($sendTo instanceof Lead) {
212
            $sendTo = [$sendTo];
213
        } elseif (!is_array($sendTo)) {
214
            $sendTo = [$sendTo];
215
        }
216
217
        $sentCount       = 0;
218
        $failedCount     = 0;
219
        $results         = [];
220
        $contacts        = [];
221
        $fetchContacts   = [];
222
        foreach ($sendTo as $lead) {
223
            if (!$lead instanceof Lead) {
224
                $fetchContacts[] = $lead;
225
            } else {
226
                $contacts[$lead->getId()] = $lead;
227
            }
228
        }
229
230
        if ($fetchContacts) {
231
            $foundContacts = $this->leadModel->getEntities(
232
                [
233
                    'ids' => $fetchContacts,
234
                ]
235
            );
236
237
            foreach ($foundContacts as $contact) {
238
                $contacts[$contact->getId()] = $contact;
239
            }
240
        }
241
        $contactIds = array_keys($contacts);
242
243
        /** @var DoNotContactRepository $dncRepo */
244
        $dncRepo = $this->em->getRepository('MauticLeadBundle:DoNotContact');
245
        $dnc     = $dncRepo->getChannelList('sms', $contactIds);
246
247
        if (!empty($dnc)) {
248
            foreach ($dnc as $removeMeId => $removeMeReason) {
249
                $results[$removeMeId] = [
250
                    'sent'   => false,
251
                    'status' => 'mautic.sms.campaign.failed.not_contactable',
252
                ];
253
254
                unset($contacts[$removeMeId], $contactIds[$removeMeId]);
255
            }
256
        }
257
258
        if (!empty($contacts)) {
259
            $messageQueue    = (isset($options['resend_message_queue'])) ? $options['resend_message_queue'] : null;
260
            $campaignEventId = (is_array($channel) && 'campaign.event' === $channel[0] && !empty($channel[1])) ? $channel[1] : null;
261
262
            $queued = $this->messageQueueModel->processFrequencyRules(
263
                $contacts,
264
                'sms',
265
                $sms->getId(),
266
                $campaignEventId,
267
                3,
268
                MessageQueue::PRIORITY_NORMAL,
269
                $messageQueue,
270
                'sms_message_stats'
271
            );
272
273
            if ($queued) {
274
                foreach ($queued as $queue) {
275
                    $results[$queue] = [
276
                        'sent'   => false,
277
                        'status' => 'mautic.sms.timeline.status.scheduled',
278
                    ];
279
280
                    unset($contacts[$queue]);
281
                }
282
            }
283
284
            $stats = [];
285
            // @todo we should allow batch sending based on transport, MessageBird does support 20 SMS at once
286
            // the transport chain is already prepared for it
287
            if (count($contacts)) {
288
                /** @var Lead $lead */
289
                foreach ($contacts as $lead) {
290
                    $leadId          = $lead->getId();
291
                    $stat            = $this->createStatEntry($sms, $lead, $channel, false, $listId);
292
293
                    $leadPhoneNumber = $lead->getLeadPhoneNumber();
294
295
                    if (empty($leadPhoneNumber)) {
296
                        $results[$leadId] = [
297
                            'sent'   => false,
298
                            'status' => 'mautic.sms.campaign.failed.missing_number',
299
                        ];
300
301
                        continue;
302
                    }
303
304
                    $smsEvent = new SmsSendEvent($sms->getMessage(), $lead);
305
                    $smsEvent->setSmsId($sms->getId());
306
                    $this->dispatcher->dispatch(SmsEvents::SMS_ON_SEND, $smsEvent);
307
308
                    $tokenEvent = $this->dispatcher->dispatch(
309
                        SmsEvents::TOKEN_REPLACEMENT,
310
                        new TokenReplacementEvent(
311
                            $smsEvent->getContent(),
312
                            $lead,
313
                            [
314
                                'channel' => [
315
                                    'sms',          // Keep BC pre 2.14.1
316
                                    $sms->getId(),  // Keep BC pre 2.14.1
317
                                    'sms' => $sms->getId(),
318
                                ],
319
                                'stat'    => $stat->getTrackingHash(),
320
                            ]
321
                        )
322
                    );
323
324
                    $sendResult = [
325
                        'sent'    => false,
326
                        'type'    => 'mautic.sms.sms',
327
                        'status'  => 'mautic.sms.timeline.status.delivered',
328
                        'id'      => $sms->getId(),
329
                        'name'    => $sms->getName(),
330
                        'content' => $tokenEvent->getContent(),
331
                    ];
332
333
                    $metadata = $this->transport->sendSms($lead, $tokenEvent->getContent(), $stat);
334
                    if (true !== $metadata) {
335
                        $sendResult['status'] = $metadata;
336
                        $stat->setIsFailed(true);
337
                        if (is_string($metadata)) {
338
                            $stat->addDetail('failed', $metadata);
339
                        }
340
                        ++$failedCount;
341
                    } else {
342
                        $sendResult['sent'] = true;
343
                        ++$sentCount;
344
                    }
345
346
                    $stats[]            = $stat;
347
                    unset($stat);
348
                    $results[$leadId] = $sendResult;
349
350
                    unset($smsEvent, $tokenEvent, $sendResult, $metadata);
351
                }
352
            }
353
        }
354
355
        if ($sentCount || $failedCount) {
356
            $this->getRepository()->upCount($sms->getId(), 'sent', $sentCount);
357
            $this->getStatRepository()->saveEntities($stats);
358
359
            foreach ($stats as $stat) {
360
                if (!$stat->isFailed()) {
361
                    $results[$stat->getLead()->getId()]['statId'] = $stat->getId();
362
                }
363
            }
364
365
            $this->em->clear(Stat::class);
366
        }
367
368
        return $results;
369
    }
370
371
    /**
372
     * @param null $source
373
     * @param bool $persist
374
     * @param null $listId
375
     *
376
     * @return Stat
377
     *
378
     * @throws \Exception
379
     */
380
    public function createStatEntry(Sms $sms, Lead $lead, $source = null, $persist = true, $listId = null)
381
    {
382
        $stat = new Stat();
383
        $stat->setDateSent(new \DateTime());
384
        $stat->setLead($lead);
385
        $stat->setSms($sms);
386
        if (null !== $listId) {
0 ignored issues
show
The condition null !== $listId is always false.
Loading history...
387
            $stat->setList($this->leadModel->getLeadListRepository()->getEntity($listId));
388
        }
389
        if (is_array($source)) {
390
            $stat->setSourceId($source[1]);
391
            $source = $source[0];
392
        }
393
        $stat->setSource($source);
394
        $stat->setTrackingHash(str_replace('.', '', uniqid('', true)));
395
396
        if ($persist) {
397
            $this->getStatRepository()->saveEntity($stat);
398
        }
399
400
        return $stat;
401
    }
402
403
    /**
404
     * {@inheritdoc}
405
     *
406
     * @param $action
407
     * @param $event
408
     * @param $entity
409
     * @param $isNew
410
     *
411
     * @throws \Symfony\Component\HttpKernel\Exception\MethodNotAllowedHttpException
412
     */
413
    protected function dispatchEvent($action, &$entity, $isNew = false, Event $event = null)
414
    {
415
        if (!$entity instanceof Sms) {
416
            throw new MethodNotAllowedHttpException(['Sms']);
417
        }
418
419
        switch ($action) {
420
            case 'pre_save':
421
                $name = SmsEvents::SMS_PRE_SAVE;
422
                break;
423
            case 'post_save':
424
                $name = SmsEvents::SMS_POST_SAVE;
425
                break;
426
            case 'pre_delete':
427
                $name = SmsEvents::SMS_PRE_DELETE;
428
                break;
429
            case 'post_delete':
430
                $name = SmsEvents::SMS_POST_DELETE;
431
                break;
432
            default:
433
                return;
434
        }
435
436
        if ($this->dispatcher->hasListeners($name)) {
437
            if (empty($event)) {
438
                $event = new SmsEvent($entity, $isNew);
439
                $event->setEntityManager($this->em);
440
            }
441
442
            $this->dispatcher->dispatch($name, $event);
443
444
            return $event;
445
        } else {
446
            return;
447
        }
448
    }
449
450
    /**
451
     * Joins the page table and limits created_by to currently logged in user.
452
     */
453
    public function limitQueryToCreator(QueryBuilder &$q)
454
    {
455
        $q->join('t', MAUTIC_TABLE_PREFIX.'sms_messages', 's', 's.id = t.sms_id')
456
            ->andWhere('s.created_by = :userId')
457
            ->setParameter('userId', $this->userHelper->getUser()->getId());
458
    }
459
460
    /**
461
     * Get line chart data of hits.
462
     *
463
     * @param char   $unit          {@link php.net/manual/en/function.date.php#refsect1-function.date-parameters}
464
     * @param string $dateFormat
465
     * @param array  $filter
466
     * @param bool   $canViewOthers
467
     *
468
     * @return array
469
     */
470
    public function getHitsLineChartData($unit, \DateTime $dateFrom, \DateTime $dateTo, $dateFormat = null, $filter = [], $canViewOthers = true)
471
    {
472
        $flag = null;
473
474
        if (isset($filter['flag'])) {
475
            $flag = $filter['flag'];
476
            unset($filter['flag']);
477
        }
478
479
        $chart = new LineChart($unit, $dateFrom, $dateTo, $dateFormat);
480
        $query = new ChartQuery($this->em->getConnection(), $dateFrom, $dateTo);
481
482
        if (!$flag || 'total_and_unique' === $flag) {
483
            $filter['is_failed'] = 0;
484
            $q                   = $query->prepareTimeDataQuery('sms_message_stats', 'date_sent', $filter);
485
486
            if (!$canViewOthers) {
487
                $this->limitQueryToCreator($q);
488
            }
489
490
            $data = $query->loadAndBuildTimeData($q);
491
            $chart->setDataset($this->translator->trans('mautic.sms.show.total.sent'), $data);
492
        }
493
494
        if (!$flag || 'failed' === $flag) {
495
            $filter['is_failed'] = 1;
496
            $q                   = $query->prepareTimeDataQuery('sms_message_stats', 'date_sent', $filter);
497
            if (!$canViewOthers) {
498
                $this->limitQueryToCreator($q);
499
            }
500
501
            $data = $query->loadAndBuildTimeData($q);
502
            $chart->setDataset($this->translator->trans('mautic.sms.show.failed'), $data);
503
        }
504
505
        return $chart->render();
506
    }
507
508
    /**
509
     * @param $idHash
510
     *
511
     * @return Stat
512
     */
513
    public function getSmsStatus($idHash)
514
    {
515
        return $this->getStatRepository()->getSmsStatus($idHash);
516
    }
517
518
    /**
519
     * Search for an sms stat by sms and lead IDs.
520
     *
521
     * @param $smsId
522
     * @param $leadId
523
     *
524
     * @return array
525
     */
526
    public function getSmsStatByLeadId($smsId, $leadId)
527
    {
528
        return $this->getStatRepository()->findBy(
529
            [
530
                'sms'  => (int) $smsId,
531
                'lead' => (int) $leadId,
532
            ],
533
            ['dateSent' => 'DESC']
534
        );
535
    }
536
537
    /**
538
     * Get an array of tracked links.
539
     *
540
     * @param $smsId
541
     *
542
     * @return array
543
     */
544
    public function getSmsClickStats($smsId)
545
    {
546
        return $this->pageTrackableModel->getTrackableList('sms', $smsId);
547
    }
548
549
    /**
550
     * @param        $type
551
     * @param string $filter
552
     * @param int    $limit
553
     * @param int    $start
554
     * @param array  $options
555
     *
556
     * @return array
557
     */
558
    public function getLookupResults($type, $filter = '', $limit = 10, $start = 0, $options = [])
559
    {
560
        $results = [];
561
        switch ($type) {
562
            case 'sms':
563
            case SmsType::class:
564
                $entities = $this->getRepository()->getSmsList(
565
                    $filter,
566
                    $limit,
567
                    $start,
568
                    $this->security->isGranted($this->getPermissionBase().':viewother'),
569
                    isset($options['sms_type']) ? $options['sms_type'] : null
570
                );
571
572
                foreach ($entities as $entity) {
573
                    $results[$entity['language']][$entity['id']] = $entity['name'];
574
                }
575
576
                //sort by language
577
                ksort($results);
578
579
                break;
580
        }
581
582
        return $results;
583
    }
584
}
585