diff --git a/src/Symfony/Component/HttpFoundation/Response.php b/src/Symfony/Component/HttpFoundation/Response.php index 9d112d15785d0..94cd611439468 100644 --- a/src/Symfony/Component/HttpFoundation/Response.php +++ b/src/Symfony/Component/HttpFoundation/Response.php @@ -530,7 +530,7 @@ public function isCacheable() return false; } - return $this->isValidateable() || $this->isFresh(); + return $this->isValidateable() || $this->isExpirable(); } /** @@ -547,6 +547,19 @@ public function isFresh() return $this->getTtl() > 0; } + /** + * Returns true if the response includes headers that indicate time-based + * content expiration. + * + * @return bool true if the response expires when it reaches a particular age, false otherwise. + */ + public function isExpirable() + { + return $this->headers->hasCacheControlDirective('s-maxage') + || $this->headers->hasCacheControlDirective('max-age') + || $this->headers->has('expires'); + } + /** * Returns true if the response includes headers that can be used to validate * the response with the origin server using a conditional GET request. diff --git a/src/Symfony/Component/HttpFoundation/Tests/ResponseTest.php b/src/Symfony/Component/HttpFoundation/Tests/ResponseTest.php index d73dd3d344c3f..e4b7ebdc4671f 100644 --- a/src/Symfony/Component/HttpFoundation/Tests/ResponseTest.php +++ b/src/Symfony/Component/HttpFoundation/Tests/ResponseTest.php @@ -258,6 +258,24 @@ public function testIsValidateable() $this->assertFalse($response->isValidateable(), '->isValidateable() returns false when no validator is present'); } + public function testIsExpirable() + { + $response = new Response('', 200); + $response->setSharedMaxAge(100); + $this->assertTrue($response->isExpirable(), '->isExpirable() returns true if a shared-maxage is set'); + + $response = new Response('', 200); + $response->setMaxAge(100); + $this->assertTrue($response->isExpirable(), '->isExpirable() returns true if a maxage is set'); + + $response = new Response('', 200); + $response->setExpires($this->createDateTimeOneHourLater()); + $this->assertTrue($response->isExpirable(), '->isExpirable() returns true if an Expires date present'); + + $response = new Response(); + $this->assertFalse($response->isExpirable(), '->isExpirable() returns false when no max-age or expire date is present'); + } + public function testGetDate() { $oneHourAgo = $this->createDateTimeOneHourAgo(); @@ -304,6 +322,14 @@ public function testGetMaxAge() $this->assertNull($response->getMaxAge(), '->getMaxAge() returns null if no freshness information available'); } + public function testMaxAge0IsStillCacheable() + { + $response = new Response(); + $response->setSharedMaxAge(0); + + $this->assertTrue($response->isCacheable(), '->isCacheable() returns true even if the Response must always be revalidated'); + } + public function testSetSharedMaxAge() { $response = new Response(); diff --git a/src/Symfony/Component/HttpKernel/Tests/HttpCache/HttpCacheTest.php b/src/Symfony/Component/HttpKernel/Tests/HttpCache/HttpCacheTest.php index 7751f6e0ccfdc..6c6b9b157f336 100644 --- a/src/Symfony/Component/HttpKernel/Tests/HttpCache/HttpCacheTest.php +++ b/src/Symfony/Component/HttpKernel/Tests/HttpCache/HttpCacheTest.php @@ -586,6 +586,31 @@ public function testHitsCachedResponseWithSMaxAgeDirective() $this->assertEquals('Hello World', $this->response->getContent()); } + public function testCachesResponseWithSMaxAge0ButRevalidatesIt() + { + $this->setNextResponse(200, array('Cache-Control' => 's-maxage=0'), 'Hello World'); + + $this->request('GET', '/'); + $this->assertHttpKernelIsCalled(); + $this->assertEquals(200, $this->response->getStatusCode()); + $this->assertTraceContains('miss'); + $this->assertTraceContains('store'); + $this->assertEquals(0, $this->response->getAge()); + $this->assertEquals('Hello World', $this->response->getContent()); + + sleep(2); + + $this->setNextResponse(304, array('Cache-Control' => 's-maxage=0')); + + $this->request('GET', '/'); + $this->assertHttpKernelIsCalled(); + $this->assertEquals(200, $this->response->getStatusCode()); + $this->assertTraceContains('stale'); + $this->assertTraceContains('valid'); + $this->assertEquals(0, $this->response->getAge()); + $this->assertEquals('Hello World', $this->response->getContent()); + } + public function testAssignsDefaultTtlWhenResponseHasNoFreshnessInformation() { $this->setNextResponse(); diff --git a/src/Symfony/Component/HttpKernel/Tests/HttpCache/HttpCacheTestCase.php b/src/Symfony/Component/HttpKernel/Tests/HttpCache/HttpCacheTestCase.php index 96a66771a06bc..a4aa15bc664d4 100644 --- a/src/Symfony/Component/HttpKernel/Tests/HttpCache/HttpCacheTestCase.php +++ b/src/Symfony/Component/HttpKernel/Tests/HttpCache/HttpCacheTestCase.php @@ -13,6 +13,7 @@ use PHPUnit\Framework\TestCase; use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpKernel\HttpCache\Esi; use Symfony\Component\HttpKernel\HttpCache\HttpCache; use Symfony\Component\HttpKernel\HttpCache\Store; @@ -25,6 +26,8 @@ class HttpCacheTestCase extends TestCase protected $caches; protected $cacheConfig; protected $request; + + /** @var Response */ protected $response; protected $responses; protected $catch; diff --git a/src/Symfony/Component/HttpKernel/composer.json b/src/Symfony/Component/HttpKernel/composer.json index 14a36bbd4a283..b9a1a5f235133 100644 --- a/src/Symfony/Component/HttpKernel/composer.json +++ b/src/Symfony/Component/HttpKernel/composer.json @@ -18,7 +18,7 @@ "require": { "php": ">=5.3.9", "symfony/event-dispatcher": "^2.6.7|~3.0.0", - "symfony/http-foundation": "~2.7.20|~2.8.13|~3.1.6", + "symfony/http-foundation": "^2.8.19|~3.1.6", "symfony/debug": "^2.6.2", "psr/log": "~1.0" },