diff --git a/core/core.services.yml b/core/core.services.yml index 183f7e2..30093f3 100644 --- a/core/core.services.yml +++ b/core/core.services.yml @@ -714,16 +714,44 @@ services: tags: - { name: event_subscriber } arguments: ['@config.manager', '@config.storage', '@config.storage.snapshot'] - exception_controller: - class: Drupal\Core\Controller\ExceptionController - arguments: ['@content_negotiation', '@title_resolver', '@html_page_renderer', '@html_fragment_renderer', '@string_translation', '@url_generator', '@logger.factory'] - calls: - - [setContainer, ['@service_container']] - exception_listener: - class: Drupal\Core\EventSubscriber\ExceptionListener + exception.default_json: + class: Drupal\Core\EventSubscriber\ExceptionJsonSubscriber + tags: + - { name: event_subscriber } + exception.default_html: + class: Drupal\Core\EventSubscriber\DefaultExceptionHtmlSubscriber + tags: + - { name: event_subscriber } + arguments: ['@html_fragment_renderer', '@html_page_renderer', '@config.factory'] + exception.default: + class: Drupal\Core\EventSubscriber\DefaultExceptionSubscriber + tags: + - { name: event_subscriber } + arguments: ['@html_fragment_renderer', '@html_page_renderer', '@config.factory'] + exception.logger: + class: Drupal\Core\EventSubscriber\ExceptionLoggingSubscriber + tags: + - { name: event_subscriber } + arguments: ['@logger.factory'] + exception.custom_page_html: + class: Drupal\Core\EventSubscriber\ExceptionJsonSubscriber + tags: + - { name: event_subscriber } + arguments: ['@config.factory', '@path.alias_manager', '@http_kernel'] + exception.custom_page_html: + class: Drupal\Core\EventSubscriber\CustomPageExceptionHtmlSubscriber + tags: + - { name: event_subscriber } + arguments: ['@config.factory', '@path.alias_manager', '@http_kernel'] + exception.fast_404_html: + class: Drupal\Core\EventSubscriber\Fast404ExceptionHtmlSubscriber + tags: + - { name: event_subscriber } + arguments: ['@config.factory', '@http_kernel'] + exception.test_site: + class: Drupal\Core\EventSubscriber\ExceptionTestSiteSubscriber tags: - { name: event_subscriber } - arguments: [['@exception_controller', execute]] route_processor_manager: class: Drupal\Core\RouteProcessor\RouteProcessorManager tags: diff --git a/core/lib/Drupal/Core/Controller/ExceptionController.php b/core/lib/Drupal/Core/Controller/ExceptionController.php deleted file mode 100644 index 69a477b..0000000 --- a/core/lib/Drupal/Core/Controller/ExceptionController.php +++ /dev/null @@ -1,452 +0,0 @@ -negotiation = $negotiation; - $this->htmlPageRenderer = $renderer; - $this->fragmentRenderer = $fragment_renderer; - $this->stringTranslation = $string_translation; - $this->loggerFactory = $logger_factory; - } - - /** - * Sets the Container associated with this Controller. - * - * @param \Symfony\Component\DependencyInjection\ContainerInterface $container - * A ContainerInterface instance. - * - * @api - */ - public function setContainer(ContainerInterface $container = NULL) { - $this->container = $container; - } - - /** - * Handles an exception on a request. - * - * @param \Symfony\Component\Debug\Exception\FlattenException $exception - * The flattened exception. - * @param \Symfony\Component\HttpFoundation\Request $request - * The request that generated the exception. - * - * @return \Symfony\Component\HttpFoundation\Response - * A response object. - */ - public function execute(FlattenException $exception, Request $request) { - $method = 'on' . $exception->getStatusCode() . $this->negotiation->getContentType($request); - - if (method_exists($this, $method)) { - return $this->$method($exception, $request); - } - - return new Response('A fatal error occurred: ' . $exception->getMessage(), $exception->getStatusCode(), $exception->getHeaders()); - } - - /** - * Processes a MethodNotAllowed exception into an HTTP 405 response. - * - * @param \Symfony\Component\Debug\Exception\FlattenException $exception - * The flattened exception. - * @param \Symfony\Component\HttpFoundation\Request $request - * The request object that triggered this exception. - * - * @return \Symfony\Component\HttpFoundation\Response - * A response object. - */ - public function on405Html(FlattenException $exception, Request $request) { - return new Response('Method Not Allowed', 405); - } - - /** - * Processes an AccessDenied exception into an HTTP 403 response. - * - * @param \Symfony\Component\Debug\Exception\FlattenException $exception - * The flattened exception. - * @param \Symfony\Component\HttpFoundation\Request $request - * The request object that triggered this exception. - * - * @return \Symfony\Component\HttpFoundation\Response - * A response object. - */ - public function on403Html(FlattenException $exception, Request $request) { - // @todo Remove dependency on the internal _system_path attribute: - // https://www.drupal.org/node/2293523. - $system_path = $request->attributes->get('_system_path'); - $this->loggerFactory->get('access denied')->warning($system_path); - - $system_config = $this->container->get('config.factory')->get('system.site'); - $path = $this->container->get('path.alias_manager')->getPathByAlias($system_config->get('page.403')); - if ($path && $path != $system_path) { - if ($request->getMethod() === 'POST') { - $subrequest = Request::create($request->getBaseUrl() . '/' . $path, 'POST', array('destination' => $system_path, '_exception_statuscode' => 403) + $request->request->all(), $request->cookies->all(), array(), $request->server->all()); - } - else { - $subrequest = Request::create($request->getBaseUrl() . '/' . $path, 'GET', array('destination' => $system_path, '_exception_statuscode' => 403), $request->cookies->all(), array(), $request->server->all()); - } - - $response = $this->container->get('http_kernel')->handle($subrequest, HttpKernelInterface::SUB_REQUEST); - $response->setStatusCode(403, 'Access denied'); - } - else { - $page_content = array( - '#markup' => $this->t('You are not authorized to access this page.'), - '#title' => $this->t('Access denied'), - ); - - $fragment = $this->createHtmlFragment($page_content, $request); - $page = $this->fragmentRenderer->render($fragment, 403); - $response = new Response($this->htmlPageRenderer->render($page), $page->getStatusCode()); - return $response; - } - - return $response; - } - - /** - * Processes a NotFound exception into an HTTP 404 response. - * - * @param \Symfony\Component\Debug\Exception\FlattenException $exception - * The flattened exception. - * @param \Symfony\Component\HttpFoundation\Request $request - * The request object that triggered this exception. - * - * @return \Symfony\Component\HttpFoundation\Response - * A response object. - */ - public function on404Html(FlattenException $exception, Request $request) { - $this->loggerFactory->get('page not found')->warning(String::checkPlain($request->attributes->get('_system_path'))); - - // Check for and return a fast 404 page if configured. - $config = \Drupal::config('system.performance'); - - $exclude_paths = $config->get('fast_404.exclude_paths'); - if ($config->get('fast_404.enabled') && $exclude_paths && !preg_match($exclude_paths, $request->getPathInfo())) { - $fast_paths = $config->get('fast_404.paths'); - if ($fast_paths && preg_match($fast_paths, $request->getPathInfo())) { - $fast_404_html = $config->get('fast_404.html'); - $fast_404_html = strtr($fast_404_html, array('@path' => String::checkPlain($request->getUri()))); - return new Response($fast_404_html, 404); - } - } - - // @todo Remove dependency on the internal _system_path attribute: - // https://www.drupal.org/node/2293523. - $system_path = $request->attributes->get('_system_path'); - - $path = $this->container->get('path.alias_manager')->getPathByAlias(\Drupal::config('system.site')->get('page.404')); - if ($path && $path != $system_path) { - // @todo Um, how do I specify an override URL again? Totally not clear. Do - // that and sub-call the kernel rather than using meah(). - // @todo The create() method expects a slash-prefixed path, but we store a - // normal system path in the site_404 variable. - if ($request->getMethod() === 'POST') { - $subrequest = Request::create($request->getBaseUrl() . '/' . $path, 'POST', array('destination' => $system_path, '_exception_statuscode' => 404) + $request->request->all(), $request->cookies->all(), array(), $request->server->all()); - } - else { - $subrequest = Request::create($request->getBaseUrl() . '/' . $path, 'GET', array('destination' => $system_path, '_exception_statuscode' => 404), $request->cookies->all(), array(), $request->server->all()); - } - - $response = $this->container->get('http_kernel')->handle($subrequest, HttpKernelInterface::SUB_REQUEST); - $response->setStatusCode(404, 'Not Found'); - } - else { - $page_content = array( - '#markup' => $this->t('The requested page "@path" could not be found.', array('@path' => $request->getPathInfo())), - '#title' => $this->t('Page not found'), - ); - - $fragment = $this->createHtmlFragment($page_content, $request); - $page = $this->fragmentRenderer->render($fragment, 404); - $response = new Response($this->htmlPageRenderer->render($page), $page->getStatusCode()); - return $response; - } - - return $response; - } - - /** - * Processes a generic exception into an HTTP 500 response. - * - * @param \Symfony\Component\Debug\Exception\FlattenException $exception - * Metadata about the exception that was thrown. - * @param \Symfony\Component\HttpFoundation\Request $request - * The request object that triggered this exception. - * - * @return \Symfony\Component\HttpFoundation\Response - * A response object. - */ - public function on500Html(FlattenException $exception, Request $request) { - $error = $this->decodeException($exception); - - // Because the kernel doesn't run until full bootstrap, we know that - // most subsystems are already initialized. - - $headers = array(); - - // When running inside the testing framework, we relay the errors - // to the tested site by the way of HTTP headers. - if (DRUPAL_TEST_IN_CHILD_SITE && !headers_sent() && (!defined('SIMPLETEST_COLLECT_ERRORS') || SIMPLETEST_COLLECT_ERRORS)) { - // $number does not use drupal_static as it should not be reset - // as it uniquely identifies each PHP error. - static $number = 0; - $assertion = array( - $error['!message'], - $error['%type'], - array( - 'function' => $error['%function'], - 'file' => $error['%file'], - 'line' => $error['%line'], - ), - ); - $headers['X-Drupal-Assertion-' . $number] = rawurlencode(serialize($assertion)); - $number++; - } - - $this->loggerFactory->get('php')->log($error['severity_level'], '%type: !message in %function (line %line of %file).', $error); - - // Display the message if the current error reporting level allows this type - // of message to be displayed, and unconditionnaly in update.php. - if (error_displayable($error)) { - $class = 'error'; - - // If error type is 'User notice' then treat it as debug information - // instead of an error message. - // @see debug() - if ($error['%type'] == 'User notice') { - $error['%type'] = 'Debug'; - $class = 'status'; - } - - // Attempt to reduce verbosity by removing DRUPAL_ROOT from the file path - // in the message. This does not happen for (false) security. - $root_length = strlen(DRUPAL_ROOT); - if (substr($error['%file'], 0, $root_length) == DRUPAL_ROOT) { - $error['%file'] = substr($error['%file'], $root_length + 1); - } - // Should not translate the string to avoid errors producing more errors. - $message = String::format('%type: !message in %function (line %line of %file).', $error); - - // Check if verbose error reporting is on. - $error_level = $this->container->get('config.factory')->get('system.logging')->get('error_level'); - - if ($error_level == ERROR_REPORTING_DISPLAY_VERBOSE) { - $backtrace_exception = $exception; - while ($backtrace_exception->getPrevious()) { - $backtrace_exception = $backtrace_exception->getPrevious(); - } - $backtrace = $backtrace_exception->getTrace(); - // First trace is the error itself, already contained in the message. - // While the second trace is the error source and also contained in the - // message, the message doesn't contain argument values, so we output it - // once more in the backtrace. - array_shift($backtrace); - // Generate a backtrace containing only scalar argument values. - $message .= '
' . Error::formatFlattenedBacktrace($backtrace) . '
'; - } - drupal_set_message(SafeMarkup::set($message), $class, TRUE); - } - - $content = $this->t('The website has encountered an error. Please try again later.'); - $output = DefaultHtmlPageRenderer::renderPage($content, $this->t('Error')); - $response = new Response($output); - $response->setStatusCode(500, '500 Service unavailable (with message)'); - - return $response; - } - - /** - * Processes an AccessDenied exception that occurred on a JSON request. - * - * @param \Symfony\Component\Debug\Exception\FlattenException $exception - * The flattened exception. - * @param \Symfony\Component\HttpFoundation\Request $request - * The request object that triggered this exception. - * - * @return \Symfony\Component\HttpFoundation\JsonResponse - * A JSON response object. - */ - public function on403Json(FlattenException $exception, Request $request) { - $response = new JsonResponse(); - $response->setStatusCode(403, 'Access Denied'); - return $response; - } - - /** - * Processes a NotFound exception that occurred on a JSON request. - * - * @param \Symfony\Component\Debug\Exception\FlattenException $exception - * The flattened exception. - * @param \Symfony\Component\HttpFoundation\Request $request - * The request object that triggered this exception. - * - * @return \Symfony\Component\HttpFoundation\JsonResponse - * A JSON response object. - */ - public function on404Json(FlattenException $exception, Request $request) { - $response = new JsonResponse(); - $response->setStatusCode(404, 'Not Found'); - return $response; - } - - /** - * Processes a MethodNotAllowed exception that occurred on a JSON request. - * - * @param \Symfony\Component\Debug\Exception\FlattenException $exception - * The flattened exception. - * @param \Symfony\Component\HttpFoundation\Request $request - * The request object that triggered this exception. - * - * @return \Symfony\Component\HttpFoundation\JsonResponse - * A JSON response object. - */ - public function on405Json(FlattenException $exception, Request $request) { - $response = new JsonResponse(); - $response->setStatusCode(405, 'Method Not Allowed'); - return $response; - } - - - /** - * This method is a temporary port of _drupal_decode_exception(). - * - * @todo This should get refactored. FlattenException could use some - * improvement as well. - * - * @param \Symfony\Component\Debug\Exception\FlattenException $exception - * The flattened exception. - * - * @return array - * An array of string-substitution tokens for formatting a message about the - * exception. - */ - protected function decodeException(FlattenException $exception) { - $message = $exception->getMessage(); - - $backtrace = $exception->getTrace(); - - // This value is missing from the stack for some reason in the - // FlattenException version of the backtrace. - $backtrace[0]['line'] = $exception->getLine(); - - // For database errors, we try to return the initial caller, - // skipping internal functions of the database layer. - if (strpos($exception->getClass(), 'DatabaseExceptionWrapper') !== FALSE) { - // A DatabaseExceptionWrapper exception is actually just a courier for - // the original PDOException. It's the stack trace from that exception - // that we care about. - $backtrace = $exception->getPrevious()->getTrace(); - $backtrace[0]['line'] = $exception->getLine(); - - // The first element in the stack is the call, the second element gives us the caller. - // We skip calls that occurred in one of the classes of the database layer - // or in one of its global functions. - $db_functions = array('db_query', 'db_query_range'); - while (!empty($backtrace[1]) && ($caller = $backtrace[1]) && - ((strpos($caller['namespace'], 'Drupal\Core\Database') !== FALSE || strpos($caller['class'], 'PDO') !== FALSE)) || - in_array($caller['function'], $db_functions)) { - // We remove that call. - array_shift($backtrace); - } - } - - $caller = Error::getLastCaller($backtrace); - - return array( - '%type' => $exception->getClass(), - // The standard PHP exception handler considers that the exception message - // is plain-text. We mimick this behavior here. - '!message' => String::checkPlain($message), - '%function' => $caller['function'], - '%file' => $caller['file'], - '%line' => $caller['line'], - 'severity_level' => WATCHDOG_ERROR, - ); - } - -} diff --git a/core/lib/Drupal/Core/EventSubscriber/CustomPageExceptionHtmlSubscriber.php b/core/lib/Drupal/Core/EventSubscriber/CustomPageExceptionHtmlSubscriber.php new file mode 100644 index 0000000..fa20119 --- /dev/null +++ b/core/lib/Drupal/Core/EventSubscriber/CustomPageExceptionHtmlSubscriber.php @@ -0,0 +1,126 @@ +systemSiteSettings = $config_factory->get('system.site'); + $this->aliasManager = $alias_manager; + $this->httpKernel = $http_kernel; + } + + /** + * {@inheritdoc} + */ + protected static function getPriority() { + return -50; + } + + /** + * {@inheritDoc} + */ + protected function getHandledFormats() { + return ['html']; + } + + /** + * Handles a 403 error for HTML. + * + * @param \Symfony\Component\HttpKernel\Event\GetResponseForExceptionEvent $event + * The event to process. + */ + public function on403(GetResponseForExceptionEvent $event) { + $request = $event->getRequest(); + + // @todo Remove dependency on the internal _system_path attribute: + // https://www.drupal.org/node/2293523. + $system_path = $request->attributes->get('_system_path'); + + $path = $this->aliasManager->getPathByAlias($this->systemSiteSettings->get('page.403')); + if ($path && $path != $system_path) { + if ($request->getMethod() === 'POST') { + $subrequest = Request::create($request->getBaseUrl() . '/' . $path, 'POST', array('destination' => $system_path, '_exception_statuscode' => Response::HTTP_FORBIDDEN) + $request->request->all(), $request->cookies->all(), array(), $request->server->all()); + } + else { + $subrequest = Request::create($request->getBaseUrl() . '/' . $path, 'GET', array('destination' => $system_path, '_exception_statuscode' => Response::HTTP_FORBIDDEN), $request->cookies->all(), [], $request->server->all()); + } + + $response = $this->httpKernel->handle($subrequest, HttpKernelInterface::SUB_REQUEST); + $response->setStatusCode(Response::HTTP_FORBIDDEN, 'Access denied'); + $event->setResponse($response); + } + } + + /** + * Handles a 404 error for HTML. + * + * @param \Symfony\Component\HttpKernel\Event\GetResponseForExceptionEvent $event + * The event to process. + */ + public function on404(GetResponseForExceptionEvent $event) { + $request = $event->getRequest(); + + // @todo Remove dependency on the internal _system_path attribute: + // https://www.drupal.org/node/2293523. + $system_path = $request->attributes->get('_system_path'); + + $path = $this->aliasManager->getPathByAlias($this->systemSiteSettings->get('page.404')); + if ($path && $path != $system_path) { + // @todo Um, how do I specify an override URL again? Totally not clear. Do + // that and sub-call the kernel rather than using meah(). + // @todo The create() method expects a slash-prefixed path, but we store a + // normal system path in the site_404 variable. + if ($request->getMethod() === 'POST') { + $subrequest = Request::create($request->getBaseUrl() . '/' . $path, 'POST', array('destination' => $system_path, '_exception_statuscode' => 404) + $request->request->all(), $request->cookies->all(), array(), $request->server->all()); + } + else { + $subrequest = Request::create($request->getBaseUrl() . '/' . $path, 'GET', array('destination' => $system_path, '_exception_statuscode' => 404), $request->cookies->all(), array(), $request->server->all()); + } + + $response = $this->httpKernel->handle($subrequest, HttpKernelInterface::SUB_REQUEST); + $response->setStatusCode(Response::HTTP_NOT_FOUND, 'Not Found'); + $event->setResponse($response); + } + } +} diff --git a/core/lib/Drupal/Core/EventSubscriber/DefaultExceptionHtmlSubscriber.php b/core/lib/Drupal/Core/EventSubscriber/DefaultExceptionHtmlSubscriber.php new file mode 100644 index 0000000..8f020f8 --- /dev/null +++ b/core/lib/Drupal/Core/EventSubscriber/DefaultExceptionHtmlSubscriber.php @@ -0,0 +1,123 @@ +fragmentRenderer = $fragment_renderer; + $this->htmlPageRenderer = $page_renderer; + $this->errorLevel = $config_factory->get('system.logging')->get('error_level'); + } + + /** + * {@inheritdoc} + */ + protected static function getPriority() { + // A very low priority so that custom handlers are almost certain to fire + // before it, even if someone forgets to set a priority. + return -128; + } + + /** + * {@inheritDoc} + */ + protected function getHandledFormats() { + return ['html']; + } + + /** + * Handles a 403 error for HTML. + * + * @param \Symfony\Component\HttpKernel\Event\GetResponseForExceptionEvent $event + * The event to process. + */ + public function on403(GetResponseForExceptionEvent $event) { + $response = $this->createResponse($this->t('Access denied'), $this->t('You are not authorized to access this page.'), Response::HTTP_FORBIDDEN); + $event->setResponse($response); + } + + /** + * Handles a 404 error for HTML. + * + * @param \Symfony\Component\HttpKernel\Event\GetResponseForExceptionEvent $event + * The event to process. + */ + public function on404(GetResponseForExceptionEvent $event) { + $path = $event->getRequest()->getPathInfo(); + $response = $this->createResponse($this->t('Page not found'), $this->t('The requested page "@path" could not be found.', ['@path' => $path]), Response::HTTP_NOT_FOUND); + $event->setResponse($response); + } + + /** + * Handles a 405 error for HTML. + * + * @param \Symfony\Component\HttpKernel\Event\GetResponseForExceptionEvent $event + * The event to process. + */ + public function on405(GetResponseForExceptionEvent $event) { + $response = new Response('Method Not Allowed', Response::HTTP_METHOD_NOT_ALLOWED); + $event->setResponse($response); + } + + /** + * @param $title + * The page title of the response. + * @param $body + * The body of the error page. + * @param $response_code + * The HTTP response code of the response. + * @return Response + * An error Response object ready to return to the browser. + */ + protected function createResponse($title, $body, $response_code) { + $fragment = new HtmlFragment($body); + $fragment->setTitle($title); + + $page = $this->fragmentRenderer->render($fragment, $response_code); + return new Response($this->htmlPageRenderer->render($page), $page->getStatusCode()); + } +} \ No newline at end of file diff --git a/core/lib/Drupal/Core/EventSubscriber/DefaultExceptionSubscriber.php b/core/lib/Drupal/Core/EventSubscriber/DefaultExceptionSubscriber.php new file mode 100644 index 0000000..2b8425a --- /dev/null +++ b/core/lib/Drupal/Core/EventSubscriber/DefaultExceptionSubscriber.php @@ -0,0 +1,197 @@ +fragmentRenderer = $fragment_renderer; + $this->htmlPageRenderer = $page_renderer; + $this->errorLevel = $config_factory->get('system.logging')->get('error_level'); + } + + /** + * Handles any exception as a generic error page for HTML. + * + * @param \Symfony\Component\HttpKernel\Event\GetResponseForExceptionEvent $event + * The event to process. + */ + protected function onHtml(GetResponseForExceptionEvent $event) { + $exception = $event->getException(); + $error = Error::decodeException($exception); + + // Display the message if the current error reporting level allows this type + // of message to be displayed, and unconditionnaly in update.php. + if (error_displayable($error)) { + $class = 'error'; + + // If error type is 'User notice' then treat it as debug information + // instead of an error message. + // @see debug() + if ($error['%type'] == 'User notice') { + $error['%type'] = 'Debug'; + $class = 'status'; + } + + // Attempt to reduce verbosity by removing DRUPAL_ROOT from the file path + // in the message. This does not happen for (false) security. + $root_length = strlen(DRUPAL_ROOT); + if (substr($error['%file'], 0, $root_length) == DRUPAL_ROOT) { + $error['%file'] = substr($error['%file'], $root_length + 1); + } + // Do not translate the string to avoid errors producing more errors. + $message = String::format('%type: !message in %function (line %line of %file).', $error); + + // Check if verbose error reporting is on. + if ($this->errorLevel == ERROR_REPORTING_DISPLAY_VERBOSE) { + $backtrace_exception = $exception; + while ($backtrace_exception->getPrevious()) { + $backtrace_exception = $backtrace_exception->getPrevious(); + } + $backtrace = $backtrace_exception->getTrace(); + // First trace is the error itself, already contained in the message. + // While the second trace is the error source and also contained in the + // message, the message doesn't contain argument values, so we output it + // once more in the backtrace. + array_shift($backtrace); + // Generate a backtrace containing only scalar argument values. + $message .= '
' . Error::formatFlattenedBacktrace($backtrace) . '
'; + } + drupal_set_message(SafeMarkup::set($message), $class, TRUE); + } + + $content = $this->t('The website has encountered an error. Please try again later.'); + $output = DefaultHtmlPageRenderer::renderPage($content, $this->t('Error')); + $response = new Response($output); + $response->setStatusCode(Response::HTTP_INTERNAL_SERVER_ERROR, '500 Service unavailable (with message)'); + + $event->setResponse($response); + } + + /** + * Handles any exception as a generic error page for JSON. + * + * @todo This should probably check the error reporting leve. + * + * @param \Symfony\Component\HttpKernel\Event\GetResponseForExceptionEvent $event + * The event to process. + */ + protected function onJson(GetResponseForExceptionEvent $event) { + $exception = $event->getException(); + $error = Error::decodeException($exception); + + // Display the message if the current error reporting level allows this type + // of message to be displayed, + $message = error_displayable($error) ? $exception->getMessage() : ''; + + $response = new JsonResponse([$message], Response::HTTP_INTERNAL_SERVER_ERROR); + $event->setResponse($response); + } + + /** + * Creates an Html response for the provided criteria. + * + * @param $title + * The page title of the response. + * @param $body + * The body of the error page. + * @param $response_code + * The HTTP response code of the response. + * @return \Symfony\Component\HttpFoundation\Response + * An error Response object ready to return to the browser. + */ + protected function createHtmlResponse($title, $body, $response_code) { + $fragment = new HtmlFragment($body); + $fragment->setTitle($title); + + $page = $this->fragmentRenderer->render($fragment, $response_code); + return new Response($this->htmlPageRenderer->render($page), $page->getStatusCode()); + } + + /** + * Handles errors for this subscriber. + * + * @param \Symfony\Component\HttpKernel\Event\GetResponseForExceptionEvent $event + * The event to process. + */ + public function onException(GetResponseForExceptionEvent $event) { + $format = $event->getRequest()->getRequestFormat(); + + // These are all JSON errors for our purposes. Any special handling for + // them can/should happen in earlier listeners if desired. + if (in_array($format, ['drupal_modal', 'drupal_dialog', 'drupal_ajax'])) { + $format = 'json'; + } + + // If it's an unrecognized format, assume HTML. + $method = 'on' . $format; + if (!method_exists($this, $method)) { + $method = 'onHtml'; + } + $this->$method($event); + } + + /** + * Registers the methods in this class that should be listeners. + * + * @return array + * An array of event listener definitions. + */ + public static function getSubscribedEvents() { + $events[KernelEvents::EXCEPTION][] = ['onException', -256]; + return $events; + } +} diff --git a/core/lib/Drupal/Core/EventSubscriber/ExceptionJsonSubscriber.php b/core/lib/Drupal/Core/EventSubscriber/ExceptionJsonSubscriber.php new file mode 100644 index 0000000..18af507 --- /dev/null +++ b/core/lib/Drupal/Core/EventSubscriber/ExceptionJsonSubscriber.php @@ -0,0 +1,67 @@ +setResponse($response); + } + + /** + * Handles a 404 error for JSON. + * + * @param \Symfony\Component\HttpKernel\Event\GetResponseForExceptionEvent $event + * The event to process. + */ + public function on404(GetResponseForExceptionEvent $event) { + $response = new JsonResponse(NULL, Response::HTTP_NOT_FOUND); + $event->setResponse($response); + } + + /** + * Handles a 405 error for JSON. + * + * @param \Symfony\Component\HttpKernel\Event\GetResponseForExceptionEvent $event + * The event to process. + */ + public function on405(GetResponseForExceptionEvent $event) { + $response = new JsonResponse(NULL, Response::HTTP_METHOD_NOT_ALLOWED); + $event->setResponse($response); + } +} diff --git a/core/lib/Drupal/Core/EventSubscriber/ExceptionListener.php b/core/lib/Drupal/Core/EventSubscriber/ExceptionListener.php deleted file mode 100644 index 0944703..0000000 --- a/core/lib/Drupal/Core/EventSubscriber/ExceptionListener.php +++ /dev/null @@ -1,36 +0,0 @@ - $this->controller, - 'exception' => FlattenException::create($exception), - 'logger' => $this->logger instanceof DebugLoggerInterface ? $this->logger : NULL, - 'format' => $request->getRequestFormat(), - ); - return $request->duplicate(NULL, NULL, $attributes); - } - -} diff --git a/core/lib/Drupal/Core/EventSubscriber/ExceptionLoggingSubscriber.php b/core/lib/Drupal/Core/EventSubscriber/ExceptionLoggingSubscriber.php new file mode 100644 index 0000000..22ce88f --- /dev/null +++ b/core/lib/Drupal/Core/EventSubscriber/ExceptionLoggingSubscriber.php @@ -0,0 +1,104 @@ +logger = $logger; + } + + /** + * Log 403 errors. + * + * @param \Symfony\Component\HttpKernel\Event\GetResponseForExceptionEvent $event + * The event to process. + */ + public function on403(GetResponseForExceptionEvent $event) { + $request = $event->getRequest(); + // @todo Remove dependency on the internal _system_path attribute: + // https://www.drupal.org/node/2293523. + $this->logger->get('access denied')->warning(String::checkPlain($request->attributes->get('_system_path'))); + } + + /** + * Log 404 errors. + * + * @param \Symfony\Component\HttpKernel\Event\GetResponseForExceptionEvent $event + * The event to process. + */ + public function on404(GetResponseForExceptionEvent $event) { + $request = $event->getRequest(); + // @todo Remove dependency on the internal _system_path attribute: + // https://www.drupal.org/node/2293523. + $this->logger->get('page not found')->warning(String::checkPlain($request->attributes->get('_system_path'))); + } + + /** + * Log not-otherwise-specified errors, including HTTP 500. + * + * @param \Symfony\Component\HttpKernel\Event\GetResponseForExceptionEvent $event + * The event to process. + */ + public function onError(GetResponseForExceptionEvent $event) { + $exception = $event->getException(); + $error = Error::decodeException($exception); + $this->logger->get('php')->log($error['severity_level'], '%type: !message in %function (line %line of %file).', $error); + } + + /** + * Log all exceptions. + * + * @param \Symfony\Component\HttpKernel\Event\GetResponseForExceptionEvent $event + * The event to process. + */ + public function onException(GetResponseForExceptionEvent $event) { + $exception = $event->getException(); + + $method = 'onError'; + + // Treat any non-HTTP exception as if it were one, so we log it the same. + if ($exception instanceof HttpException) { + $possible_method = 'on500'; + if (method_exists($this, $possible_method)) { + $method = $possible_method; + } + } + + $this->$method($event); + } + + /** + * {@inheritdoc} + */ + public static function getSubscribedEvents() { + $events[KernelEvents::EXCEPTION][] = ['onException', 50]; + return $events; + } +} diff --git a/core/lib/Drupal/Core/EventSubscriber/ExceptionTestSiteSubscriber.php b/core/lib/Drupal/Core/EventSubscriber/ExceptionTestSiteSubscriber.php new file mode 100644 index 0000000..96a2e4a --- /dev/null +++ b/core/lib/Drupal/Core/EventSubscriber/ExceptionTestSiteSubscriber.php @@ -0,0 +1,65 @@ +getException(); + $error = Error::decodeException($exception); + + $headers = array(); + + // When running inside the testing framework, we relay the errors + // to the tested site by the way of HTTP headers. + if (DRUPAL_TEST_IN_CHILD_SITE && !headers_sent() && (!defined('SIMPLETEST_COLLECT_ERRORS') || SIMPLETEST_COLLECT_ERRORS)) { + // $number does not use drupal_static as it should not be reset + // as it uniquely identifies each PHP error. + static $number = 0; + $assertion = array( + $error['!message'], + $error['%type'], + array( + 'function' => $error['%function'], + 'file' => $error['%file'], + 'line' => $error['%line'], + ), + ); + $headers['X-Drupal-Assertion-' . $number] = rawurlencode(serialize($assertion)); + $number++; + } + } +} diff --git a/core/lib/Drupal/Core/EventSubscriber/Fast404ExceptionHtmlSubscriber.php b/core/lib/Drupal/Core/EventSubscriber/Fast404ExceptionHtmlSubscriber.php new file mode 100644 index 0000000..d00023c --- /dev/null +++ b/core/lib/Drupal/Core/EventSubscriber/Fast404ExceptionHtmlSubscriber.php @@ -0,0 +1,84 @@ +systemPerformance = $config_factory->get('system.performance'); + $this->httpKernel = $http_kernel; + } + + /** + * {@inheritdoc} + */ + protected static function getPriority() { + // A very high priority so that it can take precedent over anything else, + // and thus be fast. + return 100; + } + + /** + * {@inheritDoc} + */ + protected function getHandledFormats() { + return ['html']; + } + + /** + * Handles a 404 error for HTML. + * + * @param \Symfony\Component\HttpKernel\Event\GetResponseForExceptionEvent $event + * The event to process. + */ + public function on404(GetResponseForExceptionEvent $event) { + $request = $event->getRequest(); + + $exclude_paths = $this->systemPerformance->get('fast_404.exclude_paths'); + if ($this->systemPerformance->get('fast_404.enabled') && $exclude_paths && !preg_match($exclude_paths, $request->getPathInfo())) { + $fast_paths = $this->systemPerformance->get('fast_404.paths'); + if ($fast_paths && preg_match($fast_paths, $request->getPathInfo())) { + $fast_404_html = strtr($this->systemPerformance->get('fast_404.html'), ['@path' => String::checkPlain($request->getUri())]); + $response = new Response($fast_404_html, Response::HTTP_NOT_FOUND); + $event->setResponse($response); + } + } + } +} diff --git a/core/lib/Drupal/Core/EventSubscriber/HttpExceptionSubscriberBase.php b/core/lib/Drupal/Core/EventSubscriber/HttpExceptionSubscriberBase.php new file mode 100644 index 0000000..b550399 --- /dev/null +++ b/core/lib/Drupal/Core/EventSubscriber/HttpExceptionSubscriberBase.php @@ -0,0 +1,89 @@ +setResponse() to set the response object + * for the exception. Alternatively, it may opt not to do so and then other + * listeners will have the opportunity to handle the exception. + */ +abstract class HttpExceptionSubscriberBase implements EventSubscriberInterface { + + /** + * Specifies the request formats this subscriber will respond to. + * + * @return array + * An indexed array of the format machine names that this subscriber will + * attempt ot process,such as "html" or "json". Returning an empty array + * will apply to all formats. + * + * @see \Symfony\Component\HttpFoundation\Request + */ + abstract protected function getHandledFormats(); + + /** + * Specifies the priority of all listeners in this class. + * + * The default priority is 1, which is very low. To have listeners that have + * a "first attempt" at handling exceptions return a higher priority. + * + * @return int + * The event priority of this subscriber. + */ + protected static function getPriority() { + return 1; + } + + /** + * Handles errors for this subscriber. + * + * @param \Symfony\Component\HttpKernel\Event\GetResponseForExceptionEvent $event + * The event to process. + */ + public function onException(GetResponseForExceptionEvent $event) { + $exception = $event->getException(); + $handled_formats = $this->getHandledFormats(); + if ($exception instanceof HttpException && (empty($handled_formats) || in_array($event->getRequest()->getRequestFormat(), $handled_formats))) { + $method = 'on' . $exception->getStatusCode(); + // We want to allow the method to be called and still not set a response + // if it has additional filtering logic to determine when it will apply. + // It is therefore the method's responsibility to set the response on the + // event if appropriate. + if (method_exists($this, $method)) { + $this->$method($event); + } + } + } + + /** + * Registers the methods in this class that should be listeners. + * + * @return array + * An array of event listener definitions. + */ + public static function getSubscribedEvents() { + $events[KernelEvents::EXCEPTION][] = ['onException', static::getPriority()]; + return $events; + } + +} diff --git a/core/lib/Drupal/Core/Routing/Enhancer/ParamConversionEnhancer.php b/core/lib/Drupal/Core/Routing/Enhancer/ParamConversionEnhancer.php index 614ac96..e94a249 100644 --- a/core/lib/Drupal/Core/Routing/Enhancer/ParamConversionEnhancer.php +++ b/core/lib/Drupal/Core/Routing/Enhancer/ParamConversionEnhancer.php @@ -86,7 +86,7 @@ public function onException(GetResponseForExceptionEvent $event) { * {@inheritdoc} */ public static function getSubscribedEvents() { - $events[KernelEvents::EXCEPTION][] = array('onException', 0); + $events[KernelEvents::EXCEPTION][] = array('onException', 75); return $events; } diff --git a/core/tests/Drupal/Tests/Core/Controller/ExceptionControllerTest.php b/core/tests/Drupal/Tests/Core/Controller/ExceptionControllerTest.php deleted file mode 100644 index 3a796ec..0000000 --- a/core/tests/Drupal/Tests/Core/Controller/ExceptionControllerTest.php +++ /dev/null @@ -1,58 +0,0 @@ -getMock('Drupal\Core\Page\HtmlPageRendererInterface'); - $html_fragment_renderer = $this->getMock('Drupal\Core\Page\HtmlFragmentRendererInterface'); - $title_resolver = $this->getMock('Drupal\Core\Controller\TitleResolverInterface'); - $translation = $this->getMock('Drupal\Core\StringTranslation\TranslationInterface'); - $url_generator = $this->getMock('Drupal\Core\Routing\UrlGeneratorInterface'); - $logger_factory = $this->getMock('Drupal\Core\Logger\LoggerChannelFactoryInterface'); - - $content_negotiation = $this->getMock('Drupal\Core\ContentNegotiation'); - $content_negotiation->expects($this->any()) - ->method('getContentType') - ->will($this->returnValue('html')); - - $exception_controller = new ExceptionController($content_negotiation, $title_resolver, $html_page_renderer, $html_fragment_renderer, $translation, $url_generator, $logger_factory); - $response = $exception_controller->execute($flat_exception, new Request()); - $this->assertEquals($response->getStatusCode(), 405, 'HTTP status of response is correct.'); - $this->assertEquals($response->getContent(), 'Method Not Allowed', 'HTTP response body is correct.'); - } - -} - -} - -namespace { - use Drupal\Core\Language\Language; - - if (!function_exists('language_default')) { - function language_default() { - $language = new Language(array('langcode' => 'en')); - return $language; - } - } -} diff --git a/core/tests/Drupal/Tests/Core/EventSubscriber/ExceptionListenerTest.php b/core/tests/Drupal/Tests/Core/EventSubscriber/ExceptionListenerTest.php deleted file mode 100644 index 5815ee1..0000000 --- a/core/tests/Drupal/Tests/Core/EventSubscriber/ExceptionListenerTest.php +++ /dev/null @@ -1,98 +0,0 @@ -exceptionListener = new ExceptionListener('example'); - $this->kernel = $this->getMock('Symfony\Component\HttpKernel\HttpKernelInterface'); - - // You can't create an exception in PHP without throwing it. Store the - // current error_log, and disable it temporarily. - $this->errorLog = ini_set('error_log', file_exists('/dev/null') ? '/dev/null' : 'nul'); - } - - /** - * {@inheritdoc} - */ - protected function tearDown() { - ini_set('error_log', $this->errorLog); - } - - /** - * Tests onHandleException with a POST request. - */ - public function testHandleWithPostRequest() { - $request = Request::create('/test', 'POST', array('name' => 'druplicon', 'pass' => '12345')); - - $this->kernel->expects($this->once())->method('handle')->will($this->returnCallback(function (Request $request) { - return new Response($request->getMethod()); - })); - - $event = new GetResponseForExceptionEvent($this->kernel, $request, 'foo', new \Exception('foo')); - - $this->exceptionListener->onKernelException($event); - - $response = $event->getResponse(); - $this->assertEquals('POST name=druplicon&pass=12345', $response->getContent() . " " . UrlHelper::buildQuery($request->request->all())); - } - - /** - * Tests onHandleException with a GET request. - */ - public function testHandleWithGetRequest() { - $request = Request::create('/test', 'GET', array('name' => 'druplicon', 'pass' => '12345')); - - $this->kernel->expects($this->once())->method('handle')->will($this->returnCallback(function (Request $request) { - return new Response($request->getMethod() . ' ' . UrlHelper::buildQuery($request->query->all())); - })); - - $event = new GetResponseForExceptionEvent($this->kernel, $request, 'foo', new \Exception('foo')); - $this->exceptionListener->onKernelException($event); - - $response = $event->getResponse(); - $this->assertEquals('GET name=druplicon&pass=12345 ', $response->getContent() . " " . UrlHelper::buildQuery($request->request->all())); - } - -}