diff --git a/UPGRADE-3.3.md b/UPGRADE-3.3.md index 2253bea93b51d..90eebfd2f8dd2 100644 --- a/UPGRADE-3.3.md +++ b/UPGRADE-3.3.md @@ -76,6 +76,11 @@ HttpKernel * The `Psr6CacheClearer::addPool()` method has been deprecated. Pass an array of pools indexed by name to the constructor instead. + + * 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 1335b0e6e04d3..d4c84800a0d1f 100644 --- a/UPGRADE-4.0.md +++ b/UPGRADE-4.0.md @@ -236,6 +236,11 @@ HttpKernel * The `Psr6CacheClearer::addPool()` method has been removed. Pass an array of pools indexed by name to the constructor instead. + + * 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/src/Symfony/Component/HttpKernel/Event/GetResponseForExceptionEvent.php b/src/Symfony/Component/HttpKernel/Event/GetResponseForExceptionEvent.php index 003953feac513..751b74515b48b 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 @@ public function setException(\Exception $exception) { $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 cad23df99ad39..8d55ccde1c648 100644 --- a/src/Symfony/Component/HttpKernel/HttpKernel.php +++ b/src/Symfony/Component/HttpKernel/HttpKernel.php @@ -242,10 +242,12 @@ private function handleException(\Exception $e, $request, $type) // 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 0000000000000..7242579301d9c --- /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 b58a251a5938d..637924d44e17d 100644 --- a/src/Symfony/Component/HttpKernel/Tests/HttpKernelTest.php +++ b/src/Symfony/Component/HttpKernel/Tests/HttpKernelTest.php @@ -17,6 +17,7 @@ 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 @@ public function testHandleHttpException() } /** + * @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 @@ public function getStatusCodes() ); } + /** + * @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 c734db065e16f..8e2d1f2a6ec1c 100644 --- a/src/Symfony/Component/Security/Http/EntryPoint/FormAuthenticationEntryPoint.php +++ b/src/Symfony/Component/Security/Http/EntryPoint/FormAuthenticationEntryPoint.php @@ -54,7 +54,7 @@ public function start(Request $request, AuthenticationException $authException = $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 3c9604ea7436b..2819018a8c2ae 100644 --- a/src/Symfony/Component/Security/Http/Firewall/ExceptionListener.php +++ b/src/Symfony/Component/Security/Http/Firewall/ExceptionListener.php @@ -112,6 +112,7 @@ private function handleAuthenticationException(GetResponseForExceptionEvent $eve try { $event->setResponse($this->startAuthentication($event->getRequest(), $exception)); + $event->allowCustomResponseCode(); } catch (\Exception $e) { $event->setException($e); } @@ -155,6 +156,7 @@ private function handleAccessDeniedException(GetResponseForExceptionEvent $event $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 75bbd978f24c7..d95c703c68bc8 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 @@ public function testStartWithUseForward() $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 a5be022452129..e9863924158ed 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 @@ public function testAuthenticationExceptionWithoutEntryPoint(\Exception $excepti /** * @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); + + $response = new Response('Forbidden', 403); - $listener = $this->createExceptionListener(null, null, null, $this->createEntryPoint()); + $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 @@ public function testAccessDeniedExceptionFullFledgedAndWithoutAccessDeniedHandle 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 @@ public function testAccessDeniedExceptionFullFledgedAndWithoutAccessDeniedHandle $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 @@ public function getAccessDeniedExceptionProvider() ); } - 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 87adbf0491623..b1458eaa93c7a 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 3a63c8eee2523..127a1173fcbda 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",