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" },