123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503 |
- <?php
- /**
- * HTTP Caching adaptor class that provides caching services to the
- * [Request_Client] class, using HTTP cache control logic as defined in
- * RFC 2616.
- *
- * @package Kohana
- * @category Base
- * @author Kohana Team
- * @copyright (c) Kohana Team
- * @license https://koseven.ga/LICENSE.md
- * @since 3.2.0
- */
- class Kohana_HTTP_Cache {
- const CACHE_STATUS_KEY = 'x-cache-status';
- const CACHE_STATUS_SAVED = 'SAVED';
- const CACHE_STATUS_HIT = 'HIT';
- const CACHE_STATUS_MISS = 'MISS';
- const CACHE_HIT_KEY = 'x-cache-hits';
- /**
- * Factory method for HTTP_Cache that provides a convenient dependency
- * injector for the Cache library.
- *
- * // Create HTTP_Cache with named cache engine
- * $http_cache = HTTP_Cache::factory('memcache', array(
- * 'allow_private_cache' => 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 = [])
- {
- 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 = [])
- {
- 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(), [
- 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([
- '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, ['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
|