src/Products/NotificationsBundle/Controller/Dashboard/MessagesController.php line 274

Open in your IDE?
  1. <?php
  2. namespace Products\NotificationsBundle\Controller\Dashboard;
  3. use App\Component\ViewLayer\AbstractView;
  4. use App\Component\ViewLayer\Views\AbstractHtmlView;
  5. use App\Component\ViewLayer\Views\AjaxHtmlView;
  6. use App\Component\ViewLayer\Views\DocHtmlView;
  7. use App\Component\ViewLayer\Views\JsonView;
  8. use App\Controller\PaginationTrait;
  9. use App\Entity\System\School;
  10. use App\Form\Forms\DummyForm;
  11. use App\Model\Messaging\SendgridEmailMessage;
  12. use App\Service\Intl\CloudTranslator;
  13. use App\Service\Messaging\MessagingContext;
  14. use App\Service\Messaging\SendgridEmailMessenger;
  15. use App\Util\Locales;
  16. use Cms\CoreBundle\Util\DateTimeUtils;
  17. use Cms\TenantBundle\Entity\Tenant;
  18. use Products\NotificationsBundle\Controller\AbstractDashboardController;
  19. use Products\NotificationsBundle\Doctrine\Repository\Notifications\Translations\TranslationRepository;
  20. use Products\NotificationsBundle\Entity\AbstractContactAttempt;
  21. use Products\NotificationsBundle\Entity\AbstractList;
  22. use Products\NotificationsBundle\Entity\AbstractNotification;
  23. use Products\NotificationsBundle\Entity\Notifications\Message;
  24. use Products\NotificationsBundle\Entity\Notifications\Template;
  25. use Products\NotificationsBundle\Entity\Notifications\Translations\Translation;
  26. use Products\NotificationsBundle\Entity\Student;
  27. use Products\NotificationsBundle\Form\Forms\Logs\ContactAttemptSearchForm;
  28. use Products\NotificationsBundle\Form\Forms\Messages\MessageChannelsForm;
  29. use Products\NotificationsBundle\Form\Forms\Messages\MessageDataForm;
  30. use Products\NotificationsBundle\Form\Forms\Messages\MessageSearchForm;
  31. use Products\NotificationsBundle\Form\Forms\Messages\MessageTestForm;
  32. use Products\NotificationsBundle\Model\Searching\ContactAttemptSearch;
  33. use Products\NotificationsBundle\Model\Searching\MessageSearch;
  34. use Products\NotificationsBundle\Model\Testing\EmailTester;
  35. use Products\NotificationsBundle\Model\Testing\PhoneTester;
  36. use Products\NotificationsBundle\Service\EmailTemplateGeneratorFactory;
  37. use Products\NotificationsBundle\Service\ListItemProvider;
  38. use Products\NotificationsBundle\Service\MergeParamsProvider;
  39. use Products\NotificationsBundle\Util\MessageContentGenerator;
  40. use Products\NotificationsBundle\Service\MessageLogic;
  41. use Products\NotificationsBundle\Service\Notifications\TranslationService;
  42. use Symfony\Component\Form\FormError;
  43. use Symfony\Component\HttpFoundation\RedirectResponse;
  44. use Symfony\Component\HttpFoundation\Request;
  45. use Symfony\Component\HttpFoundation\Response;
  46. use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
  47. use Sensio\Bundle\FrameworkExtraBundle\Configuration\ParamConverter;
  48. use Symfony\Component\Routing\Annotation\Route;
  49. /**
  50.  * Class MessagesController
  51.  * @package Products\NotificationsBundle\Controller\Dashboard
  52.  *
  53.  * @Route(
  54.  *     "/messages",
  55.  * )
  56.  */
  57. final class MessagesController extends AbstractDashboardController
  58. {
  59.     use PaginationTrait;
  60.     const ROUTES__MAIN 'app.notifications.dashboard.messages.main';
  61.     const ROUTES__CREATE 'app.notifications.dashboard.messages.create';
  62.     const ROUTES__UPDATE 'app.notifications.dashboard.messages.update';
  63.     const ROUTES__CHECK_UPDATE_FORM 'app.notifications.dashboard.messages.check_update_form';
  64.     const ROUTES__TRANSLATIONS 'app.notifications.dashboard.messages.translations';
  65.     const ROUTES__CHECK_TRANSLATION_PROGRESS 'app.notifications.dashboard.messages.check_translation_progress';
  66.     const ROUTES__MANAGE 'app.notifications.dashboard.messages.manage';
  67.     const ROUTES__DETECT_TRANSLATION_CHANGE 'app.notifications.dashboard.messages.detect_locale_change';
  68.     const ROUTES__DELETE 'app.notifications.dashboard.messages.delete';
  69.     const ROUTES__UNSCHEDULE 'app.notifications.dashboard.messages.unschedule';
  70.     const ROUTES__STUDENT_EMAIL_PREVIEW 'app.notifications.dashboard.messages.student_email_preview';
  71.     const ROUTES__EMAIL_PREVIEW 'app.notifications.dashboard.messages.email_preview';
  72.     const ROUTES__REPORT 'app.notifications.dashboard.messages.report';
  73.     const ROUTES__LOGS 'app.notifications.dashboard.messages.logs';
  74.     const ROUTES__DEBUGGING 'app.notifications.dashboard.messages.debugging';
  75.     const ROUTES__FILTERED_LOGS 'app.notifications.dashboard.messages.filtered_logs';
  76.     const ROUTES__TEST 'app.notifications.dashboard.messages.test';
  77.     const ROUTES__DETAILS 'app.notifications.dashboard.messages.details';
  78.     // DI
  79.     protected MessagingContext $mc;
  80.     protected EmailTemplateGeneratorFactory $emailTemplateGeneratorFactory;
  81.     protected CloudTranslator $cloudTranslator;
  82.     protected TranslationService $translationService;
  83.     protected ListItemProvider $listItemProvider;
  84.     protected MergeParamsProvider $mergeParamsProvider;
  85.     /**
  86.      * @param MessagingContext $mc
  87.      * @param EmailTemplateGeneratorFactory $emailTemplateGeneratorFactory
  88.      * @param CloudTranslator $cloudTranslator
  89.      * @param TranslationService $translationService
  90.      * @param ListItemProvider $listItemProvider
  91.      * @param MergeParamsProvider $mergeParamsProvider
  92.      */
  93.     public function __construct(
  94.         MessagingContext $mc,
  95.         EmailTemplateGeneratorFactory $emailTemplateGeneratorFactory,
  96.         CloudTranslator $cloudTranslator,
  97.         TranslationService $translationService,
  98.         ListItemProvider $listItemProvider,
  99.         MergeParamsProvider $mergeParamsProvider
  100.     )
  101.     {
  102.         $this->mc $mc;
  103.         $this->emailTemplateGeneratorFactory $emailTemplateGeneratorFactory;
  104.         $this->cloudTranslator $cloudTranslator;
  105.         $this->translationService $translationService;
  106.         $this->listItemProvider $listItemProvider;
  107.         $this->mergeParamsProvider $mergeParamsProvider;
  108.     }
  109.     /**
  110.      * @param int $pagination
  111.      * @return AbstractHtmlView|RedirectResponse
  112.      *
  113.      * @Route(
  114.      *     "/list/{pagination}",
  115.      *     name = self::ROUTES__MAIN,
  116.      *     requirements = {
  117.      *         "pagination" = "[1-9]\d*",
  118.      *     },
  119.      *     defaults = {
  120.      *         "pagination" = 0,
  121.      *     },
  122.      * )
  123.      */
  124.     public function mainAction(int $pagination 0)
  125.     {
  126.         // AUDIT
  127.         $this->denyAccessUnlessMaybeGranted([
  128.             'app.notifications.messaging.general',
  129.             'app.notifications.messaging.urgent',
  130.         ]);
  131.         $result $this->doSearch(
  132.             Message::class,
  133.             'messages',
  134.             MessageSearch::class,
  135.             MessageSearchForm::class,
  136.             $pagination,
  137.         );
  138.         if ($result instanceof Response) {
  139.             return $result;
  140.         }
  141.         // avoids making duplicate queries
  142.         $result['messages'] = $result['messages']->getIterator();
  143.         $result['messagesIsGrantedArray'] = array_values(
  144.             array_filter(
  145.                 array_map(
  146.                     function (Message $message) {
  147.                         try {
  148.                             $this->audit($message);
  149.                             return $message->getId();
  150.                         } catch (\Throwable $exception) {
  151.                             return null;
  152.                         }
  153.                     },
  154.                     $result['messages']->getArrayCopy(),
  155.                 )
  156.             )
  157.         );
  158.         return $this->html($result);
  159.     }
  160.     /**
  161.      * @param Request $request
  162.      * @param Message $message
  163.      * @return AbstractView|AjaxHtmlView|JsonView
  164.      *
  165.      * @Route(
  166.      *     "/{message}/delete",
  167.      *     name = MessagesController::ROUTES__DELETE,
  168.      *     requirements = {
  169.      *         "message" = "[1-9]\d*",
  170.      *     },
  171.      * )
  172.      * @ParamConverter(
  173.      *     "message",
  174.      *     class = Message::class,
  175.      * )
  176.      */
  177.     public function deleteAction(Request $requestMessage $message): AbstractView
  178.     {
  179.         // AUDIT
  180.         $this->audit($message);
  181.         if ( ! $request->isXmlHttpRequest()) {
  182.             throw new NotFoundHttpException();
  183.         }
  184.         $form $this->createForm(DummyForm::class);
  185.         if ($this->handleForm($form)) {
  186.             $id $message->getId();
  187.             $this->getMessageLogic()->delete($message);
  188.             $this->getLoggingService()->createLog($message$id);
  189.             return $this->jsonView([
  190.                 'redirect' => true,
  191.             ]);
  192.         }
  193.         return $this->ajax([
  194.             'message' => $message,
  195.             'form' => $form->createView(),
  196.         ]);
  197.     }
  198.     /**
  199.      * @param Request $request
  200.      * @param Message $message
  201.      * @return AbstractView|AjaxHtmlView|JsonView
  202.      *
  203.      * @Route(
  204.      *     "/{message}/unschedule",
  205.      *     name = MessagesController::ROUTES__UNSCHEDULE,
  206.      *     requirements = {
  207.      *         "message" = "[1-9]\d*",
  208.      *     },
  209.      * )
  210.      * @ParamConverter(
  211.      *     "message",
  212.      *     class = Message::class,
  213.      * )
  214.      */
  215.     public function unscheduleAction(Request $requestMessage $message): AbstractView
  216.     {
  217.         // AUDIT
  218.         $this->audit($message);
  219.         if ( ! $request->isXmlHttpRequest()) {
  220.             throw new NotFoundHttpException();
  221.         }
  222.         $form $this->createForm(DummyForm::class);
  223.         if ($this->handleForm($form)) {
  224.             $this->getMessageLogic()->unschedule($message);
  225.             $this->getLoggingService()->createLog($message);
  226.             return $this->jsonView([
  227.                 'redirect' => true,
  228.             ]);
  229.         }
  230.         return $this->ajax([
  231.             'message' => $message,
  232.             'form' => $form->createView(),
  233.         ]);
  234.     }
  235.     /**
  236.      * @param Request $request
  237.      * @return AbstractView|RedirectResponse
  238.      *
  239.      * @Route(
  240.      *     "/create",
  241.      *     name = MessagesController::ROUTES__CREATE,
  242.      * )
  243.      */
  244.     public function createAction(Request $request)
  245.     {
  246.         // AUDIT
  247.         $this->denyAccessUnlessMaybeGranted([
  248.             'app.notifications.messaging.general',
  249.             'app.notifications.messaging.urgent',
  250.         ]);
  251.         // check if we are cloning
  252.         if ($request->query->has('clone')) {
  253.             $starter $this->getEntityManager()->getRepository(Message::class)->find($request->query->getInt('clone'));
  254.         } else if ($request->query->has('template')) {
  255.             $starter $this->getEntityManager()->getRepository(Template::class)->find($request->query->getInt('template'));
  256.         } else {
  257.             throw new \Exception();
  258.         }
  259.         $this->denyAccessUnlessMaybeGranted(
  260.             sprintf('app.notifications.messaging.%s'$starter->isUrgent() ? 'urgent' 'general')
  261.         );
  262.         // double check to make sure the template is sendable
  263.         if ($starter instanceof Template && ! $starter->isSendable()) {
  264.             throw new \Exception();
  265.         }
  266.         $prefillParams null;
  267.         $formData $request->request->get('message_data_form');
  268.         if (isset($formData['prefillParams'])) {
  269.             $prefillParams json_decode($formData['prefillParams'], true);
  270.             if (json_last_error() !== JSON_ERROR_NONE) {
  271.                 $prefillParams null;
  272.             }
  273.         }
  274.         $messageLogic $this->getMessageLogic();
  275.         /** @var Message $message */
  276.         $message $messageLogic->init(new Message(), $starter$prefillParams);
  277.         // generate the form
  278.         $form $this->createForm(MessageDataForm::class, $message, ['prefillParams' => $messageLogic->getLastPrefillParams()]);
  279.         // handle form submission
  280.         if ($this->handleForm($form)) {
  281.             // create the new object
  282.             $this->getMessageLogic()->create($message);
  283.             $redirectRoute self::ROUTES__MANAGE;
  284.             if ( ! empty($this->translationService->getTranslationLocales()) && ! $request->query->has('jump')) {
  285.                 $redirectRoute self::ROUTES__TRANSLATIONS;
  286.                 $this->translationService->translate($message);
  287.             }
  288.             $this->getLoggingService()->createLog($message);
  289.             if (($this->generateUrl(self::ROUTES__MAIN) === $request->query->get('jump'))) {
  290.                 return $this->redirectToRoute(self::ROUTES__UPDATE, [
  291.                     'message' => $message->getId(),
  292.                     'saved' => true,
  293.                 ]);
  294.             }
  295.             return $this->jumpOrRedirect(
  296.                 $this->redirectToRoute(
  297.                     $redirectRoute,
  298.                     [
  299.                         'message' => $message->getId(),
  300.                     ]
  301.                 )
  302.             );
  303.         }
  304.         return $this->html([
  305.             'starter' => $starter,
  306.             'message' => $message,
  307.             'form' => $form->createView(),
  308.             'mergeParams' => $this->mergeParamsProvider->get($message),
  309.         ]);
  310.     }
  311.     /**
  312.      * @param Request $request
  313.      * @param Message $message
  314.      * @return AbstractView|RedirectResponse
  315.      *
  316.      * @Route(
  317.      *     "/{message}/update/check_form",
  318.      *     name = MessagesController::ROUTES__CHECK_UPDATE_FORM,
  319.      *     requirements = {
  320.      *         "message" = "[1-9]\d*",
  321.      *     },
  322.      * )
  323.      * @ParamConverter(
  324.      *     "message",
  325.      *     class = Message::class,
  326.      * )
  327.      */
  328.     public function checkIfUpdateFormValidAction(Request $requestMessage $message): JsonView
  329.     {
  330.         // AUDIT
  331.         $this->audit($message);
  332.         $form $this->createForm(MessageDataForm::class, $message);
  333.         $form->handleRequest($request);
  334.         return $this->jsonView([
  335.             'isValid' => $form->isValid(),
  336.         ]);
  337.     }
  338.     /**
  339.      * @param Request $request
  340.      * @param Message $message
  341.      * @return AbstractView|RedirectResponse
  342.      *
  343.      * @Route(
  344.      *     "/{message}/update",
  345.      *     name = MessagesController::ROUTES__UPDATE,
  346.      *     requirements = {
  347.      *         "message" = "[1-9]\d*",
  348.      *     },
  349.      * )
  350.      * @ParamConverter(
  351.      *     "message",
  352.      *     class = Message::class,
  353.      * )
  354.      */
  355.     public function updateAction(Request $requestMessage $message)
  356.     {
  357.         // AUDIT
  358.         $this->audit($message);
  359.         $form $this->createForm(MessageDataForm::class, $message);
  360.         if ($this->handleForm($form)) {
  361.             if ($this->getMessageLogic()->isTranslatableFieldsModified($message)) {
  362.                 $message->markFlag(AbstractNotification::FLAGS__MODIFIED_SINCE_TRANSLATION);
  363.             }
  364.             $this->getMessageLogic()->update($message);
  365.             $redirectRoute self::ROUTES__MANAGE;
  366.             $shouldRedirectToTranslatePage = (
  367.                 ! empty($this->translationService->getTranslationLocales()) &&
  368.                 ! $request->query->has('jump')
  369.             );
  370.             if ($shouldRedirectToTranslatePage) {
  371.                 $redirectRoute self::ROUTES__TRANSLATIONS;
  372.                 $translatedLocales array_map(function (Translation $translation) {
  373.                     return $translation->getLocale();
  374.                 }, $message->getTranslationsAsArray());
  375.                 $untranslatedLocales array_diff($this->translationService->getTranslationLocales(), $translatedLocales);
  376.                 $shouldDismissManualTranslationEdits $form->get('forceTranslation')->getData() === '1';
  377.                 $shouldTranslate = (
  378.                     !empty($untranslatedLocales) ||
  379.                     $message->hasFlag(AbstractNotification::FLAGS__MODIFIED_SINCE_TRANSLATION) ||
  380.                     $shouldDismissManualTranslationEdits
  381.                 );
  382.                 if ($shouldTranslate) {
  383.                     $this->translationService->translate($message$shouldDismissManualTranslationEdits);
  384.                 }
  385.             }
  386.             $this->getLoggingService()->createLog($message);
  387.             return $this->jumpOrRedirect(
  388.                 $this->redirectToRoute($redirectRoute, [
  389.                     'message' => $message->getId(),
  390.                 ])
  391.             );
  392.         }
  393.         $hasManualTranslation false;
  394.         foreach ($message->getTranslations() as $translation) {
  395.             if ($translation->hasFlag(Translation::FLAGS__MANUAL)) {
  396.                 $hasManualTranslation true;
  397.             }
  398.         }
  399.         return $this->html([
  400.             'hasManualTranslation' => $hasManualTranslation,
  401.             'message' => $message,
  402.             'form' => $form->createView(),
  403.             'mergeParams' => $this->mergeParamsProvider->get($message),
  404.         ]);
  405.     }
  406.     /**
  407.      * @param Message $message
  408.      * @return AbstractView|RedirectResponse
  409.      *
  410.      * @Route(
  411.      *     "/{message}/translations",
  412.      *     name = MessagesController::ROUTES__TRANSLATIONS,
  413.      *     requirements = {
  414.      *         "message" = "[1-9]\d*",
  415.      *     },
  416.      * )
  417.      * @ParamConverter(
  418.      *     "message",
  419.      *     class = Message::class,
  420.      * )
  421.      */
  422.     public function translationsAction(Message $message)
  423.     {
  424.         // AUDIT
  425.         $this->audit($message);
  426.         return $this->html([
  427.             'message' => $message,
  428.             'statuses' => $this->checkTranslationsAction($message)->getData(),
  429.             'humanReadableLocaleNames' => Locales::RFC4646_HUMAN_READABLE,
  430.         ]);
  431.     }
  432.     /**
  433.      * @param Message $message
  434.      * @return JsonView
  435.      *
  436.      * @Route(
  437.      *     "/{message}/check_translation_progress",
  438.      *     name = MessagesController::ROUTES__CHECK_TRANSLATION_PROGRESS,
  439.      *     requirements = {
  440.      *         "message" = "[1-9]\d*",
  441.      *     }
  442.      * )
  443.      * @ParamConverter(
  444.      *     "message",
  445.      *     class = Message::class,
  446.      * )
  447.      */
  448.     public function checkTranslationsAction(Message $message): JsonView
  449.     {
  450.         // AUDIT
  451.         $this->audit($message);
  452.         /** @var TranslationRepository $translationRepository */
  453.         $translationRepository $this->getEntityManager()->getRepository(Translation::class);
  454.         // create array to hold stats of all the translations
  455.         $statuses array_fill_keys(
  456.             $this->translationService->getTranslationLocales(),
  457.             null,
  458.         );
  459.         // loop over each translation
  460.         $ready true;
  461.         foreach (array_keys($statuses) as $locale) {
  462.             // attempt to find the translation
  463.             $translation $translationRepository->findOneBy([
  464.                 'notification' => $message,
  465.                 'locale' => $locale,
  466.             ]);
  467.             // if not found, we have not been started yet (return null status)
  468.             if ( ! $translation instanceof Translation) {
  469.                 $statuses[$locale] = null;
  470.                 $ready false;
  471.                 continue;
  472.             }
  473.             // it was found, set if it is ready or not
  474.             $statuses[$locale] = $translation->isReady();
  475.             // track the status of all
  476.             $ready $ready && $statuses[$locale];
  477.         }
  478.         return $this->jsonView([
  479.             'ready' => $ready,
  480.             'statuses' => $statuses,
  481.         ]);
  482.     }
  483.     /**
  484.      * @param Message $message
  485.      * @return AbstractView|RedirectResponse
  486.      *
  487.      * @Route(
  488.      *     "/{message}/manage",
  489.      *     name = MessagesController::ROUTES__MANAGE,
  490.      *     requirements = {
  491.      *         "message" = "[1-9]\d*",
  492.      *     },
  493.      * )
  494.      * @ParamConverter(
  495.      *     "message",
  496.      *     class = Message::class,
  497.      * )
  498.      */
  499.     public function manageAction(Message $message)
  500.     {
  501.         // AUDIT
  502.         $this->audit($message);
  503.         // init the date to end of day if not already set
  504.         if (empty($message->getWebsiteEndDateTime())) {
  505.             $message->setWebsiteEndDateTime(DateTimeUtils::tomorrow(
  506.                 $this->getLocaleManager()->effectiveTimezone()
  507.             ));
  508.         }
  509.         $languages = [];
  510.         foreach ($this->translationService->getTranslationLocales() as $locale) {
  511.             if (!isset(Locales::RFC4646_HUMAN_READABLE[$locale])) {
  512.                 throw new \Exception('Locale is missing human readable name');
  513.             }
  514.             $humanReadableLocaleName Locales::RFC4646_HUMAN_READABLE[$locale];
  515.             $languages[$locale] = [
  516.                 'locale' => $locale,
  517.                 'humanReadableLocaleName' => $humanReadableLocaleName,
  518.                 'isActive' => false,
  519.             ];
  520.         }
  521.         foreach ($message->getTranslations() as $translation) {
  522.             if (!isset(Locales::RFC4646_HUMAN_READABLE[$translation->getLocale()])) {
  523.                 throw new \Exception('Locale is missing human readable name');
  524.             }
  525.             $humanReadableLocaleName Locales::RFC4646_HUMAN_READABLE[$translation->getLocale()];
  526.             $languages[$translation->getLocale()] = [
  527.                 'locale' => $translation->getLocale(),
  528.                 'humanReadableLocaleName' => $humanReadableLocaleName,
  529.                 'isActive' => $translation->hasFlag(Translation::FLAGS__MANUAL),
  530.             ];
  531.         }
  532.         /** @var TranslationRepository $translationRepository */
  533.         $translationRepository $this->getRepository(Translation::class);
  534.         $translations = [];
  535.         foreach ($this->translationService->getTranslationLocales() as $locale) {
  536.             $existingTranslation $translationRepository->findOneBy([
  537.                 'locale' => $locale,
  538.                 'notification' => $message,
  539.             ]);
  540.             if ($existingTranslation instanceof Translation) {
  541.                 $translations[$locale] = $existingTranslation;
  542.             } else {
  543.                 $translation = new Translation();
  544.                 $translation->setLocale($locale);
  545.                 $translation->setNotification($message);
  546.                 $translation->setTenant($message->getTenant());
  547.                 $message->addTranslation($translation);
  548.             }
  549.         }
  550.         // make form
  551.         $form $this->createForm(MessageChannelsForm::class, $message);
  552.         // attempt form submit
  553.         if ($this->handleForm($form)) {
  554.             foreach ($message->getTranslationsAsArray() as $translation) {
  555.                 if ($this->translationService->isModified($translation)) {
  556.                     $translation->markFlag(Translation::FLAGS__MANUAL);
  557.                     $this->getEntityManager()->persist($translation);
  558.                 }
  559.             }
  560.             // save changes to message
  561.             $this->getMessageLogic()->update($message);
  562.             $this->getLoggingService()->createLog($message);
  563.             // determine if we are sending and handle it if so
  564.             if ($form->get('action')->getData() === 'send') {
  565.                 $this->getMessageLogic()->broadcast(
  566.                     $message->setScheduledAt(
  567.                         ($form->get('scheduled')->getData() === true)
  568.                             ? $form->get('scheduledAt')->getData()
  569.                             : null
  570.                     ),
  571.                 );
  572.                 return $this->jumpOrRedirectToRoute(self::ROUTES__MAIN);
  573.             }
  574.             return $this->jumpOrRedirectToRoute(self::ROUTES__MANAGE, [
  575.                 'message' => $message->getId(),
  576.                 'saved' => true,
  577.             ]);
  578.         }
  579.         return $this->html([
  580.             'message' => $message,
  581.             'form' => $form->createView(),
  582.             'configs' => $this->mc->getConfigs(),
  583.             'humanReadableLocales' => Locales::RFC4646_HUMAN_READABLE,
  584.             'languages' => $languages,
  585.             'translations' => $translations,
  586.             'counts' => [
  587.                 'total' => $this->listItemProvider->countByLists($message->getListsAsArray()),
  588.                 'lists' =>
  589.                     array_combine(
  590.                         array_map(
  591.                             static function (AbstractList $list) {
  592.                                 return $list->getId();
  593.                             },
  594.                             $message->getListsAsArray(),
  595.                         ),
  596.                         array_map(
  597.                             function (AbstractList $list) {
  598.                                 return $this->listItemProvider->countByLists($list);
  599.                             },
  600.                             $message->getListsAsArray(),
  601.                         )
  602.                     ),
  603.             ],
  604.         ]);
  605.     }
  606.     /**
  607.      * @param Message $message
  608.      * @param string $locale
  609.      * @return JsonView
  610.      *
  611.      * @Route(
  612.      *     "/{message}/{locale}/is_translation_modified",
  613.      *     name = MessagesController::ROUTES__DETECT_TRANSLATION_CHANGE,
  614.      *     requirements = {
  615.      *         "message" = "[1-9]\d*",
  616.      *     },
  617.      * )
  618.      * @ParamConverter(
  619.      *     "message",
  620.      *     class = Message::class,
  621.      * )
  622.      */
  623.     public function isTranslationModifiedAction(Message $messagestring $locale)
  624.     {
  625.         // AUDIT
  626.         $this->audit($message);
  627.         $form $this->createForm(MessageChannelsForm::class, $message);
  628.         $form->handleRequest($this->getRequest());
  629.         $translation $message->getTranslation($locale);
  630.         if ( ! ($translation instanceof Translation)) {
  631.             return $this->jsonView([
  632.                 'isModifiedOverall' => false,
  633.                 'isScriptModified' => false,
  634.                 'isRecordingModified' => false,
  635.             ]);
  636.         }
  637.         if ($translation->hasFlag(Translation::FLAGS__MANUAL) || $this->translationService->isModified($translation)) {
  638.             return $this->jsonView([
  639.                 'isModifiedOverall' => true,
  640.                 'isScriptModified' => $this->translationService->isPropertyModified('script'$translation),
  641.                 'isRecordingModified' => $this->translationService->isRecordingModified($translation),
  642.             ]);
  643.         }
  644.         return $this->jsonView([
  645.             'isModifiedOverall' => false,
  646.             'isScriptModified' => false,
  647.             'isRecordingModified' => false,
  648.         ]);
  649.     }
  650.     /**
  651.      * @param School $school
  652.      * @param Template $template
  653.      * @param Student $student
  654.      * @return Response
  655.      *
  656.      * @Route(
  657.      *     "/email/preview/school/{school}/template/{template}/student/{student}",
  658.      *     name = MessagesController::ROUTES__STUDENT_EMAIL_PREVIEW,
  659.      *     requirements={
  660.      *         "school" = "[1-9]\d*",
  661.      *         "template" = "[1-9]\d*",
  662.      *         "student" = "[1-9]\d*",
  663.      *     }
  664.      * )
  665.      * @ParamConverter(
  666.      *     "school",
  667.      *     class = School::class,
  668.      * )
  669.      * @ParamConverter(
  670.      *     "template",
  671.      *     class = Template::class,
  672.      * )
  673.      * @ParamConverter(
  674.      *     "student",
  675.      *     class = Student::class,
  676.      * )
  677.      */
  678.     public function studentEmailPreviewAction(School $schoolTemplate $templateStudent $student): Response
  679.     {
  680.         // AUDIT
  681.         $this->denyAccessUnlessGranted(
  682.             sprintf('app.notifications.messaging.%s'$template->isUrgent() ? 'urgent' 'general')
  683.         );
  684.         // disable the profiler
  685.         $this->disableProfiling();
  686.         $merger = (new MessageContentGenerator())
  687.             ->setStudent($student)
  688.             ->setLocale($template->getLocale());
  689.         if ($template->getTenant() instanceof Tenant) {
  690.             $merger->setTimezone($template->getTenant()->getLocale()->getTimezone());
  691.         }
  692.         $message = new Message();
  693.         $message->setTenant($template->getTenant());
  694.         $message->setLocale($template->getLocale() ?? Locales::RFC4646_DEFAULT);
  695.         $message->setEmailFrom('');
  696.         $message->setEmailName('');
  697.         $message->setStatus(AbstractNotification::STATUSES__READY);
  698.         $message->setHtml(true);
  699.         $message->setDescription($template->getDescription());
  700.         $twigParameters $this->emailTemplateGeneratorFactory->create()
  701.             ->applyCore($message)
  702.             ->applySchool($school)
  703.             ->applyStudent($student);
  704.         $twigParameters->add(['translate' => '']);
  705.         $sendgridEmailMessage = (new SendgridEmailMessage())
  706.             ->setTenant($template->getTenant())
  707.             ->setSubject(html_entity_decode($merger->render($message->getEmailSubject()), ENT_QUOTES))
  708.             ->setBodyHtml($merger->render($message->getEmailContent()))
  709.             ->setTwigTemplate('@ProductsNotifications/emails/customer.html.twig')
  710.             ->setTwigParameters($twigParameters->all());
  711.         return new Response($this->getSendgridEmailMessenger()->render($sendgridEmailMessage));
  712.     }
  713.     /**
  714.      * @param Message $message
  715.      * @param string|null $locale
  716.      * @return Response
  717.      *
  718.      * @Route(
  719.      *     "/{message}/manage/email/preview/{school}/{locale}",
  720.      *     name = MessagesController::ROUTES__EMAIL_PREVIEW,
  721.      *     requirements = {
  722.      *         "message" = "[1-9]\d*",
  723.      *     },
  724.      *     defaults = {
  725.      *         "school" = null,
  726.      *         "locale" = Locales::RFC4646_DEFAULT,
  727.      *     },
  728.      * )
  729.      * @ParamConverter(
  730.      *     "message",
  731.      *     class = Message::class,
  732.      * )
  733.      * @ParamConverter(
  734.      *     "school",
  735.      *     class = School::class,
  736.      * )
  737.      */
  738.     public function emailPreviewAction(Message $messageSchool $school null, ?string $locale Locales::RFC4646_DEFAULT): Response
  739.     {
  740.         // AUDIT
  741.         $this->audit($message);
  742.         // disable the profiler
  743.         $this->disableProfiling();
  744.         // if empty school, load up from the message or load up the district
  745.         if ( ! $school) {
  746.             $school $message->getBranding() ?: $this->getEntityManager()->getRepository(School::class)->findOneBy([
  747.                 'type' => School::TYPES__DISTRICT,
  748.             ]);
  749.         }
  750.         if ( ! $school instanceof School) {
  751.             throw new \Exception();
  752.         }
  753.         $notification $message->getNotification($locale);
  754.         return new Response(
  755.             $this->getSendgridEmailMessenger()->render(
  756.                 (new SendgridEmailMessage())
  757.                     ->setTenant($message->getTenant())
  758.                     ->setTemplateRenderDisabled($message->isHtml())
  759.                     ->setBodyHtml($notification->getEmailContent())
  760.                     ->setTwigTemplate('@ProductsNotifications/emails/customer.html.twig')
  761.                     ->setTwigParameters(
  762.                         $this->emailTemplateGeneratorFactory->create()
  763.                             ->applyCore($message)
  764.                             ->applySchool($school)
  765.                             ->applyMessage($message)
  766.                             ->all()
  767.                     )
  768.             )
  769.         );
  770.     }
  771.     /**
  772.      * @param Message $message
  773.      * @return DocHtmlView
  774.      *
  775.      * @Route(
  776.      *     "/{message}/report",
  777.      *     name = MessagesController::ROUTES__REPORT,
  778.      *     requirements = {
  779.      *         "message" = "[1-9]\d*",
  780.      *     },
  781.      * )
  782.      * @ParamConverter(
  783.      *     "message",
  784.      *     class = Message::class,
  785.      * )
  786.      */
  787.     public function reportAction(Message $message): DocHtmlView
  788.     {
  789.         // AUDIT
  790.         $this->audit($message);
  791.         return $this->html([
  792.             'configs' => $this->mc->getConfigs(),
  793.             'message' => $message,
  794.         ]);
  795.     }
  796.     /**
  797.      * @param Message $message
  798.      * @param int $pagination
  799.      * @return DocHtmlView|RedirectResponse
  800.      *
  801.      * @Route(
  802.      *     "/{message}/logs/{pagination}",
  803.      *     name = MessagesController::ROUTES__LOGS,
  804.      *     requirements = {
  805.      *         "message" = "[1-9]\d*",
  806.      *         "pagination" = "[1-9]\d*",
  807.      *     },
  808.      *     defaults = {
  809.      *         "pagination" = 0,
  810.      *     },
  811.      * )
  812.      * @ParamConverter(
  813.      *     "message",
  814.      *     class = Message::class,
  815.      * )
  816.      */
  817.     public function logsAction(Message $messageint $pagination 0)
  818.     {
  819.         // AUDIT
  820.         $this->audit($message);
  821.         $result $this->doSearch(
  822.             AbstractContactAttempt::class,
  823.             'attempts',
  824.             (new ContactAttemptSearch())->setNotification($message),
  825.             ContactAttemptSearchForm::class,
  826.             $pagination,
  827.         );
  828.         return ($result instanceof Response) ? $result $this->html(
  829.             array_merge(
  830.                 $result,
  831.                 [
  832.                     'configs' => $this->mc->getConfigs(),
  833.                     'message' => $message,
  834.                 ],
  835.             ),
  836.         );
  837.     }
  838.     /**
  839.      * @param Message $message
  840.      * @return DocHtmlView
  841.      *
  842.      * @Route(
  843.      *     "/{message}/debugging",
  844.      *     name = MessagesController::ROUTES__DEBUGGING,
  845.      *     requirements = {
  846.      *         "message" = "[1-9]\d*",
  847.      *     },
  848.      * )
  849.      * @ParamConverter(
  850.      *     "message",
  851.      *     class = Message::class,
  852.      * )
  853.      */
  854.     public function debuggingAction(Message $message): DocHtmlView
  855.     {
  856.         // AUDIT
  857.         $this->audit($message);
  858.         return $this->html([
  859.             'configs' => $this->mc->getConfigs(),
  860.             'message' => $message,
  861.         ]);
  862.     }
  863.     /**
  864.      * @param Message $message
  865.      * @return AbstractHtmlView
  866.      *
  867.      * @Route(
  868.      *     "/{message}/filtered_logs/{pagination}",
  869.      *     name = self::ROUTES__FILTERED_LOGS,
  870.      *     requirements = {
  871.      *         "message" = "[1-9]\d*",
  872.      *         "pagination" = "[1-9]\d*",
  873.      *     },
  874.      *     defaults = {
  875.      *         "pagination" = 0,
  876.      *     },
  877.      * )
  878.      * @ParamConverter(
  879.      *     "message",
  880.      *     class = Message::class,
  881.      * )
  882.      */
  883.     public function filteredLogsAction(Message $messageint $pagination 0): AbstractHtmlView
  884.     {
  885.         // AUDIT
  886.         $this->audit($message);
  887.         $result $this->doSearch(
  888.             AbstractContactAttempt::class,
  889.             'attempts',
  890.             (new ContactAttemptSearch())->setNotification($message),
  891.             ContactAttemptSearchForm::class,
  892.             $pagination,
  893.         );
  894.         return $this->ajax(
  895.             array_merge(
  896.                 $result,
  897.                 [
  898.                     'configs' => $this->mc->getConfigs(),
  899.                     'message' => $message,
  900.                 ],
  901.             ),
  902.         );
  903.     }
  904.     /**
  905.      * @param Request $request
  906.      * @param Message $message
  907.      * @return AjaxHtmlView|JsonView
  908.      *
  909.      * @Route(
  910.      *     "/{message}/_test",
  911.      *     name = MessagesController::ROUTES__TEST,
  912.      *     requirements = {
  913.      *         "message" = "[1-9]\d*",
  914.      *     },
  915.      * )
  916.      * @ParamConverter(
  917.      *     "message",
  918.      *     class = Message::class,
  919.      * )
  920.      */
  921.     public function testAction(Request $requestMessage $message)
  922.     {
  923.         // AUDIT
  924.         $this->audit($message);
  925.         // enforce ajax
  926.         if ( ! $request->isXmlHttpRequest()) {
  927.             throw new NotFoundHttpException();
  928.         }
  929.         // determine the logged in user
  930.         $user $this->getGlobalContext()->getEffectiveAccount();
  931.         // create form to capture test contacts
  932.         $form $this->createForm(MessageTestForm::class, [
  933.             'contacts' => [
  934.                 array_values(array_filter([
  935.                     new EmailTester($user->getEmail()),
  936.                     ($user->getSystemProfile()->getMobilePhone()) ? new PhoneTester($user->getSystemProfile()->getMobilePhone()) : null,
  937.                 ]))
  938.             ],
  939.         ]);
  940.         // make form to handle the possibly new changes to the message as based on the manage page
  941.         $messageForm $this->createForm(MessageChannelsForm::class, $message);
  942.         
  943.         // handle contacts form
  944.         if ($this->handleForm($form)) {
  945.             $messageData = [];
  946.             parse_str($form->getData()['data'], $messageData);
  947.             // handle this form submission as well
  948.             // have to pull data from the query string
  949.             if ($this->submitForm($messageForm$messageData[$messageForm->getName()])) {
  950.                 // message should be setup as we need it, can now run the test
  951.                 try {
  952.                     $test $this->getMessageLogic()->test(
  953.                         $message,
  954.                         array_merge(...$form->getData()['contacts'])
  955.                     );
  956.                 } catch (\Exception $e) {
  957.                     $form->addError(new FormError('Error sending tests, not all tests may have been sent.'));
  958.                 }
  959.             }
  960.             // close the modal only if no errors
  961.             if ( ! $form->getErrors()->count()) {
  962.                 return $this->jsonView([
  963.                     'close' => true,
  964.                 ]);
  965.             }
  966.         }
  967.         return $this->ajax([
  968.             'message' => $message,
  969.             'form' => $form->createView(),
  970.             'messageFormName' => $messageForm->getName(),
  971.         ]);
  972.     }
  973.     /**
  974.      * @param Message $message
  975.      * @return DocHtmlView
  976.      *
  977.      * @Route(
  978.      *     "/{message}/details",
  979.      *     name = MessagesController::ROUTES__DETAILS,
  980.      *     requirements = {
  981.      *         "message" = "[1-9]\d*",
  982.      *     },
  983.      * )
  984.      * @ParamConverter(
  985.      *     "message",
  986.      *     class = Message::class,
  987.      * )
  988.      */
  989.     public function detailsAction(Message $message): DocHtmlView
  990.     {
  991.         // AUDIT
  992.         $this->denyAccessUnlessGranted(
  993.             sprintf('app.notifications.messaging.%s'$message->isUrgent() ? 'urgent' 'general')
  994.         );
  995.         return $this->html([
  996.             'message' => $message,
  997.             'humanReadableLocales' => Locales::RFC4646_HUMAN_READABLE
  998.         ]);
  999.     }
  1000.     /**
  1001.      * @return MessageLogic|object
  1002.      */
  1003.     private function getMessageLogic(): MessageLogic
  1004.     {
  1005.         return $this->get(__METHOD__);
  1006.     }
  1007.     /**
  1008.      * @return SendgridEmailMessenger|object
  1009.      */
  1010.     private function getSendgridEmailMessenger(): SendgridEmailMessenger
  1011.     {
  1012.         return $this->get(__METHOD__);
  1013.     }
  1014. }