vendor/contao/core-bundle/src/EventListener/PrettyErrorScreenListener.php line 72

Open in your IDE?
  1. <?php
  2. declare(strict_types=1);
  3. /*
  4.  * This file is part of Contao.
  5.  *
  6.  * (c) Leo Feyer
  7.  *
  8.  * @license LGPL-3.0-or-later
  9.  */
  10. namespace Contao\CoreBundle\EventListener;
  11. use Contao\Config;
  12. use Contao\CoreBundle\Exception\InvalidRequestTokenException;
  13. use Contao\CoreBundle\Exception\ResponseException;
  14. use Contao\CoreBundle\Exception\RouteParametersException;
  15. use Contao\CoreBundle\Framework\ContaoFramework;
  16. use Contao\CoreBundle\Util\LocaleUtil;
  17. use Contao\PageError404;
  18. use Contao\StringUtil;
  19. use Symfony\Component\HttpFoundation\AcceptHeader;
  20. use Symfony\Component\HttpFoundation\Response;
  21. use Symfony\Component\HttpKernel\Event\ExceptionEvent;
  22. use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
  23. use Symfony\Component\HttpKernel\Exception\HttpException;
  24. use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
  25. use Symfony\Component\HttpKernel\Exception\ServiceUnavailableHttpException;
  26. use Symfony\Component\HttpKernel\Exception\UnauthorizedHttpException;
  27. use Symfony\Component\Security\Core\Exception\AuthenticationCredentialsNotFoundException;
  28. use Symfony\Component\Security\Core\Security;
  29. use Twig\Environment;
  30. use Twig\Error\Error;
  31. /**
  32.  * @internal
  33.  */
  34. class PrettyErrorScreenListener
  35. {
  36.     /**
  37.      * @var bool
  38.      */
  39.     private $prettyErrorScreens;
  40.     /**
  41.      * @var Environment
  42.      */
  43.     private $twig;
  44.     /**
  45.      * @var ContaoFramework
  46.      */
  47.     private $framework;
  48.     /**
  49.      * @var Security
  50.      */
  51.     private $security;
  52.     public function __construct(bool $prettyErrorScreensEnvironment $twigContaoFramework $frameworkSecurity $security)
  53.     {
  54.         $this->prettyErrorScreens $prettyErrorScreens;
  55.         $this->twig $twig;
  56.         $this->framework $framework;
  57.         $this->security $security;
  58.     }
  59.     /**
  60.      * Map an exception to an error screen.
  61.      */
  62.     public function __invoke(ExceptionEvent $event): void
  63.     {
  64.         if (!$event->isMasterRequest()) {
  65.             return;
  66.         }
  67.         $request $event->getRequest();
  68.         if ('html' !== $request->getRequestFormat()) {
  69.             return;
  70.         }
  71.         if (!AcceptHeader::fromString($request->headers->get('Accept'))->has('text/html')) {
  72.             return;
  73.         }
  74.         $this->handleException($event);
  75.     }
  76.     private function handleException(ExceptionEvent $event): void
  77.     {
  78.         $exception $event->getThrowable();
  79.         try {
  80.             $isBackendUser $this->security->isGranted('ROLE_USER');
  81.         } catch (AuthenticationCredentialsNotFoundException $e) {
  82.             $isBackendUser false;
  83.         }
  84.         switch (true) {
  85.             case $isBackendUser:
  86.                 $this->renderBackendException($event);
  87.                 break;
  88.             case $exception instanceof UnauthorizedHttpException:
  89.                 $this->renderErrorScreenByType(401$event);
  90.                 break;
  91.             case $exception instanceof AccessDeniedHttpException:
  92.                 $this->renderErrorScreenByType(403$event);
  93.                 break;
  94.             case $exception instanceof NotFoundHttpException:
  95.                 $this->renderErrorScreenByType(404$event);
  96.                 break;
  97.             case $exception instanceof ServiceUnavailableHttpException:
  98.                 $this->renderTemplate('service_unavailable'503$event);
  99.                 break;
  100.             default:
  101.                 $this->renderErrorScreenByException($event);
  102.         }
  103.     }
  104.     private function renderBackendException(ExceptionEvent $event): void
  105.     {
  106.         $exception $event->getThrowable();
  107.         if ($exception instanceof RouteParametersException) {
  108.             $this->renderTemplate('missing_route_parameters'501$event);
  109.             return;
  110.         }
  111.         $this->renderTemplate('backend'$this->getStatusCodeForException($exception), $event);
  112.     }
  113.     private function renderErrorScreenByType(int $typeExceptionEvent $event): void
  114.     {
  115.         static $processing;
  116.         if (true === $processing) {
  117.             return;
  118.         }
  119.         $processing true;
  120.         if (null !== ($response $this->getResponseFromPageHandler($type))) {
  121.             $event->setResponse($response);
  122.         }
  123.         $processing false;
  124.     }
  125.     private function getResponseFromPageHandler(int $type): ?Response
  126.     {
  127.         $this->framework->initialize(true);
  128.         $key 'error_'.$type;
  129.         if (!isset($GLOBALS['TL_PTY'][$key]) || !class_exists($GLOBALS['TL_PTY'][$key])) {
  130.             return null;
  131.         }
  132.         /** @var PageError404 $pageHandler */
  133.         $pageHandler = new $GLOBALS['TL_PTY'][$key]();
  134.         try {
  135.             return $pageHandler->getResponse();
  136.         } catch (ResponseException $e) {
  137.             return $e->getResponse();
  138.         } catch (\Exception $e) {
  139.             return null;
  140.         }
  141.     }
  142.     /**
  143.      * Checks the exception chain for a known exception.
  144.      */
  145.     private function renderErrorScreenByException(ExceptionEvent $event): void
  146.     {
  147.         $exception $event->getThrowable();
  148.         $statusCode $this->getStatusCodeForException($exception);
  149.         $template null;
  150.         // Look for a template
  151.         do {
  152.             if ($exception instanceof InvalidRequestTokenException) {
  153.                 $template 'invalid_request_token';
  154.             }
  155.         } while (null === $template && null !== ($exception $exception->getPrevious()));
  156.         $this->renderTemplate($template ?: 'error'$statusCode$event);
  157.     }
  158.     private function renderTemplate(string $templateint $statusCodeExceptionEvent $event): void
  159.     {
  160.         if (!$this->prettyErrorScreens) {
  161.             return;
  162.         }
  163.         $view '@ContaoCore/Error/'.$template.'.html.twig';
  164.         $parameters $this->getTemplateParameters($view$statusCode$event);
  165.         try {
  166.             $event->setResponse(new Response($this->twig->render($view$parameters), $statusCode));
  167.         } catch (Error $e) {
  168.             $event->setResponse(new Response($this->twig->render('@ContaoCore/Error/error.html.twig'), 500));
  169.         }
  170.     }
  171.     /**
  172.      * @return array<string,mixed>
  173.      */
  174.     private function getTemplateParameters(string $viewint $statusCodeExceptionEvent $event): array
  175.     {
  176.         /** @var Config $config */
  177.         $config $this->framework->getAdapter(Config::class);
  178.         $encoded StringUtil::encodeEmail($config->get('adminEmail'));
  179.         try {
  180.             $isBackendUser $this->security->isGranted('ROLE_USER');
  181.         } catch (AuthenticationCredentialsNotFoundException $e) {
  182.             $isBackendUser false;
  183.         }
  184.         return [
  185.             'statusCode' => $statusCode,
  186.             'statusName' => Response::$statusTexts[$statusCode],
  187.             'template' => $view,
  188.             'base' => $event->getRequest()->getBasePath(),
  189.             'language' => LocaleUtil::formatAsLanguageTag($event->getRequest()->getLocale()),
  190.             'adminEmail' => '&#109;&#97;&#105;&#108;&#116;&#111;&#58;'.$encoded,
  191.             'isBackendUser' => $isBackendUser,
  192.             'exception' => $event->getThrowable()->getMessage(),
  193.             'throwable' => $event->getThrowable(),
  194.         ];
  195.     }
  196.     private function getStatusCodeForException(\Throwable $exception): int
  197.     {
  198.         if ($exception instanceof HttpException) {
  199.             return (int) $exception->getStatusCode();
  200.         }
  201.         return 500;
  202.     }
  203. }