vendor/symfony/http-kernel/EventListener/ErrorListener.php line 119

  1. <?php
  2. /*
  3. * This file is part of the Symfony package.
  4. *
  5. * (c) Fabien Potencier <fabien@symfony.com>
  6. *
  7. * For the full copyright and license information, please view the LICENSE
  8. * file that was distributed with this source code.
  9. */
  10. namespace Symfony\Component\HttpKernel\EventListener;
  11. use Psr\Log\LoggerInterface;
  12. use Psr\Log\LogLevel;
  13. use Symfony\Component\ErrorHandler\ErrorHandler;
  14. use Symfony\Component\ErrorHandler\Exception\FlattenException;
  15. use Symfony\Component\EventDispatcher\EventSubscriberInterface;
  16. use Symfony\Component\HttpFoundation\Request;
  17. use Symfony\Component\HttpKernel\Attribute\WithHttpStatus;
  18. use Symfony\Component\HttpKernel\Attribute\WithLogLevel;
  19. use Symfony\Component\HttpKernel\Event\ControllerArgumentsEvent;
  20. use Symfony\Component\HttpKernel\Event\ExceptionEvent;
  21. use Symfony\Component\HttpKernel\Event\ResponseEvent;
  22. use Symfony\Component\HttpKernel\Exception\HttpException;
  23. use Symfony\Component\HttpKernel\Exception\HttpExceptionInterface;
  24. use Symfony\Component\HttpKernel\HttpKernelInterface;
  25. use Symfony\Component\HttpKernel\KernelEvents;
  26. use Symfony\Component\HttpKernel\Log\DebugLoggerConfigurator;
  27. /**
  28. * @author Fabien Potencier <fabien@symfony.com>
  29. */
  30. class ErrorListener implements EventSubscriberInterface
  31. {
  32. /**
  33. * @param array<class-string, array{log_level: string|null, status_code: int<100,599>|null}> $exceptionsMapping
  34. */
  35. public function __construct(
  36. protected string|object|array|null $controller,
  37. protected ?LoggerInterface $logger = null,
  38. protected bool $debug = false,
  39. protected array $exceptionsMapping = [],
  40. ) {
  41. }
  42. public function logKernelException(ExceptionEvent $event): void
  43. {
  44. $throwable = $event->getThrowable();
  45. $logLevel = $this->resolveLogLevel($throwable);
  46. foreach ($this->exceptionsMapping as $class => $config) {
  47. if (!$throwable instanceof $class || !$config['status_code']) {
  48. continue;
  49. }
  50. if (!$throwable instanceof HttpExceptionInterface || $throwable->getStatusCode() !== $config['status_code']) {
  51. $headers = $throwable instanceof HttpExceptionInterface ? $throwable->getHeaders() : [];
  52. $throwable = HttpException::fromStatusCode($config['status_code'], $throwable->getMessage(), $throwable, $headers);
  53. $event->setThrowable($throwable);
  54. }
  55. break;
  56. }
  57. // There's no specific status code defined in the configuration for this exception
  58. if (!$throwable instanceof HttpExceptionInterface && $withHttpStatus = $this->getInheritedAttribute($throwable::class, WithHttpStatus::class)) {
  59. $throwable = HttpException::fromStatusCode($withHttpStatus->statusCode, $throwable->getMessage(), $throwable, $withHttpStatus->headers);
  60. $event->setThrowable($throwable);
  61. }
  62. $e = FlattenException::createFromThrowable($throwable);
  63. $this->logException($throwable, sprintf('Uncaught PHP Exception %s: "%s" at %s line %s', $e->getClass(), $e->getMessage(), basename($e->getFile()), $e->getLine()), $logLevel);
  64. }
  65. public function onKernelException(ExceptionEvent $event): void
  66. {
  67. if (null === $this->controller) {
  68. return;
  69. }
  70. if (!$this->debug && $event->isKernelTerminating()) {
  71. return;
  72. }
  73. $throwable = $event->getThrowable();
  74. $exceptionHandler = set_exception_handler('var_dump');
  75. restore_exception_handler();
  76. if (\is_array($exceptionHandler) && $exceptionHandler[0] instanceof ErrorHandler) {
  77. $throwable = $exceptionHandler[0]->enhanceError($event->getThrowable());
  78. }
  79. $request = $this->duplicateRequest($throwable, $event->getRequest());
  80. try {
  81. $response = $event->getKernel()->handle($request, HttpKernelInterface::SUB_REQUEST, false);
  82. } catch (\Exception $e) {
  83. $f = FlattenException::createFromThrowable($e);
  84. $this->logException($e, sprintf('Exception thrown when handling an exception (%s: %s at %s line %s)', $f->getClass(), $f->getMessage(), basename($e->getFile()), $e->getLine()));
  85. $prev = $e;
  86. do {
  87. if ($throwable === $wrapper = $prev) {
  88. throw $e;
  89. }
  90. } while ($prev = $wrapper->getPrevious());
  91. $prev = new \ReflectionProperty($wrapper instanceof \Exception ? \Exception::class : \Error::class, 'previous');
  92. $prev->setValue($wrapper, $throwable);
  93. throw $e;
  94. }
  95. $event->setResponse($response);
  96. if ($this->debug) {
  97. $event->getRequest()->attributes->set('_remove_csp_headers', true);
  98. }
  99. }
  100. public function removeCspHeader(ResponseEvent $event): void
  101. {
  102. if ($this->debug && $event->getRequest()->attributes->get('_remove_csp_headers', false)) {
  103. $event->getResponse()->headers->remove('Content-Security-Policy');
  104. }
  105. }
  106. public function onControllerArguments(ControllerArgumentsEvent $event): void
  107. {
  108. $e = $event->getRequest()->attributes->get('exception');
  109. if (!$e instanceof \Throwable || false === $k = array_search($e, $event->getArguments(), true)) {
  110. return;
  111. }
  112. $r = new \ReflectionFunction($event->getController()(...));
  113. $r = $r->getParameters()[$k] ?? null;
  114. if ($r && (!($r = $r->getType()) instanceof \ReflectionNamedType || FlattenException::class === $r->getName())) {
  115. $arguments = $event->getArguments();
  116. $arguments[$k] = FlattenException::createFromThrowable($e);
  117. $event->setArguments($arguments);
  118. }
  119. }
  120. public static function getSubscribedEvents(): array
  121. {
  122. return [
  123. KernelEvents::CONTROLLER_ARGUMENTS => 'onControllerArguments',
  124. KernelEvents::EXCEPTION => [
  125. ['logKernelException', 0],
  126. ['onKernelException', -128],
  127. ],
  128. KernelEvents::RESPONSE => ['removeCspHeader', -128],
  129. ];
  130. }
  131. /**
  132. * Logs an exception.
  133. */
  134. protected function logException(\Throwable $exception, string $message, ?string $logLevel = null): void
  135. {
  136. if (null === $this->logger) {
  137. return;
  138. }
  139. $logLevel ??= $this->resolveLogLevel($exception);
  140. $this->logger->log($logLevel, $message, ['exception' => $exception]);
  141. }
  142. /**
  143. * Resolves the level to be used when logging the exception.
  144. */
  145. private function resolveLogLevel(\Throwable $throwable): string
  146. {
  147. foreach ($this->exceptionsMapping as $class => $config) {
  148. if ($throwable instanceof $class && $config['log_level']) {
  149. return $config['log_level'];
  150. }
  151. }
  152. if ($withLogLevel = $this->getInheritedAttribute($throwable::class, WithLogLevel::class)) {
  153. return $withLogLevel->level;
  154. }
  155. if (!$throwable instanceof HttpExceptionInterface || $throwable->getStatusCode() >= 500) {
  156. return LogLevel::CRITICAL;
  157. }
  158. return LogLevel::ERROR;
  159. }
  160. /**
  161. * Clones the request for the exception.
  162. */
  163. protected function duplicateRequest(\Throwable $exception, Request $request): Request
  164. {
  165. $attributes = [
  166. '_controller' => $this->controller,
  167. 'exception' => $exception,
  168. 'logger' => DebugLoggerConfigurator::getDebugLogger($this->logger),
  169. ];
  170. $request = $request->duplicate(null, null, $attributes);
  171. $request->setMethod('GET');
  172. return $request;
  173. }
  174. /**
  175. * @template T
  176. *
  177. * @param class-string<T> $attribute
  178. *
  179. * @return T|null
  180. */
  181. private function getInheritedAttribute(string $class, string $attribute): ?object
  182. {
  183. $class = new \ReflectionClass($class);
  184. $interfaces = [];
  185. $attributeReflector = null;
  186. $parentInterfaces = [];
  187. $ownInterfaces = [];
  188. do {
  189. if ($attributes = $class->getAttributes($attribute, \ReflectionAttribute::IS_INSTANCEOF)) {
  190. $attributeReflector = $attributes[0];
  191. $parentInterfaces = class_implements($class->name);
  192. break;
  193. }
  194. $interfaces[] = class_implements($class->name);
  195. } while ($class = $class->getParentClass());
  196. while ($interfaces) {
  197. $ownInterfaces = array_diff_key(array_pop($interfaces), $parentInterfaces);
  198. $parentInterfaces += $ownInterfaces;
  199. foreach ($ownInterfaces as $interface) {
  200. $class = new \ReflectionClass($interface);
  201. if ($attributes = $class->getAttributes($attribute, \ReflectionAttribute::IS_INSTANCEOF)) {
  202. $attributeReflector = $attributes[0];
  203. }
  204. }
  205. }
  206. return $attributeReflector?->newInstance();
  207. }
  208. }