diff --git a/UPGRADE-3.3.md b/UPGRADE-3.3.md index 5097daf334..398d16f01d 100644 --- a/UPGRADE-3.3.md +++ b/UPGRADE-3.3.md @@ -139,15 +139,20 @@ FrameworkBundle deprecated and will be removed in 4.0. Use `Symfony\Component\Config\DependencyInjection\ConfigCachePass` class instead. - HttpKernel ----------- - * The `Psr6CacheClearer::addPool()` method has been deprecated. Pass an array of pools indexed - by name to the constructor instead. - - * The `LazyLoadingFragmentHandler::addRendererService()` method has been deprecated and - will be removed in 4.0. + * The `Psr6CacheClearer::addPool()` method has been deprecated. Pass an array + of pools indexed by name to the constructor instead. + + * The `LazyLoadingFragmentHandler::addRendererService()` method has been + deprecated and will be removed in 4.0. + + * The `X-Status-Code` header method of setting a custom status code in the + response when handling exceptions has been removed. There is now a new + `GetResponseForExceptionEvent::allowCustomResponseCode()` method instead, + which will tell the Kernel to use the response code set on the event's + response object. Process ------- diff --git a/UPGRADE-4.0.md b/UPGRADE-4.0.md index cba0e04c49..5e9f233ff4 100644 --- a/UPGRADE-4.0.md +++ b/UPGRADE-4.0.md @@ -243,6 +243,12 @@ HttpKernel * The `LazyLoadingFragmentHandler::addRendererService()` method has been removed. + * The `X-Status-Code` header method of setting a custom status code in the + response when handling exceptions has been removed. There is now a new + `GetResponseForExceptionEvent::allowCustomResponseCode()` method instead, + which will tell the Kernel to use the response code set on the event's + response object. + Ldap ---- diff --git a/src/Symfony/Component/HttpKernel/Event/GetResponseForExceptionEvent.php b/src/Symfony/Component/HttpKernel/Event/GetResponseForExceptionEvent.php index 003953feac..751b74515b 100644 --- a/src/Symfony/Component/HttpKernel/Event/GetResponseForExceptionEvent.php +++ b/src/Symfony/Component/HttpKernel/Event/GetResponseForExceptionEvent.php @@ -36,6 +36,11 @@ class GetResponseForExceptionEvent extends GetResponseEvent */ private $exception; + /** + * @var bool + */ + private $allowCustomResponseCode = false; + public function __construct(HttpKernelInterface $kernel, Request $request, $requestType, \Exception $e) { parent::__construct($kernel, $request, $requestType); @@ -64,4 +69,22 @@ class GetResponseForExceptionEvent extends GetResponseEvent { $this->exception = $exception; } + + /** + * Mark the event as allowing a custom response code. + */ + public function allowCustomResponseCode() + { + $this->allowCustomResponseCode = true; + } + + /** + * Returns true if the event allows a custom response code. + * + * @return bool + */ + public function isAllowingCustomResponseCode() + { + return $this->allowCustomResponseCode; + } } diff --git a/src/Symfony/Component/HttpKernel/HttpKernel.php b/src/Symfony/Component/HttpKernel/HttpKernel.php index cad23df99a..8d55ccde1c 100644 --- a/src/Symfony/Component/HttpKernel/HttpKernel.php +++ b/src/Symfony/Component/HttpKernel/HttpKernel.php @@ -242,10 +242,12 @@ class HttpKernel implements HttpKernelInterface, TerminableInterface // the developer asked for a specific status code if ($response->headers->has('X-Status-Code')) { + @trigger_error(sprintf('Using the X-Status-Code header is deprecated since version 3.3 and will be removed in 4.0. Use %s::allowCustomResponseCode() instead.', GetResponseForExceptionEvent::class), E_USER_DEPRECATED); + $response->setStatusCode($response->headers->get('X-Status-Code')); $response->headers->remove('X-Status-Code'); - } elseif (!$response->isClientError() && !$response->isServerError() && !$response->isRedirect()) { + } elseif (!$event->isAllowingCustomResponseCode() && !$response->isClientError() && !$response->isServerError() && !$response->isRedirect()) { // ensure that we actually have an error response if ($e instanceof HttpExceptionInterface) { // keep the HTTP status code and headers diff --git a/src/Symfony/Component/HttpKernel/Tests/Event/GetResponseForExceptionEventTest.php b/src/Symfony/Component/HttpKernel/Tests/Event/GetResponseForExceptionEventTest.php new file mode 100644 index 0000000000..7242579301 --- /dev/null +++ b/src/Symfony/Component/HttpKernel/Tests/Event/GetResponseForExceptionEventTest.php @@ -0,0 +1,27 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\HttpKernel\Tests\Event; + +use PHPUnit\Framework\TestCase; +use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\HttpKernel\Event\GetResponseForExceptionEvent; +use Symfony\Component\HttpKernel\Tests\TestHttpKernel; + +class GetResponseForExceptionEventTest extends TestCase +{ + public function testAllowSuccessfulResponseIsFalseByDefault() + { + $event = new GetResponseForExceptionEvent(new TestHttpKernel(), new Request(), 1, new \Exception()); + + $this->assertFalse($event->isAllowingCustomResponseCode()); + } +} diff --git a/src/Symfony/Component/HttpKernel/Tests/HttpKernelTest.php b/src/Symfony/Component/HttpKernel/Tests/HttpKernelTest.php index b58a251a59..637924d44e 100644 --- a/src/Symfony/Component/HttpKernel/Tests/HttpKernelTest.php +++ b/src/Symfony/Component/HttpKernel/Tests/HttpKernelTest.php @@ -17,6 +17,7 @@ use Symfony\Component\HttpFoundation\RequestStack; use Symfony\Component\HttpKernel\Controller\ArgumentResolverInterface; use Symfony\Component\HttpKernel\Controller\ControllerResolverInterface; use Symfony\Component\HttpKernel\Event\FilterControllerArgumentsEvent; +use Symfony\Component\HttpKernel\Event\GetResponseForExceptionEvent; use Symfony\Component\HttpKernel\HttpKernel; use Symfony\Component\HttpKernel\HttpKernelInterface; use Symfony\Component\HttpKernel\KernelEvents; @@ -111,9 +112,10 @@ class HttpKernelTest extends TestCase } /** + * @group legacy * @dataProvider getStatusCodes */ - public function testHandleWhenAnExceptionIsHandledWithASpecificStatusCode($responseStatusCode, $expectedStatusCode) + public function testLegacyHandleWhenAnExceptionIsHandledWithASpecificStatusCode($responseStatusCode, $expectedStatusCode) { $dispatcher = new EventDispatcher(); $dispatcher->addListener(KernelEvents::EXCEPTION, function ($event) use ($responseStatusCode, $expectedStatusCode) { @@ -137,6 +139,32 @@ class HttpKernelTest extends TestCase ); } + /** + * @dataProvider getSpecificStatusCodes + */ + public function testHandleWhenAnExceptionIsHandledWithASpecificStatusCode($expectedStatusCode) + { + $dispatcher = new EventDispatcher(); + $dispatcher->addListener(KernelEvents::EXCEPTION, function (GetResponseForExceptionEvent $event) use ($expectedStatusCode) { + $event->allowCustomResponseCode(); + $event->setResponse(new Response('', $expectedStatusCode)); + }); + + $kernel = $this->getHttpKernel($dispatcher, function () { throw new \RuntimeException(); }); + $response = $kernel->handle(new Request()); + + $this->assertEquals($expectedStatusCode, $response->getStatusCode()); + } + + public function getSpecificStatusCodes() + { + return array( + array(200), + array(302), + array(403), + ); + } + public function testHandleWhenAListenerReturnsAResponse() { $dispatcher = new EventDispatcher(); diff --git a/src/Symfony/Component/Security/Http/EntryPoint/FormAuthenticationEntryPoint.php b/src/Symfony/Component/Security/Http/EntryPoint/FormAuthenticationEntryPoint.php index c734db065e..8e2d1f2a6e 100644 --- a/src/Symfony/Component/Security/Http/EntryPoint/FormAuthenticationEntryPoint.php +++ b/src/Symfony/Component/Security/Http/EntryPoint/FormAuthenticationEntryPoint.php @@ -54,7 +54,7 @@ class FormAuthenticationEntryPoint implements AuthenticationEntryPointInterface $response = $this->httpKernel->handle($subRequest, HttpKernelInterface::SUB_REQUEST); if (200 === $response->getStatusCode()) { - $response->headers->set('X-Status-Code', 401); + $response->setStatusCode(401); } return $response; diff --git a/src/Symfony/Component/Security/Http/Firewall/ExceptionListener.php b/src/Symfony/Component/Security/Http/Firewall/ExceptionListener.php index 3c9604ea74..2819018a8c 100644 --- a/src/Symfony/Component/Security/Http/Firewall/ExceptionListener.php +++ b/src/Symfony/Component/Security/Http/Firewall/ExceptionListener.php @@ -112,6 +112,7 @@ class ExceptionListener try { $event->setResponse($this->startAuthentication($event->getRequest(), $exception)); + $event->allowCustomResponseCode(); } catch (\Exception $e) { $event->setException($e); } @@ -155,6 +156,7 @@ class ExceptionListener $subRequest->attributes->set(Security::ACCESS_DENIED_ERROR, $exception); $event->setResponse($event->getKernel()->handle($subRequest, HttpKernelInterface::SUB_REQUEST, true)); + $event->allowCustomResponseCode(); } } catch (\Exception $e) { if (null !== $this->logger) { diff --git a/src/Symfony/Component/Security/Http/Tests/EntryPoint/FormAuthenticationEntryPointTest.php b/src/Symfony/Component/Security/Http/Tests/EntryPoint/FormAuthenticationEntryPointTest.php index 75bbd978f2..d95c703c68 100644 --- a/src/Symfony/Component/Security/Http/Tests/EntryPoint/FormAuthenticationEntryPointTest.php +++ b/src/Symfony/Component/Security/Http/Tests/EntryPoint/FormAuthenticationEntryPointTest.php @@ -64,6 +64,6 @@ class FormAuthenticationEntryPointTest extends TestCase $entryPointResponse = $entryPoint->start($request); $this->assertEquals($response, $entryPointResponse); - $this->assertEquals(401, $entryPointResponse->headers->get('X-Status-Code')); + $this->assertEquals(401, $entryPointResponse->getStatusCode()); } } diff --git a/src/Symfony/Component/Security/Http/Tests/Firewall/ExceptionListenerTest.php b/src/Symfony/Component/Security/Http/Tests/Firewall/ExceptionListenerTest.php index a5be022452..e986392415 100644 --- a/src/Symfony/Component/Security/Http/Tests/Firewall/ExceptionListenerTest.php +++ b/src/Symfony/Component/Security/Http/Tests/Firewall/ExceptionListenerTest.php @@ -44,14 +44,19 @@ class ExceptionListenerTest extends TestCase /** * @dataProvider getAuthenticationExceptionProvider */ - public function testAuthenticationExceptionWithEntryPoint(\Exception $exception, \Exception $eventException = null) + public function testAuthenticationExceptionWithEntryPoint(\Exception $exception) { - $event = $this->createEvent($exception = new AuthenticationException()); + $event = $this->createEvent($exception); - $listener = $this->createExceptionListener(null, null, null, $this->createEntryPoint()); + $response = new Response('Forbidden', 403); + + $listener = $this->createExceptionListener(null, null, null, $this->createEntryPoint($response)); $listener->onKernelException($event); - $this->assertEquals('OK', $event->getResponse()->getContent()); + $this->assertTrue($event->isAllowingCustomResponseCode()); + + $this->assertEquals('Forbidden', $event->getResponse()->getContent()); + $this->assertEquals(403, $event->getResponse()->getStatusCode()); $this->assertSame($exception, $event->getException()); } @@ -100,7 +105,7 @@ class ExceptionListenerTest extends TestCase public function testAccessDeniedExceptionFullFledgedAndWithoutAccessDeniedHandlerAndWithErrorPage(\Exception $exception, \Exception $eventException = null) { $kernel = $this->getMockBuilder('Symfony\Component\HttpKernel\HttpKernelInterface')->getMock(); - $kernel->expects($this->once())->method('handle')->will($this->returnValue(new Response('error'))); + $kernel->expects($this->once())->method('handle')->will($this->returnValue(new Response('Unauthorized', 401))); $event = $this->createEvent($exception, $kernel); @@ -110,7 +115,10 @@ class ExceptionListenerTest extends TestCase $listener = $this->createExceptionListener(null, $this->createTrustResolver(true), $httpUtils, null, '/error'); $listener->onKernelException($event); - $this->assertEquals('error', $event->getResponse()->getContent()); + $this->assertTrue($event->isAllowingCustomResponseCode()); + + $this->assertEquals('Unauthorized', $event->getResponse()->getContent()); + $this->assertEquals(401, $event->getResponse()->getStatusCode()); $this->assertSame(null === $eventException ? $exception : $eventException, $event->getException()->getPrevious()); } @@ -159,10 +167,10 @@ class ExceptionListenerTest extends TestCase ); } - private function createEntryPoint() + private function createEntryPoint(Response $response = null) { $entryPoint = $this->getMockBuilder('Symfony\Component\Security\Http\EntryPoint\AuthenticationEntryPointInterface')->getMock(); - $entryPoint->expects($this->once())->method('start')->will($this->returnValue(new Response('OK'))); + $entryPoint->expects($this->once())->method('start')->will($this->returnValue($response ?: new Response('OK'))); return $entryPoint; } diff --git a/src/Symfony/Component/Security/Http/composer.json b/src/Symfony/Component/Security/Http/composer.json index 87adbf0491..b1458eaa93 100644 --- a/src/Symfony/Component/Security/Http/composer.json +++ b/src/Symfony/Component/Security/Http/composer.json @@ -20,7 +20,7 @@ "symfony/security-core": "~3.2", "symfony/event-dispatcher": "~2.8|~3.0", "symfony/http-foundation": "~2.8|~3.0", - "symfony/http-kernel": "~2.8|~3.0", + "symfony/http-kernel": "~3.3", "symfony/polyfill-php56": "~1.0", "symfony/polyfill-php70": "~1.0", "symfony/property-access": "~2.8|~3.0" diff --git a/src/Symfony/Component/Security/composer.json b/src/Symfony/Component/Security/composer.json index 633cfe8405..e6835fe883 100644 --- a/src/Symfony/Component/Security/composer.json +++ b/src/Symfony/Component/Security/composer.json @@ -19,7 +19,7 @@ "php": ">=5.5.9", "symfony/event-dispatcher": "~2.8|~3.0", "symfony/http-foundation": "~2.8|~3.0", - "symfony/http-kernel": "~2.8|~3.0", + "symfony/http-kernel": "~3.3", "symfony/polyfill-php56": "~1.0", "symfony/polyfill-php70": "~1.0", "symfony/polyfill-util": "~1.0",