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' => [],
  164. //            'family' => ($this->getCurrentUser()->isRoleFamily())
  165. //                ? $this->getEntityManager()->getRepository(Profile::class)->findByFamily(
  166. //                    $this->getCurrentUser()
  167. //                )
  168. //                : [],
  169.             'relationships' => ($this->getCurrentUser()->isRoleFamily())
  170.                 ? $this->getCurrentUser()->getRelationships()
  171.                 : [],
  172.             'schools' => $this->schoolResolver->resolveSchoolsByStudents($this->getCurrentUser()->getStudents()->toArray()),
  173.         ]);
  174.     }
  175.     /**
  176.      * @param Request $request
  177.      * @param ProfileContact $contact
  178.      * @return JsonView
  179.      *
  180.      * @Route(
  181.      *     "/_toggle_enabled/{contact}",
  182.      *     name = self::ROUTES__TOGGLE_ENABLED,
  183.      *     methods = {"POST"},
  184.      *     requirements = {
  185.      *         "contact" = "[1-9]\d*",
  186.      *     },
  187.      * )
  188.      * @ParamConverter(
  189.      *     "contact",
  190.      *     class = ProfileContact::class,
  191.      * )
  192.      */
  193.     public function toggleEnabledAction(Request $requestProfileContact $contact): JsonView
  194.     {
  195.         // get profile
  196.         $profile $this->getCurrentUser();
  197.         if ( ! $profile instanceof Profile) {
  198.             throw new \Exception();
  199.         }
  200.         // make sure the contact is for the profile
  201.         if ($profile->getId() !== $contact->getProfile()->getId()) {
  202.             throw new \Exception();
  203.         }
  204.         // set the enabled setting
  205.         $this->getPortalService()->toggleEnabled(
  206.             $contact,
  207.             $request->request->getBoolean('value')
  208.         );
  209.         // return something for ajax call
  210.         return $this->jsonView([
  211.             'profile' => $profile->getId(),
  212.             'contact' => $contact->getId(),
  213.             'value' => $contact->isEnabled(),
  214.         ]);
  215.     }
  216.     /**
  217.      * @param Request $request
  218.      * @param ProfileContact $contact
  219.      * @return JsonView
  220.      *
  221.      * @Route(
  222.      *     "/_toggle_primary_preferences/{contact}",
  223.      *     name = self::ROUTES__TOGGLE_PRIMARY_PREFERENCES,
  224.      *     methods = {"POST"},
  225.      *     requirements = {
  226.      *         "contact" = "[1-9]\d*",
  227.      *     },
  228.      * )
  229.      * @ParamConverter(
  230.      *     "contact",
  231.      *     class = ProfileContact::class,
  232.      * )
  233.      */
  234.     public function togglePrimaryPreferencesAction(Request $requestProfileContact $contact): JsonView
  235.     {
  236.         // get profile
  237.         $profile $this->getCurrentUser();
  238.         if ( ! $profile instanceof Profile) {
  239.             throw new \Exception();
  240.         }
  241.         // make sure the contact is for the profile
  242.         if ($profile->getId() !== $contact->getProfile()->getId()) {
  243.             throw new \Exception();
  244.         }
  245.         // do the toggling
  246.         $this->getPortalService()->togglePrimaryPreference(
  247.             $contact,
  248.             $preference $request->request->getAlpha('preference'),
  249.             $request->request->getBoolean('value')
  250.         );
  251.         // return something for ajax call
  252.         return $this->jsonView([
  253.             'profile' => $profile->getId(),
  254.             'contact' => $contact->getId(),
  255.             'preference' => Preferences::identity($preference),
  256.             'value' => $contact->hasPrimaryPreference($preference),
  257.         ]);
  258.     }
  259.     /**
  260.      * @param Request $request
  261.      * @param ProfileContact $contact
  262.      * @return JsonView
  263.      *
  264.      * @Route(
  265.      *     "/_toggle_secondary_preferences/{contact}",
  266.      *     name = self::ROUTES__TOGGLE_SECONDARY_PREFERENCES,
  267.      *     methods = {"POST"},
  268.      *     requirements = {
  269.      *         "contact" = "[1-9]\d*",
  270.      *     },
  271.      * )
  272.      * @ParamConverter(
  273.      *     "contact",
  274.      *     class = ProfileContact::class,
  275.      * )
  276.      */
  277.     public function toggleSecondaryPreferencesAction(Request $requestProfileContact $contact): JsonView
  278.     {
  279.         // get profile
  280.         $profile $this->getCurrentUser();
  281.         if ( ! $profile instanceof Profile) {
  282.             throw new \Exception();
  283.         }
  284.         // make sure profile and contact matches
  285.         if ($profile->getId() !== $contact->getProfile()->getId()) {
  286.             throw new \Exception();
  287.         }
  288.         // do the toggling
  289.         $this->getPortalService()->toggleSecondaryPreference(
  290.             $contact,
  291.             $preference $request->request->getAlpha('preference'),
  292.             $request->request->getBoolean('value')
  293.         );
  294.         // return something for ajax call
  295.         return $this->jsonView([
  296.             'profile' => $profile->getId(),
  297.             'contact' => $contact->getId(),
  298.             'preference' => Preferences::identity($preference),
  299.             'value' => $contact->hasSecondaryPreference($preference),
  300.         ]);
  301.     }
  302.     /**
  303.      * @return DocHtmlView|RedirectResponse
  304.      *
  305.      * @Route(
  306.      *     "/add-contact",
  307.      *     name = self::ROUTES__ADD_CONTACT__CHOOSE,
  308.      * )
  309.      */
  310.     public function addContactChooseAction()
  311.     {
  312.         $district $this->getResolverManager()->getSchoolResolver()->resolveDistrictByTenant(
  313.             $this->getCurrentUser()
  314.         );
  315.         if ($district === null) {
  316.             throw  new NotFoundHttpException();
  317.         }
  318.         $details $district->getDetails();
  319.         if ($details->isContactManagement() && empty($details->getSisUrl())) {
  320.             return $this->redirectToRoute(self::ROUTES__ADD_CONTACT__INPUT);
  321.         }
  322.         return $this->html([
  323.             'district' => $district,
  324.         ]);
  325.     }
  326.     /**
  327.      * @param Request $request
  328.      * @return DocHtmlView|RedirectResponse
  329.      *
  330.      * @Route(
  331.      *     "/add-contact/input",
  332.      *     name = self::ROUTES__ADD_CONTACT__INPUT,
  333.      * )
  334.      */
  335.     public function addContactInputAction(Request $request)
  336.     {
  337.         $profile $this->getCurrentUser();
  338.         if ( ! $profile instanceof Profile) {
  339.             throw new \Exception();
  340.         }
  341.         $form $this
  342.             ->createFormBuilder([
  343.                 'input' => null,
  344.             ])
  345.             ->add('input'TextType::class, [
  346.                 'required' => true,
  347.                 'constraints' => [
  348.                     new NotNull(),
  349.                     new NotBlank(),
  350.                     // check for email pattern
  351.                     new Callback([
  352.                         'callback' => function (?string $inputExecutionContextInterface $context) {
  353.                             if (strpos($input'@') !== false) {
  354.                                 $violations $context->getValidator()->validate($input, [
  355.                                     new Email(),
  356.                                 ]);
  357.                                 if ($violations->count()) {
  358.                                     $context->getViolations()->addAll($violations);
  359.                                 }
  360.                             }
  361.                         },
  362.                     ]),
  363.                     // check for phone pattern
  364.                     new Callback([
  365.                         'callback' => function (?string $inputExecutionContextInterface $context) {
  366.                             if (strpos($input'@') === false) {
  367.                                 try {
  368.                                     $this->getPhoneNumberService()->normalize($input);
  369.                                 } catch (\Exception $e) {
  370.                                     $context->addViolation(
  371.                                         'Input is not a valid phone number format.'
  372.                                     );
  373.                                 }
  374.                             }
  375.                         },
  376.                     ]),
  377.                 ],
  378.             ])
  379.             ->getForm();
  380.         if ($this->handleForm($form)) {
  381.             $attempt $this->getPortalService()->triggerAddition(
  382.                 $profile,
  383.                 $form->getData()['input'],
  384.                 $request
  385.             );
  386.             if ($attempt) {
  387.                 return $this->redirectToRoute(self::ROUTES__ADD_CONTACT__VERIFY, [
  388.                     'attempt' => $attempt->getUidString(),
  389.                 ]);
  390.             }
  391.             $form->addError(new FormError(
  392.                 'This contact is already associated to your account.'
  393.             ));
  394.         }
  395.         return $this->html([
  396.             'form' => $form->createView(),
  397.         ]);
  398.     }
  399.     /**
  400.      * @param PortalLoginAttempt $attempt
  401.      * @return DocHtmlView|RedirectResponse
  402.      *
  403.      * @Route(
  404.      *     "/add-contact/verify/{attempt}",
  405.      *     name = self::ROUTES__ADD_CONTACT__VERIFY,
  406.      *     requirements = {
  407.      *         "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}",
  408.      *     },
  409.      * )
  410.      * @ParamConverter(
  411.      *     "attempt",
  412.      *     class = PortalLoginAttempt::class,
  413.      *     options = {
  414.      *         "repository_method" = "findOneByUid",
  415.      *     },
  416.      * )
  417.      */
  418.     public function addContactVerifyAction(PortalLoginAttempt $attempt)
  419.     {
  420.         $profile $this->getCurrentUser();
  421.         if ( ! $profile instanceof Profile) {
  422.             throw new \Exception();
  423.         }
  424.         if ($attempt->getProfile() !== $profile) {
  425.             throw new \Exception();
  426.         }
  427.         $form $this
  428.             ->createFormBuilder([
  429.                 'code' => null,
  430.             ])
  431.             ->add('code'TextType::class, [
  432.                 'required' => true,
  433.                 'constraints' => [
  434.                     new NotNull(),
  435.                     new NotBlank(),
  436.                 ],
  437.             ])
  438.             ->getForm();
  439.         if ($this->handleForm($form)) {
  440.             try {
  441.                 $this->getPortalService()->verifyAddition(
  442.                     $attempt,
  443.                     $form->getData()['code']
  444.                 );
  445.                 return $this->redirectToRoute(self::ROUTES__MAIN);
  446.             } catch (\Exception $e) {
  447.                 $form->addError(new FormError($e->getMessage()));
  448.             }
  449.         }
  450.         return $this->html([
  451.             'attempt' => $attempt,
  452.             'form' => $form->createView(),
  453.         ]);
  454.     }
  455.     /**
  456.      * @param Request $request
  457.      * @param ProfileContact $contact
  458.      * @return DocHtmlView|RedirectResponse
  459.      *
  460.      * @Route(
  461.      *     "/change-contact/{contact}/input",
  462.      *     name = self::ROUTES__CHANGE_CONTACT__INPUT,
  463.      *     requirements = {
  464.      *         "contact" = "[1-9]\d*",
  465.      *     },
  466.      * )
  467.      * @ParamConverter(
  468.      *     "contact",
  469.      *     class = ProfileContact::class,
  470.      * )
  471.      */
  472.     public function changeContactInputAction(Request $requestProfileContact $contact)
  473.     {
  474.         $profile $this->getCurrentUser();
  475.         if ( ! $profile instanceof Profile) {
  476.             throw new \Exception();
  477.         }
  478.         if ($contact->getProfile() !== $profile) {
  479.             throw new \Exception();
  480.         }
  481.         $form $this
  482.             ->createFormBuilder([
  483.                 'input' => $contact->getRecipient()->getContact(),
  484.             ])
  485.             ->add('input'TextType::class, [
  486.                 'required' => true,
  487.                 'attr' => [
  488.                     'autocomplete' => 'off',
  489.                     'inputmode' => ($contact->isPhone()) ? 'tel' 'email',
  490.                 ],
  491.                 'constraints' => array_values(array_filter([
  492.                     new NotNull(),
  493.                     new NotBlank(),
  494.                     // check for email pattern
  495.                     $contact->isEmail() ? new Callback([
  496.                         'callback' => function (?string $inputExecutionContextInterface $context) {
  497.                             $violations $context->getValidator()->validate($input, [
  498.                                 new Email(),
  499.                             ]);
  500.                             if ($violations->count()) {
  501.                                 $context->getViolations()->addAll($violations);
  502.                             }
  503.                         },
  504.                     ]) : null,
  505.                     // check for phone pattern
  506.                     $contact->isPhone() ? new Callback([
  507.                         'callback' => function (?string $inputExecutionContextInterface $context) {
  508.                             try {
  509.                                 $this->getPhoneNumberService()->normalize($input);
  510.                             } catch (\Exception $e) {
  511.                                 $context->addViolation(
  512.                                     'Input is not a valid phone number format.'
  513.                                 );
  514.                             }
  515.                         },
  516.                     ]) : null,
  517.                 ])),
  518.             ])
  519.             ->getForm();
  520.         if ($this->handleForm($form)) {
  521.             $attempt $this->getPortalService()->triggerAddition(
  522.                 $profile,
  523.                 $form->getData()['input'],
  524.                 $request
  525.             );
  526.             if ($attempt) {
  527.                 return $this->redirectToRoute(self::ROUTES__CHANGE_CONTACT__VERIFY, [
  528.                     'contact' => $contact->getId(),
  529.                     'attempt' => $attempt->getUidString(),
  530.                 ]);
  531.             }
  532.             $form->addError(new FormError(
  533.                 'This contact is already associated to your account.'
  534.             ));
  535.         }
  536.         return $this->html([
  537.             'contact' => $contact,
  538.             'form' => $form->createView(),
  539.         ]);
  540.     }
  541.     /**
  542.      * @param ProfileContact $contact
  543.      * @param PortalLoginAttempt $attempt
  544.      * @return DocHtmlView|RedirectResponse
  545.      *
  546.      * @Route(
  547.      *     "/change-contact/{contact}/verify/{attempt}",
  548.      *     name = self::ROUTES__CHANGE_CONTACT__VERIFY,
  549.      *     requirements = {
  550.      *         "contact" = "[1-9]\d*",
  551.      *         "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}",
  552.      *     },
  553.      * )
  554.      * @ParamConverter(
  555.      *     "contact",
  556.      *     class = ProfileContact::class,
  557.      * )
  558.      * @ParamConverter(
  559.      *     "attempt",
  560.      *     class = PortalLoginAttempt::class,
  561.      *     options = {
  562.      *         "repository_method" = "findOneByUid",
  563.      *     },
  564.      * )
  565.      */
  566.     public function changeContactVerifyAction(ProfileContact $contactPortalLoginAttempt $attempt)
  567.     {
  568.         $profile $this->getCurrentUser();
  569.         if ( ! $profile instanceof Profile) {
  570.             throw new \Exception();
  571.         }
  572.         if ($contact->getProfile() !== $profile) {
  573.             throw new \Exception();
  574.         }
  575.         if ($attempt->getProfile() !== $profile) {
  576.             throw new \Exception();
  577.         }
  578.         $form $this
  579.             ->createFormBuilder([
  580.                 'code' => null,
  581.             ])
  582.             ->add('code'TextType::class, [
  583.                 'required' => true,
  584.                 'constraints' => [
  585.                     new NotNull(),
  586.                     new NotBlank(),
  587.                 ],
  588.             ])
  589.             ->getForm();
  590.         if ($this->handleForm($form)) {
  591.             try {
  592.                 $this->getPortalService()->verifyAddition(
  593.                     $attempt,
  594.                     $form->getData()['code'],
  595.                     $contact
  596.                 );
  597.                 return $this->redirectToRoute(self::ROUTES__MAIN);
  598.             } catch (\Exception $e) {
  599.                 $form->addError(new FormError($e->getMessage()));
  600.             }
  601.         }
  602.         return $this->html([
  603.             'attempt' => $attempt,
  604.             'form' => $form->createView(),
  605.         ]);
  606.     }
  607.     /**
  608.      * @return PortalService|object
  609.      */
  610.     private function getPortalService(): PortalService
  611.     {
  612.         return $this->get(__METHOD__);
  613.     }
  614.     /**
  615.      * @return ResolverManager|object
  616.      */
  617.     private function getResolverManager(): ResolverManager
  618.     {
  619.         return $this->get(__METHOD__);
  620.     }
  621.     /**
  622.      * @return PhoneNumberService|object
  623.      */
  624.     private function getPhoneNumberService(): PhoneNumberService
  625.     {
  626.         return $this->get(__METHOD__);
  627.     }
  628. }