diff --git a/src/Symfony/Component/HttpFoundation/CHANGELOG.md b/src/Symfony/Component/HttpFoundation/CHANGELOG.md index 9b50cf7fa6..e4b3d63f48 100644 --- a/src/Symfony/Component/HttpFoundation/CHANGELOG.md +++ b/src/Symfony/Component/HttpFoundation/CHANGELOG.md @@ -7,6 +7,8 @@ CHANGELOG * Deprecate `Response::create()`, `JsonResponse::create()`, `RedirectResponse::create()`, and `StreamedResponse::create()` methods (use `__construct()` instead) + * added `Request::preferSafeContent()` and `Response::setContentSafe()` to handle "safe" HTTP preference + according to [RFC 8674](https://tools.ietf.org/html/rfc8674) 5.0.0 ----- diff --git a/src/Symfony/Component/HttpFoundation/Request.php b/src/Symfony/Component/HttpFoundation/Request.php index 21cb15744e..847c44b943 100644 --- a/src/Symfony/Component/HttpFoundation/Request.php +++ b/src/Symfony/Component/HttpFoundation/Request.php @@ -199,6 +199,11 @@ class Request private $isHostValid = true; private $isForwardedValid = true; + /** + * @var bool|null + */ + private $isSafeContentPreferred; + private static $trustedHeaderSet = -1; private static $forwardedParams = [ @@ -1702,6 +1707,29 @@ class Request return 'XMLHttpRequest' == $this->headers->get('X-Requested-With'); } + /** + * Checks whether the client browser prefers safe content or not according to RFC8674. + * + * @see https://tools.ietf.org/html/rfc8674 + */ + public function preferSafeContent(): bool + { + if (null !== $this->isSafeContentPreferred) { + return $this->isSafeContentPreferred; + } + + if (!$this->isSecure()) { + // see https://tools.ietf.org/html/rfc8674#section-3 + $this->isSafeContentPreferred = false; + + return $this->isSafeContentPreferred; + } + + $this->isSafeContentPreferred = AcceptHeader::fromString($this->headers->get('Prefer'))->has('safe'); + + return $this->isSafeContentPreferred; + } + /* * The following methods are derived from code of the Zend Framework (1.10dev - 2010-01-24) * diff --git a/src/Symfony/Component/HttpFoundation/Response.php b/src/Symfony/Component/HttpFoundation/Response.php index 6e2d289ad0..7e8c5f1f5c 100644 --- a/src/Symfony/Component/HttpFoundation/Response.php +++ b/src/Symfony/Component/HttpFoundation/Response.php @@ -1210,6 +1210,22 @@ class Response } } + /** + * Mark a response as safe according to RFC8674. + * + * @see https://tools.ietf.org/html/rfc8674 + */ + public function setContentSafe(bool $safe = true): void + { + if ($safe) { + $this->headers->set('Preference-Applied', 'safe'); + } elseif ('safe' === $this->headers->get('Preference-Applied')) { + $this->headers->remove('Preference-Applied'); + } + + $this->setVary('Prefer', false); + } + /** * Checks if we need to remove Cache-Control for SSL encrypted downloads when using IE < 9. * diff --git a/src/Symfony/Component/HttpFoundation/Tests/RequestTest.php b/src/Symfony/Component/HttpFoundation/Tests/RequestTest.php index 3c206f9f0a..b8c57fc924 100644 --- a/src/Symfony/Component/HttpFoundation/Tests/RequestTest.php +++ b/src/Symfony/Component/HttpFoundation/Tests/RequestTest.php @@ -2325,6 +2325,64 @@ class RequestTest extends TestCase [null, ['REMOTE_ADDR', '2.2.2.2'], ['2.2.2.2']], ]; } + + /** + * @dataProvider preferSafeContentData + */ + public function testPreferSafeContent($server, bool $safePreferenceExpected) + { + $request = new Request([], [], [], [], [], $server); + + $this->assertEquals($safePreferenceExpected, $request->preferSafeContent()); + } + + public function preferSafeContentData() + { + return [ + [[], false], + [ + [ + 'HTTPS' => 'on', + ], + false, + ], + [ + [ + 'HTTPS' => 'off', + 'HTTP_PREFER' => 'safe', + ], + false, + ], + [ + [ + 'HTTPS' => 'on', + 'HTTP_PREFER' => 'safe', + ], + true, + ], + [ + [ + 'HTTPS' => 'on', + 'HTTP_PREFER' => 'unknown-preference', + ], + false, + ], + [ + [ + 'HTTPS' => 'on', + 'HTTP_PREFER' => 'unknown-preference=42, safe', + ], + true, + ], + [ + [ + 'HTTPS' => 'on', + 'HTTP_PREFER' => 'safe, unknown-preference=42', + ], + true, + ], + ]; + } } class RequestContentProxy extends Request diff --git a/src/Symfony/Component/HttpFoundation/Tests/ResponseTest.php b/src/Symfony/Component/HttpFoundation/Tests/ResponseTest.php index 9d1713d065..ca75881daf 100644 --- a/src/Symfony/Component/HttpFoundation/Tests/ResponseTest.php +++ b/src/Symfony/Component/HttpFoundation/Tests/ResponseTest.php @@ -1040,6 +1040,24 @@ class ResponseTest extends ResponseTestCase { $this->assertEquals($reasonPhrase, Response::$statusTexts[$code]); } + + public function testSetContentSafe() + { + $response = new Response(); + + $this->assertFalse($response->headers->has('Preference-Applied')); + $this->assertFalse($response->headers->has('Vary')); + + $response->setContentSafe(); + + $this->assertSame('safe', $response->headers->get('Preference-Applied')); + $this->assertSame('Prefer', $response->headers->get('Vary')); + + $response->setContentSafe(false); + + $this->assertFalse($response->headers->has('Preference-Applied')); + $this->assertSame('Prefer', $response->headers->get('Vary')); + } } class StringableObject