app/Service/EventSubscriber/KernelErrorSubscriber.php line 91

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