src/Products/NotificationsBundle/Controller/Portal/ManagementController.php line 84

Open in your IDE?
  1. <?php
  2. namespace Products\NotificationsBundle\Controller\Portal;
  3. use App\Component\ViewLayer\Views\DocHtmlView;
  4. use App\Component\ViewLayer\Views\JsonView;
  5. use App\Service\Data\PhoneNumberService;
  6. use Cms\FrontendBundle\Service\ResolverManager;
  7. use Cms\FrontendBundle\Service\Resolvers\SchoolResolver;
  8. use Products\NotificationsBundle\Controller\AbstractPortalController;
  9. use Products\NotificationsBundle\Entity\PortalLoginAttempt;
  10. use Products\NotificationsBundle\Entity\Profile;
  11. use Products\NotificationsBundle\Entity\ProfileContact;
  12. use Products\NotificationsBundle\Entity\Recipients\AppRecipient;
  13. use Products\NotificationsBundle\Service\PortalService;
  14. use Products\NotificationsBundle\Service\ProfileLogic;
  15. use Products\NotificationsBundle\Util\Preferences;
  16. use Sensio\Bundle\FrameworkExtraBundle\Configuration\ParamConverter;
  17. use Symfony\Component\EventDispatcher\EventSubscriberInterface;
  18. use Symfony\Component\Form\Extension\Core\Type\TextType;
  19. use Symfony\Component\Form\FormError;
  20. use Symfony\Component\HttpFoundation\RedirectResponse;
  21. use Symfony\Component\HttpFoundation\Request;
  22. use Symfony\Component\HttpFoundation\Response;
  23. use Symfony\Component\HttpKernel\Event\ControllerEvent;
  24. use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
  25. use Symfony\Component\HttpKernel\KernelEvents;
  26. use Symfony\Component\Routing\Annotation\Route;
  27. use Symfony\Component\Validator\Constraints\Callback;
  28. use Symfony\Component\Validator\Constraints\Email;
  29. use Symfony\Component\Validator\Constraints\NotBlank;
  30. use Symfony\Component\Validator\Constraints\NotNull;
  31. use Symfony\Component\Validator\Context\ExecutionContextInterface;
  32. /**
  33.  * Class ManagementController
  34.  * @package Products\NotificationsBundle\Controller\Portal
  35.  *
  36.  * @Route(
  37.  *     "/notifications",
  38.  * )
  39.  */
  40. final class ManagementController
  41.     extends AbstractPortalController
  42.     implements EventSubscriberInterface
  43. {
  44.     const ROUTES__FIREBASE 'app.notifications.portal.management.firebase';
  45.     const ROUTES__MAIN 'app.notifications.portal.management.main';
  46.     const ROUTES__TOGGLE_ENABLED 'app.notifications.portal.management.toggle_enabled';
  47.     const ROUTES__TOGGLE_PRIMARY_PREFERENCES 'app.notifications.portal.management.toggle_primary_preferences';
  48.     const ROUTES__TOGGLE_SECONDARY_PREFERENCES 'app.notifications.portal.management.toggle_secondary_preferences';
  49.     const ROUTES__ADD_CONTACT__CHOOSE 'app.notifications.portal.management.add_contact.choose';
  50.     const ROUTES__ADD_CONTACT__INPUT 'app.notifications.portal.management.add_contact.input';
  51.     const ROUTES__ADD_CONTACT__VERIFY 'app.notifications.portal.management.add_contact.verify';
  52.     const ROUTES__CHANGE_CONTACT__INPUT 'app.notifications.portal.management.change_contact.input';
  53.     const ROUTES__CHANGE_CONTACT__VERIFY 'app.notifications.portal.management.change_contact.verify';
  54.     protected SchoolResolver $schoolResolver;
  55.     /**
  56.      * @param SchoolResolver $schoolResolver
  57.      */
  58.     public function __construct(SchoolResolver $schoolResolver)
  59.     {
  60.         $this->schoolResolver $schoolResolver;
  61.     }
  62.     /**
  63.      * {@inheritDoc}
  64.      */
  65.     public static function getSubscribedEvents(): array
  66.     {
  67.         return [
  68.             KernelEvents::CONTROLLER => [
  69.                 ['onKernelController'0],
  70.             ],
  71.         ];
  72.     }
  73.     /**
  74.      * @param ControllerEvent $event
  75.      * @return void
  76.      */
  77.     public function onKernelController(ControllerEvent $event): void
  78.     {
  79.         // get controller
  80.         $controller $event->getController();
  81.         if (is_array($controller)) {
  82.             $controller $controller[0];
  83.         }
  84.         // make sure it is us
  85.         if ($controller instanceof self) {
  86.             // get user
  87.             $profile $this->getCurrentUser();
  88.             if ( ! $profile instanceof Profile) {
  89.                 throw new \Exception();
  90.             }
  91.             // first check and see if we need to perform a checkup
  92.             if ( ! $profile->getCheckups()->count()) {
  93.                 $event->setController(
  94.                     function () {
  95.                         return $this->redirectToRoute(CheckupController::ROUTES__START);
  96.                     }
  97.                 );
  98.                 return;
  99.             }
  100.             // if we have an unhandled checkup, we want to lock people out
  101.             if ($profile->getUnhandledCheckup()) {
  102.                 $event->setController(
  103.                     function () {
  104.                         return $this->redirectToRoute(CheckupController::ROUTES__LOCKED);
  105.                     }
  106.                 );
  107.                 return;
  108.             }
  109.         }
  110.     }
  111.     /**
  112.      * @param Request $request
  113.      * @return DocHtmlView|Response
  114.      *
  115.      * @Route(
  116.      *     "/firebase",
  117.      *     name = self::ROUTES__FIREBASE,
  118.      *     methods = {"GET","POST","DELETE"}
  119.      * )
  120.      */
  121.     public function firebaseAction(Request $request)
  122.     {
  123.         switch ($request->getMethod()) {
  124.             case 'GET':
  125.                 return $this->html([]);
  126.             case 'POST':
  127.                 $this->getPortalService()->registerPush(
  128.                     $this->getCurrentUser(),
  129.                     $this->getCurrentUser()->getTenant()->getUidString(),
  130.                     $request->request->get('token'),
  131.                     'web',
  132.                     'Firebase Web Test',
  133.                 );
  134.                 return $this->resp(200);
  135.             case 'DELETE':
  136.                 $recip $this->getEntityManager()->getRepository(AppRecipient::class)->findOneBy([
  137.                     'installation' => $this->getCurrentUser()->getTenant()->getUidString(),
  138.                     'contact' => $request->request->get('token'),
  139.                     'platform' => AppRecipient::PLATFORMS__WEB,
  140.                 ]);
  141.                 if ( ! $recip) {
  142.                     return $this->resp(404);
  143.                 }
  144.                 $this->getEntityManager()->delete($recip);
  145.                 return $this->resp(200);
  146.         }
  147.         throw new \Exception();
  148.     }
  149.     /**
  150.      * @return DocHtmlView
  151.      *
  152.      * @Route(
  153.      *     "",
  154.      *     name = self::ROUTES__MAIN,
  155.      * )
  156.      */
  157.     public function mainAction(): DocHtmlView
  158.     {
  159.         return $this->html([
  160.             'profile' => $this->getCurrentUser(),
  161.             'contacts' => $this->getEntityManager()->getRepository(ProfileContact::class)
  162.                 ->findByProfile($this->getCurrentUser()),
  163.             'family' => ($this->getCurrentUser()->isRoleFamily())
  164.                 ? $this->getEntityManager()->getRepository(Profile::class)->findByFamily(
  165.                     $this->getCurrentUser()
  166.                 )
  167.                 : [],
  168.             'relationships' => ($this->getCurrentUser()->isRoleFamily())
  169.                 ? $this->getCurrentUser()->getRelationships()
  170.                 : [],
  171.             'schools' => $this->schoolResolver->resolveSchoolsByStudents($this->getCurrentUser()->getStudents()->toArray()),
  172.         ]);
  173.     }
  174.     /**
  175.      * @param Request $request
  176.      * @param ProfileContact $contact
  177.      * @return JsonView
  178.      *
  179.      * @Route(
  180.      *     "/_toggle_enabled/{contact}",
  181.      *     name = self::ROUTES__TOGGLE_ENABLED,
  182.      *     methods = {"POST"},
  183.      *     requirements = {
  184.      *         "contact" = "[1-9]\d*",
  185.      *     },
  186.      * )
  187.      * @ParamConverter(
  188.      *     "contact",
  189.      *     class = ProfileContact::class,
  190.      * )
  191.      */
  192.     public function toggleEnabledAction(Request $requestProfileContact $contact): JsonView
  193.     {
  194.         // get profile
  195.         $profile $this->getCurrentUser();
  196.         if ( ! $profile instanceof Profile) {
  197.             throw new \Exception();
  198.         }
  199.         // make sure the contact is for the profile
  200.         if ($profile->getId() !== $contact->getProfile()->getId()) {
  201.             throw new \Exception();
  202.         }
  203.         // set the enabled setting
  204.         $this->getPortalService()->toggleEnabled(
  205.             $contact,
  206.             $request->request->getBoolean('value')
  207.         );
  208.         // return something for ajax call
  209.         return $this->jsonView([
  210.             'profile' => $profile->getId(),
  211.             'contact' => $contact->getId(),
  212.             'value' => $contact->isEnabled(),
  213.         ]);
  214.     }
  215.     /**
  216.      * @param Request $request
  217.      * @param ProfileContact $contact
  218.      * @return JsonView
  219.      *
  220.      * @Route(
  221.      *     "/_toggle_primary_preferences/{contact}",
  222.      *     name = self::ROUTES__TOGGLE_PRIMARY_PREFERENCES,
  223.      *     methods = {"POST"},
  224.      *     requirements = {
  225.      *         "contact" = "[1-9]\d*",
  226.      *     },
  227.      * )
  228.      * @ParamConverter(
  229.      *     "contact",
  230.      *     class = ProfileContact::class,
  231.      * )
  232.      */
  233.     public function togglePrimaryPreferencesAction(Request $requestProfileContact $contact): JsonView
  234.     {
  235.         // get profile
  236.         $profile $this->getCurrentUser();
  237.         if ( ! $profile instanceof Profile) {
  238.             throw new \Exception();
  239.         }
  240.         // make sure the contact is for the profile
  241.         if ($profile->getId() !== $contact->getProfile()->getId()) {
  242.             throw new \Exception();
  243.         }
  244.         // do the toggling
  245.         $this->getPortalService()->togglePrimaryPreference(
  246.             $contact,
  247.             $preference $request->request->getAlpha('preference'),
  248.             $request->request->getBoolean('value')
  249.         );
  250.         // return something for ajax call
  251.         return $this->jsonView([
  252.             'profile' => $profile->getId(),
  253.             'contact' => $contact->getId(),
  254.             'preference' => Preferences::identity($preference),
  255.             'value' => $contact->hasPrimaryPreference($preference),
  256.         ]);
  257.     }
  258.     /**
  259.      * @param Request $request
  260.      * @param ProfileContact $contact
  261.      * @return JsonView
  262.      *
  263.      * @Route(
  264.      *     "/_toggle_secondary_preferences/{contact}",
  265.      *     name = self::ROUTES__TOGGLE_SECONDARY_PREFERENCES,
  266.      *     methods = {"POST"},
  267.      *     requirements = {
  268.      *         "contact" = "[1-9]\d*",
  269.      *     },
  270.      * )
  271.      * @ParamConverter(
  272.      *     "contact",
  273.      *     class = ProfileContact::class,
  274.      * )
  275.      */
  276.     public function toggleSecondaryPreferencesAction(Request $requestProfileContact $contact): JsonView
  277.     {
  278.         // get profile
  279.         $profile $this->getCurrentUser();
  280.         if ( ! $profile instanceof Profile) {
  281.             throw new \Exception();
  282.         }
  283.         // make sure profile and contact matches
  284.         if ($profile->getId() !== $contact->getProfile()->getId()) {
  285.             throw new \Exception();
  286.         }
  287.         // do the toggling
  288.         $this->getPortalService()->toggleSecondaryPreference(
  289.             $contact,
  290.             $preference $request->request->getAlpha('preference'),
  291.             $request->request->getBoolean('value')
  292.         );
  293.         // return something for ajax call
  294.         return $this->jsonView([
  295.             'profile' => $profile->getId(),
  296.             'contact' => $contact->getId(),
  297.             'preference' => Preferences::identity($preference),
  298.             'value' => $contact->hasSecondaryPreference($preference),
  299.         ]);
  300.     }
  301.     /**
  302.      * @return DocHtmlView|RedirectResponse
  303.      *
  304.      * @Route(
  305.      *     "/add-contact",
  306.      *     name = self::ROUTES__ADD_CONTACT__CHOOSE,
  307.      * )
  308.      */
  309.     public function addContactChooseAction()
  310.     {
  311.         $district $this->getResolverManager()->getSchoolResolver()->resolveDistrictByTenant(
  312.             $this->getCurrentUser()
  313.         );
  314.         if ($district === null) {
  315.             throw  new NotFoundHttpException();
  316.         }
  317.         $details $district->getDetails();
  318.         if ($details->isContactManagement() && empty($details->getSisUrl())) {
  319.             return $this->redirectToRoute(self::ROUTES__ADD_CONTACT__INPUT);
  320.         }
  321.         return $this->html([
  322.             'district' => $district,
  323.         ]);
  324.     }
  325.     /**
  326.      * @param Request $request
  327.      * @return DocHtmlView|RedirectResponse
  328.      *
  329.      * @Route(
  330.      *     "/add-contact/input",
  331.      *     name = self::ROUTES__ADD_CONTACT__INPUT,
  332.      * )
  333.      */
  334.     public function addContactInputAction(Request $request)
  335.     {
  336.         $profile $this->getCurrentUser();
  337.         if ( ! $profile instanceof Profile) {
  338.             throw new \Exception();
  339.         }
  340.         $form $this
  341.             ->createFormBuilder([
  342.                 'input' => null,
  343.             ])
  344.             ->add('input'TextType::class, [
  345.                 'required' => true,
  346.                 'constraints' => [
  347.                     new NotNull(),
  348.                     new NotBlank(),
  349.                     // check for email pattern
  350.                     new Callback([
  351.                         'callback' => function (?string $inputExecutionContextInterface $context) {
  352.                             if (strpos($input'@') !== false) {
  353.                                 $violations $context->getValidator()->validate($input, [
  354.                                     new Email(),
  355.                                 ]);
  356.                                 if ($violations->count()) {
  357.                                     $context->getViolations()->addAll($violations);
  358.                                 }
  359.                             }
  360.                         },
  361.                     ]),
  362.                     // check for phone pattern
  363.                     new Callback([
  364.                         'callback' => function (?string $inputExecutionContextInterface $context) {
  365.                             if (strpos($input'@') === false) {
  366.                                 try {
  367.                                     $this->getPhoneNumberService()->normalize($input);
  368.                                 } catch (\Exception $e) {
  369.                                     $context->addViolation(
  370.                                         'Input is not a valid phone number format.'
  371.                                     );
  372.                                 }
  373.                             }
  374.                         },
  375.                     ]),
  376.                 ],
  377.             ])
  378.             ->getForm();
  379.         if ($this->handleForm($form)) {
  380.             $attempt $this->getPortalService()->triggerAddition(
  381.                 $profile,
  382.                 $form->getData()['input'],
  383.                 $request
  384.             );
  385.             if ($attempt) {
  386.                 return $this->redirectToRoute(self::ROUTES__ADD_CONTACT__VERIFY, [
  387.                     'attempt' => $attempt->getUidString(),
  388.                 ]);
  389.             }
  390.             $form->addError(new FormError(
  391.                 'This contact is already associated to your account.'
  392.             ));
  393.         }
  394.         return $this->html([
  395.             'form' => $form->createView(),
  396.         ]);
  397.     }
  398.     /**
  399.      * @param PortalLoginAttempt $attempt
  400.      * @return DocHtmlView|RedirectResponse
  401.      *
  402.      * @Route(
  403.      *     "/add-contact/verify/{attempt}",
  404.      *     name = self::ROUTES__ADD_CONTACT__VERIFY,
  405.      *     requirements = {
  406.      *         "attempt" = "[0-9a-fA-F]{8}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{12}",
  407.      *     },
  408.      * )
  409.      * @ParamConverter(
  410.      *     "attempt",
  411.      *     class = PortalLoginAttempt::class,
  412.      *     options = {
  413.      *         "repository_method" = "findOneByUid",
  414.      *     },
  415.      * )
  416.      */
  417.     public function addContactVerifyAction(PortalLoginAttempt $attempt)
  418.     {
  419.         $profile $this->getCurrentUser();
  420.         if ( ! $profile instanceof Profile) {
  421.             throw new \Exception();
  422.         }
  423.         if ($attempt->getProfile() !== $profile) {
  424.             throw new \Exception();
  425.         }
  426.         $form $this
  427.             ->createFormBuilder([
  428.                 'code' => null,
  429.             ])
  430.             ->add('code'TextType::class, [
  431.                 'required' => true,
  432.                 'constraints' => [
  433.                     new NotNull(),
  434.                     new NotBlank(),
  435.                 ],
  436.             ])
  437.             ->getForm();
  438.         if ($this->handleForm($form)) {
  439.             try {
  440.                 $this->getPortalService()->verifyAddition(
  441.                     $attempt,
  442.                     $form->getData()['code']
  443.                 );
  444.                 return $this->redirectToRoute(self::ROUTES__MAIN);
  445.             } catch (\Exception $e) {
  446.                 $form->addError(new FormError($e->getMessage()));
  447.             }
  448.         }
  449.         return $this->html([
  450.             'attempt' => $attempt,
  451.             'form' => $form->createView(),
  452.         ]);
  453.     }
  454.     /**
  455.      * @param Request $request
  456.      * @param ProfileContact $contact
  457.      * @return DocHtmlView|RedirectResponse
  458.      *
  459.      * @Route(
  460.      *     "/change-contact/{contact}/input",
  461.      *     name = self::ROUTES__CHANGE_CONTACT__INPUT,
  462.      *     requirements = {
  463.      *         "contact" = "[1-9]\d*",
  464.      *     },
  465.      * )
  466.      * @ParamConverter(
  467.      *     "contact",
  468.      *     class = ProfileContact::class,
  469.      * )
  470.      */
  471.     public function changeContactInputAction(Request $requestProfileContact $contact)
  472.     {
  473.         $profile $this->getCurrentUser();
  474.         if ( ! $profile instanceof Profile) {
  475.             throw new \Exception();
  476.         }
  477.         if ($contact->getProfile() !== $profile) {
  478.             throw new \Exception();
  479.         }
  480.         $form $this
  481.             ->createFormBuilder([
  482.                 'input' => $contact->getRecipient()->getContact(),
  483.             ])
  484.             ->add('input'TextType::class, [
  485.                 'required' => true,
  486.                 'attr' => [
  487.                     'autocomplete' => 'off',
  488.                     'inputmode' => ($contact->isPhone()) ? 'tel' 'email',
  489.                 ],
  490.                 'constraints' => array_values(array_filter([
  491.                     new NotNull(),
  492.                     new NotBlank(),
  493.                     // check for email pattern
  494.                     $contact->isEmail() ? new Callback([
  495.                         'callback' => function (?string $inputExecutionContextInterface $context) {
  496.                             $violations $context->getValidator()->validate($input, [
  497.                                 new Email(),
  498.                             ]);
  499.                             if ($violations->count()) {
  500.                                 $context->getViolations()->addAll($violations);
  501.                             }
  502.                         },
  503.                     ]) : null,
  504.                     // check for phone pattern
  505.                     $contact->isPhone() ? new Callback([
  506.                         'callback' => function (?string $inputExecutionContextInterface $context) {
  507.                             try {
  508.                                 $this->getPhoneNumberService()->normalize($input);
  509.                             } catch (\Exception $e) {
  510.                                 $context->addViolation(
  511.                                     'Input is not a valid phone number format.'
  512.                                 );
  513.                             }
  514.                         },
  515.                     ]) : null,
  516.                 ])),
  517.             ])
  518.             ->getForm();
  519.         if ($this->handleForm($form)) {
  520.             $attempt $this->getPortalService()->triggerAddition(
  521.                 $profile,
  522.                 $form->getData()['input'],
  523.                 $request
  524.             );
  525.             if ($attempt) {
  526.                 return $this->redirectToRoute(self::ROUTES__CHANGE_CONTACT__VERIFY, [
  527.                     'contact' => $contact->getId(),
  528.                     'attempt' => $attempt->getUidString(),
  529.                 ]);
  530.             }
  531.             $form->addError(new FormError(
  532.                 'This contact is already associated to your account.'
  533.             ));
  534.         }
  535.         return $this->html([
  536.             'contact' => $contact,
  537.             'form' => $form->createView(),
  538.         ]);
  539.     }
  540.     /**
  541.      * @param ProfileContact $contact
  542.      * @param PortalLoginAttempt $attempt
  543.      * @return DocHtmlView|RedirectResponse
  544.      *
  545.      * @Route(
  546.      *     "/change-contact/{contact}/verify/{attempt}",
  547.      *     name = self::ROUTES__CHANGE_CONTACT__VERIFY,
  548.      *     requirements = {
  549.      *         "contact" = "[1-9]\d*",
  550.      *         "attempt" = "[0-9a-fA-F]{8}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{12}",
  551.      *     },
  552.      * )
  553.      * @ParamConverter(
  554.      *     "contact",
  555.      *     class = ProfileContact::class,
  556.      * )
  557.      * @ParamConverter(
  558.      *     "attempt",
  559.      *     class = PortalLoginAttempt::class,
  560.      *     options = {
  561.      *         "repository_method" = "findOneByUid",
  562.      *     },
  563.      * )
  564.      */
  565.     public function changeContactVerifyAction(ProfileContact $contactPortalLoginAttempt $attempt)
  566.     {
  567.         $profile $this->getCurrentUser();
  568.         if ( ! $profile instanceof Profile) {
  569.             throw new \Exception();
  570.         }
  571.         if ($contact->getProfile() !== $profile) {
  572.             throw new \Exception();
  573.         }
  574.         if ($attempt->getProfile() !== $profile) {
  575.             throw new \Exception();
  576.         }
  577.         $form $this
  578.             ->createFormBuilder([
  579.                 'code' => null,
  580.             ])
  581.             ->add('code'TextType::class, [
  582.                 'required' => true,
  583.                 'constraints' => [
  584.                     new NotNull(),
  585.                     new NotBlank(),
  586.                 ],
  587.             ])
  588.             ->getForm();
  589.         if ($this->handleForm($form)) {
  590.             try {
  591.                 $this->getPortalService()->verifyAddition(
  592.                     $attempt,
  593.                     $form->getData()['code'],
  594.                     $contact
  595.                 );
  596.                 return $this->redirectToRoute(self::ROUTES__MAIN);
  597.             } catch (\Exception $e) {
  598.                 $form->addError(new FormError($e->getMessage()));
  599.             }
  600.         }
  601.         return $this->html([
  602.             'attempt' => $attempt,
  603.             'form' => $form->createView(),
  604.         ]);
  605.     }
  606.     /**
  607.      * @return PortalService|object
  608.      */
  609.     private function getPortalService(): PortalService
  610.     {
  611.         return $this->get(__METHOD__);
  612.     }
  613.     /**
  614.      * @return ResolverManager|object
  615.      */
  616.     private function getResolverManager(): ResolverManager
  617.     {
  618.         return $this->get(__METHOD__);
  619.     }
  620.     /**
  621.      * @return PhoneNumberService|object
  622.      */
  623.     private function getPhoneNumberService(): PhoneNumberService
  624.     {
  625.         return $this->get(__METHOD__);
  626.     }
  627. }