FALSE * ) * ); * * // Create HTTP_Cache with supplied cache engine * $http_cache = HTTP_Cache::factory(Cache::instance('memcache'), * array( * 'allow_private_cache' => FALSE * ) * ); * * @uses Cache * @param mixed $cache cache engine to use * @param array $options options to set to this class * @return HTTP_Cache */ public static function factory($cache, array $options = array()) { if ( ! $cache instanceof Cache) { $cache = Cache::instance($cache); } $options['cache'] = $cache; return new HTTP_Cache($options); } /** * Basic cache key generator that hashes the entire request and returns * it. This is fine for static content, or dynamic content where user * specific information is encoded into the request. * * // Generate cache key * $cache_key = HTTP_Cache::basic_cache_key_generator($request); * * @param Request $request * @return string */ public static function basic_cache_key_generator(Request $request) { $uri = $request->uri(); $query = $request->query(); $headers = $request->headers()->getArrayCopy(); $body = $request->body(); return sha1($uri.'?'.http_build_query($query, NULL, '&').'~'.implode('~', $headers).'~'.$body); } /** * @var Cache cache driver to use for HTTP caching */ protected $_cache; /** * @var callback Cache key generator callback */ protected $_cache_key_callback; /** * @var boolean Defines whether this client should cache `private` cache directives * @link http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.9 */ protected $_allow_private_cache = FALSE; /** * @var int The timestamp of the request */ protected $_request_time; /** * @var int The timestamp of the response */ protected $_response_time; /** * Constructor method for this class. Allows dependency injection of the * required components such as `Cache` and the cache key generator. * * @param array $options */ public function __construct(array $options = array()) { foreach ($options as $key => $value) { if (method_exists($this, $key)) { $this->$key($value); } } if ($this->_cache_key_callback === NULL) { $this->cache_key_callback('HTTP_Cache::basic_cache_key_generator'); } } /** * Executes the supplied [Request] with the supplied [Request_Client]. * Before execution, the HTTP_Cache adapter checks the request type, * destructive requests such as `POST`, `PUT` and `DELETE` will bypass * cache completely and ensure the response is not cached. All other * Request methods will allow caching, if the rules are met. * * @param Request_Client $client client to execute with Cache-Control * @param Request $request request to execute with client * @return [Response] */ public function execute(Request_Client $client, Request $request, Response $response) { if ( ! $this->_cache instanceof Cache) return $client->execute_request($request, $response); // If this is a destructive request, by-pass cache completely if (in_array($request->method(), array( HTTP_Request::POST, HTTP_Request::PUT, HTTP_Request::DELETE))) { // Kill existing caches for this request $this->invalidate_cache($request); $response = $client->execute_request($request, $response); $cache_control = HTTP_Header::create_cache_control(array( 'no-cache', 'must-revalidate' )); // Ensure client respects destructive action return $response->headers('cache-control', $cache_control); } // Create the cache key $cache_key = $this->create_cache_key($request, $this->_cache_key_callback); // Try and return cached version if (($cached_response = $this->cache_response($cache_key, $request)) instanceof Response) return $cached_response; // Start request time $this->_request_time = time(); // Execute the request with the Request client $response = $client->execute_request($request, $response); // Stop response time $this->_response_time = (time() - $this->_request_time); // Cache the response $this->cache_response($cache_key, $request, $response); $response->headers(HTTP_Cache::CACHE_STATUS_KEY, HTTP_Cache::CACHE_STATUS_MISS); return $response; } /** * Invalidate a cached response for the [Request] supplied. * This has the effect of deleting the response from the * [Cache] entry. * * @param Request $request Response to remove from cache * @return void */ public function invalidate_cache(Request $request) { if (($cache = $this->cache()) instanceof Cache) { $cache->delete($this->create_cache_key($request, $this->_cache_key_callback)); } return; } /** * Getter and setter for the internal caching engine, * used to cache responses if available and valid. * * @param Kohana_Cache $cache engine to use for caching * @return Kohana_Cache * @return Kohana_Request_Client */ public function cache(Cache $cache = NULL) { if ($cache === NULL) return $this->_cache; $this->_cache = $cache; return $this; } /** * Gets or sets the [Request_Client::allow_private_cache] setting. * If set to `TRUE`, the client will also cache cache-control directives * that have the `private` setting. * * @link http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.9 * @param boolean $setting allow caching of privately marked responses * @return boolean * @return [Request_Client] */ public function allow_private_cache($setting = NULL) { if ($setting === NULL) return $this->_allow_private_cache; $this->_allow_private_cache = (bool) $setting; return $this; } /** * Sets or gets the cache key generator callback for this caching * class. The cache key generator provides a unique hash based on the * `Request` object passed to it. * * The default generator is [HTTP_Cache::basic_cache_key_generator()], which * serializes the entire `HTTP_Request` into a unique sha1 hash. This will * provide basic caching for static and simple dynamic pages. More complex * algorithms can be defined and then passed into `HTTP_Cache` using this * method. * * // Get the cache key callback * $callback = $http_cache->cache_key_callback(); * * // Set the cache key callback * $http_cache->cache_key_callback('Foo::cache_key'); * * // Alternatively, in PHP 5.3 use a closure * $http_cache->cache_key_callback(function (Request $request) { * return sha1($request->render()); * }); * * @param callback $callback * @return mixed * @throws HTTP_Exception */ public function cache_key_callback($callback = NULL) { if ($callback === NULL) return $this->_cache_key_callback; if ( ! is_callable($callback)) throw new Kohana_Exception('cache_key_callback must be callable!'); $this->_cache_key_callback = $callback; return $this; } /** * Creates a cache key for the request to use for caching * [Kohana_Response] returned by [Request::execute]. * * This is the default cache key generating logic, but can be overridden * by setting [HTTP_Cache::cache_key_callback()]. * * @param Request $request request to create key for * @param callback $callback optional callback to use instead of built-in method * @return string */ public function create_cache_key(Request $request, $callback = FALSE) { if (is_callable($callback)) return call_user_func($callback, $request); else return HTTP_Cache::basic_cache_key_generator($request); } /** * Controls whether the response can be cached. Uses HTTP * protocol to determine whether the response can be cached. * * @link http://www.w3.org/Protocols/rfc2616/rfc2616.html RFC 2616 * @param Response $response The Response * @return boolean */ public function set_cache(Response $response) { $headers = $response->headers()->getArrayCopy(); if ($cache_control = Arr::get($headers, 'cache-control')) { // Parse the cache control $cache_control = HTTP_Header::parse_cache_control($cache_control); // If the no-cache or no-store directive is set, return if (array_intersect($cache_control, array('no-cache', 'no-store'))) return FALSE; // Check for private cache and get out of here if invalid if ( ! $this->_allow_private_cache AND in_array('private', $cache_control)) { if ( ! isset($cache_control['s-maxage'])) return FALSE; // If there is a s-maxage directive we can use that $cache_control['max-age'] = $cache_control['s-maxage']; } // Check that max-age has been set and if it is valid for caching if (isset($cache_control['max-age']) AND $cache_control['max-age'] < 1) return FALSE; } if ($expires = Arr::get($headers, 'expires') AND ! isset($cache_control['max-age'])) { // Can't cache things that have expired already if (strtotime($expires) <= time()) return FALSE; } return TRUE; } /** * Caches a [Response] using the supplied [Cache] * and the key generated by [Request_Client::_create_cache_key]. * * If not response is supplied, the cache will be checked for an existing * one that is available. * * @param string $key the cache key to use * @param Request $request the HTTP Request * @param Response $response the HTTP Response * @return mixed */ public function cache_response($key, Request $request, Response $response = NULL) { if ( ! $this->_cache instanceof Cache) return FALSE; // Check for Pragma: no-cache if ($pragma = $request->headers('pragma')) { if ($pragma == 'no-cache') return FALSE; elseif (is_array($pragma) AND in_array('no-cache', $pragma)) return FALSE; } // If there is no response, lookup an existing cached response if ($response === NULL) { $response = $this->_cache->get($key); if ( ! $response instanceof Response) return FALSE; // Do cache hit arithmetic, using fast arithmetic if available if ($this->_cache instanceof Cache_Arithmetic) { $hit_count = $this->_cache->increment(HTTP_Cache::CACHE_HIT_KEY.$key); } else { $hit_count = $this->_cache->get(HTTP_Cache::CACHE_HIT_KEY.$key); $this->_cache->set(HTTP_Cache::CACHE_HIT_KEY.$key, ++$hit_count); } // Update the header to have correct HIT status and count $response->headers(HTTP_Cache::CACHE_STATUS_KEY, HTTP_Cache::CACHE_STATUS_HIT) ->headers(HTTP_Cache::CACHE_HIT_KEY, $hit_count); return $response; } else { if (($ttl = $this->cache_lifetime($response)) === FALSE) return FALSE; $response->headers(HTTP_Cache::CACHE_STATUS_KEY, HTTP_Cache::CACHE_STATUS_SAVED); // Set the hit count to zero $this->_cache->set(HTTP_Cache::CACHE_HIT_KEY.$key, 0); return $this->_cache->set($key, $response, $ttl); } } /** * Calculates the total Time To Live based on the specification * RFC 2616 cache lifetime rules. * * @param Response $response Response to evaluate * @return mixed TTL value or false if the response should not be cached */ public function cache_lifetime(Response $response) { // Get out of here if this cannot be cached if ( ! $this->set_cache($response)) return FALSE; // Calculate apparent age if ($date = $response->headers('date')) { $apparent_age = max(0, $this->_response_time - strtotime($date)); } else { $apparent_age = max(0, $this->_response_time); } // Calculate corrected received age if ($age = $response->headers('age')) { $corrected_received_age = max($apparent_age, intval($age)); } else { $corrected_received_age = $apparent_age; } // Corrected initial age $corrected_initial_age = $corrected_received_age + $this->request_execution_time(); // Resident time $resident_time = time() - $this->_response_time; // Current age $current_age = $corrected_initial_age + $resident_time; // Prepare the cache freshness lifetime $ttl = NULL; // Cache control overrides if ($cache_control = $response->headers('cache-control')) { // Parse the cache control header $cache_control = HTTP_Header::parse_cache_control($cache_control); if (isset($cache_control['max-age'])) { $ttl = $cache_control['max-age']; } if (isset($cache_control['s-maxage']) AND isset($cache_control['private']) AND $this->_allow_private_cache) { $ttl = $cache_control['s-maxage']; } if (isset($cache_control['max-stale']) AND ! isset($cache_control['must-revalidate'])) { $ttl = $current_age + $cache_control['max-stale']; } } // If we have a TTL at this point, return if ($ttl !== NULL) return $ttl; if ($expires = $response->headers('expires')) return strtotime($expires) - $current_age; return FALSE; } /** * Returns the duration of the last request execution. * Either returns the time of completed requests or * `FALSE` if the request hasn't finished executing, or * is yet to be run. * * @return mixed */ public function request_execution_time() { if ($this->_request_time === NULL OR $this->_response_time === NULL) return FALSE; return $this->_response_time - $this->_request_time; } } // End Kohana_HTTP_Cache