Skip to content

Commit d6662b1

Browse files
committed
Merge branch '5.4' into 6.4
* 5.4: minor #58472 CS: clean some whitespaces/indentation (keradus) Fix newline harden test to not depend on the system's configured default timezone [Form] Support intl.use_exceptions/error_level in NumberToLocalizedStringTransformer [ExpressionLanguage] Add missing test case for `Lexer` [FrameworkBundle] Fix passing request_stack to session.listener ensure session storages are opened in tests before destroying them [HttpKernel] Correctly merge `max-age`/`s-maxage` and `Expires` headers [Security][Validator] Check translations for Czech [Security] Fix serialized object representation in tests [DoctrineBridge] Fix risky test warnings
2 parents c227b1a + 9dfc68b commit d6662b1

File tree

2 files changed

+88
-32
lines changed

2 files changed

+88
-32
lines changed

HttpCache/ResponseCacheStrategy.php

Lines changed: 27 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ class ResponseCacheStrategy implements ResponseCacheStrategyInterface
5151
private array $ageDirectives = [
5252
'max-age' => null,
5353
's-maxage' => null,
54-
'expires' => null,
54+
'expires' => false,
5555
];
5656

5757
/**
@@ -82,15 +82,30 @@ public function add(Response $response)
8282
return;
8383
}
8484

85-
$isHeuristicallyCacheable = $response->headers->hasCacheControlDirective('public');
8685
$maxAge = $response->headers->hasCacheControlDirective('max-age') ? (int) $response->headers->getCacheControlDirective('max-age') : null;
87-
$this->storeRelativeAgeDirective('max-age', $maxAge, $age, $isHeuristicallyCacheable);
8886
$sharedMaxAge = $response->headers->hasCacheControlDirective('s-maxage') ? (int) $response->headers->getCacheControlDirective('s-maxage') : $maxAge;
89-
$this->storeRelativeAgeDirective('s-maxage', $sharedMaxAge, $age, $isHeuristicallyCacheable);
90-
9187
$expires = $response->getExpires();
9288
$expires = null !== $expires ? (int) $expires->format('U') - (int) $response->getDate()->format('U') : null;
93-
$this->storeRelativeAgeDirective('expires', $expires >= 0 ? $expires : null, 0, $isHeuristicallyCacheable);
89+
90+
// See https://datatracker.ietf.org/doc/html/rfc7234#section-4.2.2
91+
// If a response is "public" but does not have maximum lifetime, heuristics might be applied.
92+
// Do not store NULL values so the final response can have more limiting value from other responses.
93+
$isHeuristicallyCacheable = $response->headers->hasCacheControlDirective('public')
94+
&& null === $maxAge
95+
&& null === $sharedMaxAge
96+
&& null === $expires;
97+
98+
if (!$isHeuristicallyCacheable || null !== $maxAge || null !== $expires) {
99+
$this->storeRelativeAgeDirective('max-age', $maxAge, $expires, $age);
100+
}
101+
102+
if (!$isHeuristicallyCacheable || null !== $sharedMaxAge || null !== $expires) {
103+
$this->storeRelativeAgeDirective('s-maxage', $sharedMaxAge, $expires, $age);
104+
}
105+
106+
if (null !== $expires) {
107+
$this->ageDirectives['expires'] = true;
108+
}
94109

95110
if (false !== $this->lastModified) {
96111
$lastModified = $response->getLastModified();
@@ -152,9 +167,9 @@ public function update(Response $response)
152167
}
153168
}
154169

155-
if (is_numeric($this->ageDirectives['expires'])) {
170+
if ($this->ageDirectives['expires'] && null !== $maxAge) {
156171
$date = clone $response->getDate();
157-
$date = $date->modify('+'.($this->ageDirectives['expires'] + $this->age).' seconds');
172+
$date = $date->modify('+'.$maxAge.' seconds');
158173
$response->setExpires($date);
159174
}
160175
}
@@ -204,33 +219,16 @@ private function willMakeFinalResponseUncacheable(Response $response): bool
204219
* we have to subtract the age so that the value is normalized for an age of 0.
205220
*
206221
* If the value is lower than the currently stored value, we update the value, to keep a rolling
207-
* minimal value of each instruction.
208-
*
209-
* If the value is NULL and the isHeuristicallyCacheable parameter is false, the directive will
210-
* not be set on the final response. In this case, not all responses had the directive set and no
211-
* value can be found that satisfies the requirements of all responses. The directive will be dropped
212-
* from the final response.
213-
*
214-
* If the isHeuristicallyCacheable parameter is true, however, the current response has been marked
215-
* as cacheable in a public (shared) cache, but did not provide an explicit lifetime that would serve
216-
* as an upper bound. In this case, we can proceed and possibly keep the directive on the final response.
222+
* minimal value of each instruction. If the value is NULL, the directive will not be set on the final response.
217223
*/
218-
private function storeRelativeAgeDirective(string $directive, ?int $value, int $age, bool $isHeuristicallyCacheable): void
224+
private function storeRelativeAgeDirective(string $directive, ?int $value, ?int $expires, int $age): void
219225
{
220-
if (null === $value) {
221-
if ($isHeuristicallyCacheable) {
222-
/*
223-
* See https://datatracker.ietf.org/doc/html/rfc7234#section-4.2.2
224-
* This particular response does not require maximum lifetime; heuristics might be applied.
225-
* Other responses, however, might have more stringent requirements on maximum lifetime.
226-
* So, return early here so that the final response can have the more limiting value set.
227-
*/
228-
return;
229-
}
226+
if (null === $value && null === $expires) {
230227
$this->ageDirectives[$directive] = false;
231228
}
232229

233230
if (false !== $this->ageDirectives[$directive]) {
231+
$value = min($value ?? PHP_INT_MAX, $expires ?? PHP_INT_MAX);
234232
$value -= $age;
235233
$this->ageDirectives[$directive] = null !== $this->ageDirectives[$directive] ? min($this->ageDirectives[$directive], $value) : $value;
236234
}

Tests/HttpCache/ResponseCacheStrategyTest.php

Lines changed: 61 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,64 @@ public function testSharedMaxAgeNotSetIfNotSetInMainRequest()
7272
$this->assertFalse($response->headers->hasCacheControlDirective('s-maxage'));
7373
}
7474

75+
public function testExpiresHeaderUpdatedFromMaxAge()
76+
{
77+
$cacheStrategy = new ResponseCacheStrategy();
78+
79+
$response1 = new Response();
80+
$response1->setExpires(new \DateTime('+ 1 hour'));
81+
$response1->setPublic();
82+
$cacheStrategy->add($response1);
83+
84+
$response = new Response();
85+
$response->setMaxAge(0);
86+
$response->setSharedMaxAge(86400);
87+
$cacheStrategy->update($response);
88+
89+
$this->assertSame('0', $response->headers->getCacheControlDirective('max-age'));
90+
$this->assertSame('3600', $response->headers->getCacheControlDirective('s-maxage'));
91+
92+
// Expires header must be same as Date header because "max-age" is 0.
93+
$this->assertSame($response->headers->get('Date'), $response->headers->get('Expires'));
94+
}
95+
96+
public function testMaxAgeUpdatedFromExpiresHeader()
97+
{
98+
$cacheStrategy = new ResponseCacheStrategy();
99+
100+
$response1 = new Response();
101+
$response1->setExpires(new \DateTime('+ 1 hour', new \DateTimeZone('UTC')));
102+
$response1->setPublic();
103+
$cacheStrategy->add($response1);
104+
105+
$response = new Response();
106+
$response->setMaxAge(86400);
107+
$cacheStrategy->update($response);
108+
109+
$this->assertSame('3600', $response->headers->getCacheControlDirective('max-age'));
110+
$this->assertNull($response->headers->getCacheControlDirective('s-maxage'));
111+
$this->assertSame((new \DateTime('+ 1 hour', new \DateTimeZone('UTC')))->format('D, d M Y H:i:s').' GMT', $response->headers->get('Expires'));
112+
}
113+
114+
public function testMaxAgeAndSharedMaxAgeUpdatedFromExpiresHeader()
115+
{
116+
$cacheStrategy = new ResponseCacheStrategy();
117+
118+
$response1 = new Response();
119+
$response1->setExpires(new \DateTime('+ 1 day', new \DateTimeZone('UTC')));
120+
$response1->setPublic();
121+
$cacheStrategy->add($response1);
122+
123+
$response = new Response();
124+
$response->setMaxAge(3600);
125+
$response->setSharedMaxAge(86400);
126+
$cacheStrategy->update($response);
127+
128+
$this->assertSame('3600', $response->headers->getCacheControlDirective('max-age'));
129+
$this->assertSame('86400', $response->headers->getCacheControlDirective('s-maxage'));
130+
$this->assertSame((new \DateTime('+ 1 hour', new \DateTimeZone('UTC')))->format('D, d M Y H:i:s').' GMT', $response->headers->get('Expires'));
131+
}
132+
75133
public function testMainResponseNotCacheableWhenEmbeddedResponseRequiresValidation()
76134
{
77135
$cacheStrategy = new ResponseCacheStrategy();
@@ -287,7 +345,7 @@ public function testResponseIsExpirableButNotValidateableWhenMainResponseCombine
287345
*
288346
* @dataProvider cacheControlMergingProvider
289347
*/
290-
public function testCacheControlMerging(array $expects, array $master, array $surrogates)
348+
public function testCacheControlMerging(array $expects, array $main, array $surrogates)
291349
{
292350
$cacheStrategy = new ResponseCacheStrategy();
293351
$buildResponse = function ($config) {
@@ -333,7 +391,7 @@ public function testCacheControlMerging(array $expects, array $master, array $su
333391
$cacheStrategy->add($buildResponse($config));
334392
}
335393

336-
$response = $buildResponse($master);
394+
$response = $buildResponse($main);
337395
$cacheStrategy->update($response);
338396

339397
foreach ($expects as $key => $value) {
@@ -415,7 +473,7 @@ public static function cacheControlMergingProvider()
415473
];
416474

417475
yield 'merge max-age and s-maxage' => [
418-
['public' => true, 'max-age' => '60'],
476+
['public' => true, 'max-age' => null, 's-maxage' => '60'],
419477
['public' => true, 's-maxage' => 3600],
420478
[
421479
['public' => true, 'max-age' => 60],

0 commit comments

Comments
 (0)