DefaultExceptionHtmlSubscriber.php 7.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197
  1. <?php
  2. namespace Drupal\Core\EventSubscriber;
  3. use Drupal\Core\Cache\CacheableResponseInterface;
  4. use Drupal\Core\Routing\RedirectDestinationInterface;
  5. use Drupal\Core\Utility\Error;
  6. use Psr\Log\LoggerInterface;
  7. use Symfony\Component\HttpFoundation\Response;
  8. use Symfony\Component\HttpKernel\Event\GetResponseForExceptionEvent;
  9. use Symfony\Component\HttpKernel\HttpKernelInterface;
  10. use Symfony\Component\HttpKernel\Exception\HttpExceptionInterface;
  11. use Symfony\Component\Routing\Matcher\UrlMatcherInterface;
  12. /**
  13. * Exception subscriber for handling core default HTML error pages.
  14. */
  15. class DefaultExceptionHtmlSubscriber extends HttpExceptionSubscriberBase {
  16. /**
  17. * The HTTP kernel.
  18. *
  19. * @var \Symfony\Component\HttpKernel\HttpKernelInterface
  20. */
  21. protected $httpKernel;
  22. /**
  23. * The logger instance.
  24. *
  25. * @var \Psr\Log\LoggerInterface
  26. */
  27. protected $logger;
  28. /**
  29. * The redirect destination service.
  30. *
  31. * @var \Drupal\Core\Routing\RedirectDestinationInterface
  32. */
  33. protected $redirectDestination;
  34. /**
  35. * A router implementation which does not check access.
  36. *
  37. * @var \Symfony\Component\Routing\Matcher\UrlMatcherInterface
  38. */
  39. protected $accessUnawareRouter;
  40. /**
  41. * Constructs a new DefaultExceptionHtmlSubscriber.
  42. *
  43. * @param \Symfony\Component\HttpKernel\HttpKernelInterface $http_kernel
  44. * The HTTP kernel.
  45. * @param \Psr\Log\LoggerInterface $logger
  46. * The logger service.
  47. * @param \Drupal\Core\Routing\RedirectDestinationInterface $redirect_destination
  48. * The redirect destination service.
  49. * @param \Symfony\Component\Routing\Matcher\UrlMatcherInterface $access_unaware_router
  50. * A router implementation which does not check access.
  51. */
  52. public function __construct(HttpKernelInterface $http_kernel, LoggerInterface $logger, RedirectDestinationInterface $redirect_destination, UrlMatcherInterface $access_unaware_router) {
  53. $this->httpKernel = $http_kernel;
  54. $this->logger = $logger;
  55. $this->redirectDestination = $redirect_destination;
  56. $this->accessUnawareRouter = $access_unaware_router;
  57. }
  58. /**
  59. * {@inheritdoc}
  60. */
  61. protected static function getPriority() {
  62. // A very low priority so that custom handlers are almost certain to fire
  63. // before it, even if someone forgets to set a priority.
  64. return -128;
  65. }
  66. /**
  67. * {@inheritdoc}
  68. */
  69. protected function getHandledFormats() {
  70. return ['html'];
  71. }
  72. /**
  73. * Handles a 4xx error for HTML.
  74. *
  75. * @param \Symfony\Component\HttpKernel\Event\GetResponseForExceptionEvent $event
  76. * The event to process.
  77. */
  78. public function on4xx(GetResponseForExceptionEvent $event) {
  79. if (($exception = $event->getException()) && $exception instanceof HttpExceptionInterface) {
  80. $this->makeSubrequest($event, '/system/4xx', $exception->getStatusCode());
  81. }
  82. }
  83. /**
  84. * Handles a 401 error for HTML.
  85. *
  86. * @param \Symfony\Component\HttpKernel\Event\GetResponseForExceptionEvent $event
  87. * The event to process.
  88. */
  89. public function on401(GetResponseForExceptionEvent $event) {
  90. $this->makeSubrequest($event, '/system/401', Response::HTTP_UNAUTHORIZED);
  91. }
  92. /**
  93. * Handles a 403 error for HTML.
  94. *
  95. * @param \Symfony\Component\HttpKernel\Event\GetResponseForExceptionEvent $event
  96. * The event to process.
  97. */
  98. public function on403(GetResponseForExceptionEvent $event) {
  99. $this->makeSubrequest($event, '/system/403', Response::HTTP_FORBIDDEN);
  100. }
  101. /**
  102. * Handles a 404 error for HTML.
  103. *
  104. * @param \Symfony\Component\HttpKernel\Event\GetResponseForExceptionEvent $event
  105. * The event to process.
  106. */
  107. public function on404(GetResponseForExceptionEvent $event) {
  108. $this->makeSubrequest($event, '/system/404', Response::HTTP_NOT_FOUND);
  109. }
  110. /**
  111. * Makes a subrequest to retrieve the default error page.
  112. *
  113. * @param \Symfony\Component\HttpKernel\Event\GetResponseForExceptionEvent $event
  114. * The event to process.
  115. * @param string $url
  116. * The path/url to which to make a subrequest for this error message.
  117. * @param int $status_code
  118. * The status code for the error being handled.
  119. */
  120. protected function makeSubrequest(GetResponseForExceptionEvent $event, $url, $status_code) {
  121. $request = $event->getRequest();
  122. $exception = $event->getException();
  123. try {
  124. // Reuse the exact same request (so keep the same URL, keep the access
  125. // result, the exception, et cetera) but override the routing information.
  126. // This means that aside from routing, this is identical to the master
  127. // request. This allows us to generate a response that is executed on
  128. // behalf of the master request, i.e. for the original URL. This is what
  129. // allows us to e.g. generate a 404 response for the original URL; if we
  130. // would execute a subrequest with the 404 route's URL, then it'd be
  131. // generated for *that* URL, not the *original* URL.
  132. $sub_request = clone $request;
  133. // The routing to the 404 page should be done as GET request because it is
  134. // restricted to GET and POST requests only. Otherwise a DELETE request
  135. // would for example trigger a method not allowed exception.
  136. $request_context = clone ($this->accessUnawareRouter->getContext());
  137. $request_context->setMethod('GET');
  138. $this->accessUnawareRouter->setContext($request_context);
  139. $sub_request->attributes->add($this->accessUnawareRouter->match($url));
  140. // Add to query (GET) or request (POST) parameters:
  141. // - 'destination' (to ensure e.g. the login form in a 403 response
  142. // redirects to the original URL)
  143. // - '_exception_statuscode'
  144. $parameters = $sub_request->isMethod('GET') ? $sub_request->query : $sub_request->request;
  145. $parameters->add($this->redirectDestination->getAsArray() + ['_exception_statuscode' => $status_code]);
  146. $response = $this->httpKernel->handle($sub_request, HttpKernelInterface::SUB_REQUEST);
  147. // Only 2xx responses should have their status code overridden; any
  148. // other status code should be passed on: redirects (3xx), error (5xx)…
  149. // @see https://www.drupal.org/node/2603788#comment-10504916
  150. if ($response->isSuccessful()) {
  151. $response->setStatusCode($status_code);
  152. }
  153. // Persist the exception's cacheability metadata, if any. If the exception
  154. // itself isn't cacheable, then this will make the response uncacheable:
  155. // max-age=0 will be set.
  156. if ($response instanceof CacheableResponseInterface) {
  157. $response->addCacheableDependency($exception);
  158. }
  159. // Persist any special HTTP headers that were set on the exception.
  160. if ($exception instanceof HttpExceptionInterface) {
  161. $response->headers->add($exception->getHeaders());
  162. }
  163. $event->setResponse($response);
  164. }
  165. catch (\Exception $e) {
  166. // If an error happened in the subrequest we can't do much else. Instead,
  167. // just log it. The DefaultExceptionSubscriber will catch the original
  168. // exception and handle it normally.
  169. $error = Error::decodeException($e);
  170. $this->logger->log($error['severity_level'], '%type: @message in %function (line %line of %file).', $error);
  171. }
  172. }
  173. }