![]() Server : Apache System : Linux server2.corals.io 4.18.0-348.2.1.el8_5.x86_64 #1 SMP Mon Nov 15 09:17:08 EST 2021 x86_64 User : corals ( 1002) PHP Version : 7.4.33 Disable Function : exec,passthru,shell_exec,system Directory : /home/corals/mautic.corals.io/app/bundles/SmsBundle/Model/ |
<?php namespace Mautic\SmsBundle\Model; use Doctrine\DBAL\Query\QueryBuilder; use Doctrine\ORM\EntityManagerInterface; use Mautic\ChannelBundle\Entity\MessageQueue; use Mautic\ChannelBundle\Model\MessageQueueModel; use Mautic\CoreBundle\Event\TokenReplacementEvent; use Mautic\CoreBundle\Helper\CacheStorageHelper; use Mautic\CoreBundle\Helper\Chart\ChartQuery; use Mautic\CoreBundle\Helper\Chart\LineChart; use Mautic\CoreBundle\Helper\CoreParametersHelper; use Mautic\CoreBundle\Helper\UserHelper; use Mautic\CoreBundle\Model\AjaxLookupModelInterface; use Mautic\CoreBundle\Model\FormModel; use Mautic\CoreBundle\Security\Permissions\CorePermissions; use Mautic\CoreBundle\Translation\Translator; use Mautic\LeadBundle\Entity\DoNotContactRepository; use Mautic\LeadBundle\Entity\Lead; use Mautic\LeadBundle\Model\LeadModel; use Mautic\PageBundle\Model\TrackableModel; use Mautic\SmsBundle\Entity\Sms; use Mautic\SmsBundle\Entity\Stat; use Mautic\SmsBundle\Event\SmsEvent; use Mautic\SmsBundle\Event\SmsSendEvent; use Mautic\SmsBundle\Form\Type\SmsType; use Mautic\SmsBundle\Sms\TransportChain; use Mautic\SmsBundle\SmsEvents; use Psr\Log\LoggerInterface; use Symfony\Component\EventDispatcher\EventDispatcherInterface; use Symfony\Component\Form\FormFactoryInterface; use Symfony\Component\Form\FormInterface; use Symfony\Component\HttpKernel\Exception\MethodNotAllowedHttpException; use Symfony\Component\Routing\Generator\UrlGeneratorInterface; use Symfony\Contracts\EventDispatcher\Event; /** * @extends FormModel<Sms> * * @implements AjaxLookupModelInterface<Sms> */ class SmsModel extends FormModel implements AjaxLookupModelInterface { public function __construct( protected TrackableModel $pageTrackableModel, protected LeadModel $leadModel, protected MessageQueueModel $messageQueueModel, protected TransportChain $transport, private CacheStorageHelper $cacheStorageHelper, EntityManagerInterface $em, CorePermissions $security, EventDispatcherInterface $dispatcher, UrlGeneratorInterface $router, Translator $translator, UserHelper $userHelper, LoggerInterface $mauticLogger, CoreParametersHelper $coreParametersHelper ) { parent::__construct($em, $security, $dispatcher, $router, $translator, $userHelper, $mauticLogger, $coreParametersHelper); } /** * @return \Mautic\SmsBundle\Entity\SmsRepository */ public function getRepository() { return $this->em->getRepository(Sms::class); } /** * @return \Mautic\SmsBundle\Entity\StatRepository */ public function getStatRepository() { return $this->em->getRepository(Stat::class); } public function getPermissionBase(): string { return 'sms:smses'; } /** * Save an array of entities. * * @param iterable<Sms> $entities */ public function saveEntities($entities, $unlock = true): void { // iterate over the results so the events are dispatched on each delete $batchSize = 20; $i = 0; foreach ($entities as $entity) { $isNew = ($entity->getId()) ? false : true; // set some defaults $this->setTimestamps($entity, $isNew, $unlock); if ($dispatchEvent = $entity instanceof Sms) { $event = $this->dispatchEvent('pre_save', $entity, $isNew); } $this->getRepository()->saveEntity($entity, false); if ($dispatchEvent) { $this->dispatchEvent('post_save', $entity, $isNew, $event); } if (0 === ++$i % $batchSize) { $this->em->flush(); } } $this->em->flush(); } /** * @param array $options * * @throws \Symfony\Component\HttpKernel\Exception\NotFoundHttpException * @throws MethodNotAllowedHttpException */ public function createForm($entity, FormFactoryInterface $formFactory, $action = null, $options = []): FormInterface { if (!$entity instanceof Sms) { throw new MethodNotAllowedHttpException(['Sms']); } if (!empty($action)) { $options['action'] = $action; } return $formFactory->create(SmsType::class, $entity, $options); } /** * Get a specific entity or generate a new one if id is empty. */ public function getEntity($id = null): ?Sms { if (null === $id) { $entity = new Sms(); } else { $entity = parent::getEntity($id); } return $entity; } /** * Return a list of entities. * * @param array $args [start, limit, filter, orderBy, orderByDir] * * @return \Doctrine\ORM\Tools\Pagination\Paginator|array */ public function getEntities(array $args = []) { $entities = parent::getEntities($args); foreach ($entities as $entity) { $pending = $this->cacheStorageHelper->get(sprintf('%s|%s|%s', 'sms', $entity->getId(), 'pending')); if (false !== $pending) { $entity->setPendingCount($pending); } } return $entities; } /** * @param array $options * @param array<int, Lead> $leads */ public function sendSms(Sms $sms, $sendTo, $options = [], array &$leads = []): array { $channel = $options['channel'] ?? null; $listId = $options['listId'] ?? null; if ($sendTo instanceof Lead) { $sendTo = [$sendTo]; } elseif (!is_array($sendTo)) { $sendTo = [$sendTo]; } $sentCount = 0; $failedCount = 0; $results = []; $contacts = []; $fetchContacts = []; foreach ($sendTo as $lead) { if (!$lead instanceof Lead) { $fetchContacts[] = $lead; } else { $contacts[$lead->getId()] = $lead; $leads[$lead->getId()] = $lead; } } if ($fetchContacts) { $foundContacts = $this->leadModel->getEntities( [ 'ids' => $fetchContacts, ] ); foreach ($foundContacts as $contact) { $contacts[$contact->getId()] = $contact; $leads[$contact->getId()] = $contact; } } if (!$sms->isPublished()) { foreach ($contacts as $leadId => $lead) { $results[$leadId] = [ 'sent' => false, 'status' => 'mautic.sms.campaign.failed.unpublished', ]; } return $results; } $contactIds = array_keys($contacts); /** @var DoNotContactRepository $dncRepo */ $dncRepo = $this->em->getRepository(\Mautic\LeadBundle\Entity\DoNotContact::class); $dnc = $dncRepo->getChannelList('sms', $contactIds); foreach ($dnc as $removeMeId => $removeMeReason) { $results[$removeMeId] = [ 'sent' => false, 'status' => 'mautic.sms.campaign.failed.not_contactable', ]; unset($contacts[$removeMeId], $contactIds[$removeMeId]); } if (!empty($contacts)) { $messageQueue = $options['resend_message_queue'] ?? null; $campaignEventId = (is_array($channel) && 'campaign.event' === $channel[0] && !empty($channel[1])) ? $channel[1] : null; $queued = $this->messageQueueModel->processFrequencyRules( $contacts, 'sms', $sms->getId(), $campaignEventId, 3, MessageQueue::PRIORITY_NORMAL, $messageQueue, 'sms_message_stats' ); foreach ($queued as $queue) { $results[$queue] = [ 'sent' => false, 'status' => 'mautic.sms.timeline.status.scheduled', ]; unset($contacts[$queue]); } $stats = []; // @todo we should allow batch sending based on transport, MessageBird does support 20 SMS at once // the transport chain is already prepared for it if (count($contacts)) { /** @var Lead $lead */ foreach ($contacts as $lead) { $leadId = $lead->getId(); $stat = $this->createStatEntry($sms, $lead, $channel, false, $listId); $leadPhoneNumber = $lead->getLeadPhoneNumber(); if (empty($leadPhoneNumber)) { $results[$leadId] = [ 'sent' => false, 'status' => 'mautic.sms.campaign.failed.missing_number', ]; continue; } $smsEvent = new SmsSendEvent($sms->getMessage(), $lead); $smsEvent->setSmsId($sms->getId()); $this->dispatcher->dispatch($smsEvent, SmsEvents::SMS_ON_SEND); $tokenEvent = $this->dispatcher->dispatch( new TokenReplacementEvent( $smsEvent->getContent(), $lead, [ 'channel' => [ 'sms', // Keep BC pre 2.14.1 $sms->getId(), // Keep BC pre 2.14.1 'sms' => $sms->getId(), ], 'stat' => $stat->getTrackingHash(), ] ), SmsEvents::TOKEN_REPLACEMENT ); $sendResult = [ 'sent' => false, 'type' => 'mautic.sms.sms', 'status' => 'mautic.sms.timeline.status.delivered', 'id' => $sms->getId(), 'name' => $sms->getName(), 'content' => $tokenEvent->getContent(), ]; $metadata = $this->transport->sendSms($lead, $tokenEvent->getContent(), $stat); if (true !== $metadata) { $sendResult['status'] = $metadata; $stat->setIsFailed(true); if (is_string($metadata)) { $stat->addDetail('failed', $metadata); } ++$failedCount; } else { $sendResult['sent'] = true; ++$sentCount; } $stats[] = $stat; unset($stat); $results[$leadId] = $sendResult; unset($smsEvent, $tokenEvent, $sendResult, $metadata); } } } if ($sentCount || $failedCount) { $this->getRepository()->upCount($sms->getId(), 'sent', $sentCount); $this->getStatRepository()->saveEntities($stats); foreach ($stats as $stat) { if (!$stat->isFailed()) { $results[$stat->getLead()->getId()]['statId'] = $stat->getId(); } $this->getRepository()->detachEntity($stat); $this->getRepository()->detachEntity($stat->getLead()); } } return $results; } /** * @param bool $persist * * @throws \Exception */ public function createStatEntry(Sms $sms, Lead $lead, $source = null, $persist = true, $listId = null): Stat { $stat = new Stat(); $stat->setDateSent(new \DateTime()); $stat->setLead($lead); $stat->setSms($sms); if (null !== $listId) { $stat->setList($this->leadModel->getLeadListRepository()->getEntity($listId)); } if (is_array($source)) { $stat->setSourceId($source[1]); $source = $source[0]; } $stat->setSource($source); $stat->setTrackingHash(str_replace('.', '', uniqid('', true))); if ($persist) { $this->getStatRepository()->saveEntity($stat); } return $stat; } /** * @throws MethodNotAllowedHttpException */ protected function dispatchEvent($action, &$entity, $isNew = false, Event $event = null): ?Event { if (!$entity instanceof Sms) { throw new MethodNotAllowedHttpException(['Sms']); } switch ($action) { case 'pre_save': $name = SmsEvents::SMS_PRE_SAVE; break; case 'post_save': $name = SmsEvents::SMS_POST_SAVE; break; case 'pre_delete': $name = SmsEvents::SMS_PRE_DELETE; break; case 'post_delete': $name = SmsEvents::SMS_POST_DELETE; break; default: return null; } if ($this->dispatcher->hasListeners($name)) { if (empty($event)) { $event = new SmsEvent($entity, $isNew); $event->setEntityManager($this->em); } $this->dispatcher->dispatch($event, $name); return $event; } else { return null; } } /** * Joins the page table and limits created_by to currently logged in user. */ public function limitQueryToCreator(QueryBuilder &$q): void { $q->join('t', MAUTIC_TABLE_PREFIX.'sms_messages', 's', 's.id = t.sms_id') ->andWhere('s.created_by = :userId') ->setParameter('userId', $this->userHelper->getUser()->getId()); } /** * Get line chart data of hits. * * @param char $unit {@link php.net/manual/en/function.date.php#refsect1-function.date-parameters} * @param string $dateFormat * @param array $filter * @param bool $canViewOthers */ public function getHitsLineChartData($unit, \DateTime $dateFrom, \DateTime $dateTo, $dateFormat = null, $filter = [], $canViewOthers = true): array { $flag = null; if (isset($filter['flag'])) { $flag = $filter['flag']; unset($filter['flag']); } $chart = new LineChart($unit, $dateFrom, $dateTo, $dateFormat); $query = new ChartQuery($this->em->getConnection(), $dateFrom, $dateTo); if (!$flag || 'total_and_unique' === $flag) { $filter['is_failed'] = 0; $q = $query->prepareTimeDataQuery('sms_message_stats', 'date_sent', $filter); if (!$canViewOthers) { $this->limitQueryToCreator($q); } $data = $query->loadAndBuildTimeData($q); $chart->setDataset($this->translator->trans('mautic.sms.show.total.sent'), $data); } if (!$flag || 'failed' === $flag) { $filter['is_failed'] = 1; $q = $query->prepareTimeDataQuery('sms_message_stats', 'date_sent', $filter); if (!$canViewOthers) { $this->limitQueryToCreator($q); } $data = $query->loadAndBuildTimeData($q); $chart->setDataset($this->translator->trans('mautic.sms.show.failed'), $data); } return $chart->render(); } /** * @return Stat */ public function getSmsStatus($idHash) { return $this->getStatRepository()->getSmsStatus($idHash); } /** * Search for an sms stat by sms and lead IDs. * * @return array */ public function getSmsStatByLeadId($smsId, $leadId) { return $this->getStatRepository()->findBy( [ 'sms' => (int) $smsId, 'lead' => (int) $leadId, ], ['dateSent' => 'DESC'] ); } /** * Get an array of tracked links. */ public function getSmsClickStats($smsId): array { return $this->pageTrackableModel->getTrackableList('sms', $smsId); } /** * @param string $filter * @param int $limit * @param int $start * @param array $options */ public function getLookupResults($type, $filter = '', $limit = 10, $start = 0, $options = []): array { $results = []; switch ($type) { case 'sms': case SmsType::class: $entities = $this->getRepository()->getSmsList( $filter, $limit, $start, $this->security->isGranted($this->getPermissionBase().':viewother'), $options['sms_type'] ?? null ); foreach ($entities as $entity) { $results[$entity['language']][$entity['id']] = $entity['name']; } // sort by language ksort($results); break; } return $results; } }