diff --git a/src/Symfony/Component/HttpFoundation/CHANGELOG.md b/src/Symfony/Component/HttpFoundation/CHANGELOG.md index 9b50cf7fa646..e4b3d63f4850 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 21cb15744e70..847c44b943b1 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 @@ public function isXmlHttpRequest() 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 6e2d289ad08e..7e8c5f1f5cb7 100644 --- a/src/Symfony/Component/HttpFoundation/Response.php +++ b/src/Symfony/Component/HttpFoundation/Response.php @@ -1210,6 +1210,22 @@ public static function closeOutputBuffers(int $targetLevel, bool $flush): void } } + /** + * 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 3c206f9f0ade..b8c57fc92418 100644 --- a/src/Symfony/Component/HttpFoundation/Tests/RequestTest.php +++ b/src/Symfony/Component/HttpFoundation/Tests/RequestTest.php @@ -2325,6 +2325,64 @@ public function trustedProxiesRemoteAddr() [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 9d1713d06542..ca75881dafc7 100644 --- a/src/Symfony/Component/HttpFoundation/Tests/ResponseTest.php +++ b/src/Symfony/Component/HttpFoundation/Tests/ResponseTest.php @@ -1040,6 +1040,24 @@ public function testReasonPhraseDefaultsAgainstIana($code, $reasonPhrase) { $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