From ecc8c33bf57bf8002f095ead4ee72218b47227bf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20Andr=C3=A9?= Date: Thu, 26 Dec 2024 01:19:19 +0100 Subject: [PATCH] [Cache][HttpKernel] Add a `noStore` argument to the `#` attribute --- .../Component/HttpKernel/Attribute/Cache.php | 12 +++++ .../EventListener/CacheAttributeListener.php | 9 ++++ .../CacheAttributeListenerTest.php | 47 +++++++++++++++++++ 3 files changed, 68 insertions(+) diff --git a/src/Symfony/Component/HttpKernel/Attribute/Cache.php b/src/Symfony/Component/HttpKernel/Attribute/Cache.php index 19d13e9228d64..fa2401a78c8a8 100644 --- a/src/Symfony/Component/HttpKernel/Attribute/Cache.php +++ b/src/Symfony/Component/HttpKernel/Attribute/Cache.php @@ -102,6 +102,18 @@ public function __construct( * It can be expressed in seconds or with a relative time format (1 day, 2 weeks, ...). */ public int|string|null $staleIfError = null, + + /** + * Add the "no-store" Cache-Control directive when set to true. + * + * This directive indicates that no part of the response can be cached + * in any cache (not in a shared cache, nor in a private cache). + * + * Supersedes the "$public" and "$smaxage" values. + * + * @see https://datatracker.ietf.org/doc/html/rfc7234#section-5.2.2.3 + */ + public ?bool $noStore = null, ) { } } diff --git a/src/Symfony/Component/HttpKernel/EventListener/CacheAttributeListener.php b/src/Symfony/Component/HttpKernel/EventListener/CacheAttributeListener.php index f428ea946251c..e913edf9e538a 100644 --- a/src/Symfony/Component/HttpKernel/EventListener/CacheAttributeListener.php +++ b/src/Symfony/Component/HttpKernel/EventListener/CacheAttributeListener.php @@ -163,6 +163,15 @@ public function onKernelResponse(ResponseEvent $event): void if (false === $cache->public) { $response->setPrivate(); } + + if (true === $cache->noStore) { + $response->setPrivate(); + $response->headers->addCacheControlDirective('no-store'); + } + + if (false === $cache->noStore) { + $response->headers->removeCacheControlDirective('no-store'); + } } } diff --git a/src/Symfony/Component/HttpKernel/Tests/EventListener/CacheAttributeListenerTest.php b/src/Symfony/Component/HttpKernel/Tests/EventListener/CacheAttributeListenerTest.php index b888579b80d3c..b185ea8994b1f 100644 --- a/src/Symfony/Component/HttpKernel/Tests/EventListener/CacheAttributeListenerTest.php +++ b/src/Symfony/Component/HttpKernel/Tests/EventListener/CacheAttributeListenerTest.php @@ -91,6 +91,50 @@ public function testResponseIsPrivateIfConfigurationIsPublicFalse() $this->assertTrue($this->response->headers->hasCacheControlDirective('private')); } + public function testResponseIsPublicIfConfigurationIsPublicTrueNoStoreFalse() + { + $request = $this->createRequest(new Cache(public: true, noStore: false)); + + $this->listener->onKernelResponse($this->createEventMock($request, $this->response)); + + $this->assertTrue($this->response->headers->hasCacheControlDirective('public')); + $this->assertFalse($this->response->headers->hasCacheControlDirective('private')); + $this->assertFalse($this->response->headers->hasCacheControlDirective('no-store')); + } + + public function testResponseIsPrivateIfConfigurationIsPublicTrueNoStoreTrue() + { + $request = $this->createRequest(new Cache(public: true, noStore: true)); + + $this->listener->onKernelResponse($this->createEventMock($request, $this->response)); + + $this->assertFalse($this->response->headers->hasCacheControlDirective('public')); + $this->assertTrue($this->response->headers->hasCacheControlDirective('private')); + $this->assertTrue($this->response->headers->hasCacheControlDirective('no-store')); + } + + public function testResponseIsPrivateNoStoreIfConfigurationIsNoStoreTrue() + { + $request = $this->createRequest(new Cache(noStore: true)); + + $this->listener->onKernelResponse($this->createEventMock($request, $this->response)); + + $this->assertFalse($this->response->headers->hasCacheControlDirective('public')); + $this->assertTrue($this->response->headers->hasCacheControlDirective('private')); + $this->assertTrue($this->response->headers->hasCacheControlDirective('no-store')); + } + + public function testResponseIsPrivateIfSharedMaxAgeSetAndNoStoreIsTrue() + { + $request = $this->createRequest(new Cache(smaxage: 1, noStore: true)); + + $this->listener->onKernelResponse($this->createEventMock($request, $this->response)); + + $this->assertFalse($this->response->headers->hasCacheControlDirective('public')); + $this->assertTrue($this->response->headers->hasCacheControlDirective('private')); + $this->assertTrue($this->response->headers->hasCacheControlDirective('no-store')); + } + public function testResponseVary() { $vary = ['foobar']; @@ -132,6 +176,7 @@ public function testAttributeConfigurationsAreSetOnResponse() $this->assertFalse($this->response->headers->hasCacheControlDirective('max-stale')); $this->assertFalse($this->response->headers->hasCacheControlDirective('stale-while-revalidate')); $this->assertFalse($this->response->headers->hasCacheControlDirective('stale-if-error')); + $this->assertFalse($this->response->headers->hasCacheControlDirective('no-store')); $this->request->attributes->set('_cache', [new Cache( expires: 'tomorrow', @@ -140,6 +185,7 @@ public function testAttributeConfigurationsAreSetOnResponse() maxStale: '5', staleWhileRevalidate: '6', staleIfError: '7', + noStore: true, )]); $this->listener->onKernelResponse($this->event); @@ -149,6 +195,7 @@ public function testAttributeConfigurationsAreSetOnResponse() $this->assertSame('5', $this->response->headers->getCacheControlDirective('max-stale')); $this->assertSame('6', $this->response->headers->getCacheControlDirective('stale-while-revalidate')); $this->assertSame('7', $this->response->headers->getCacheControlDirective('stale-if-error')); + $this->assertTrue($this->response->headers->hasCacheControlDirective('no-store')); $this->assertInstanceOf(\DateTimeInterface::class, $this->response->getExpires()); }