*/ class FocusModel extends FormModel implements GlobalSearchInterface { public function __construct( protected \Mautic\FormBundle\Model\FormModel $formModel, protected TrackableModel $trackableModel, protected Environment $twig, protected FieldModel $leadFieldModel, protected ContactTracker $contactTracker, EntityManagerInterface $em, CorePermissions $security, EventDispatcherInterface $dispatcher, UrlGeneratorInterface $router, Translator $translator, UserHelper $userHelper, LoggerInterface $mauticLogger, CoreParametersHelper $coreParametersHelper, ) { $this->dispatcher = $dispatcher; parent::__construct($em, $security, $dispatcher, $router, $translator, $userHelper, $mauticLogger, $coreParametersHelper); } public function getActionRouteBase(): string { return 'focus'; } public function getPermissionBase(): string { return 'focus:items'; } /** * @param object $entity * @param string|null $action * @param array $options * * @throws NotFoundHttpException */ public function createForm($entity, FormFactoryInterface $formFactory, $action = null, $options = []): \Symfony\Component\Form\FormInterface { if (!$entity instanceof Focus) { throw new MethodNotAllowedHttpException(['Focus']); } if (!empty($action)) { $options['action'] = $action; } return $formFactory->create(FocusType::class, $entity, $options); } /** * @return \MauticPlugin\MauticFocusBundle\Entity\FocusRepository */ public function getRepository() { return $this->em->getRepository(Focus::class); } /** * @return \MauticPlugin\MauticFocusBundle\Entity\StatRepository */ public function getStatRepository() { return $this->em->getRepository(Stat::class); } /** * @param int|null $id */ public function getEntity($id = null): ?Focus { if (null === $id) { return new Focus(); } return parent::getEntity($id); } /** * @param Focus $entity * @param bool|false $unlock */ public function saveEntity($entity, $unlock = true): void { parent::saveEntity($entity, $unlock); $this->generateTrackableUrl($entity); } /** * @param bool $isPreview */ public function generateJavascript(Focus $focus, $isPreview = false): string { $lead = $this->contactTracker->getContact(); $focusArray = $focus->toArray(); $url = ''; if ($trackableUrl = $this->generateTrackableUrl($focus, $lead)) { $url = '{focusClickUrl}'; } $javascript = $this->twig->render( '@MauticFocus/Builder/generate.js.twig', [ 'focus' => $focus, 'preview' => $isPreview, 'clickUrl' => $url, ] ); $content = $this->getContent($focusArray, $isPreview, $url); $data = [ 'js' => (new Minify\JS($javascript))->minify(), 'focus' => InputHelper::minifyHTML($content['focus']), 'form' => InputHelper::minifyHTML($content['form']), ]; // Replace tokens to ensure clickthroughs, lead tokens etc. are appropriate $tokenEvent = new TokenReplacementEvent($data['focus'], $lead, ['focus_id' => $focus->getId()]); if ($trackableUrl) { $tokenEvent->addToken($url, $trackableUrl); } $this->dispatcher->dispatch($tokenEvent, FocusEvents::TOKEN_REPLACEMENT); $focusContent = $tokenEvent->getContent(); $focusContent = str_replace('{focus_form}', $data['form'], $focusContent, $formReplaced); if (!$formReplaced && !empty($data['form'])) { // Form token missing so just append the form $focusContent .= $data['form']; } $focusContent = $this->twig->getRuntime(EscaperRuntime::class)->escape($focusContent, 'js'); return str_replace('{focus_content}', $focusContent, $data['js']); } /** * @param bool $isPreview * @param string $url * * @return array */ public function getContent(array $focus, $isPreview = false, $url = '#') { $form = (!empty($focus['form']) && 'form' === $focus['type']) ? $this->formModel->getEntity($focus['form']) : null; if (isset($focus['html_mode'])) { $htmlMode = $focus['htmlMode'] = $focus['html_mode']; } elseif (isset($focus['htmlMode'])) { $htmlMode = $focus['htmlMode']; } else { $htmlMode = 'basic'; } if (isset($focus[$htmlMode])) { $focus[$htmlMode] = htmlspecialchars_decode($focus[$htmlMode]); } $content = $this->twig->render( '@MauticFocus/Builder/content.html.twig', [ 'focus' => $focus, 'preview' => $isPreview, 'htmlMode' => $htmlMode, 'clickUrl' => $url, ] ); // Form has to be generated outside of the content or else the form src // will be converted to clickables $fields = $form ? $form->getFields()->toArray() : []; [$pages, $lastPage] = $this->formModel->getPages($fields); $displayManager = $viewOnlyFields = null; if ($form) { $viewOnlyFields = $this->formModel->getCustomComponents()['viewOnlyFields']; $displayManager = new DisplayManager($form, !empty($viewOnlyFields) ? $viewOnlyFields : []); } $formContent = (!empty($form)) ? $this->twig->render( '@MauticFocus/Builder/form.html.twig', [ 'form' => $form, 'pages' => $pages, 'lastPage' => $lastPage, 'style' => $focus['style'], 'focusId' => $focus['id'], 'preview' => $isPreview, 'contactFields' => $this->leadFieldModel->getFieldListWithProperties(), 'companyFields' => $this->leadFieldModel->getFieldListWithProperties('company'), 'viewOnlyFields' => $viewOnlyFields, 'displayManager' => $displayManager, ] ) : ''; if ($isPreview) { $content = str_replace('{focus_form}', $formContent, $content, $formReplaced); if (!$formReplaced && !empty($formContent)) { $content .= $formContent; } return $content; } return [ 'focus' => $content, 'form' => $formContent, ]; } /** * Get whether the color is light or dark. */ public static function isLightColor($hex, $level = 200): bool { $hex = str_replace('#', '', $hex); $r = hexdec(substr($hex, 0, 2)); $g = hexdec(substr($hex, 2, 2)); $b = hexdec(substr($hex, 4, 2)); $compareWith = ((($r * 299) + ($g * 587) + ($b * 114)) / 1000); return $compareWith >= $level; } /** * Add a stat entry. * * @param mixed $type * @param mixed $data * @param array>|Lead|Submission|null $lead */ public function addStat(Focus $focus, $type, $data = null, $lead = null): ?Stat { if (empty($lead)) { return null; } if ($lead instanceof Lead && !$lead->getId()) { return null; } if (is_array($lead)) { if (empty($lead['id'])) { return null; } $lead = $this->em->getReference(Lead::class, $lead['id']); } switch ($type) { case Stat::TYPE_FORM: case Stat::TYPE_CLICK: /** @var \Mautic\PageBundle\Entity\Hit|Submission $data */ $typeId = $data->getId(); break; case Stat::TYPE_NOTIFICATION: $typeId = null; break; } $stat = new Stat(); $stat->setFocus($focus) ->setDateAdded(new \DateTime()) ->setType($type) ->setTypeId($typeId) ->setLead($lead); $this->getStatRepository()->saveEntity($stat); return $stat; } /** * @throws MethodNotAllowedHttpException */ protected function dispatchEvent($action, &$entity, $isNew = false, ?Event $event = null): ?Event { if (!$entity instanceof Focus) { throw new MethodNotAllowedHttpException(['Focus']); } switch ($action) { case 'pre_save': $name = FocusEvents::PRE_SAVE; break; case 'post_save': $name = FocusEvents::POST_SAVE; break; case 'pre_delete': $name = FocusEvents::PRE_DELETE; break; case 'post_delete': $name = FocusEvents::POST_DELETE; break; default: return null; } if ($this->dispatcher->hasListeners($name)) { if (empty($event)) { $event = new FocusEvent($entity, $isNew); $event->setEntityManager($this->em); } $this->dispatcher->dispatch($event, $name); return $event; } else { return null; } } /** * @param bool $canViewOthers */ public function getStats(Focus $focus, $unit, \DateTime $dateFrom, \DateTime $dateTo, $dateFormat = null, $canViewOthers = true): array { $chart = new LineChart($unit, $dateFrom, $dateTo, $dateFormat); $query = new ChartQuery($this->em->getConnection(), $dateFrom, $dateTo, $unit); $q = $query->prepareTimeDataQuery('focus_stats', 'date_added', ['type' => Stat::TYPE_NOTIFICATION, 'focus_id' => $focus->getId()]); if (!$canViewOthers) { $this->limitQueryToCreator($q); } $data = $query->loadAndBuildTimeData($q); $chart->setDataset($this->translator->trans('mautic.focus.graph.views'), $data); if ('notification' != $focus->getType()) { if ('link' == $focus->getType()) { $q = $query->prepareTimeDataQuery('focus_stats', 'date_added', ['type' => Stat::TYPE_CLICK, 'focus_id' => $focus->getId()]); if (!$canViewOthers) { $this->limitQueryToCreator($q); } $data = $query->loadAndBuildTimeData($q); $chart->setDataset($this->translator->trans('mautic.focus.graph.clicks'), $data); } else { $q = $query->prepareTimeDataQuery('focus_stats', 'date_added', ['type' => Stat::TYPE_FORM, 'focus_id' => $focus->getId()]); if (!$canViewOthers) { $this->limitQueryToCreator($q); } $data = $query->loadAndBuildTimeData($q); $chart->setDataset($this->translator->trans('mautic.focus.graph.submissions'), $data); } } return $chart->render(); } /** * Joins the email table and limits created_by to currently logged in user. */ public function limitQueryToCreator(QueryBuilder $q): void { $q->join('t', MAUTIC_TABLE_PREFIX.'focus', 'm', 'e.id = t.focus_id') ->andWhere('m.created_by = :userId') ->setParameter('userId', $this->userHelper->getUser()->getId()); } public function getViewsCount(Focus $focus): int { return $this->getStatRepository()->getViewsCount($focus->getId()); } public function getUniqueViewsCount(Focus $focus): int { return $this->getStatRepository()->getUniqueViewsCount($focus->getId()); } public function getClickThroughCount(Focus $focus): int { return $this->getStatRepository()->getClickThroughCount($focus->getId()); } private function generateTrackableUrl(Focus $focus, ?Lead $lead = null): ?string { $focusArray = $focus->toArray(); if ('link' != $focusArray['type'] || !($linkUrl = $focusArray['properties']['content']['link_url'])) { return null; } return $this->trackableModel->generateTrackableUrl( $this->trackableModel->getTrackableByUrl($linkUrl, 'focus', $focus->getId()), [ 'channel' => ['focus', $focus->getId()], 'lead' => $lead ? $lead->getId() : null, ], false, $focus->getUtmTags() ); } }