foldershare-8.x-1.2/src/EventSubscriber/FolderShareHttpExceptionSubscriber.php
src/EventSubscriber/FolderShareHttpExceptionSubscriber.php
<?php
namespace Drupal\foldershare\EventSubscriber;
use Psr\Log\LoggerInterface;
use Drupal\Core\EventSubscriber\DefaultExceptionHtmlSubscriber;
use Drupal\Core\EventSubscriber\MainContentViewSubscriber;
use Drupal\Core\Logger\LoggerChannelFactoryInterface;
use Drupal\Core\Cache\CacheableDependencyInterface;
use Drupal\Core\Cache\CacheableJsonResponse;
use Drupal\Core\Routing\RedirectDestinationInterface;
use Symfony\Component\HttpKernel\HttpKernelInterface;
use Symfony\Component\HttpKernel\Event\GetResponseForExceptionEvent;
use Symfony\Component\HttpKernel\KernelEvents;
use Symfony\Component\HttpKernel\Exception\HttpExceptionInterface;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\Routing\Matcher\UrlMatcherInterface;
/**
* Handles improved logging and responses for HTTP exceptions.
*
* In normal activity, Drupal code can respond to a user's request with
* an HTTP exception, such as for access denied or an item not being found.
* Thrown HTTP exceptions are handled by a series of registered event
* subscribers. Those subscribers primarily log the exception and return
* a response to the user.
*
* The default exception logger, ExceptionLoggingSubscriber, has special
* handling for 403 (Access denied) and 404 (Not found) HTTP exceptions.
* That handling omits the message associated with the exception and simply
* outputs a URL. This is rather poor, and particularly for REST requests
* that all go to just one or two URLs. The default terse exception messages
* are so poor that they can mislead an administrator into thinking something
* is wrong with the REST interface.
*
* To address these poor default messages, this subscriber is registered at
* a higher priority so that it gets a chance at the exceptions first,
* before the default logger. This subscriber then handles a variety of
* HTTP exceptions generatable by the FolderShare module. For each one,
* the exception's own message is logged and returned as a response to the
* user. For REST-generated exceptions, the HTTP request headers are included
* in the logged message because they provide the necessary parameters for
* the REST request.
*
* To register this event subscriber, an entry in "foldershare.services.yml"
* is required:
*
* @code
* services:
* foldershare.httpexception.subscriber:
* class: Drupal\foldershare\EventSubscriber\FolderShareHttpExceptionSubscriber
* arguments: ['@logger.factory']
* tags:
* - { name: event_subscriber }
* @endcode
*
* @ingroup foldershare
*
* @see foldershare.services.yml
*/
class FolderShareHttpExceptionSubscriber extends DefaultExceptionHtmlSubscriber {
/*---------------------------------------------------------------------
*
* Constants.
*
*---------------------------------------------------------------------*/
/**
* The status codes supported by this class.
*
* For each status code, two text strings are provided:
* - The channel name for the exception.
* - The default message if the exception doesn't have one.
*/
const STATUS_CODES_SUPPORTED = [
400 => [
'channel' => 'Bad request',
'default' => 'A malformed request was received.',
'severity' => 'error',
],
403 => [
'channel' => 'Entity access denied',
'default' => 'Access is denied for the requested item.',
'severity' => 'warning',
],
404 => [
'channel' => 'Entity not found',
'default' => 'The requested item was not found.',
'severity' => 'warning',
],
400 => [
'channel' => 'Entity access conflict',
'default' => 'The requested item is in use and is unavailable at this time.',
'severity' => 'warning',
],
410 => [
'channel' => 'Entity gone',
'default' => 'The requested item is no longer available.',
'severity' => 'notice',
],
415 => [
'channel' => 'Unsupported media type',
'default' => 'The requested media type is not supported.',
'severity' => 'warning',
],
500 => [
'channel' => 'Internal server error',
'default' => 'An internal server error has occurred.',
'severity' => 'critical',
],
501 => [
'channel' => 'Not implemented',
'default' => 'The requested feature is not yet implemented.',
'severity' => 'notice',
],
507 => [
'channel' => 'Insufficient storage',
'default' => 'There is insufficient storage to complete the request.',
'severity' => 'critical',
],
];
/*---------------------------------------------------------------------
*
* Fields.
*
*---------------------------------------------------------------------*/
/**
* The logger instance.
*
* @var \Drupal\Core\Logger\LoggerChannelFactoryInterface
*/
protected $loggerFactory;
/*---------------------------------------------------------------------
*
* Construct.
*
*---------------------------------------------------------------------*/
/**
* Constructs an event subscriber to handle HTTP exceptions on FolderShare.
*
* @param \Symfony\Component\HttpKernel\HttpKernelInterface $httpKernel
* The HTTP kernel.
* @param \Psr\Log\LoggerInterface $logger
* The PHP logger service.
* @param \Drupal\Core\Routing\RedirectDestinationInterface $redirectDestination
* The redirect destination service.
* @param \Symfony\Component\Routing\Matcher\UrlMatcherInterface $accessUnawareRouter
* A router implementation which does not check access.
* @param \Drupal\Core\Logger\LoggerChannelFactoryInterface $loggerFactory
* The Drupal logger factory for multiple logger channels.
*/
public function __construct(
HttpKernelInterface $httpKernel,
LoggerInterface $logger,
RedirectDestinationInterface $redirectDestination,
UrlMatcherInterface $accessUnawareRouter,
LoggerChannelFactoryInterface $loggerFactory) {
parent::__construct(
$httpKernel,
$logger,
$redirectDestination,
$accessUnawareRouter);
$this->loggerFactory = $loggerFactory;
}
/*---------------------------------------------------------------------
*
* Configure.
*
*---------------------------------------------------------------------*/
/**
* {@inheritdoc}
*/
protected function getHandledFormats() {
return ['html', 'json'];
}
/**
* {@inheritdoc}
*/
protected static function getPriority() {
// Subscribe at a priority higher than the ExceptionLoggingSubscriber
// in order to override its logging. That logger is priority 50.
return 60;
}
/**
* {@inheritdoc}
*/
public static function getSubscribedEvents() {
$events[KernelEvents::EXCEPTION][] = [
'onException',
static::getPriority(),
];
return $events;
}
/*---------------------------------------------------------------------
*
* Special exception handling.
*
*---------------------------------------------------------------------*/
/**
* Logs and responds to module HTTP exceptions.
*
* The event is checked to see if it meets the following criteria:
* - It is an instance of HttpExceptionInterface.
* - It is directly from this module's source.
* - It is requesting an HTML or JSON response.
* - It is for a 400-series or 500-series HTTP status code.
* - It is for one of the specific status codes supported by this class.
*
* If any of the above are not true, the event is not handled and it
* will float downward to lower priority subscribers, such as the default
* subscribers in Drupal core.
*
* If the event is handled, log and response messages are built that
* include the event's exception message and request headers.
*
* @param \Symfony\Component\HttpKernel\Event\GetResponseForExceptionEvent $event
* The event to process.
*/
public function onException(GetResponseForExceptionEvent $event) {
//
// Verify exception type.
// ----------------------
// Check the exception class. If it is not an HTTP exception, return
// immediately and let the event flow through the remaining event
// subscribers.
$exception = $event->getException();
if (($exception instanceof HttpExceptionInterface) === FALSE) {
// Not an HTTP exception.
return;
}
//
// Verify exception is from FolderShare.
// -------------------------------------
// Check if the exception comes directly from a source file in the
// FolderShare module. If not, return immediately and let the event
// flow through the remaining event subscribers.
$file = $exception->getFile();
if (strpos($file, "foldershare") === FALSE) {
// Not from a FolderShare module file.
return;
}
$fromRest = (strpos($file, "rest") !== FALSE);
//
// Verify handled format.
// ----------------------
// Check if the request is for a format supported here. If not, return
// immediately and let the event flow through the remaining event
// subscribers.
$request = $event->getRequest();
$format = $request->query->get(
MainContentViewSubscriber::WRAPPER_FORMAT,
$request->getRequestFormat());
if (in_array($format, $this->getHandledFormats()) === FALSE) {
// Not in a format handled here.
return;
}
//
// Verify handled status code.
// ---------------------------
// Check if the HTTP status code is one handled here. If not, return
// immediately and let the event flow through the remaining event
// subscribers.
$statusCode = (int) $exception->getStatusCode();
if ($statusCode < 400 || $statusCode > 600) {
// Not a status code handled here.
return;
}
if (isset(self::STATUS_CODES_SUPPORTED[$statusCode]) === FALSE) {
// Not a status code handled here.
return;
}
//
// Select a logger channel.
// ------------------------
// Logger channels are labels for logger entries. These are not
// standardized, but Drupal core uses the following (from a search
// through the code):
// - access denied.
// - action.
// - aggregator.
// - block_content.
// - comment.
// - config_sync.
// - contact.
// - content.
// - cron.
// - entity_reference.
// - example.
// - field.
// - file system.
// - file.
// - filter.
// - forum.
// - image.
// - language.
// - locale.
// - media.
// - menu.
// - migrate.
// - migrate_drupal_ui.
// - my_module.
// - node.
// - page not found.
// - php.
// - responsive_image.
// - rest.
// - security.
// - system.
// - taxonomy.
// - test_logger.
// - theme.
// - tracker.
// - user.
// - views.
// - workspaces.
//
// Normally, 403 exceptions are posted to 'access denied', while 404
// exceptions are posted to 'page not found'. However, for FolderShare
// we specifically need to distinguish between exceptions from REST
// web services vs. those from page generation. Further, REST web services
// do not generate "pages", so a "page not found" channel is incorrect.
if (empty(self::STATUS_CODES_SUPPORTED[$statusCode]['channel']) === FALSE) {
$channel = self::STATUS_CODES_SUPPORTED[$statusCode]['channel'];
}
else {
$channel = 'Bad request';
}
//
// Get and simplify headers.
// -------------------------
// All REST operations route through the same few URLs and rely upon
// additional HTTP headers to provide arguments to operations. It is
// therefore important for logging to include those headers.
//
// Reduce the headers to only those relevant for FolderShare. Only do
// this for REST exceptions.
$headerHtml = '';
if ($fromRest === TRUE) {
// Sift through the headers and find the FolderShare-specific ones.
$allHeaders = $request->headers;
$moduleHeaders = [];
foreach ($allHeaders as $key => $values) {
if (strpos($key, "foldershare") !== FALSE ||
strpos($key, "content-disposition") !== FALSE) {
$moduleHeaders[$key] = $values;
}
}
if (empty($moduleHeaders) === FALSE) {
$headerHtml = '<details open><summary>Request header</summary>';
foreach ($moduleHeaders as $key => $values) {
$name = implode('-', array_map('ucfirst', explode('-', $key)));
foreach ($values as $value) {
$v = rawurldecode($value);
$headerHtml .= "$name: $v\r\n</br>";
}
}
$headerHtml .= '</details>';
}
}
//
// Log the exception.
// ------------------
// Logging automatically adds the URL in the location part of the log.
// So, omit the URL, but include the exception's message and header.
$message = (string) $exception->getMessage();
if (empty($message) === TRUE) {
if (empty(self::STATUS_CODES_SUPPORTED[$statusCode]['default']) === FALSE) {
$message = self::STATUS_CODES_SUPPORTED[$statusCode]['default'];
}
else {
$message = 'An error occurred.';
}
}
if (empty(self::STATUS_CODES_SUPPORTED[$statusCode]['severity']) === FALSE) {
$severity = self::STATUS_CODES_SUPPORTED[$statusCode]['severity'];
}
else {
$severity = 'error';
}
$logMessage = $message . "\r\n";
if ($fromRest === TRUE) {
$logMessage .= $headerHtml;
}
$this->loggerFactory->get($channel)->log($severity, $logMessage);
//
// Return a response.
// ------------------
// Attaching a response to the event marks it as handled and prevents
// further event subscribers from processing the same event.
if ($format === 'html') {
// The parent class handles HTML responses for common exceptions.
// Anything that it doesn't handle will float on to another subscriber.
parent::onException($event);
}
elseif ($format === 'json') {
if ($exception instanceof CacheableDependencyInterface) {
// The exception is cacheable, so generate a cacheable response.
$response = new CacheableJsonResponse(
[
'message' => $message,
],
$statusCode,
$exception->getHeaders());
$response->addCacheableDependency($exception);
}
else {
// The exception is not cacheable, so generate a non-cacheable response.
$response = new JsonResponse(
[
'message' => $message,
],
$statusCode,
$exception->getHeaders());
}
$event->setResponse($response);
}
}
}
