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);
    }
  }

}

Главная | Обратная связь

drupal hosting | друпал хостинг | it patrol .inc