From b79532ab0e7168cf805a117ee6f17fa05ffeb1c9 Mon Sep 17 00:00:00 2001 From: Yonel Ceruto Date: Wed, 21 Aug 2019 02:21:47 -0400 Subject: [PATCH] Add ErrorController to preview and render errors --- UPGRADE-4.4.md | 25 +++- UPGRADE-5.0.md | 4 +- .../Bundle/FrameworkBundle/CHANGELOG.md | 3 +- .../DependencyInjection/Configuration.php | 4 +- .../FrameworkExtension.php | 1 + .../Resources/config/debug_prod.xml | 2 - .../Resources/config/routing/errors.xml | 12 ++ .../Resources/config/schema/symfony-1.0.xsd | 1 + .../FrameworkBundle/Resources/config/web.xml | 14 ++ .../Fixture/TestAppKernel.php | 2 +- .../DependencyInjection/ConfigurationTest.php | 1 + .../Functional/app/BundlePaths/config.yml | 2 +- .../Tests/Functional/app/Fragment/config.yml | 2 +- .../Functional/MissingUserProviderTest.php | 8 +- .../Functional/app/ExceptionController.php | 37 ------ .../Functional/app/JsonLogin/bundles.php | 1 - .../Tests/Functional/app/JsonLogin/config.yml | 2 +- .../app/JsonLogin/custom_handlers.yml | 2 +- .../Tests/Functional/app/config/twig.yml | 2 +- src/Symfony/Bundle/TwigBundle/CHANGELOG.md | 5 +- .../Controller/ExceptionController.php | 4 +- .../Controller/PreviewErrorController.php | 16 +-- .../Compiler/ExceptionListenerPass.php | 19 ++- .../DependencyInjection/Configuration.php | 10 +- .../TwigBundle/Resources/config/twig.xml | 3 +- .../Controller/PreviewErrorControllerTest.php | 3 + .../DependencyInjection/ConfigurationTest.php | 8 +- .../php/customTemplateEscapingGuesser.php | 2 +- .../Fixtures/php/empty.php | 2 +- .../Fixtures/php/formats.php | 2 +- .../DependencyInjection/Fixtures/php/full.php | 2 +- .../DependencyInjection/TwigExtensionTest.php | 2 +- .../Functional/NoTemplatingEntryTest.php | 2 +- src/Symfony/Component/HttpKernel/CHANGELOG.md | 1 + .../HttpKernel/Controller/ErrorController.php | 67 ++++++++++ .../EventListener/DebugHandlersListener.php | 41 +----- .../Tests/Controller/ErrorControllerTest.php | 123 ++++++++++++++++++ .../DebugHandlersListenerTest.php | 1 - 38 files changed, 308 insertions(+), 130 deletions(-) create mode 100644 src/Symfony/Bundle/FrameworkBundle/Resources/config/routing/errors.xml delete mode 100644 src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/ExceptionController.php create mode 100644 src/Symfony/Component/HttpKernel/Controller/ErrorController.php create mode 100644 src/Symfony/Component/HttpKernel/Tests/Controller/ErrorControllerTest.php diff --git a/UPGRADE-4.4.md b/UPGRADE-4.4.md index 3c04fc45bed3..aeaedc3c8501 100644 --- a/UPGRADE-4.4.md +++ b/UPGRADE-4.4.md @@ -217,9 +217,25 @@ TwigBridge TwigBundle ---------- - * Deprecated default value `twig.controller.exception::showAction` of the `twig.exception_controller` configuration option, - set it to `null` instead. This will also change the default error response format according to https://tools.ietf.org/html/rfc7807 - for `json`, `xml`, `atom` and `txt` formats: + * Deprecated `twig.exception_controller` configuration option, set it to "null" and use `framework.error_controller` instead: + + Before: + ```yaml + twig: + exception_controller: 'App\Controller\MyExceptionController' + ``` + + After: + ```yaml + twig: + exception_controller: null + + framework: + error_controller: 'App\Controller\MyExceptionController' + ``` + + The new default exception controller will also change the error response content according to + https://tools.ietf.org/html/rfc7807 for `json`, `xml`, `atom` and `txt` formats: Before: ```json @@ -240,7 +256,8 @@ TwigBundle } ``` - * Deprecated the `ExceptionController` and all built-in error templates, use the error renderer mechanism of the `ErrorRenderer` component + * Deprecated the `ExceptionController` and `PreviewErrorController` controllers, use `ErrorController` from the HttpKernel component instead + * Deprecated all built-in error templates, use the error renderer mechanism of the `ErrorRenderer` component * Deprecated loading custom error templates in non-html formats. Custom HTML error pages based on Twig keep working as before: Before (`templates/bundles/TwigBundle/Exception/error.jsonld.twig`): diff --git a/UPGRADE-5.0.md b/UPGRADE-5.0.md index 94ec159545e7..4059931105ac 100644 --- a/UPGRADE-5.0.md +++ b/UPGRADE-5.0.md @@ -538,8 +538,8 @@ TwigBundle * The default value (`false`) of the `twig.strict_variables` configuration option has been changed to `%kernel.debug%`. * The `transchoice` tag and filter have been removed, use the `trans` ones instead with a `%count%` parameter. * Removed support for legacy templates directories `src/Resources/views/` and `src/Resources//views/`, use `templates/` and `templates/bundles//` instead. - * The default value (`twig.controller.exception::showAction`) of the `twig.exception_controller` configuration option has been changed to `null`. - * Removed `ExceptionController` class and all built-in error templates + * The `twig.exception_controller` configuration option has been removed, use `framework.error_controller` instead. + * Removed `ExceptionController`, `PreviewErrorController` classes and all built-in error templates TwigBridge ---------- diff --git a/src/Symfony/Bundle/FrameworkBundle/CHANGELOG.md b/src/Symfony/Bundle/FrameworkBundle/CHANGELOG.md index 446a26a3e0a4..9fe310373206 100644 --- a/src/Symfony/Bundle/FrameworkBundle/CHANGELOG.md +++ b/src/Symfony/Bundle/FrameworkBundle/CHANGELOG.md @@ -14,7 +14,8 @@ CHANGELOG * Deprecated `routing.loader.service`, use `routing.loader.container` instead. * Not tagging service route loaders with `routing.route_loader` has been deprecated. * Overriding the methods `KernelTestCase::tearDown()` and `WebTestCase::tearDown()` without the `void` return-type is deprecated. - + * Added new `error_controller` configuration to handle system exceptions + 4.3.0 ----- diff --git a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Configuration.php b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Configuration.php index 84dd91b0ef06..bf16ddc96f9c 100644 --- a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Configuration.php +++ b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Configuration.php @@ -28,7 +28,6 @@ use Symfony\Component\Mailer\Mailer; use Symfony\Component\Messenger\MessageBusInterface; use Symfony\Component\PropertyInfo\PropertyInfoExtractorInterface; -use Symfony\Component\Security\Csrf\CsrfTokenManagerInterface; use Symfony\Component\Serializer\Serializer; use Symfony\Component\Translation\Translator; use Symfony\Component\Validator\Validation; @@ -84,6 +83,9 @@ public function getConfigTreeBuilder() ->beforeNormalization()->ifString()->then(function ($v) { return [$v]; })->end() ->prototype('scalar')->end() ->end() + ->scalarNode('error_controller') + ->defaultValue('error_controller') + ->end() ->end() ; diff --git a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php index 0fa3211e33fb..309a48f2442a 100644 --- a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php +++ b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php @@ -212,6 +212,7 @@ public function load(array $configs, ContainerBuilder $container) $container->setParameter('kernel.http_method_override', $config['http_method_override']); $container->setParameter('kernel.trusted_hosts', $config['trusted_hosts']); $container->setParameter('kernel.default_locale', $config['default_locale']); + $container->setParameter('kernel.error_controller', $config['error_controller']); if (!$container->hasParameter('debug.file_link_format')) { if (!$container->hasParameter('templating.helper.code.file_link_format')) { diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/config/debug_prod.xml b/src/Symfony/Bundle/FrameworkBundle/Resources/config/debug_prod.xml index f95b218d52de..786158dd899e 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Resources/config/debug_prod.xml +++ b/src/Symfony/Bundle/FrameworkBundle/Resources/config/debug_prod.xml @@ -21,8 +21,6 @@ %kernel.debug% %kernel.debug% - %kernel.charset% - diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/config/routing/errors.xml b/src/Symfony/Bundle/FrameworkBundle/Resources/config/routing/errors.xml new file mode 100644 index 000000000000..13a9cc4076c7 --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Resources/config/routing/errors.xml @@ -0,0 +1,12 @@ + + + + + + error_controller::preview + html + \d+ + + diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/config/schema/symfony-1.0.xsd b/src/Symfony/Bundle/FrameworkBundle/Resources/config/schema/symfony-1.0.xsd index f1dae61035fc..ae1c75dcc96e 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Resources/config/schema/symfony-1.0.xsd +++ b/src/Symfony/Bundle/FrameworkBundle/Resources/config/schema/symfony-1.0.xsd @@ -41,6 +41,7 @@ + diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/config/web.xml b/src/Symfony/Bundle/FrameworkBundle/Resources/config/web.xml index 8cc62a72a68e..ddbab05b42e2 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Resources/config/web.xml +++ b/src/Symfony/Bundle/FrameworkBundle/Resources/config/web.xml @@ -88,5 +88,19 @@ + + + + %kernel.error_controller% + + + + + + + %kernel.error_controller% + + %kernel.debug% + diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Command/CacheClearCommand/Fixture/TestAppKernel.php b/src/Symfony/Bundle/FrameworkBundle/Tests/Command/CacheClearCommand/Fixture/TestAppKernel.php index 26d03ef9b99b..d6a58798c136 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/Command/CacheClearCommand/Fixture/TestAppKernel.php +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Command/CacheClearCommand/Fixture/TestAppKernel.php @@ -38,7 +38,7 @@ public function registerContainerConfiguration(LoaderInterface $loader) public function setAnnotatedClassCache(array $annotatedClasses) { - $annotatedClasses = array_diff($annotatedClasses, ['Symfony\Bundle\WebProfilerBundle\Controller\ExceptionController', 'Symfony\Bundle\TwigBundle\Controller\ExceptionController']); + $annotatedClasses = array_diff($annotatedClasses, ['Symfony\Bundle\WebProfilerBundle\Controller\ExceptionController', 'Symfony\Bundle\TwigBundle\Controller\ExceptionController', 'Symfony\Bundle\TwigBundle\Controller\PreviewErrorController']); parent::setAnnotatedClassCache($annotatedClasses); } diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/ConfigurationTest.php b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/ConfigurationTest.php index efead383b11c..f2faf79d6909 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/ConfigurationTest.php +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/ConfigurationTest.php @@ -373,6 +373,7 @@ class_exists(SemaphoreStore::class) && SemaphoreStore::isSupported() ? 'semaphor 'transports' => [], 'enabled' => !class_exists(FullStack::class) && class_exists(Mailer::class), ], + 'error_controller' => 'error_controller', ]; } } diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/app/BundlePaths/config.yml b/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/app/BundlePaths/config.yml index 4a2d4c57ef6a..3e1e53738cf9 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/app/BundlePaths/config.yml +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/app/BundlePaths/config.yml @@ -8,4 +8,4 @@ framework: twig: strict_variables: '%kernel.debug%' - exception_controller: ~ + exception_controller: null # to be removed in 5.0 diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/app/Fragment/config.yml b/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/app/Fragment/config.yml index f48b4444fbde..ceeea37a1001 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/app/Fragment/config.yml +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/app/Fragment/config.yml @@ -7,4 +7,4 @@ framework: twig: strict_variables: '%kernel.debug%' - exception_controller: ~ + exception_controller: null # to be removed in 5.0 diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/Functional/MissingUserProviderTest.php b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/MissingUserProviderTest.php index 6231bde6414d..47d68b4f682d 100644 --- a/src/Symfony/Bundle/SecurityBundle/Tests/Functional/MissingUserProviderTest.php +++ b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/MissingUserProviderTest.php @@ -15,13 +15,15 @@ class MissingUserProviderTest extends AbstractWebTestCase { public function testUserProviderIsNeeded() { - $this->expectException('Symfony\Component\Config\Definition\Exception\InvalidConfigurationException'); - $this->expectExceptionMessage('"default" firewall requires a user provider but none was defined.'); - $client = $this->createClient(['test_case' => 'MissingUserProvider', 'root_config' => 'config.yml']); + $client = $this->createClient(['test_case' => 'MissingUserProvider', 'root_config' => 'config.yml', 'debug' => true]); $client->request('GET', '/', [], [], [ 'PHP_AUTH_USER' => 'username', 'PHP_AUTH_PW' => 'pa$$word', ]); + + $response = $client->getResponse(); + $this->assertSame(500, $response->getStatusCode()); + $this->stringContains('"default" firewall requires a user provider but none was defined.', $response->getContent()); } } diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/ExceptionController.php b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/ExceptionController.php deleted file mode 100644 index 7a22a599b74d..000000000000 --- a/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/ExceptionController.php +++ /dev/null @@ -1,37 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Symfony\Bundle\SecurityBundle\Tests\Functional\app; - -use Symfony\Component\ErrorRenderer\ErrorRenderer; -use Symfony\Component\ErrorRenderer\ErrorRenderer\HtmlErrorRenderer; -use Symfony\Component\ErrorRenderer\ErrorRenderer\JsonErrorRenderer; -use Symfony\Component\ErrorRenderer\Exception\FlattenException; -use Symfony\Component\HttpFoundation\Request; -use Symfony\Component\HttpFoundation\Response; - -class ExceptionController -{ - private $errorRenderer; - - public function __construct() - { - $this->errorRenderer = new ErrorRenderer([ - new HtmlErrorRenderer(), - new JsonErrorRenderer(), - ]); - } - - public function __invoke(Request $request, FlattenException $exception) - { - return new Response($this->errorRenderer->render($exception, $request->getPreferredFormat()), $exception->getStatusCode()); - } -} diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/JsonLogin/bundles.php b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/JsonLogin/bundles.php index 7dbd6e438072..cd367a95b477 100644 --- a/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/JsonLogin/bundles.php +++ b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/JsonLogin/bundles.php @@ -12,6 +12,5 @@ return [ new Symfony\Bundle\SecurityBundle\SecurityBundle(), new Symfony\Bundle\FrameworkBundle\FrameworkBundle(), - new Symfony\Bundle\TwigBundle\TwigBundle(), new Symfony\Bundle\SecurityBundle\Tests\Functional\Bundle\JsonLoginBundle\JsonLoginBundle(), ]; diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/JsonLogin/config.yml b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/JsonLogin/config.yml index cf92920f4bc2..d6ed10e896ff 100644 --- a/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/JsonLogin/config.yml +++ b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/JsonLogin/config.yml @@ -1,5 +1,5 @@ imports: - - { resource: ./../config/default.yml } + - { resource: ./../config/framework.yml } security: encoders: diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/JsonLogin/custom_handlers.yml b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/JsonLogin/custom_handlers.yml index dff93273e804..e15e203c626c 100644 --- a/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/JsonLogin/custom_handlers.yml +++ b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/JsonLogin/custom_handlers.yml @@ -1,5 +1,5 @@ imports: - - { resource: ./../config/default.yml } + - { resource: ./../config/framework.yml } security: encoders: diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/config/twig.yml b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/config/twig.yml index e53084cda7c0..f578e4b51037 100644 --- a/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/config/twig.yml +++ b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/config/twig.yml @@ -2,4 +2,4 @@ twig: debug: '%kernel.debug%' strict_variables: '%kernel.debug%' - exception_controller: Symfony\Bundle\SecurityBundle\Tests\Functional\app\ExceptionController + exception_controller: null # to be removed in 5.0 diff --git a/src/Symfony/Bundle/TwigBundle/CHANGELOG.md b/src/Symfony/Bundle/TwigBundle/CHANGELOG.md index eb1f93246a34..99b14db8d52c 100644 --- a/src/Symfony/Bundle/TwigBundle/CHANGELOG.md +++ b/src/Symfony/Bundle/TwigBundle/CHANGELOG.md @@ -7,8 +7,9 @@ CHANGELOG * marked the `TemplateIterator` as `internal` * added HTML comment to beginning and end of `exception_full.html.twig` * added a new `TwigHtmlErrorRenderer` for `html` format, integrated with the `ErrorRenderer` component - * deprecated `ExceptionController` class and all built-in error templates in favor of the new error renderer mechanism - * deprecated default value `twig.controller.exception::showAction` of `twig.exception_controller` configuration option, set it to `null` instead + * deprecated `ExceptionController` and `PreviewErrorController` controllers, use `ErrorController` from the `HttpKernel` component instead + * deprecated all built-in error templates in favor of the new error renderer mechanism + * deprecated `twig.exception_controller` configuration option, set it to "null" and use `framework.error_controller` configuration instead 4.2.0 ----- diff --git a/src/Symfony/Bundle/TwigBundle/Controller/ExceptionController.php b/src/Symfony/Bundle/TwigBundle/Controller/ExceptionController.php index 003bf47c4d63..8c6174f876bf 100644 --- a/src/Symfony/Bundle/TwigBundle/Controller/ExceptionController.php +++ b/src/Symfony/Bundle/TwigBundle/Controller/ExceptionController.php @@ -19,7 +19,7 @@ use Twig\Error\LoaderError; use Twig\Loader\ExistsLoaderInterface; -@trigger_error(sprintf('The "%s" class is deprecated since Symfony 4.4, use the ErrorRenderer component instead.', ExceptionController::class), E_USER_DEPRECATED); +@trigger_error(sprintf('The "%s" class is deprecated since Symfony 4.4, use "%s" instead.', ExceptionController::class, \Symfony\Component\HttpKernel\Controller\ErrorController::class), E_USER_DEPRECATED); /** * ExceptionController renders error or exception pages for a given @@ -28,7 +28,7 @@ * @author Fabien Potencier * @author Matthias Pigulla * - * @deprecated since Symfony 4.4, use the ErrorRenderer component instead. + * @deprecated since Symfony 4.4, use Symfony\Component\HttpKernel\Controller\ErrorController instead. */ class ExceptionController { diff --git a/src/Symfony/Bundle/TwigBundle/Controller/PreviewErrorController.php b/src/Symfony/Bundle/TwigBundle/Controller/PreviewErrorController.php index bc15a968e9c1..7e82c3e68a9a 100644 --- a/src/Symfony/Bundle/TwigBundle/Controller/PreviewErrorController.php +++ b/src/Symfony/Bundle/TwigBundle/Controller/PreviewErrorController.php @@ -11,39 +11,35 @@ namespace Symfony\Bundle\TwigBundle\Controller; -use Symfony\Component\ErrorRenderer\ErrorRenderer; use Symfony\Component\ErrorRenderer\Exception\FlattenException; use Symfony\Component\HttpFoundation\Request; -use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpKernel\HttpKernelInterface; +@trigger_error(sprintf('The "%s" class is deprecated since Symfony 4.4, use the "%s" instead.', PreviewErrorController::class, \Symfony\Component\HttpKernel\Controller\ErrorController::class), E_USER_DEPRECATED); + /** * PreviewErrorController can be used to test error pages. * * It will create a test exception and forward it to another controller. * * @author Matthias Pigulla + * + * @deprecated since Symfony 4.4, use the Symfony\Component\HttpKernel\Controller\ErrorController instead. */ class PreviewErrorController { protected $kernel; protected $controller; - private $errorRenderer; - public function __construct(HttpKernelInterface $kernel, $controller, ErrorRenderer $errorRenderer = null) + public function __construct(HttpKernelInterface $kernel, $controller) { $this->kernel = $kernel; $this->controller = $controller; - $this->errorRenderer = $errorRenderer; } public function previewErrorPageAction(Request $request, $code) { - $exception = FlattenException::createFromThrowable(new \Exception('Something has intentionally gone wrong.'), $code, ['X-Debug' => false]); - - if (null === $this->controller && null !== $this->errorRenderer) { - return new Response($this->errorRenderer->render($exception, $request->getPreferredFormat()), $code); - } + $exception = FlattenException::createFromThrowable(new \Exception('Something has intentionally gone wrong.'), $code); /* * This Request mimics the parameters set by diff --git a/src/Symfony/Bundle/TwigBundle/DependencyInjection/Compiler/ExceptionListenerPass.php b/src/Symfony/Bundle/TwigBundle/DependencyInjection/Compiler/ExceptionListenerPass.php index ff5a0e220796..d06b8e8199c2 100644 --- a/src/Symfony/Bundle/TwigBundle/DependencyInjection/Compiler/ExceptionListenerPass.php +++ b/src/Symfony/Bundle/TwigBundle/DependencyInjection/Compiler/ExceptionListenerPass.php @@ -18,6 +18,8 @@ * Registers the Twig exception listener if Twig is registered as a templating engine. * * @author Fabien Potencier + * + * @internal */ class ExceptionListenerPass implements CompilerPassInterface { @@ -27,13 +29,18 @@ public function process(ContainerBuilder $container) return; } - // register the exception controller only if Twig is enabled and required dependencies do exist - if (!class_exists('Symfony\Component\ErrorRenderer\Exception\FlattenException') || !interface_exists('Symfony\Component\EventDispatcher\EventSubscriberInterface')) { + // to be removed in 5.0 + // register the exception listener only if it's currently used, else use the provided by FrameworkBundle + if (null === $container->getParameter('twig.exception_listener.controller') && $container->hasDefinition('exception_listener')) { $container->removeDefinition('twig.exception_listener'); - } elseif ($container->hasParameter('templating.engines')) { - $engines = $container->getParameter('templating.engines'); - if (!\in_array('twig', $engines)) { - $container->removeDefinition('twig.exception_listener'); + } else { + $container->removeDefinition('exception_listener'); + + if ($container->hasParameter('templating.engines')) { + $engines = $container->getParameter('templating.engines'); + if (!\in_array('twig', $engines, true)) { + $container->removeDefinition('twig.exception_listener'); + } } } } diff --git a/src/Symfony/Bundle/TwigBundle/DependencyInjection/Configuration.php b/src/Symfony/Bundle/TwigBundle/DependencyInjection/Configuration.php index ebbf8d3d325f..99be854a0cc4 100644 --- a/src/Symfony/Bundle/TwigBundle/DependencyInjection/Configuration.php +++ b/src/Symfony/Bundle/TwigBundle/DependencyInjection/Configuration.php @@ -36,10 +36,18 @@ public function getConfigTreeBuilder() ->children() ->scalarNode('exception_controller') ->defaultValue(static function () { - @trigger_error('Relying on the default value ("twig.controller.exception::showAction") of the "twig.exception_controller" configuration option is deprecated since Symfony 4.4, set it to "null" explicitly instead, which will be the new default in 5.0.', E_USER_DEPRECATED); + @trigger_error('The "twig.exception_controller" configuration key has been deprecated in Symfony 4.4, set it to "null" and use "framework.error_controller" configuration key instead.', E_USER_DEPRECATED); return 'twig.controller.exception::showAction'; }) + ->validate() + ->ifTrue(static function ($v) { return null !== $v; }) + ->then(static function ($v) { + @trigger_error('The "twig.exception_controller" configuration key has been deprecated in Symfony 4.4, set it to "null" and use "framework.error_controller" configuration key instead.', E_USER_DEPRECATED); + + return $v; + }) + ->end() ->end() ->end() ; diff --git a/src/Symfony/Bundle/TwigBundle/Resources/config/twig.xml b/src/Symfony/Bundle/TwigBundle/Resources/config/twig.xml index 52723177a1fb..fa886981e900 100644 --- a/src/Symfony/Bundle/TwigBundle/Resources/config/twig.xml +++ b/src/Symfony/Bundle/TwigBundle/Resources/config/twig.xml @@ -134,6 +134,7 @@ %twig.exception_listener.controller% %kernel.debug% + The "%service_id%" service is deprecated since Symfony 4.4. @@ -145,7 +146,7 @@ %twig.exception_listener.controller% - + The "%service_id%" service is deprecated since Symfony 4.4. diff --git a/src/Symfony/Bundle/TwigBundle/Tests/Controller/PreviewErrorControllerTest.php b/src/Symfony/Bundle/TwigBundle/Tests/Controller/PreviewErrorControllerTest.php index f007e630e614..0178276ee5d8 100644 --- a/src/Symfony/Bundle/TwigBundle/Tests/Controller/PreviewErrorControllerTest.php +++ b/src/Symfony/Bundle/TwigBundle/Tests/Controller/PreviewErrorControllerTest.php @@ -18,6 +18,9 @@ use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpKernel\HttpKernelInterface; +/** + * @group legacy + */ class PreviewErrorControllerTest extends TestCase { public function testForwardRequestToConfiguredController() diff --git a/src/Symfony/Bundle/TwigBundle/Tests/DependencyInjection/ConfigurationTest.php b/src/Symfony/Bundle/TwigBundle/Tests/DependencyInjection/ConfigurationTest.php index 33300336d11c..522db25a6cfe 100644 --- a/src/Symfony/Bundle/TwigBundle/Tests/DependencyInjection/ConfigurationTest.php +++ b/src/Symfony/Bundle/TwigBundle/Tests/DependencyInjection/ConfigurationTest.php @@ -21,7 +21,7 @@ public function testDoNoDuplicateDefaultFormResources() { $input = [ 'strict_variables' => false, // to be removed in 5.0 relying on default - 'exception_controller' => null, // to be removed in 5.0 relying on default + 'exception_controller' => null, // to be removed in 5.0 'form_themes' => ['form_div_layout.html.twig'], ]; @@ -45,14 +45,14 @@ public function testGetStrictVariablesDefaultFalse() /** * @group legacy - * @expectedDeprecation Relying on the default value ("twig.controller.exception::showAction") of the "twig.exception_controller" configuration option is deprecated since Symfony 4.4, set it to "null" explicitly instead, which will be the new default in 5.0. + * @expectedDeprecation The "twig.exception_controller" configuration key has been deprecated in Symfony 4.4, set it to "null" and use "framework.error_controller" configuration key instead. */ public function testGetExceptionControllerDefault() { $processor = new Processor(); - $config = $processor->processConfiguration(new Configuration(), [[]]); + $config = $processor->processConfiguration(new Configuration(), [['exception_controller' => 'exception_controller']]); - $this->assertSame('twig.controller.exception::showAction', $config['exception_controller']); + $this->assertSame('exception_controller', $config['exception_controller']); } public function testGlobalsAreNotNormalized() diff --git a/src/Symfony/Bundle/TwigBundle/Tests/DependencyInjection/Fixtures/php/customTemplateEscapingGuesser.php b/src/Symfony/Bundle/TwigBundle/Tests/DependencyInjection/Fixtures/php/customTemplateEscapingGuesser.php index 481f57cdc5a9..ab5cf941c031 100644 --- a/src/Symfony/Bundle/TwigBundle/Tests/DependencyInjection/Fixtures/php/customTemplateEscapingGuesser.php +++ b/src/Symfony/Bundle/TwigBundle/Tests/DependencyInjection/Fixtures/php/customTemplateEscapingGuesser.php @@ -4,5 +4,5 @@ 'autoescape_service' => 'my_project.some_bundle.template_escaping_guesser', 'autoescape_service_method' => 'guess', 'strict_variables' => false, // to be removed in 5.0 relying on default - 'exception_controller' => null, // to be removed in 5.0 relying on default + 'exception_controller' => null, // to be removed in 5.0 ]); diff --git a/src/Symfony/Bundle/TwigBundle/Tests/DependencyInjection/Fixtures/php/empty.php b/src/Symfony/Bundle/TwigBundle/Tests/DependencyInjection/Fixtures/php/empty.php index e4d9638c5292..fcc1402151ec 100644 --- a/src/Symfony/Bundle/TwigBundle/Tests/DependencyInjection/Fixtures/php/empty.php +++ b/src/Symfony/Bundle/TwigBundle/Tests/DependencyInjection/Fixtures/php/empty.php @@ -2,5 +2,5 @@ $container->loadFromExtension('twig', [ 'strict_variables' => false, // to be removed in 5.0 relying on default - 'exception_controller' => null, // to be removed in 5.0 relying on default + 'exception_controller' => null, // to be removed in 5.0 ]); diff --git a/src/Symfony/Bundle/TwigBundle/Tests/DependencyInjection/Fixtures/php/formats.php b/src/Symfony/Bundle/TwigBundle/Tests/DependencyInjection/Fixtures/php/formats.php index 907217bf4040..c4383a671a62 100644 --- a/src/Symfony/Bundle/TwigBundle/Tests/DependencyInjection/Fixtures/php/formats.php +++ b/src/Symfony/Bundle/TwigBundle/Tests/DependencyInjection/Fixtures/php/formats.php @@ -12,5 +12,5 @@ 'thousands_separator' => '.', ], 'strict_variables' => false, // to be removed in 5.0 relying on default - 'exception_controller' => null, // to be removed in 5.0 relying on default + 'exception_controller' => null, // to be removed in 5.0 ]); diff --git a/src/Symfony/Bundle/TwigBundle/Tests/DependencyInjection/Fixtures/php/full.php b/src/Symfony/Bundle/TwigBundle/Tests/DependencyInjection/Fixtures/php/full.php index 5356e4434725..18d0ba50f90f 100644 --- a/src/Symfony/Bundle/TwigBundle/Tests/DependencyInjection/Fixtures/php/full.php +++ b/src/Symfony/Bundle/TwigBundle/Tests/DependencyInjection/Fixtures/php/full.php @@ -17,7 +17,7 @@ 'charset' => 'ISO-8859-1', 'debug' => true, 'strict_variables' => true, - 'exception_controller' => null, + 'exception_controller' => null, // to be removed in 5.0 'default_path' => '%kernel.project_dir%/Fixtures/templates', 'paths' => [ 'path1', diff --git a/src/Symfony/Bundle/TwigBundle/Tests/DependencyInjection/TwigExtensionTest.php b/src/Symfony/Bundle/TwigBundle/Tests/DependencyInjection/TwigExtensionTest.php index 56c87c7d2352..01abbd166715 100644 --- a/src/Symfony/Bundle/TwigBundle/Tests/DependencyInjection/TwigExtensionTest.php +++ b/src/Symfony/Bundle/TwigBundle/Tests/DependencyInjection/TwigExtensionTest.php @@ -32,7 +32,7 @@ public function testLoadEmptyConfiguration() $container->registerExtension(new TwigExtension()); $container->loadFromExtension('twig', [ 'strict_variables' => false, // to be removed in 5.0 relying on default - 'exception_controller' => null, // to be removed in 5.0 relying on default + 'exception_controller' => null, // to be removed in 5.0 ]); $this->compileContainer($container); diff --git a/src/Symfony/Bundle/TwigBundle/Tests/Functional/NoTemplatingEntryTest.php b/src/Symfony/Bundle/TwigBundle/Tests/Functional/NoTemplatingEntryTest.php index 75bc4297b1cb..0961ffe22c58 100644 --- a/src/Symfony/Bundle/TwigBundle/Tests/Functional/NoTemplatingEntryTest.php +++ b/src/Symfony/Bundle/TwigBundle/Tests/Functional/NoTemplatingEntryTest.php @@ -68,7 +68,7 @@ public function registerContainerConfiguration(LoaderInterface $loader) ]) ->loadFromExtension('twig', [ 'strict_variables' => false, // to be removed in 5.0 relying on default - 'exception_controller' => null, // to be removed in 5.0 relying on default + 'exception_controller' => null, // to be removed in 5.0 'default_path' => __DIR__.'/templates', ]) ; diff --git a/src/Symfony/Component/HttpKernel/CHANGELOG.md b/src/Symfony/Component/HttpKernel/CHANGELOG.md index 383f155cc79a..5eb335ac309f 100644 --- a/src/Symfony/Component/HttpKernel/CHANGELOG.md +++ b/src/Symfony/Component/HttpKernel/CHANGELOG.md @@ -13,6 +13,7 @@ CHANGELOG current directory or with a glob pattern. The fallback directories have never been advocated so you likely do not use those in any app based on the SF Standard or Flex edition. * Marked all dispatched event classes as `@final` + * Added `ErrorController` to enable the preview and error rendering mechanism 4.3.0 ----- diff --git a/src/Symfony/Component/HttpKernel/Controller/ErrorController.php b/src/Symfony/Component/HttpKernel/Controller/ErrorController.php new file mode 100644 index 000000000000..3efa4e96dca7 --- /dev/null +++ b/src/Symfony/Component/HttpKernel/Controller/ErrorController.php @@ -0,0 +1,67 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\HttpKernel\Controller; + +use Symfony\Component\ErrorRenderer\ErrorRenderer; +use Symfony\Component\ErrorRenderer\Exception\ErrorRendererNotFoundException; +use Symfony\Component\ErrorRenderer\Exception\FlattenException; +use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\HttpFoundation\Response; +use Symfony\Component\HttpKernel\HttpKernelInterface; + +/** + * Renders error or exception pages from a given FlattenException. + * + * @author Yonel Ceruto + * @author Matthias Pigulla + */ +class ErrorController +{ + private $kernel; + private $controller; + private $errorRenderer; + + public function __construct(HttpKernelInterface $kernel, $controller, ErrorRenderer $errorRenderer) + { + $this->kernel = $kernel; + $this->controller = $controller; + $this->errorRenderer = $errorRenderer; + } + + public function __invoke(Request $request, FlattenException $exception): Response + { + try { + return new Response($this->errorRenderer->render($exception, $request->getPreferredFormat()), $exception->getStatusCode(), $exception->getHeaders()); + } catch (ErrorRendererNotFoundException $e) { + return new Response($this->errorRenderer->render($exception), $exception->getStatusCode(), $exception->getHeaders()); + } + } + + public function preview(Request $request, int $code): Response + { + $exception = FlattenException::createFromThrowable(new \Exception('This is a sample exception.'), $code, ['X-Debug' => false]); + + /* + * This Request mimics the parameters set by + * \Symfony\Component\HttpKernel\EventListener\ExceptionListener::duplicateRequest, with + * the additional "showException" flag. + */ + $subRequest = $request->duplicate(null, null, [ + '_controller' => $this->controller, + 'exception' => $exception, + 'logger' => null, + 'showException' => false, + ]); + + return $this->kernel->handle($subRequest, HttpKernelInterface::SUB_REQUEST); + } +} diff --git a/src/Symfony/Component/HttpKernel/EventListener/DebugHandlersListener.php b/src/Symfony/Component/HttpKernel/EventListener/DebugHandlersListener.php index 182a53667c4c..8a01569c9a6f 100644 --- a/src/Symfony/Component/HttpKernel/EventListener/DebugHandlersListener.php +++ b/src/Symfony/Component/HttpKernel/EventListener/DebugHandlersListener.php @@ -16,15 +16,9 @@ use Symfony\Component\Console\Event\ConsoleEvent; use Symfony\Component\Console\Output\ConsoleOutputInterface; use Symfony\Component\ErrorHandler\ErrorHandler; -use Symfony\Component\ErrorRenderer\ErrorRenderer; -use Symfony\Component\ErrorRenderer\ErrorRenderer\HtmlErrorRenderer; -use Symfony\Component\ErrorRenderer\Exception\ErrorRendererNotFoundException; use Symfony\Component\EventDispatcher\Event; use Symfony\Component\EventDispatcher\EventSubscriberInterface; -use Symfony\Component\HttpFoundation\Request; -use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpKernel\Debug\FileLinkFormatter; -use Symfony\Component\HttpKernel\Event\GetResponseForExceptionEvent; use Symfony\Component\HttpKernel\Event\KernelEvent; use Symfony\Component\HttpKernel\KernelEvents; @@ -44,8 +38,6 @@ class DebugHandlersListener implements EventSubscriberInterface private $scream; private $fileLinkFormat; private $scope; - private $charset; - private $errorRenderer; private $firstCall = true; private $hasTerminatedWithException; @@ -57,7 +49,7 @@ class DebugHandlersListener implements EventSubscriberInterface * @param string|FileLinkFormatter|null $fileLinkFormat The format for links to source files * @param bool $scope Enables/disables scoping mode */ - public function __construct(callable $exceptionHandler = null, LoggerInterface $logger = null, $levels = E_ALL, ?int $throwAt = E_ALL, bool $scream = true, $fileLinkFormat = null, bool $scope = true, string $charset = null, ErrorRenderer $errorRenderer = null) + public function __construct(callable $exceptionHandler = null, LoggerInterface $logger = null, $levels = E_ALL, ?int $throwAt = E_ALL, bool $scream = true, $fileLinkFormat = null, bool $scope = true) { $this->exceptionHandler = $exceptionHandler; $this->logger = $logger; @@ -66,8 +58,6 @@ public function __construct(callable $exceptionHandler = null, LoggerInterface $ $this->scream = $scream; $this->fileLinkFormat = $fileLinkFormat; $this->scope = $scope; - $this->charset = $charset; - $this->errorRenderer = $errorRenderer; } /** @@ -142,33 +132,6 @@ public function configure(Event $event = null) } } - /** - * @internal - */ - public function onKernelException(GetResponseForExceptionEvent $event) - { - if (!$this->hasTerminatedWithException || !$event->isMasterRequest()) { - return; - } - - $debug = $this->scream && $this->scope; - $controller = function (Request $request) use ($debug) { - if (null === $this->errorRenderer) { - $this->errorRenderer = new ErrorRenderer([new HtmlErrorRenderer($debug, $this->charset, $this->fileLinkFormat)]); - } - - $e = $request->attributes->get('exception'); - - try { - return new Response($this->errorRenderer->render($e, $request->getPreferredFormat()), $e->getStatusCode(), $e->getHeaders()); - } catch (ErrorRendererNotFoundException $_) { - return new Response($this->errorRenderer->render($e), $e->getStatusCode(), $e->getHeaders()); - } - }; - - (new ExceptionListener($controller, $this->logger, $debug))->onKernelException($event); - } - public static function getSubscribedEvents() { $events = [KernelEvents::REQUEST => ['configure', 2048]]; @@ -177,8 +140,6 @@ public static function getSubscribedEvents() $events[ConsoleEvents::COMMAND] = ['configure', 2048]; } - $events[KernelEvents::EXCEPTION] = ['onKernelException', -2048]; - return $events; } } diff --git a/src/Symfony/Component/HttpKernel/Tests/Controller/ErrorControllerTest.php b/src/Symfony/Component/HttpKernel/Tests/Controller/ErrorControllerTest.php new file mode 100644 index 000000000000..bc37a0e15eca --- /dev/null +++ b/src/Symfony/Component/HttpKernel/Tests/Controller/ErrorControllerTest.php @@ -0,0 +1,123 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\HttpKernel\Tests\Controller; + +use PHPUnit\Framework\TestCase; +use Symfony\Component\ErrorRenderer\ErrorRenderer; +use Symfony\Component\ErrorRenderer\ErrorRenderer\HtmlErrorRenderer; +use Symfony\Component\ErrorRenderer\ErrorRenderer\JsonErrorRenderer; +use Symfony\Component\ErrorRenderer\Exception\FlattenException; +use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\HttpFoundation\Response; +use Symfony\Component\HttpKernel\Controller\ErrorController; +use Symfony\Component\HttpKernel\Exception\HttpException; +use Symfony\Component\HttpKernel\Exception\NotFoundHttpException; +use Symfony\Component\HttpKernel\HttpKernelInterface; + +class ErrorControllerTest extends TestCase +{ + /** + * @dataProvider getInvokeControllerDataProvider + */ + public function testInvokeController(Request $request, FlattenException $exception, int $statusCode, string $content) + { + $kernel = $this->getMockBuilder(HttpKernelInterface::class)->getMock(); + $errorRenderer = new ErrorRenderer([new HtmlErrorRenderer(), new JsonErrorRenderer()]); + $controller = new ErrorController($kernel, null, $errorRenderer); + $response = $controller($request, $exception); + + $this->assertSame($statusCode, $response->getStatusCode()); + self::assertStringContainsString($content, strtr($response->getContent(), ["\n" => '', ' ' => ''])); + } + + public function getInvokeControllerDataProvider() + { + yield 'default status code and HTML format' => [ + new Request(), + FlattenException::createFromThrowable(new \Exception()), + 500, + 'The server returned a "500 Internal Server Error".', + ]; + + yield 'custom status code' => [ + new Request(), + FlattenException::createFromThrowable(new NotFoundHttpException('Page not found.')), + 404, + 'The server returned a "404 Not Found".', + ]; + + $request = new Request(); + $request->attributes->set('_format', 'json'); + yield 'custom format via _format attribute' => [ + $request, + FlattenException::createFromThrowable(new \Exception('foo')), + 500, + '{"title": "Internal Server Error","status": 500,"detail": "foo"}', + ]; + + $request = new Request(); + $request->headers->set('Accept', 'application/json'); + yield 'custom format via Accept header' => [ + $request, + FlattenException::createFromThrowable(new HttpException(405, 'Invalid request.')), + 405, + '{"title": "Method Not Allowed","status": 405,"detail": "Invalid request."}', + ]; + + $request = new Request(); + $request->headers->set('Content-Type', 'application/json'); + yield 'custom format via Content-Type header' => [ + $request, + FlattenException::createFromThrowable(new HttpException(405, 'Invalid request.')), + 405, + '{"title": "Method Not Allowed","status": 405,"detail": "Invalid request."}', + ]; + + $request = new Request(); + $request->attributes->set('_format', 'unknown'); + yield 'default HTML format for unknown formats' => [ + $request, + FlattenException::createFromThrowable(new HttpException(405, 'Invalid request.')), + 405, + 'The server returned a "405 Method Not Allowed".', + ]; + } + + public function testPreviewController() + { + $_controller = 'error_controller'; + $code = 404; + + $kernel = $this->getMockBuilder(HttpKernelInterface::class)->getMock(); + $kernel + ->expects($this->once()) + ->method('handle') + ->with( + $this->callback(function (Request $request) use ($_controller, $code) { + $exception = $request->attributes->get('exception'); + + $this->assertSame($_controller, $request->attributes->get('_controller')); + $this->assertInstanceOf(FlattenException::class, $exception); + $this->assertSame($code, $exception->getStatusCode()); + $this->assertFalse($request->attributes->get('showException')); + + return true; + }), + $this->equalTo(HttpKernelInterface::SUB_REQUEST) + ) + ->willReturn($response = new Response()); + + $controller = new ErrorController($kernel, $_controller, new ErrorRenderer([])); + + $this->assertSame($response, $controller->preview(new Request(), $code)); + } +} diff --git a/src/Symfony/Component/HttpKernel/Tests/EventListener/DebugHandlersListenerTest.php b/src/Symfony/Component/HttpKernel/Tests/EventListener/DebugHandlersListenerTest.php index f9c0f6b6b730..b8ec8f3e73ab 100644 --- a/src/Symfony/Component/HttpKernel/Tests/EventListener/DebugHandlersListenerTest.php +++ b/src/Symfony/Component/HttpKernel/Tests/EventListener/DebugHandlersListenerTest.php @@ -101,7 +101,6 @@ public function testConsoleEvent() $xListeners = [ KernelEvents::REQUEST => [[$listener, 'configure']], ConsoleEvents::COMMAND => [[$listener, 'configure']], - KernelEvents::EXCEPTION => [[$listener, 'onKernelException']], ]; $this->assertSame($xListeners, $dispatcher->getListeners());