From 6f67f0e0c05899db352d575d1ee13cd5a08eac82 Mon Sep 17 00:00:00 2001 From: Nicolas Grekas Date: Sat, 9 Nov 2019 17:42:52 +0100 Subject: [PATCH] [HttpKernel] make ExceptionEvent able to propagate any throwable --- UPGRADE-4.4.md | 2 + UPGRADE-5.0.md | 2 + .../FrameworkBundle/Console/Application.php | 4 +- .../FrameworkBundle/Resources/config/web.xml | 2 +- .../Tests/Kernel/ConcreteMicroKernel.php | 6 +- .../Controller/PreviewErrorController.php | 2 +- src/Symfony/Component/Console/Application.php | 7 +- .../Debug/Exception/FatalErrorException.php | 4 +- .../Debug/Exception/FatalThrowableError.php | 4 +- .../ErrorHandler/Exception/ErrorException.php | 42 ---- .../Exception/FlattenException.php | 3 +- .../Tests/Exception/FlattenExceptionTest.php | 11 -- src/Symfony/Component/HttpKernel/CHANGELOG.md | 2 + .../HttpKernel/Controller/ErrorController.php | 2 +- .../Event/GetResponseForExceptionEvent.php | 44 +++-- .../EventListener/DebugHandlersListener.php | 8 +- .../EventListener/ErrorListener.php | 134 +++++++++++++ .../EventListener/ExceptionListener.php | 6 +- .../EventListener/ProfilerListener.php | 2 +- .../EventListener/RouterListener.php | 2 +- .../Component/HttpKernel/HttpKernel.php | 2 +- .../Tests/EventListener/ErrorListenerTest.php | 180 ++++++++++++++++++ .../EventListener/ExceptionListenerTest.php | 27 +-- .../EventListener/RouterListenerTest.php | 4 +- .../HttpKernel/Tests/HttpKernelTest.php | 4 +- .../Http/Firewall/ExceptionListener.php | 10 +- .../Tests/Firewall/ExceptionListenerTest.php | 23 +-- .../Component/Security/Http/composer.json | 2 +- src/Symfony/Component/Security/composer.json | 2 +- 29 files changed, 393 insertions(+), 150 deletions(-) delete mode 100644 src/Symfony/Component/ErrorHandler/Exception/ErrorException.php create mode 100644 src/Symfony/Component/HttpKernel/EventListener/ErrorListener.php create mode 100644 src/Symfony/Component/HttpKernel/Tests/EventListener/ErrorListenerTest.php diff --git a/UPGRADE-4.4.md b/UPGRADE-4.4.md index 2253f5fd80c47..b4fb6a0a8e248 100644 --- a/UPGRADE-4.4.md +++ b/UPGRADE-4.4.md @@ -154,6 +154,8 @@ HttpKernel * Marked the `RouterDataCollector::collect()` method as `@final`. * The `DataCollectorInterface::collect()` and `Profiler::collect()` methods third parameter signature will be `\Throwable $exception = null` instead of `\Exception $exception = null` in Symfony 5.0. + * Deprecated methods `ExceptionEvent::get/setException()`, use `get/setThrowable()` instead + * Deprecated class `ExceptionListener`, use `ErrorListener` instead Lock ---- diff --git a/UPGRADE-5.0.md b/UPGRADE-5.0.md index e821a0a1382dd..8fc53005b312b 100644 --- a/UPGRADE-5.0.md +++ b/UPGRADE-5.0.md @@ -290,6 +290,8 @@ HttpKernel * Removed `TranslatorListener` in favor of `LocaleAwareListener` * The `DebugHandlersListener` class has been made `final` * Removed `SaveSessionListener` in favor of `AbstractSessionListener` + * Removed methods `ExceptionEvent::get/setException()`, use `get/setThrowable()` instead + * Removed class `ExceptionListener`, use `ErrorListener` instead * Added new Bundle directory convention consistent with standard skeletons: ``` diff --git a/src/Symfony/Bundle/FrameworkBundle/Console/Application.php b/src/Symfony/Bundle/FrameworkBundle/Console/Application.php index 32ebf9cc13596..dddde43dda4a1 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Console/Application.php +++ b/src/Symfony/Bundle/FrameworkBundle/Console/Application.php @@ -19,8 +19,8 @@ use Symfony\Component\Console\Output\ConsoleOutputInterface; use Symfony\Component\Console\Output\OutputInterface; use Symfony\Component\Console\Style\SymfonyStyle; +use Symfony\Component\Debug\Exception\FatalThrowableError; use Symfony\Component\DependencyInjection\ContainerAwareInterface; -use Symfony\Component\ErrorHandler\Exception\ErrorException; use Symfony\Component\HttpKernel\Bundle\Bundle; use Symfony\Component\HttpKernel\Kernel; use Symfony\Component\HttpKernel\KernelInterface; @@ -211,7 +211,7 @@ private function renderRegistrationErrors(InputInterface $input, OutputInterface $this->doRenderThrowable($error, $output); } else { if (!$error instanceof \Exception) { - $error = new ErrorException($error); + $error = new FatalThrowableError($error); } $this->doRenderException($error, $output); diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/config/web.xml b/src/Symfony/Bundle/FrameworkBundle/Resources/config/web.xml index ddbab05b42e21..aff90a584b87d 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Resources/config/web.xml +++ b/src/Symfony/Bundle/FrameworkBundle/Resources/config/web.xml @@ -95,7 +95,7 @@ - + %kernel.error_controller% diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Kernel/ConcreteMicroKernel.php b/src/Symfony/Bundle/FrameworkBundle/Tests/Kernel/ConcreteMicroKernel.php index 954eef5948eed..b2a84ed536863 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/Kernel/ConcreteMicroKernel.php +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Kernel/ConcreteMicroKernel.php @@ -19,7 +19,7 @@ use Symfony\Component\EventDispatcher\EventSubscriberInterface; use Symfony\Component\Filesystem\Filesystem; use Symfony\Component\HttpFoundation\Response; -use Symfony\Component\HttpKernel\Event\RequestEvent; +use Symfony\Component\HttpKernel\Event\ExceptionEvent; use Symfony\Component\HttpKernel\Kernel; use Symfony\Component\HttpKernel\KernelEvents; use Symfony\Component\Routing\RouteCollectionBuilder; @@ -30,9 +30,9 @@ class ConcreteMicroKernel extends Kernel implements EventSubscriberInterface private $cacheDir; - public function onKernelException(RequestEvent $event) + public function onKernelException(ExceptionEvent $event) { - if ($event->getException() instanceof Danger) { + if ($event->getThrowable() instanceof Danger) { $event->setResponse(Response::create('It\'s dangerous to go alone. Take this ⚔')); } } diff --git a/src/Symfony/Bundle/TwigBundle/Controller/PreviewErrorController.php b/src/Symfony/Bundle/TwigBundle/Controller/PreviewErrorController.php index 970e7f031c5ee..e8f66d0f049a6 100644 --- a/src/Symfony/Bundle/TwigBundle/Controller/PreviewErrorController.php +++ b/src/Symfony/Bundle/TwigBundle/Controller/PreviewErrorController.php @@ -43,7 +43,7 @@ public function previewErrorPageAction(Request $request, $code) /* * This Request mimics the parameters set by - * \Symfony\Component\HttpKernel\EventListener\ExceptionListener::duplicateRequest, with + * \Symfony\Component\HttpKernel\EventListener\ErrorListener::duplicateRequest, with * the additional "showException" flag. */ diff --git a/src/Symfony/Component/Console/Application.php b/src/Symfony/Component/Console/Application.php index 6fa82815ab356..6d17709279906 100644 --- a/src/Symfony/Component/Console/Application.php +++ b/src/Symfony/Component/Console/Application.php @@ -42,9 +42,8 @@ use Symfony\Component\Console\Output\OutputInterface; use Symfony\Component\Console\Style\SymfonyStyle; use Symfony\Component\Debug\ErrorHandler as LegacyErrorHandler; -use Symfony\Component\Debug\Exception\FatalThrowableError as LegacyFatalThrowableError; +use Symfony\Component\Debug\Exception\FatalThrowableError; use Symfony\Component\ErrorHandler\ErrorHandler; -use Symfony\Component\ErrorHandler\Exception\ErrorException; use Symfony\Component\EventDispatcher\EventDispatcherInterface; use Symfony\Component\EventDispatcher\LegacyEventDispatcherProxy; use Symfony\Contracts\Service\ResetInterface; @@ -809,7 +808,7 @@ public function renderThrowable(\Throwable $e, OutputInterface $output): void @trigger_error(sprintf('The "%s::renderException()" method is deprecated since Symfony 4.4, use "renderThrowable()" instead.', __CLASS__), E_USER_DEPRECATED); if (!$e instanceof \Exception) { - $e = class_exists(ErrorException::class) ? new ErrorException($e) : (class_exists(LegacyFatalThrowableError::class) ? new LegacyFatalThrowableError($e) : new \ErrorException($e->getMessage(), $e->getCode(), E_ERROR, $e->getFile(), $e->getLine())); + $e = class_exists(FatalThrowableError::class) ? new FatalThrowableError($e) : new \ErrorException($e->getMessage(), $e->getCode(), E_ERROR, $e->getFile(), $e->getLine()); } $this->renderException($e, $output); @@ -848,7 +847,7 @@ protected function doRenderThrowable(\Throwable $e, OutputInterface $output): vo @trigger_error(sprintf('The "%s::doRenderException()" method is deprecated since Symfony 4.4, use "doRenderThrowable()" instead.', __CLASS__), E_USER_DEPRECATED); if (!$e instanceof \Exception) { - $e = class_exists(ErrorException::class) ? new ErrorException($e) : (class_exists(LegacyFatalThrowableError::class) ? new LegacyFatalThrowableError($e) : new \ErrorException($e->getMessage(), $e->getCode(), E_ERROR, $e->getFile(), $e->getLine())); + $e = class_exists(FatalThrowableError::class) ? new FatalThrowableError($e) : new \ErrorException($e->getMessage(), $e->getCode(), E_ERROR, $e->getFile(), $e->getLine()); } $this->doRenderException($e, $output); diff --git a/src/Symfony/Component/Debug/Exception/FatalErrorException.php b/src/Symfony/Component/Debug/Exception/FatalErrorException.php index 571f3975da494..4eb445dcdbd5a 100644 --- a/src/Symfony/Component/Debug/Exception/FatalErrorException.php +++ b/src/Symfony/Component/Debug/Exception/FatalErrorException.php @@ -11,14 +11,14 @@ namespace Symfony\Component\Debug\Exception; -@trigger_error(sprintf('The "%s" class is deprecated since Symfony 4.4, use "%s" instead.', FatalErrorException::class, \Symfony\Component\ErrorHandler\Exception\ErrorException::class), E_USER_DEPRECATED); +@trigger_error(sprintf('The "%s" class is deprecated since Symfony 4.4, use "%s" instead.', FatalErrorException::class, \Symfony\Component\ErrorHandler\Error\FatalError::class), E_USER_DEPRECATED); /** * Fatal Error Exception. * * @author Konstanton Myakshin * - * @deprecated since Symfony 4.4, use Symfony\Component\ErrorHandler\Exception\ErrorException instead. + * @deprecated since Symfony 4.4, use Symfony\Component\ErrorHandler\Error\FatalError instead. */ class FatalErrorException extends \ErrorException { diff --git a/src/Symfony/Component/Debug/Exception/FatalThrowableError.php b/src/Symfony/Component/Debug/Exception/FatalThrowableError.php index 53c410b014b1d..e13b0172f0588 100644 --- a/src/Symfony/Component/Debug/Exception/FatalThrowableError.php +++ b/src/Symfony/Component/Debug/Exception/FatalThrowableError.php @@ -11,14 +11,14 @@ namespace Symfony\Component\Debug\Exception; -@trigger_error(sprintf('The "%s" class is deprecated since Symfony 4.4, use "%s" instead.', FatalThrowableError::class, \Symfony\Component\ErrorHandler\Exception\ErrorException::class), E_USER_DEPRECATED); +@trigger_error(sprintf('The "%s" class is deprecated since Symfony 4.4.', FatalThrowableError::class), E_USER_DEPRECATED); /** * Fatal Throwable Error. * * @author Nicolas Grekas * - * @deprecated since Symfony 4.4, use Symfony\Component\ErrorHandler\Exception\ErrorException instead. + * @deprecated since Symfony 4.4 */ class FatalThrowableError extends FatalErrorException { diff --git a/src/Symfony/Component/ErrorHandler/Exception/ErrorException.php b/src/Symfony/Component/ErrorHandler/Exception/ErrorException.php deleted file mode 100644 index 759d3fdc47a3f..0000000000000 --- a/src/Symfony/Component/ErrorHandler/Exception/ErrorException.php +++ /dev/null @@ -1,42 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Symfony\Component\ErrorHandler\Exception; - -use Symfony\Component\ErrorHandler\ThrowableUtils; - -class ErrorException extends \ErrorException -{ - private $originalClassName; - - public function __construct(\Throwable $e) - { - $this->originalClassName = \get_class($e); - - parent::__construct( - $e->getMessage(), - $e->getCode(), - ThrowableUtils::getSeverity($e), - $e->getFile(), - $e->getLine(), - $e->getPrevious() - ); - - $refl = new \ReflectionProperty(\Exception::class, 'trace'); - $refl->setAccessible(true); - $refl->setValue($this, $e->getTrace()); - } - - public function getOriginalClassName(): string - { - return $this->originalClassName; - } -} diff --git a/src/Symfony/Component/ErrorRenderer/Exception/FlattenException.php b/src/Symfony/Component/ErrorRenderer/Exception/FlattenException.php index da3454e05eb04..dd86f5b74bb07 100644 --- a/src/Symfony/Component/ErrorRenderer/Exception/FlattenException.php +++ b/src/Symfony/Component/ErrorRenderer/Exception/FlattenException.php @@ -12,7 +12,6 @@ namespace Symfony\Component\ErrorRenderer\Exception; use Symfony\Component\Debug\Exception\FlattenException as LegacyFlattenException; -use Symfony\Component\ErrorHandler\Exception\ErrorException; use Symfony\Component\HttpFoundation\Exception\RequestExceptionInterface; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpKernel\Exception\HttpExceptionInterface; @@ -70,7 +69,7 @@ public static function createFromThrowable(\Throwable $exception, int $statusCod $e->setStatusCode($statusCode); $e->setHeaders($headers); $e->setTraceFromThrowable($exception); - $e->setClass($exception instanceof ErrorException ? $exception->getOriginalClassName() : \get_class($exception)); + $e->setClass(\get_class($exception)); $e->setFile($exception->getFile()); $e->setLine($exception->getLine()); diff --git a/src/Symfony/Component/ErrorRenderer/Tests/Exception/FlattenExceptionTest.php b/src/Symfony/Component/ErrorRenderer/Tests/Exception/FlattenExceptionTest.php index dc2678be8158a..dad78d152540d 100644 --- a/src/Symfony/Component/ErrorRenderer/Tests/Exception/FlattenExceptionTest.php +++ b/src/Symfony/Component/ErrorRenderer/Tests/Exception/FlattenExceptionTest.php @@ -12,7 +12,6 @@ namespace Symfony\Component\ErrorRenderer\Tests\Exception; use PHPUnit\Framework\TestCase; -use Symfony\Component\ErrorHandler\Exception\ErrorException; use Symfony\Component\ErrorRenderer\Exception\FlattenException; use Symfony\Component\HttpFoundation\Exception\SuspiciousOperationException; use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException; @@ -130,16 +129,6 @@ public function testFlattenHttpException(\Throwable $exception) $this->assertInstanceOf($flattened->getClass(), $exception, 'The class is set to the class of the original exception'); } - public function testWrappedThrowable() - { - $exception = new ErrorException(new \DivisionByZeroError('Ouch', 42)); - $flattened = FlattenException::createFromThrowable($exception); - - $this->assertSame('Ouch', $flattened->getMessage(), 'The message is copied from the original error.'); - $this->assertSame(42, $flattened->getCode(), 'The code is copied from the original error.'); - $this->assertSame('DivisionByZeroError', $flattened->getClass(), 'The class is set to the class of the original error'); - } - public function testThrowable() { $error = new \DivisionByZeroError('Ouch', 42); diff --git a/src/Symfony/Component/HttpKernel/CHANGELOG.md b/src/Symfony/Component/HttpKernel/CHANGELOG.md index 28af7d7fc0a92..08a8cfddd7332 100644 --- a/src/Symfony/Component/HttpKernel/CHANGELOG.md +++ b/src/Symfony/Component/HttpKernel/CHANGELOG.md @@ -21,6 +21,8 @@ CHANGELOG * Marked the `RouterDataCollector::collect()` method as `@final`. * The `DataCollectorInterface::collect()` and `Profiler::collect()` methods third parameter signature will be `\Throwable $exception = null` instead of `\Exception $exception = null` in Symfony 5.0. + * Deprecated methods `ExceptionEvent::get/setException()`, use `get/setThrowable()` instead + * Deprecated class `ExceptionListener`, use `ErrorListener` instead 4.3.0 ----- diff --git a/src/Symfony/Component/HttpKernel/Controller/ErrorController.php b/src/Symfony/Component/HttpKernel/Controller/ErrorController.php index 4d58a61120cd0..a86fa5c5cf391 100644 --- a/src/Symfony/Component/HttpKernel/Controller/ErrorController.php +++ b/src/Symfony/Component/HttpKernel/Controller/ErrorController.php @@ -52,7 +52,7 @@ public function preview(Request $request, int $code): Response /* * This Request mimics the parameters set by - * \Symfony\Component\HttpKernel\EventListener\ExceptionListener::duplicateRequest, with + * \Symfony\Component\HttpKernel\EventListener\ErrorListener::duplicateRequest, with * the additional "showException" flag. */ $subRequest = $request->duplicate(null, null, [ diff --git a/src/Symfony/Component/HttpKernel/Event/GetResponseForExceptionEvent.php b/src/Symfony/Component/HttpKernel/Event/GetResponseForExceptionEvent.php index 3476c7e62a0cc..8b238f0db94dc 100644 --- a/src/Symfony/Component/HttpKernel/Event/GetResponseForExceptionEvent.php +++ b/src/Symfony/Component/HttpKernel/Event/GetResponseForExceptionEvent.php @@ -19,45 +19,55 @@ */ class GetResponseForExceptionEvent extends RequestEvent { - /** - * The exception object. - * - * @var \Exception - */ + private $throwable; private $exception; - - /** - * @var bool - */ private $allowCustomResponseCode = false; - public function __construct(HttpKernelInterface $kernel, Request $request, int $requestType, \Exception $e) + public function __construct(HttpKernelInterface $kernel, Request $request, int $requestType, \Throwable $e) { parent::__construct($kernel, $request, $requestType); - $this->setException($e); + $this->setThrowable($e); + } + + public function getThrowable(): \Throwable + { + return $this->throwable; + } + + /** + * Replaces the thrown exception. + * + * This exception will be thrown if no response is set in the event. + */ + public function setThrowable(\Throwable $exception): void + { + $this->exception = null; + $this->throwable = $exception; } /** - * Returns the thrown exception. + * @deprecated since Symfony 4.4, use getThrowable instead * * @return \Exception The thrown exception */ public function getException() { - return $this->exception; + @trigger_error(sprintf('The "%s()" method is deprecated since Symfony 4.4, use "getThrowable()" instead.', __METHOD__), E_USER_DEPRECATED); + + return $this->exception ?? $this->exception = $this->throwable instanceof \Exception ? $this->throwable : new FatalThrowableError($this->throwable); } /** - * Replaces the thrown exception. - * - * This exception will be thrown if no response is set in the event. + * @deprecated since Symfony 4.4, use setThrowable instead * * @param \Exception $exception The thrown exception */ public function setException(\Exception $exception) { - $this->exception = $exception; + @trigger_error(sprintf('The "%s()" method is deprecated since Symfony 4.4, use "setThrowable()" instead.', __METHOD__), E_USER_DEPRECATED); + + $this->throwable = $this->exception = $exception; } /** diff --git a/src/Symfony/Component/HttpKernel/EventListener/DebugHandlersListener.php b/src/Symfony/Component/HttpKernel/EventListener/DebugHandlersListener.php index 9779431eb9d03..8ed6a10e528a1 100644 --- a/src/Symfony/Component/HttpKernel/EventListener/DebugHandlersListener.php +++ b/src/Symfony/Component/HttpKernel/EventListener/DebugHandlersListener.php @@ -15,8 +15,8 @@ use Symfony\Component\Console\ConsoleEvents; use Symfony\Component\Console\Event\ConsoleEvent; use Symfony\Component\Console\Output\ConsoleOutputInterface; +use Symfony\Component\Debug\Exception\FatalThrowableError; use Symfony\Component\ErrorHandler\ErrorHandler; -use Symfony\Component\ErrorHandler\Exception\ErrorException; use Symfony\Component\EventDispatcher\Event; use Symfony\Component\EventDispatcher\EventSubscriberInterface; use Symfony\Component\HttpKernel\Debug\FileLinkFormatter; @@ -112,10 +112,6 @@ public function configure(Event $event = null) throw $e; } - if (!$e instanceof \Exception) { - $e = new ErrorException($e); - } - $hasRun = true; $kernel->terminateWithException($e, $request); }; @@ -130,7 +126,7 @@ public function configure(Event $event = null) $app->renderThrowable($e, $output); } else { if (!$e instanceof \Exception) { - $e = new ErrorException($e); + $e = new FatalThrowableError($e); } $app->renderException($e, $output); diff --git a/src/Symfony/Component/HttpKernel/EventListener/ErrorListener.php b/src/Symfony/Component/HttpKernel/EventListener/ErrorListener.php new file mode 100644 index 0000000000000..9a16cde741432 --- /dev/null +++ b/src/Symfony/Component/HttpKernel/EventListener/ErrorListener.php @@ -0,0 +1,134 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\HttpKernel\EventListener; + +use Psr\Log\LoggerInterface; +use Symfony\Component\ErrorRenderer\Exception\FlattenException; +use Symfony\Component\EventDispatcher\EventDispatcherInterface; +use Symfony\Component\EventDispatcher\EventSubscriberInterface; +use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\HttpKernel\Event\ExceptionEvent; +use Symfony\Component\HttpKernel\Exception\HttpExceptionInterface; +use Symfony\Component\HttpKernel\HttpKernelInterface; +use Symfony\Component\HttpKernel\KernelEvents; +use Symfony\Component\HttpKernel\Log\DebugLoggerInterface; + +/** + * @author Fabien Potencier + */ +class ErrorListener implements EventSubscriberInterface +{ + protected $controller; + protected $logger; + protected $debug; + + public function __construct($controller, LoggerInterface $logger = null, $debug = false) + { + $this->controller = $controller; + $this->logger = $logger; + $this->debug = $debug; + } + + public function logKernelException(ExceptionEvent $event) + { + $e = FlattenException::createFromThrowable($event->getThrowable()); + + $this->logException($event->getThrowable(), sprintf('Uncaught PHP Exception %s: "%s" at %s line %s', $e->getClass(), $e->getMessage(), $e->getFile(), $e->getLine())); + } + + public function onKernelException(ExceptionEvent $event) + { + if (null === $this->controller) { + return; + } + + $exception = $event->getThrowable(); + $request = $this->duplicateRequest($exception, $event->getRequest()); + $eventDispatcher = \func_num_args() > 2 ? func_get_arg(2) : null; + + try { + $response = $event->getKernel()->handle($request, HttpKernelInterface::SUB_REQUEST, false); + } catch (\Exception $e) { + $f = FlattenException::createFromThrowable($e); + + $this->logException($e, sprintf('Exception thrown when handling an exception (%s: %s at %s line %s)', $f->getClass(), $f->getMessage(), $e->getFile(), $e->getLine())); + + $prev = $e; + do { + if ($exception === $wrapper = $prev) { + throw $e; + } + } while ($prev = $wrapper->getPrevious()); + + $prev = new \ReflectionProperty($wrapper instanceof \Exception ? \Exception::class : \Error::class, 'previous'); + $prev->setAccessible(true); + $prev->setValue($wrapper, $exception); + + throw $e; + } + + $event->setResponse($response); + + if ($this->debug && $eventDispatcher instanceof EventDispatcherInterface) { + $cspRemovalListener = function ($event) use (&$cspRemovalListener, $eventDispatcher) { + $event->getResponse()->headers->remove('Content-Security-Policy'); + $eventDispatcher->removeListener(KernelEvents::RESPONSE, $cspRemovalListener); + }; + $eventDispatcher->addListener(KernelEvents::RESPONSE, $cspRemovalListener, -128); + } + } + + public static function getSubscribedEvents() + { + return [ + KernelEvents::EXCEPTION => [ + ['logKernelException', 0], + ['onKernelException', -128], + ], + ]; + } + + /** + * Logs an exception. + * + * @param \Exception $exception The \Exception instance + * @param string $message The error message to log + */ + protected function logException(\Exception $exception, $message) + { + if (null !== $this->logger) { + if (!$exception instanceof HttpExceptionInterface || $exception->getStatusCode() >= 500) { + $this->logger->critical($message, ['exception' => $exception]); + } else { + $this->logger->error($message, ['exception' => $exception]); + } + } + } + + /** + * Clones the request for the exception. + * + * @return Request The cloned request + */ + protected function duplicateRequest(\Exception $exception, Request $request) + { + $attributes = [ + '_controller' => $this->controller, + 'exception' => FlattenException::createFromThrowable($exception), + 'logger' => $this->logger instanceof DebugLoggerInterface ? $this->logger : null, + ]; + $request = $request->duplicate(null, null, $attributes); + $request->setMethod('GET'); + + return $request; + } +} diff --git a/src/Symfony/Component/HttpKernel/EventListener/ExceptionListener.php b/src/Symfony/Component/HttpKernel/EventListener/ExceptionListener.php index ae64374c6efb4..dc2fd818ce93a 100644 --- a/src/Symfony/Component/HttpKernel/EventListener/ExceptionListener.php +++ b/src/Symfony/Component/HttpKernel/EventListener/ExceptionListener.php @@ -22,10 +22,10 @@ use Symfony\Component\HttpKernel\KernelEvents; use Symfony\Component\HttpKernel\Log\DebugLoggerInterface; +@trigger_error(sprintf('The "%s" class is deprecated since Symfony 4.4, use "ErrorListener" instead.', ExceptionListener::class), E_USER_DEPRECATED); + /** - * @author Fabien Potencier - * - * @final since Symfony 4.3 + * @deprecated since Symfony 4.4, use ErrorListener instead */ class ExceptionListener implements EventSubscriberInterface { diff --git a/src/Symfony/Component/HttpKernel/EventListener/ProfilerListener.php b/src/Symfony/Component/HttpKernel/EventListener/ProfilerListener.php index f9db55211ea83..b8464f1627353 100644 --- a/src/Symfony/Component/HttpKernel/EventListener/ProfilerListener.php +++ b/src/Symfony/Component/HttpKernel/EventListener/ProfilerListener.php @@ -62,7 +62,7 @@ public function onKernelException(GetResponseForExceptionEvent $event) return; } - $this->exception = $event->getException(); + $this->exception = $event->getThrowable(); } /** diff --git a/src/Symfony/Component/HttpKernel/EventListener/RouterListener.php b/src/Symfony/Component/HttpKernel/EventListener/RouterListener.php index 9d8e83bc71e08..ee88debae45e8 100644 --- a/src/Symfony/Component/HttpKernel/EventListener/RouterListener.php +++ b/src/Symfony/Component/HttpKernel/EventListener/RouterListener.php @@ -143,7 +143,7 @@ public function onKernelRequest(GetResponseEvent $event) public function onKernelException(GetResponseForExceptionEvent $event) { - if (!$this->debug || !($e = $event->getException()) instanceof NotFoundHttpException) { + if (!$this->debug || !($e = $event->getThrowable()) instanceof NotFoundHttpException) { return; } diff --git a/src/Symfony/Component/HttpKernel/HttpKernel.php b/src/Symfony/Component/HttpKernel/HttpKernel.php index 02b35e31ec510..c4cc3a3cc41c5 100644 --- a/src/Symfony/Component/HttpKernel/HttpKernel.php +++ b/src/Symfony/Component/HttpKernel/HttpKernel.php @@ -207,7 +207,7 @@ private function handleThrowable(\Throwable $e, Request $request, int $type): Re $this->dispatcher->dispatch($event, KernelEvents::EXCEPTION); // a listener might have replaced the exception - $e = $event->getException(); + $e = $event->getThrowable(); if (!$event->hasResponse()) { $this->finishRequest($request, $type); diff --git a/src/Symfony/Component/HttpKernel/Tests/EventListener/ErrorListenerTest.php b/src/Symfony/Component/HttpKernel/Tests/EventListener/ErrorListenerTest.php new file mode 100644 index 0000000000000..3707f3c4c920e --- /dev/null +++ b/src/Symfony/Component/HttpKernel/Tests/EventListener/ErrorListenerTest.php @@ -0,0 +1,180 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\HttpKernel\Tests\EventListener; + +use PHPUnit\Framework\TestCase; +use Symfony\Component\EventDispatcher\EventDispatcher; +use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\HttpFoundation\Response; +use Symfony\Component\HttpKernel\Event\ExceptionEvent; +use Symfony\Component\HttpKernel\Event\ResponseEvent; +use Symfony\Component\HttpKernel\EventListener\ErrorListener; +use Symfony\Component\HttpKernel\HttpKernelInterface; +use Symfony\Component\HttpKernel\KernelEvents; +use Symfony\Component\HttpKernel\Log\DebugLoggerInterface; +use Symfony\Component\HttpKernel\Tests\Logger; + +/** + * @author Robert Schönthal + * + * @group time-sensitive + */ +class ErrorListenerTest extends TestCase +{ + public function testConstruct() + { + $logger = new TestLogger(); + $l = new ErrorListener('foo', $logger); + + $_logger = new \ReflectionProperty(\get_class($l), 'logger'); + $_logger->setAccessible(true); + $_controller = new \ReflectionProperty(\get_class($l), 'controller'); + $_controller->setAccessible(true); + + $this->assertSame($logger, $_logger->getValue($l)); + $this->assertSame('foo', $_controller->getValue($l)); + } + + /** + * @dataProvider provider + */ + public function testHandleWithoutLogger($event, $event2) + { + $this->iniSet('error_log', file_exists('/dev/null') ? '/dev/null' : 'nul'); + + $l = new ErrorListener('foo'); + $l->logKernelException($event); + $l->onKernelException($event); + + $this->assertEquals(new Response('foo'), $event->getResponse()); + + try { + $l->logKernelException($event2); + $l->onKernelException($event2); + $this->fail('RuntimeException expected'); + } catch (\RuntimeException $e) { + $this->assertSame('bar', $e->getMessage()); + $this->assertSame('foo', $e->getPrevious()->getMessage()); + } + } + + /** + * @dataProvider provider + */ + public function testHandleWithLogger($event, $event2) + { + $logger = new TestLogger(); + + $l = new ErrorListener('foo', $logger); + $l->logKernelException($event); + $l->onKernelException($event); + + $this->assertEquals(new Response('foo'), $event->getResponse()); + + try { + $l->logKernelException($event2); + $l->onKernelException($event2); + $this->fail('RuntimeException expected'); + } catch (\RuntimeException $e) { + $this->assertSame('bar', $e->getMessage()); + $this->assertSame('foo', $e->getPrevious()->getMessage()); + } + + $this->assertEquals(3, $logger->countErrors()); + $this->assertCount(3, $logger->getLogs('critical')); + } + + public function provider() + { + if (!class_exists('Symfony\Component\HttpFoundation\Request')) { + return [[null, null]]; + } + + $request = new Request(); + $exception = new \Exception('foo'); + $event = new ExceptionEvent(new TestKernel(), $request, HttpKernelInterface::MASTER_REQUEST, $exception); + $event2 = new ExceptionEvent(new TestKernelThatThrowsException(), $request, HttpKernelInterface::MASTER_REQUEST, $exception); + + return [ + [$event, $event2], + ]; + } + + public function testSubRequestFormat() + { + $listener = new ErrorListener('foo', $this->getMockBuilder('Psr\Log\LoggerInterface')->getMock()); + + $kernel = $this->getMockBuilder('Symfony\Component\HttpKernel\HttpKernelInterface')->getMock(); + $kernel->expects($this->once())->method('handle')->willReturnCallback(function (Request $request) { + return new Response($request->getRequestFormat()); + }); + + $request = Request::create('/'); + $request->setRequestFormat('xml'); + + $event = new ExceptionEvent($kernel, $request, HttpKernelInterface::MASTER_REQUEST, new \Exception('foo')); + $listener->onKernelException($event); + + $response = $event->getResponse(); + $this->assertEquals('xml', $response->getContent()); + } + + public function testCSPHeaderIsRemoved() + { + $dispatcher = new EventDispatcher(); + $kernel = $this->getMockBuilder('Symfony\Component\HttpKernel\HttpKernelInterface')->getMock(); + $kernel->expects($this->once())->method('handle')->willReturnCallback(function (Request $request) { + return new Response($request->getRequestFormat()); + }); + + $listener = new ErrorListener('foo', $this->getMockBuilder('Psr\Log\LoggerInterface')->getMock(), true); + + $dispatcher->addSubscriber($listener); + + $request = Request::create('/'); + $event = new ExceptionEvent($kernel, $request, HttpKernelInterface::MASTER_REQUEST, new \Exception('foo')); + $dispatcher->dispatch($event, KernelEvents::EXCEPTION); + + $response = new Response('', 200, ['content-security-policy' => "style-src 'self'"]); + $this->assertTrue($response->headers->has('content-security-policy')); + + $event = new ResponseEvent($kernel, $request, HttpKernelInterface::MASTER_REQUEST, $response); + $dispatcher->dispatch($event, KernelEvents::RESPONSE); + + $this->assertFalse($response->headers->has('content-security-policy'), 'CSP header has been removed'); + $this->assertFalse($dispatcher->hasListeners(KernelEvents::RESPONSE), 'CSP removal listener has been removed'); + } +} + +class TestLogger extends Logger implements DebugLoggerInterface +{ + public function countErrors(Request $request = null): int + { + return \count($this->logs['critical']); + } +} + +class TestKernel implements HttpKernelInterface +{ + public function handle(Request $request, $type = self::MASTER_REQUEST, $catch = true): Response + { + return new Response('foo'); + } +} + +class TestKernelThatThrowsException implements HttpKernelInterface +{ + public function handle(Request $request, $type = self::MASTER_REQUEST, $catch = true): Response + { + throw new \RuntimeException('bar'); + } +} diff --git a/src/Symfony/Component/HttpKernel/Tests/EventListener/ExceptionListenerTest.php b/src/Symfony/Component/HttpKernel/Tests/EventListener/ExceptionListenerTest.php index 38966edf2ccbb..28113c14afaba 100644 --- a/src/Symfony/Component/HttpKernel/Tests/EventListener/ExceptionListenerTest.php +++ b/src/Symfony/Component/HttpKernel/Tests/EventListener/ExceptionListenerTest.php @@ -20,8 +20,6 @@ use Symfony\Component\HttpKernel\EventListener\ExceptionListener; use Symfony\Component\HttpKernel\HttpKernelInterface; use Symfony\Component\HttpKernel\KernelEvents; -use Symfony\Component\HttpKernel\Log\DebugLoggerInterface; -use Symfony\Component\HttpKernel\Tests\Logger; /** * ExceptionListenerTest. @@ -29,6 +27,7 @@ * @author Robert Schönthal * * @group time-sensitive + * @group legacy */ class ExceptionListenerTest extends TestCase { @@ -157,26 +156,4 @@ public function testCSPHeaderIsRemoved() } } -class TestLogger extends Logger implements DebugLoggerInterface -{ - public function countErrors(Request $request = null): int - { - return \count($this->logs['critical']); - } -} - -class TestKernel implements HttpKernelInterface -{ - public function handle(Request $request, $type = self::MASTER_REQUEST, $catch = true): Response - { - return new Response('foo'); - } -} - -class TestKernelThatThrowsException implements HttpKernelInterface -{ - public function handle(Request $request, $type = self::MASTER_REQUEST, $catch = true): Response - { - throw new \RuntimeException('bar'); - } -} +class_exists(ErrorListenerTest::class); diff --git a/src/Symfony/Component/HttpKernel/Tests/EventListener/RouterListenerTest.php b/src/Symfony/Component/HttpKernel/Tests/EventListener/RouterListenerTest.php index ea88d4b34fa31..2c1e7721b4fa1 100644 --- a/src/Symfony/Component/HttpKernel/Tests/EventListener/RouterListenerTest.php +++ b/src/Symfony/Component/HttpKernel/Tests/EventListener/RouterListenerTest.php @@ -19,7 +19,7 @@ use Symfony\Component\HttpKernel\Controller\ArgumentResolver; use Symfony\Component\HttpKernel\Controller\ControllerResolver; use Symfony\Component\HttpKernel\Event\RequestEvent; -use Symfony\Component\HttpKernel\EventListener\ExceptionListener; +use Symfony\Component\HttpKernel\EventListener\ErrorListener; use Symfony\Component\HttpKernel\EventListener\RouterListener; use Symfony\Component\HttpKernel\EventListener\ValidateRequestListener; use Symfony\Component\HttpKernel\HttpKernel; @@ -168,7 +168,7 @@ public function testWithBadRequest() $dispatcher = new EventDispatcher(); $dispatcher->addSubscriber(new ValidateRequestListener()); $dispatcher->addSubscriber(new RouterListener($requestMatcher, $requestStack, new RequestContext())); - $dispatcher->addSubscriber(new ExceptionListener(function () { + $dispatcher->addSubscriber(new ErrorListener(function () { return new Response('Exception handled', 400); })); diff --git a/src/Symfony/Component/HttpKernel/Tests/HttpKernelTest.php b/src/Symfony/Component/HttpKernel/Tests/HttpKernelTest.php index 9a6170c086d35..14a84b6752e34 100644 --- a/src/Symfony/Component/HttpKernel/Tests/HttpKernelTest.php +++ b/src/Symfony/Component/HttpKernel/Tests/HttpKernelTest.php @@ -49,7 +49,7 @@ public function testHandleWhenControllerThrowsAnExceptionAndCatchIsTrueWithAHand { $dispatcher = new EventDispatcher(); $dispatcher->addListener(KernelEvents::EXCEPTION, function ($event) { - $event->setResponse(new Response($event->getException()->getMessage())); + $event->setResponse(new Response($event->getThrowable()->getMessage())); }); $kernel = $this->getHttpKernel($dispatcher, function () { throw new \RuntimeException('foo'); }); @@ -96,7 +96,7 @@ public function testHandleHttpException() { $dispatcher = new EventDispatcher(); $dispatcher->addListener(KernelEvents::EXCEPTION, function ($event) { - $event->setResponse(new Response($event->getException()->getMessage())); + $event->setResponse(new Response($event->getThrowable()->getMessage())); }); $kernel = $this->getHttpKernel($dispatcher, function () { throw new MethodNotAllowedHttpException(['POST']); }); diff --git a/src/Symfony/Component/Security/Http/Firewall/ExceptionListener.php b/src/Symfony/Component/Security/Http/Firewall/ExceptionListener.php index 549543e3efb17..2f97c7e04cc66 100644 --- a/src/Symfony/Component/Security/Http/Firewall/ExceptionListener.php +++ b/src/Symfony/Component/Security/Http/Firewall/ExceptionListener.php @@ -90,7 +90,7 @@ public function unregister(EventDispatcherInterface $dispatcher) */ public function onKernelException(GetResponseForExceptionEvent $event) { - $exception = $event->getException(); + $exception = $event->getThrowable(); do { if ($exception instanceof AuthenticationException) { $this->handleAuthenticationException($event, $exception); @@ -128,13 +128,13 @@ private function handleAuthenticationException(GetResponseForExceptionEvent $eve $event->setResponse($this->startAuthentication($event->getRequest(), $exception)); $event->allowCustomResponseCode(); } catch (\Exception $e) { - $event->setException($e); + $event->setThrowable($e); } } private function handleAccessDeniedException(GetResponseForExceptionEvent $event, AccessDeniedException $exception) { - $event->setException(new AccessDeniedHttpException($exception->getMessage(), $exception)); + $event->setThrowable(new AccessDeniedHttpException($exception->getMessage(), $exception)); $token = $this->tokenStorage->getToken(); if (!$this->authenticationTrustResolver->isFullFledged($token)) { @@ -148,7 +148,7 @@ private function handleAccessDeniedException(GetResponseForExceptionEvent $event $event->setResponse($this->startAuthentication($event->getRequest(), $insufficientAuthenticationException)); } catch (\Exception $e) { - $event->setException($e); + $event->setThrowable($e); } return; @@ -177,7 +177,7 @@ private function handleAccessDeniedException(GetResponseForExceptionEvent $event $this->logger->error('An exception was thrown when handling an AccessDeniedException.', ['exception' => $e]); } - $event->setException(new \RuntimeException('Exception thrown when handling an exception.', 0, $e)); + $event->setThrowable(new \RuntimeException('Exception thrown when handling an exception.', 0, $e)); } } diff --git a/src/Symfony/Component/Security/Http/Tests/Firewall/ExceptionListenerTest.php b/src/Symfony/Component/Security/Http/Tests/Firewall/ExceptionListenerTest.php index f02a52894df1c..fb1db914286fb 100644 --- a/src/Symfony/Component/Security/Http/Tests/Firewall/ExceptionListenerTest.php +++ b/src/Symfony/Component/Security/Http/Tests/Firewall/ExceptionListenerTest.php @@ -15,7 +15,6 @@ use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpKernel\Event\ExceptionEvent; -use Symfony\Component\HttpKernel\Event\GetResponseForExceptionEvent; use Symfony\Component\HttpKernel\Exception\HttpException; use Symfony\Component\HttpKernel\HttpKernelInterface; use Symfony\Component\Security\Core\Authentication\AuthenticationTrustResolverInterface; @@ -40,7 +39,7 @@ public function testAuthenticationExceptionWithoutEntryPoint(\Exception $excepti $listener->onKernelException($event); $this->assertNull($event->getResponse()); - $this->assertEquals($eventException, $event->getException()); + $this->assertEquals($eventException, $event->getThrowable()); } /** @@ -59,7 +58,7 @@ public function testAuthenticationExceptionWithEntryPoint(\Exception $exception) $this->assertEquals('Forbidden', $event->getResponse()->getContent()); $this->assertEquals(403, $event->getResponse()->getStatusCode()); - $this->assertSame($exception, $event->getException()); + $this->assertSame($exception, $event->getThrowable()); } public function getAuthenticationExceptionProvider() @@ -86,8 +85,8 @@ public function testExceptionWhenEntryPointReturnsBadValue() $listener = $this->createExceptionListener(null, null, null, $entryPoint); $listener->onKernelException($event); // the exception has been replaced by our LogicException - $this->assertInstanceOf('LogicException', $event->getException()); - $this->assertStringEndsWith('start() method must return a Response object (string returned)', $event->getException()->getMessage()); + $this->assertInstanceOf('LogicException', $event->getThrowable()); + $this->assertStringEndsWith('start() method must return a Response object (string returned)', $event->getThrowable()->getMessage()); } /** @@ -101,7 +100,7 @@ public function testAccessDeniedExceptionFullFledgedAndWithoutAccessDeniedHandle $listener->onKernelException($event); $this->assertNull($event->getResponse()); - $this->assertSame(null === $eventException ? $exception : $eventException, $event->getException()->getPrevious()); + $this->assertSame(null === $eventException ? $exception : $eventException, $event->getThrowable()->getPrevious()); } /** @@ -124,7 +123,7 @@ public function testAccessDeniedExceptionFullFledgedAndWithoutAccessDeniedHandle $this->assertEquals('Unauthorized', $event->getResponse()->getContent()); $this->assertEquals(401, $event->getResponse()->getStatusCode()); - $this->assertSame(null === $eventException ? $exception : $eventException, $event->getException()->getPrevious()); + $this->assertSame(null === $eventException ? $exception : $eventException, $event->getThrowable()->getPrevious()); } /** @@ -141,7 +140,7 @@ public function testAccessDeniedExceptionFullFledgedAndWithAccessDeniedHandlerAn $listener->onKernelException($event); $this->assertEquals('error', $event->getResponse()->getContent()); - $this->assertSame(null === $eventException ? $exception : $eventException, $event->getException()->getPrevious()); + $this->assertSame(null === $eventException ? $exception : $eventException, $event->getThrowable()->getPrevious()); } /** @@ -158,7 +157,7 @@ public function testAccessDeniedExceptionNotFullFledged(\Exception $exception, \ $listener->onKernelException($event); $this->assertEquals('OK', $event->getResponse()->getContent()); - $this->assertSame(null === $eventException ? $exception : $eventException, $event->getException()->getPrevious()); + $this->assertSame(null === $eventException ? $exception : $eventException, $event->getThrowable()->getPrevious()); } public function getAccessDeniedExceptionProvider() @@ -194,11 +193,7 @@ private function createEvent(\Exception $exception, $kernel = null) $kernel = $this->getMockBuilder('Symfony\Component\HttpKernel\HttpKernelInterface')->getMock(); } - if (class_exists(ExceptionEvent::class)) { - return new ExceptionEvent($kernel, Request::create('/'), HttpKernelInterface::MASTER_REQUEST, $exception); - } - - return new GetResponseForExceptionEvent($kernel, Request::create('/'), HttpKernelInterface::MASTER_REQUEST, $exception); + return new ExceptionEvent($kernel, Request::create('/'), HttpKernelInterface::MASTER_REQUEST, $exception); } private function createExceptionListener(TokenStorageInterface $tokenStorage = null, AuthenticationTrustResolverInterface $trustResolver = null, HttpUtils $httpUtils = null, AuthenticationEntryPointInterface $authenticationEntryPoint = null, $errorPage = null, AccessDeniedHandlerInterface $accessDeniedHandler = null) diff --git a/src/Symfony/Component/Security/Http/composer.json b/src/Symfony/Component/Security/Http/composer.json index d51c9d77e438f..686b2b9f1ec54 100644 --- a/src/Symfony/Component/Security/Http/composer.json +++ b/src/Symfony/Component/Security/Http/composer.json @@ -19,7 +19,7 @@ "php": "^7.1.3", "symfony/security-core": "^4.4", "symfony/http-foundation": "^3.4|^4.0|^5.0", - "symfony/http-kernel": "^4.3", + "symfony/http-kernel": "^4.4", "symfony/property-access": "^3.4|^4.0|^5.0" }, "require-dev": { diff --git a/src/Symfony/Component/Security/composer.json b/src/Symfony/Component/Security/composer.json index fb5fd768c1265..5251cd8f005fb 100644 --- a/src/Symfony/Component/Security/composer.json +++ b/src/Symfony/Component/Security/composer.json @@ -19,7 +19,7 @@ "php": "^7.1.3", "symfony/event-dispatcher-contracts": "^1.1|^2", "symfony/http-foundation": "^3.4|^4.0|^5.0", - "symfony/http-kernel": "^4.3", + "symfony/http-kernel": "^4.4", "symfony/property-access": "^3.4|^4.0|^5.0", "symfony/service-contracts": "^1.1|^2" }, pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy