app/Service/EventSubscriber/KernelErrorSubscriber.php line 113

Open in your IDE?
  1. <?php
  2. namespace Sq\Service\EventSubscriber;
  3. use Psr\Container\ContainerInterface;
  4. use Psr\Log\LoggerInterface;
  5. use Psr\Log\LogLevel;
  6. use Sq\Event\Kernel\InitialiseContainerEvent;
  7. use Sq\Exception\Controller\AdminAccessDeniedException;
  8. use Sq\Exception\Controller\MemberAccessDeniedException;
  9. use Sq\Exception\Controller\MFARequiredException;
  10. use Sq\Exception\Controller\MFAVerificationException;
  11. use Sq\Service\Environment;
  12. use Sq\Service\FeatureChecker;
  13. use Sq\Service\ReactApp;
  14. use Symfony\Component\EventDispatcher\EventDispatcherInterface;
  15. use Symfony\Component\EventDispatcher\EventSubscriberInterface;
  16. use Symfony\Component\HttpFoundation\Exception\SuspiciousOperationException;
  17. use Symfony\Component\HttpFoundation\JsonResponse;
  18. use Symfony\Component\HttpFoundation\RedirectResponse;
  19. use Symfony\Component\HttpFoundation\Request;
  20. use Symfony\Component\HttpFoundation\Response;
  21. use Symfony\Component\HttpKernel\Event\ExceptionEvent;
  22. use Symfony\Component\HttpKernel\EventListener\ErrorListener as SymfonyErrorListener;
  23. use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
  24. use Symfony\Component\HttpKernel\Exception\HttpExceptionInterface;
  25. use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
  26. use Symfony\Component\HttpKernel\KernelEvents;
  27. use Symfony\Component\Routing\Exception\InvalidParameterException;
  28. use Symfony\Component\Routing\Exception\MethodNotAllowedException;
  29. use Symfony\Component\Routing\Exception\ResourceNotFoundException;
  30. class KernelErrorSubscriber implements EventSubscriberInterface
  31. {
  32.     /** @var Environment */
  33.     protected $environment;
  34.     /** @var EventDispatcherInterface */
  35.     protected $dispatcher;
  36.     /** @var LoggerInterface */
  37.     protected $logger;
  38.     /** @var FeatureChecker */
  39.     protected $featureChecker;
  40.     /** @var ContainerInterface */
  41.     protected $container;
  42.     /** @var ReactApp */
  43.     private $reactApp;
  44.     /** @var string[] */
  45.     protected $exceptionLogLevels = [
  46.         SuspiciousOperationException::class => LogLevel::ERROR,
  47.         MethodNotAllowedException::class => LogLevel::ERROR,
  48.         InvalidParameterException::class => LogLevel::ERROR,
  49.         BadRequestHttpException::class => LogLevel::ERROR,
  50.         ResourceNotFoundException::class => LogLevel::ERROR,
  51.     ];
  52.     public function __construct(
  53.         Environment $environment,
  54.         EventDispatcherInterface $dispatcher,
  55.         LoggerInterface $logger,
  56.         FeatureChecker $featureChecker,
  57.         ContainerInterface $container,
  58.         ReactApp $reactApp
  59.     ) {
  60.         $this->environment $environment;
  61.         $this->dispatcher $dispatcher;
  62.         $this->logger $logger;
  63.         $this->featureChecker $featureChecker;
  64.         $this->container $container;
  65.         $this->reactApp $reactApp;
  66.     }
  67.     public static function getSubscribedEvents()
  68.     {
  69.         return [
  70.             KernelEvents::EXCEPTION => ['onKernelException'],
  71.             InitialiseContainerEvent::class => ['onKernelInitialiseContainer']
  72.         ];
  73.     }
  74.     /**
  75.      * Removes the logKernelException listener as we do our own logging.
  76.      * For Symfony 5.x we will need to update this to remove the SymfonyExceptionListener.
  77.      */
  78.     public function onKernelInitialiseContainer()
  79.     {
  80.         $dispatcher $this->dispatcher;
  81.         $listeners $dispatcher->getListeners("kernel.exception");
  82.         foreach ($listeners as $listener)
  83.         {
  84.             if (is_array($listener)
  85.                 && ($listener[0] instanceof SymfonyErrorListener)
  86.                 && $listener[1] == "logKernelException"
  87.             ) {
  88.                 $dispatcher->removeListener("kernel.exception"$listener);
  89.                 break;
  90.             }
  91.         }
  92.     }
  93.     /**
  94.      * Handles kernel exceptions we want to (e.g. 404, 500, access denied).
  95.      *
  96.      * @param ExceptionEvent $event
  97.      *
  98.      * @throws \Exception
  99.      */
  100.     public function onKernelException(ExceptionEvent $event)
  101.     {
  102.         // You get the exception object from the received event
  103.         $throwable $event->getThrowable();
  104.         $response null;
  105.         switch (get_class($throwable))
  106.         {
  107.             case MFARequiredException::class:
  108.                 $response $this->handleMFARequired($event->getRequest());
  109.                 break;
  110.             case MFAVerificationException::class:
  111.                 $response $this->handleMFAVerification($event->getRequest());
  112.                 break;
  113.             case AdminAccessDeniedException::class:
  114.                 $response $this->handleAdminAccessDenied($event->getRequest());
  115.                 break;
  116.             case MemberAccessDeniedException::class:
  117.                 $response $this->handleMemberAccessDenied($event->getRequest());
  118.                 break;
  119.             case NotFoundHttpException::class:
  120.                 if ($this->reactApp->shouldUseReactForRequest())
  121.                 {
  122.                     try
  123.                     {
  124.                         $response $this->reactApp->renderReactApp();
  125.                         $event->allowCustomResponseCode();
  126.                     }
  127.                     catch (\Throwable $e)
  128.                     {
  129.                         $event->setThrowable($e);
  130.                     }
  131.                 }
  132.                 break;
  133.         }
  134.         $this->logError($throwable);
  135.         // Send the modified response object to the event
  136.         if ($response instanceof Response)
  137.         {
  138.             $event->setResponse($response);
  139.         }
  140.     }
  141.     /**
  142.      * Logs the error to the appropriate level.
  143.      *
  144.      * @param \Throwable $throwable
  145.      */
  146.     protected function logError(\Throwable $throwable)
  147.     {
  148.         $logLevel LogLevel::CRITICAL;
  149.         if ($throwable instanceof NotFoundHttpException || $throwable instanceof ResourceNotFoundException)
  150.         {
  151.             return;
  152.         }
  153.         if (($throwable instanceof HttpExceptionInterface && $throwable->getStatusCode() < 500) ||
  154.             $throwable instanceof MFARequiredException ||
  155.             $throwable instanceof MFAVerificationException ||
  156.             $throwable instanceof AdminAccessDeniedException ||
  157.             $throwable instanceof MemberAccessDeniedException
  158.         ) {
  159.             $logLevel LogLevel::ERROR;
  160.         }
  161.         if (array_key_exists(get_class($throwable), $this->exceptionLogLevels))
  162.         {
  163.             $logLevel $this->exceptionLogLevels[get_class($throwable)];
  164.         }
  165.         $this->logger->log($logLevel"{exception}", ['exception' => $throwable]);
  166.     }
  167.     /**
  168.      * Handle MFA required for this page.
  169.      * Public method, as it's used in admin_only.inc.php for legacy pages.
  170.      *
  171.      * @param Request $request
  172.      *
  173.      * @return Response|null
  174.      */
  175.     public function handleMFARequired(Request $request)
  176.     {
  177.         if ($request->isXmlHttpRequest())
  178.         {
  179.             return new JsonResponse(["status" => "ERROR""msg" => "MFA is required for this page"], 401);
  180.         }
  181.         return new RedirectResponse("/mfa/required?uri=" urlencode($request->getRequestUri()));
  182.     }
  183.     /**
  184.      * Handle MFA verification required for this page.
  185.      * Public method, as it's used in admin_only.inc.php for legacy pages.
  186.      *
  187.      * @param Request $request
  188.      *
  189.      * @return Response|null
  190.      */
  191.     public function handleMFAVerification(Request $request)
  192.     {
  193.         if ($request->isXmlHttpRequest())
  194.         {
  195.             return new JsonResponse(["status" => "ERROR""msg" => "MFA verification is required for this page"], 401);
  196.         }
  197.         return new RedirectResponse("/mfa/verify?r=" urlencode($request->getRequestUri()));
  198.     }
  199.     /**
  200.      * Handle denied attempt to a member page.
  201.      * Public method, as it's used in admin_only.inc.php for legacy pages.
  202.      *
  203.      * @param Request $request
  204.      *
  205.      * @return Response|null
  206.      */
  207.     public function handleAdminAccessDenied(Request $request)
  208.     {
  209.         if ($request->isXmlHttpRequest())
  210.         {
  211.             return new JsonResponse(["status" => "ERROR""msg" => "You must be logged in as an administrator"], 401);
  212.         }
  213.         return new RedirectResponse($request->getSchemeAndHttpHost());
  214.     }
  215.     /**
  216.      * Handle denied attempt to a member page.
  217.      * Public method, as it's used in members_only.inc.php for legacy pages.
  218.      *
  219.      * @param Request $request
  220.      *
  221.      * @return Response|null
  222.      */
  223.     public function handleMemberAccessDenied(Request $request)
  224.     {
  225.         if ($request->isXmlHttpRequest())
  226.         {
  227.             return new JsonResponse(["status" => "ERROR""msg" => "You are no longer logged in"], 401);
  228.         }
  229.         return new RedirectResponse($request->getSchemeAndHttpHost() . "/login?r=" urlencode(ltrim($request->getRequestUri(), "/")));
  230.     }
  231. }