diff --git a/src/Klein/HttpResponseCache.php b/src/Klein/HttpResponseCache.php new file mode 100644 index 00000000..4f0ac436 --- /dev/null +++ b/src/Klein/HttpResponseCache.php @@ -0,0 +1,398 @@ + + * @author Trevor Suarez (Rican7) (contributor and v2 refactorer) + * @copyright (c) Chris O'Hara + * @link https://github.com/chriso/klein.php + * @license MIT + */ + +namespace Klein; + +/** + * HttpResponseCache + * + * HTTP cache headers manager + * + * @package Klein + */ +class HttpResponseCache +{ + + /** + * HTTP Cache-Control public indicator + * + * @var bool + */ + protected $public = false; + + /** + * HTTP Cache-Control private indicator + * + * @var bool + */ + protected $private = false; + + /** + * HTTP Cache-Control no-cache indicator + * + * @var bool + */ + protected $no_cache = false; + + /** + * HTTP Cache-Control no-store indicator + * + * @var bool + */ + protected $no_store = false; + + /** + * HTTP Cache-Control no-transform indicator + * + * @var bool + */ + protected $no_transform = false; + + /** + * HTTP Cache-Control must-revalidate indicator + * + * @var bool + */ + protected $must_revalidate = false; + + /** + * HTTP Cache-Control proxy-revalidate indicator + * + * @var bool + */ + protected $proxy_revalidate = false; + + /** + * HTTP Cache-Control max-age value + * + * @var int + */ + protected $max_age = 0; + + /** + * HTTP Cache-Control s-maxage value + * + * @var int + */ + protected $s_maxage = 0; + + /** + * HTTP Cache-Control cache-extension + * + * @var array + */ + protected $extensions = array(); + + + /** + * Set response public + * + * @param bool $public + * @return HttpResponseCache + */ + public function setPublic($public = true) + { + $this->public = (bool)$public; + $this->private = false; + $this->no_cache = false; + + return $this; + } + + /** + * Indicates whether is response public + * + * @return bool + */ + public function getPublic() + { + return $this->public; + } + + /** + * Set response private + * + * @param bool $private + * @return HttpResponseCache + */ + public function setPrivate($private = true) + { + $this->private = (bool)$private; + $this->public = false; + $this->no_cache = false; + + return $this; + } + + /** + * Indicates whether is response private + * + * @return bool + */ + public function getPrivate() + { + return $this->private; + } + + /** + * Set response non cacheable + * + * @param bool $no_cache + * @return HttpResponseCache + */ + public function setNoCache($no_cache = true) + { + $this->no_cache = (bool)$no_cache; + $this->public = false; + $this->private = false; + + return true; + } + + /** + * Indicates whether is response cacheable + * + * @return bool + */ + public function getNoCache() + { + return $this->no_cache; + } + + /** + * Set response not storable + * + * @param bool $no_store + * @return HttpResponseCache + */ + public function setNoStore($no_store = true) + { + $this->no_store = (bool)$no_store; + + return $this; + } + + /** + * Indicates whether is response storable + * + * @return bool + */ + public function getNoStore() + { + return $this->no_store; + } + + /** + * Set response cannot transform + * + * @param bool $no_transform + * @return HttpResponseCache + */ + public function setNoTransform($no_transform = true) + { + $this->no_transform = (bool)$no_transform; + + return $this; + } + + /** + * Indicates whether response can transform + * + * @return bool + */ + public function getNoTransform() + { + return $this->no_transform; + } + + /** + * Set cache must revalidate stored response + * + * @param $must_revalidate + * @return HttpResponseCache + */ + public function setMustRevalidate($must_revalidate = true) + { + $this->must_revalidate = (bool)$must_revalidate; + + return $this; + } + + /** + * Indicates whether cache must revalidate stored response + * + * @return boolean + */ + public function getMustRevalidate() + { + return $this->must_revalidate; + } + + /** + * Set shared cache must revalidate stored response + * + * @param $proxy_revalidate + * @return HttpResponseCache + */ + public function setProxyRevalidate($proxy_revalidate = true) + { + $this->proxy_revalidate = (bool)$proxy_revalidate; + + return $this; + } + + /** + * Indicates whether shared cache must revalidate stored response + * + * @return boolean + */ + public function getProxyRevalidate() + { + return $this->proxy_revalidate; + } + + /** + * Set cache lifetime + * + * @param $max_age + * @return HttpResponseCache + */ + public function setMaxAge($max_age) + { + $this->max_age = (int)$max_age; + + return $this; + } + + /** + * Get cache lifetime + * + * @return int + */ + public function getMaxAge() + { + return $this->max_age; + } + + /** + * Set shared cache lifetime + * + * @param $s_maxage + * @return HttpResponseCache + */ + public function setSMaxage($s_maxage) + { + $this->s_maxage = (int)$s_maxage; + + return $this; + } + + /** + * Get shared cache lifetime + * + * @return int + */ + public function getSMaxage() + { + return $this->s_maxage; + } + + /** + * Set custom Cache-Control param + * + * @param string $key + * @param string $value + * @return HttpResponseCache + */ + public function setExtension($key, $value = null) + { + $this->extensions[$key] = (string)$value; + + return $this; + } + + /** + * Get custom Cache-Control param + * + * @param string $key + * @return string + */ + public function getExtension($key) + { + return $this->extensions[$key]; + } + + + /** + * Generate header string + * + * @return string + */ + public function generateCacheControlString() + { + $headerString = ''; + + if ($this->no_cache) { + $headerString .= ' no-cache'; + } elseif ($this->private) { + $headerString .= ' private'; + } elseif ($this->public) { + $headerString .= ' public'; + } else { + $headerString .= ' no-cache'; + } + + if ($this->no_store) { + $headerString .= ' no-store'; + } + + if ($this->no_transform) { + $headerString .= ' no-transform'; + } + + if ($this->must_revalidate) { + $headerString .= ' must-revalidate'; + } + + if ($this->proxy_revalidate) { + $headerString .= ' proxy-revalidate'; + } + + if (is_integer($this->max_age)) { + $headerString .= ' max-age=' . $this->max_age; + } + + if (is_integer($this->s_maxage)) { + $headerString .= ' s-maxage=' . $this->s_maxage; + } + + foreach ($this->extensions as $extensionName => $extensionValue) { + + if ($extensionValue === '') { + $headerString .= ' ' . $extensionName; + } else { + $headerString .= ' ' . $extensionName . '=' . $extensionValue; + } + + } + + return 'Cache-Control: ' . trim($headerString); + } + + /** + * Send headers + */ + public function send() + { + + header($this->generateCacheControlString(), true); + } +} \ No newline at end of file diff --git a/src/Klein/Response.php b/src/Klein/Response.php index ef89f54b..14ad91cb 100644 --- a/src/Klein/Response.php +++ b/src/Klein/Response.php @@ -18,8 +18,8 @@ use \Klein\Exceptions\ResponseAlreadySentException; /** - * Response - * + * Response + * * @package Klein */ class Response @@ -78,6 +78,14 @@ class Response */ protected $cookies; + /** + * HTTP response cache headers + * + * @var \Klein\HttpResponseCache + * @access protected + */ + protected $cache; + /** * Whether or not the response is "locked" from * any further modification @@ -128,6 +136,7 @@ public function __construct($body = '', $status_code = null, array $headers = ar $this->headers = new HeaderDataCollection($headers); $this->cookies = new ResponseCookieDataCollection(); + $this->cache = new HttpResponseCache(); } /** @@ -237,6 +246,27 @@ public function code($code = null) return $this->status->getCode(); } + /** + * Get (or set) the HTTP Cache-Control object + * + * @param null|HttpResponseCache $cache + * @access public + * @return HttpResponseCache|Response + */ + public function cache(HttpResponseCache $cache = null) + { + if (null !== $cache) { + // Require that the response be unlocked before changing it + $this->requireUnlocked(); + + $this->cache = $cache; + + return $this; + } + + return $this->cache; + } + /** * Prepend a string to the response's content body * @@ -358,6 +388,9 @@ public function sendHeaders($cookies_also = true, $override = false) // Send our HTTP status line header($this->httpStatusLine()); + // Send HTTP cache headers + $this->cache->send(); + // Iterate through our Headers data collection and send each header foreach ($this->headers as $key => $value) { header($key .': '. $value, false); @@ -543,8 +576,8 @@ public function cookie( */ public function noCache() { - $this->header('Pragma', 'no-cache'); - $this->header('Cache-Control', 'no-store, no-cache'); + $this->cache->setNoCache(); + $this->cache->setNoStore(); return $this; } diff --git a/tests/Klein/Tests/HttpResponseCacheTest.php b/tests/Klein/Tests/HttpResponseCacheTest.php new file mode 100644 index 00000000..f61c716e --- /dev/null +++ b/tests/Klein/Tests/HttpResponseCacheTest.php @@ -0,0 +1,217 @@ + + * @author Trevor Suarez (Rican7) (contributor and v2 refactorer) + * @copyright (c) Chris O'Hara + * @link https://github.com/chriso/klein.php + * @license MIT + */ + +namespace Klein\Tests; + + +use \Klein\HttpResponseCache; + +/** + * HttpResponseCacheTest + * + * @uses AbstractKleinTest + * @package Klein\Tests + */ +class HttpResponseCacheTest extends AbstractKleinTest +{ + public function testBasicExample() + { + $max_age = rand(1, 100); + $responseCache = new HttpResponseCache(); + + $responseCache->setPublic(); + $responseCache->setMaxAge($max_age); + $responseCache->setSMaxage($max_age); + + $CacheString = $responseCache->generateCacheControlString(); + $this->assertContains('Cache-Control: ', $CacheString); + $this->assertContains(' public', $CacheString); + $this->assertContains(' max-age=' . $max_age, $CacheString); + $this->assertContains(' s-maxage=' . $max_age, $CacheString); + + $this->assertNotContains(' private', $CacheString); + $this->assertNotContains(' no-cache', $CacheString); + $this->assertNotContains(' no-store', $CacheString); + } + + public function testGenerateCacheControlString() + { + $responseCache = new HttpResponseCache(); + + + $CacheString = $responseCache->generateCacheControlString(); + $this->assertContains('Cache-Control: ', $CacheString); + $this->assertContains(' no-cache', $CacheString); + } + + public function testPublicGetSet() + { + $responseCache = new HttpResponseCache(); + + $responseCache->setNoCache(); + $responseCache->setPrivate(); + $responseCache->setPublic(); + + $this->assertTrue($responseCache->getPublic()); + $this->assertFalse($responseCache->getPrivate()); + $this->assertFalse($responseCache->getNoCache()); + + $CacheString = $responseCache->generateCacheControlString(); + $this->assertContains('Cache-Control: ', $CacheString); + $this->assertContains(' public', $CacheString); + $this->assertNotContains(' private', $CacheString); + $this->assertNotContains(' no-cache', $CacheString); + } + + public function testPrivateGetSet() + { + $responseCache = new HttpResponseCache(); + + $responseCache->setPublic(); + $responseCache->setNoCache(); + $responseCache->setPrivate(); + + $this->assertTrue($responseCache->getPrivate()); + $this->assertFalse($responseCache->getPublic()); + $this->assertFalse($responseCache->getNoCache()); + + + $CacheString = $responseCache->generateCacheControlString(); + $this->assertContains('Cache-Control: ', $CacheString); + $this->assertContains(' private', $CacheString); + $this->assertNotContains(' public', $CacheString); + $this->assertNotContains(' no-cache', $CacheString); + } + + public function testNoCacheGetSet() + { + $responseCache = new HttpResponseCache(); + + $responseCache->setPublic(); + $responseCache->setPrivate(); + $responseCache->setNoCache(); + + $this->assertTrue($responseCache->getNoCache()); + $this->assertFalse($responseCache->getPublic()); + $this->assertFalse($responseCache->getPrivate()); + + + $CacheString = $responseCache->generateCacheControlString(); + $this->assertContains('Cache-Control: ', $CacheString); + $this->assertContains(' no-cache', $CacheString); + $this->assertNotContains(' public', $CacheString); + $this->assertNotContains(' private', $CacheString); + } + + /** + * @dataProvider methodProvider + */ + public function testOptionsGetSet($setMethod, $getMethod, $cacheString) + { + $responseCache = new HttpResponseCache(); + + $responseCache->{$setMethod}(true); + $this->assertTrue($responseCache->{$getMethod}()); + + // Test default Cache-Control + $generatedString = $responseCache->generateCacheControlString(); + $this->assertContains('Cache-Control: ', $generatedString); + $this->assertContains(' ' . $cacheString, $generatedString); + + + $responseCache->{$setMethod}(false); + $this->assertFalse($responseCache->{$getMethod}()); + + $generatedString = $responseCache->generateCacheControlString(); + $this->assertContains('Cache-Control: ', $generatedString); + $this->assertNotContains(' ' . $cacheString, $generatedString); + } + + public function methodProvider() + { + return array( + array('setNoStore', 'getNoStore', 'no-store'), + array('setNoTransform', 'getNoTransform', 'no-transform'), + array('setMustRevalidate', 'getMustRevalidate', 'must-revalidate'), + array('setProxyRevalidate', 'getProxyRevalidate', 'proxy-revalidate') + ); + } + + public function testMaxAgeGetSet() + { + $responseCache = new HttpResponseCache(); + $randomAge = rand(); + + $responseCache->setMaxAge($randomAge); + $this->assertEquals($randomAge, $responseCache->getMaxAge()); + + + $generatedString = $responseCache->generateCacheControlString(); + $this->assertContains('Cache-Control: ', $generatedString); + $this->assertContains(' max-age=' . $randomAge, $generatedString); + + + $newAge = $randomAge - 1; + $responseCache->setMaxAge($newAge); + $this->assertEquals($newAge, $responseCache->getMaxAge()); + + $generatedString = $responseCache->generateCacheControlString(); + $this->assertContains('Cache-Control: ', $generatedString); + $this->assertContains(' max-age=' . $newAge, $generatedString); + } + + public function testSMaxageGetSet() + { + $responseCache = new HttpResponseCache(); + $randomAge = rand(); + + $responseCache->setSMaxage($randomAge); + $this->assertEquals($randomAge, $responseCache->getSMaxage()); + + // Test default Cache-Control + $generatedString = $responseCache->generateCacheControlString(); + $this->assertContains('Cache-Control: ', $generatedString); + $this->assertContains(' s-maxage=' . $randomAge, $generatedString); + + + $newAge = $randomAge - 1; + $responseCache->setSMaxage($newAge); + $this->assertEquals($newAge, $responseCache->getSMaxage()); + + $generatedString = $responseCache->generateCacheControlString(); + $this->assertContains('Cache-Control: ', $generatedString); + $this->assertContains(' s-maxage=' . $newAge, $generatedString); + } + + public function testExtensionsGetSet() + { + $key = "Extension"; + $value = "Option"; + + $responseCache = new HttpResponseCache(); + + $responseCache->setExtension($key); + $this->assertEquals('', $responseCache->getExtension($key)); + + $generatedString = $responseCache->generateCacheControlString(); + $this->assertContains('Cache-Control: ', $generatedString); + $this->assertContains(' ' . $key, $generatedString); + $this->assertNotContains(' ' . $key . '=', $generatedString); + + + $responseCache->setExtension($key, $value); + $this->assertEquals($value, $responseCache->getExtension($key)); + + $generatedString = $responseCache->generateCacheControlString(); + $this->assertContains('Cache-Control: ', $generatedString); + $this->assertContains(' ' . $key . '=' . $value, $generatedString); + } +} diff --git a/tests/Klein/Tests/ResponseTest.php b/tests/Klein/Tests/ResponseTest.php index 855f2b65..52c07b8a 100644 --- a/tests/Klein/Tests/ResponseTest.php +++ b/tests/Klein/Tests/ResponseTest.php @@ -15,6 +15,7 @@ use \Klein\Klein; use \Klein\Response; use \Klein\HttpStatus; +use \Klein\HttpResponseCache; use \Klein\DataCollection\HeaderDataCollection; use \Klein\DataCollection\ResponseCookieDataCollection; use \Klein\Exceptions\LockedResponseException; @@ -22,8 +23,8 @@ use \Klein\Tests\Mocks\MockRequestFactory; /** - * ResponsesTest - * + * ResponsesTest + * * @uses AbstractKleinTest * @package Klein\Tests */ @@ -111,6 +112,19 @@ public function testCookiesGetter() $this->assertTrue($response->cookies() instanceof ResponseCookieDataCollection); } + public function testCacheGetSet() + { + $response = new Response(); + + $this->assertInternalType('object', $response->cache()); + $this->assertTrue($response->cache() instanceof HttpResponseCache); + + $newCache = new HttpResponseCache(); + $response->cache($newCache); + + $this->assertEquals($newCache, $response->cache()); + } + public function testPrepend() { $response = new Response('ein'); @@ -287,12 +301,10 @@ public function testNoCache() { $response = new Response(); - // Make sure the headers are initially empty - $this->assertEmpty($response->headers()->all()); - $response->noCache(); - $this->assertContains('no-cache', $response->headers()->all()); + $this->assertTrue($response->cache()->getNoCache()); + $this->assertTrue($response->cache()->getNoStore()); } public function testRedirect() @@ -337,6 +349,10 @@ function ($request, $response, $service) use ($file_name, $file_mime) { file_get_contents(__FILE__) ); + // Assert noCache was set + $this->assertTrue($this->klein_app->response()->cache()->getNoCache()); + $this->assertTrue($this->klein_app->response()->cache()->getNoStore()); + // Assert headers were passed $this->assertEquals( $file_mime, @@ -378,15 +394,11 @@ function ($request, $response, $service) use ($test_object) { json_encode($test_object) ); + // Assert noCache was set + $this->assertTrue($this->klein_app->response()->cache()->getNoCache()); + $this->assertTrue($this->klein_app->response()->cache()->getNoStore()); + // Assert headers were passed - $this->assertEquals( - 'no-cache', - $this->klein_app->response()->headers()->get('Pragma') - ); - $this->assertEquals( - 'no-store, no-cache', - $this->klein_app->response()->headers()->get('Cache-Control') - ); $this->assertEquals( 'application/json', $this->klein_app->response()->headers()->get('Content-Type')