<?php
namespace Products\NotificationsBundle\Controller\Portal;
use App\Component\ViewLayer\Views\DocHtmlView;
use App\Component\ViewLayer\Views\JsonView;
use App\Service\Data\PhoneNumberService;
use Cms\FrontendBundle\Service\ResolverManager;
use Cms\FrontendBundle\Service\Resolvers\SchoolResolver;
use Products\NotificationsBundle\Controller\AbstractPortalController;
use Products\NotificationsBundle\Entity\PortalLoginAttempt;
use Products\NotificationsBundle\Entity\Profile;
use Products\NotificationsBundle\Entity\ProfileContact;
use Products\NotificationsBundle\Entity\Recipients\AppRecipient;
use Products\NotificationsBundle\Service\PortalService;
use Products\NotificationsBundle\Service\ProfileLogic;
use Products\NotificationsBundle\Util\Preferences;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\ParamConverter;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\Form\Extension\Core\Type\TextType;
use Symfony\Component\Form\FormError;
use Symfony\Component\HttpFoundation\RedirectResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\Event\ControllerEvent;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
use Symfony\Component\HttpKernel\KernelEvents;
use Symfony\Component\Routing\Annotation\Route;
use Symfony\Component\Validator\Constraints\Callback;
use Symfony\Component\Validator\Constraints\Email;
use Symfony\Component\Validator\Constraints\NotBlank;
use Symfony\Component\Validator\Constraints\NotNull;
use Symfony\Component\Validator\Context\ExecutionContextInterface;
/**
* Class ManagementController
* @package Products\NotificationsBundle\Controller\Portal
*
* @Route(
* "/notifications",
* )
*/
final class ManagementController
extends AbstractPortalController
implements EventSubscriberInterface
{
const ROUTES__FIREBASE = 'app.notifications.portal.management.firebase';
const ROUTES__MAIN = 'app.notifications.portal.management.main';
const ROUTES__TOGGLE_ENABLED = 'app.notifications.portal.management.toggle_enabled';
const ROUTES__TOGGLE_PRIMARY_PREFERENCES = 'app.notifications.portal.management.toggle_primary_preferences';
const ROUTES__TOGGLE_SECONDARY_PREFERENCES = 'app.notifications.portal.management.toggle_secondary_preferences';
const ROUTES__ADD_CONTACT__CHOOSE = 'app.notifications.portal.management.add_contact.choose';
const ROUTES__ADD_CONTACT__INPUT = 'app.notifications.portal.management.add_contact.input';
const ROUTES__ADD_CONTACT__VERIFY = 'app.notifications.portal.management.add_contact.verify';
const ROUTES__CHANGE_CONTACT__INPUT = 'app.notifications.portal.management.change_contact.input';
const ROUTES__CHANGE_CONTACT__VERIFY = 'app.notifications.portal.management.change_contact.verify';
protected SchoolResolver $schoolResolver;
/**
* @param SchoolResolver $schoolResolver
*/
public function __construct(SchoolResolver $schoolResolver)
{
$this->schoolResolver = $schoolResolver;
}
/**
* {@inheritDoc}
*/
public static function getSubscribedEvents(): array
{
return [
KernelEvents::CONTROLLER => [
['onKernelController', 0],
],
];
}
/**
* @param ControllerEvent $event
* @return void
*/
public function onKernelController(ControllerEvent $event): void
{
// get controller
$controller = $event->getController();
if (is_array($controller)) {
$controller = $controller[0];
}
// make sure it is us
if ($controller instanceof self) {
// get user
$profile = $this->getCurrentUser();
if ( ! $profile instanceof Profile) {
throw new \Exception();
}
// first check and see if we need to perform a checkup
if ( ! $profile->getCheckups()->count()) {
$event->setController(
function () {
return $this->redirectToRoute(CheckupController::ROUTES__START);
}
);
return;
}
// if we have an unhandled checkup, we want to lock people out
if ($profile->getUnhandledCheckup()) {
$event->setController(
function () {
return $this->redirectToRoute(CheckupController::ROUTES__LOCKED);
}
);
return;
}
}
}
/**
* @param Request $request
* @return DocHtmlView|Response
*
* @Route(
* "/firebase",
* name = self::ROUTES__FIREBASE,
* methods = {"GET","POST","DELETE"}
* )
*/
public function firebaseAction(Request $request)
{
switch ($request->getMethod()) {
case 'GET':
return $this->html([]);
case 'POST':
$this->getPortalService()->registerPush(
$this->getCurrentUser(),
$this->getCurrentUser()->getTenant()->getUidString(),
$request->request->get('token'),
'web',
'Firebase Web Test',
);
return $this->resp(200);
case 'DELETE':
$recip = $this->getEntityManager()->getRepository(AppRecipient::class)->findOneBy([
'installation' => $this->getCurrentUser()->getTenant()->getUidString(),
'contact' => $request->request->get('token'),
'platform' => AppRecipient::PLATFORMS__WEB,
]);
if ( ! $recip) {
return $this->resp(404);
}
$this->getEntityManager()->delete($recip);
return $this->resp(200);
}
throw new \Exception();
}
/**
* @return DocHtmlView
*
* @Route(
* "",
* name = self::ROUTES__MAIN,
* )
*/
public function mainAction(): DocHtmlView
{
return $this->html([
'profile' => $this->getCurrentUser(),
'contacts' => $this->getEntityManager()->getRepository(ProfileContact::class)
->findByProfile($this->getCurrentUser()),
'family' => [],
// 'family' => ($this->getCurrentUser()->isRoleFamily())
// ? $this->getEntityManager()->getRepository(Profile::class)->findByFamily(
// $this->getCurrentUser()
// )
// : [],
'relationships' => ($this->getCurrentUser()->isRoleFamily())
? $this->getCurrentUser()->getRelationships()
: [],
'schools' => $this->schoolResolver->resolveSchoolsByStudents($this->getCurrentUser()->getStudents()->toArray()),
]);
}
/**
* @param Request $request
* @param ProfileContact $contact
* @return JsonView
*
* @Route(
* "/_toggle_enabled/{contact}",
* name = self::ROUTES__TOGGLE_ENABLED,
* methods = {"POST"},
* requirements = {
* "contact" = "[1-9]\d*",
* },
* )
* @ParamConverter(
* "contact",
* class = ProfileContact::class,
* )
*/
public function toggleEnabledAction(Request $request, ProfileContact $contact): JsonView
{
// get profile
$profile = $this->getCurrentUser();
if ( ! $profile instanceof Profile) {
throw new \Exception();
}
// make sure the contact is for the profile
if ($profile->getId() !== $contact->getProfile()->getId()) {
throw new \Exception();
}
// set the enabled setting
$this->getPortalService()->toggleEnabled(
$contact,
$request->request->getBoolean('value')
);
// return something for ajax call
return $this->jsonView([
'profile' => $profile->getId(),
'contact' => $contact->getId(),
'value' => $contact->isEnabled(),
]);
}
/**
* @param Request $request
* @param ProfileContact $contact
* @return JsonView
*
* @Route(
* "/_toggle_primary_preferences/{contact}",
* name = self::ROUTES__TOGGLE_PRIMARY_PREFERENCES,
* methods = {"POST"},
* requirements = {
* "contact" = "[1-9]\d*",
* },
* )
* @ParamConverter(
* "contact",
* class = ProfileContact::class,
* )
*/
public function togglePrimaryPreferencesAction(Request $request, ProfileContact $contact): JsonView
{
// get profile
$profile = $this->getCurrentUser();
if ( ! $profile instanceof Profile) {
throw new \Exception();
}
// make sure the contact is for the profile
if ($profile->getId() !== $contact->getProfile()->getId()) {
throw new \Exception();
}
// do the toggling
$this->getPortalService()->togglePrimaryPreference(
$contact,
$preference = $request->request->getAlpha('preference'),
$request->request->getBoolean('value')
);
// return something for ajax call
return $this->jsonView([
'profile' => $profile->getId(),
'contact' => $contact->getId(),
'preference' => Preferences::identity($preference),
'value' => $contact->hasPrimaryPreference($preference),
]);
}
/**
* @param Request $request
* @param ProfileContact $contact
* @return JsonView
*
* @Route(
* "/_toggle_secondary_preferences/{contact}",
* name = self::ROUTES__TOGGLE_SECONDARY_PREFERENCES,
* methods = {"POST"},
* requirements = {
* "contact" = "[1-9]\d*",
* },
* )
* @ParamConverter(
* "contact",
* class = ProfileContact::class,
* )
*/
public function toggleSecondaryPreferencesAction(Request $request, ProfileContact $contact): JsonView
{
// get profile
$profile = $this->getCurrentUser();
if ( ! $profile instanceof Profile) {
throw new \Exception();
}
// make sure profile and contact matches
if ($profile->getId() !== $contact->getProfile()->getId()) {
throw new \Exception();
}
// do the toggling
$this->getPortalService()->toggleSecondaryPreference(
$contact,
$preference = $request->request->getAlpha('preference'),
$request->request->getBoolean('value')
);
// return something for ajax call
return $this->jsonView([
'profile' => $profile->getId(),
'contact' => $contact->getId(),
'preference' => Preferences::identity($preference),
'value' => $contact->hasSecondaryPreference($preference),
]);
}
/**
* @return DocHtmlView|RedirectResponse
*
* @Route(
* "/add-contact",
* name = self::ROUTES__ADD_CONTACT__CHOOSE,
* )
*/
public function addContactChooseAction()
{
$district = $this->getResolverManager()->getSchoolResolver()->resolveDistrictByTenant(
$this->getCurrentUser()
);
if ($district === null) {
throw new NotFoundHttpException();
}
$details = $district->getDetails();
if ($details->isContactManagement() && empty($details->getSisUrl())) {
return $this->redirectToRoute(self::ROUTES__ADD_CONTACT__INPUT);
}
return $this->html([
'district' => $district,
]);
}
/**
* @param Request $request
* @return DocHtmlView|RedirectResponse
*
* @Route(
* "/add-contact/input",
* name = self::ROUTES__ADD_CONTACT__INPUT,
* )
*/
public function addContactInputAction(Request $request)
{
$profile = $this->getCurrentUser();
if ( ! $profile instanceof Profile) {
throw new \Exception();
}
$form = $this
->createFormBuilder([
'input' => null,
])
->add('input', TextType::class, [
'required' => true,
'constraints' => [
new NotNull(),
new NotBlank(),
// check for email pattern
new Callback([
'callback' => function (?string $input, ExecutionContextInterface $context) {
if (strpos($input, '@') !== false) {
$violations = $context->getValidator()->validate($input, [
new Email(),
]);
if ($violations->count()) {
$context->getViolations()->addAll($violations);
}
}
},
]),
// check for phone pattern
new Callback([
'callback' => function (?string $input, ExecutionContextInterface $context) {
if (strpos($input, '@') === false) {
try {
$this->getPhoneNumberService()->normalize($input);
} catch (\Exception $e) {
$context->addViolation(
'Input is not a valid phone number format.'
);
}
}
},
]),
],
])
->getForm();
if ($this->handleForm($form)) {
$attempt = $this->getPortalService()->triggerAddition(
$profile,
$form->getData()['input'],
$request
);
if ($attempt) {
return $this->redirectToRoute(self::ROUTES__ADD_CONTACT__VERIFY, [
'attempt' => $attempt->getUidString(),
]);
}
$form->addError(new FormError(
'This contact is already associated to your account.'
));
}
return $this->html([
'form' => $form->createView(),
]);
}
/**
* @param PortalLoginAttempt $attempt
* @return DocHtmlView|RedirectResponse
*
* @Route(
* "/add-contact/verify/{attempt}",
* name = self::ROUTES__ADD_CONTACT__VERIFY,
* requirements = {
* "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}",
* },
* )
* @ParamConverter(
* "attempt",
* class = PortalLoginAttempt::class,
* options = {
* "repository_method" = "findOneByUid",
* },
* )
*/
public function addContactVerifyAction(PortalLoginAttempt $attempt)
{
$profile = $this->getCurrentUser();
if ( ! $profile instanceof Profile) {
throw new \Exception();
}
if ($attempt->getProfile() !== $profile) {
throw new \Exception();
}
$form = $this
->createFormBuilder([
'code' => null,
])
->add('code', TextType::class, [
'required' => true,
'constraints' => [
new NotNull(),
new NotBlank(),
],
])
->getForm();
if ($this->handleForm($form)) {
try {
$this->getPortalService()->verifyAddition(
$attempt,
$form->getData()['code']
);
return $this->redirectToRoute(self::ROUTES__MAIN);
} catch (\Exception $e) {
$form->addError(new FormError($e->getMessage()));
}
}
return $this->html([
'attempt' => $attempt,
'form' => $form->createView(),
]);
}
/**
* @param Request $request
* @param ProfileContact $contact
* @return DocHtmlView|RedirectResponse
*
* @Route(
* "/change-contact/{contact}/input",
* name = self::ROUTES__CHANGE_CONTACT__INPUT,
* requirements = {
* "contact" = "[1-9]\d*",
* },
* )
* @ParamConverter(
* "contact",
* class = ProfileContact::class,
* )
*/
public function changeContactInputAction(Request $request, ProfileContact $contact)
{
$profile = $this->getCurrentUser();
if ( ! $profile instanceof Profile) {
throw new \Exception();
}
if ($contact->getProfile() !== $profile) {
throw new \Exception();
}
$form = $this
->createFormBuilder([
'input' => $contact->getRecipient()->getContact(),
])
->add('input', TextType::class, [
'required' => true,
'attr' => [
'autocomplete' => 'off',
'inputmode' => ($contact->isPhone()) ? 'tel' : 'email',
],
'constraints' => array_values(array_filter([
new NotNull(),
new NotBlank(),
// check for email pattern
$contact->isEmail() ? new Callback([
'callback' => function (?string $input, ExecutionContextInterface $context) {
$violations = $context->getValidator()->validate($input, [
new Email(),
]);
if ($violations->count()) {
$context->getViolations()->addAll($violations);
}
},
]) : null,
// check for phone pattern
$contact->isPhone() ? new Callback([
'callback' => function (?string $input, ExecutionContextInterface $context) {
try {
$this->getPhoneNumberService()->normalize($input);
} catch (\Exception $e) {
$context->addViolation(
'Input is not a valid phone number format.'
);
}
},
]) : null,
])),
])
->getForm();
if ($this->handleForm($form)) {
$attempt = $this->getPortalService()->triggerAddition(
$profile,
$form->getData()['input'],
$request
);
if ($attempt) {
return $this->redirectToRoute(self::ROUTES__CHANGE_CONTACT__VERIFY, [
'contact' => $contact->getId(),
'attempt' => $attempt->getUidString(),
]);
}
$form->addError(new FormError(
'This contact is already associated to your account.'
));
}
return $this->html([
'contact' => $contact,
'form' => $form->createView(),
]);
}
/**
* @param ProfileContact $contact
* @param PortalLoginAttempt $attempt
* @return DocHtmlView|RedirectResponse
*
* @Route(
* "/change-contact/{contact}/verify/{attempt}",
* name = self::ROUTES__CHANGE_CONTACT__VERIFY,
* requirements = {
* "contact" = "[1-9]\d*",
* "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}",
* },
* )
* @ParamConverter(
* "contact",
* class = ProfileContact::class,
* )
* @ParamConverter(
* "attempt",
* class = PortalLoginAttempt::class,
* options = {
* "repository_method" = "findOneByUid",
* },
* )
*/
public function changeContactVerifyAction(ProfileContact $contact, PortalLoginAttempt $attempt)
{
$profile = $this->getCurrentUser();
if ( ! $profile instanceof Profile) {
throw new \Exception();
}
if ($contact->getProfile() !== $profile) {
throw new \Exception();
}
if ($attempt->getProfile() !== $profile) {
throw new \Exception();
}
$form = $this
->createFormBuilder([
'code' => null,
])
->add('code', TextType::class, [
'required' => true,
'constraints' => [
new NotNull(),
new NotBlank(),
],
])
->getForm();
if ($this->handleForm($form)) {
try {
$this->getPortalService()->verifyAddition(
$attempt,
$form->getData()['code'],
$contact
);
return $this->redirectToRoute(self::ROUTES__MAIN);
} catch (\Exception $e) {
$form->addError(new FormError($e->getMessage()));
}
}
return $this->html([
'attempt' => $attempt,
'form' => $form->createView(),
]);
}
/**
* @return PortalService|object
*/
private function getPortalService(): PortalService
{
return $this->get(__METHOD__);
}
/**
* @return ResolverManager|object
*/
private function getResolverManager(): ResolverManager
{
return $this->get(__METHOD__);
}
/**
* @return PhoneNumberService|object
*/
private function getPhoneNumberService(): PhoneNumberService
{
return $this->get(__METHOD__);
}
}