foldershare-8.x-1.2/src/EventSubscriber/FolderShareHttpExceptionSubscriber.php
src/EventSubscriber/FolderShareHttpExceptionSubscriber.php
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 | <?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 ); } } } |