Cache.php 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504
  1. <?php
  2. /**
  3. * HTTP Caching adaptor class that provides caching services to the
  4. * [Request_Client] class, using HTTP cache control logic as defined in
  5. * RFC 2616.
  6. *
  7. * @package Kohana
  8. * @category Base
  9. *
  10. * @copyright (c) 2007-2016 Kohana Team
  11. * @copyright (c) since 2016 Koseven Team
  12. * @license https://koseven.dev/LICENSE
  13. * @since 3.2.0
  14. */
  15. class KO7_HTTP_Cache {
  16. const CACHE_STATUS_KEY = 'x-cache-status';
  17. const CACHE_STATUS_SAVED = 'SAVED';
  18. const CACHE_STATUS_HIT = 'HIT';
  19. const CACHE_STATUS_MISS = 'MISS';
  20. const CACHE_HIT_KEY = 'x-cache-hits';
  21. /**
  22. * Factory method for HTTP_Cache that provides a convenient dependency
  23. * injector for the Cache library.
  24. *
  25. * // Create HTTP_Cache with named cache engine
  26. * $http_cache = HTTP_Cache::factory('memcache', array(
  27. * 'allow_private_cache' => FALSE
  28. * )
  29. * );
  30. *
  31. * // Create HTTP_Cache with supplied cache engine
  32. * $http_cache = HTTP_Cache::factory(Cache::instance('memcache'),
  33. * array(
  34. * 'allow_private_cache' => FALSE
  35. * )
  36. * );
  37. *
  38. * @uses Cache
  39. * @param mixed $cache cache engine to use
  40. * @param array $options options to set to this class
  41. * @return HTTP_Cache
  42. */
  43. public static function factory($cache, array $options = [])
  44. {
  45. if ( ! $cache instanceof Cache)
  46. {
  47. $cache = Cache::instance($cache);
  48. }
  49. $options['cache'] = $cache;
  50. return new HTTP_Cache($options);
  51. }
  52. /**
  53. * Basic cache key generator that hashes the entire request and returns
  54. * it. This is fine for static content, or dynamic content where user
  55. * specific information is encoded into the request.
  56. *
  57. * // Generate cache key
  58. * $cache_key = HTTP_Cache::basic_cache_key_generator($request);
  59. *
  60. * @param Request $request
  61. * @return string
  62. */
  63. public static function basic_cache_key_generator(Request $request)
  64. {
  65. $uri = $request->uri();
  66. $query = $request->query();
  67. $headers = $request->headers()->getArrayCopy();
  68. $body = $request->body();
  69. return sha1($uri.'?'.http_build_query($query, NULL, '&').'~'.implode('~', $headers).'~'.$body);
  70. }
  71. /**
  72. * @var Cache cache driver to use for HTTP caching
  73. */
  74. protected $_cache;
  75. /**
  76. * @var callback Cache key generator callback
  77. */
  78. protected $_cache_key_callback;
  79. /**
  80. * @var boolean Defines whether this client should cache `private` cache directives
  81. * @link http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.9
  82. */
  83. protected $_allow_private_cache = FALSE;
  84. /**
  85. * @var int The timestamp of the request
  86. */
  87. protected $_request_time;
  88. /**
  89. * @var int The timestamp of the response
  90. */
  91. protected $_response_time;
  92. /**
  93. * Constructor method for this class. Allows dependency injection of the
  94. * required components such as `Cache` and the cache key generator.
  95. *
  96. * @param array $options
  97. */
  98. public function __construct(array $options = [])
  99. {
  100. foreach ($options as $key => $value)
  101. {
  102. if (method_exists($this, $key))
  103. {
  104. $this->$key($value);
  105. }
  106. }
  107. if ($this->_cache_key_callback === NULL)
  108. {
  109. $this->cache_key_callback('HTTP_Cache::basic_cache_key_generator');
  110. }
  111. }
  112. /**
  113. * Executes the supplied [Request] with the supplied [Request_Client].
  114. * Before execution, the HTTP_Cache adapter checks the request type,
  115. * destructive requests such as `POST`, `PUT` and `DELETE` will bypass
  116. * cache completely and ensure the response is not cached. All other
  117. * Request methods will allow caching, if the rules are met.
  118. *
  119. * @param Request_Client $client client to execute with Cache-Control
  120. * @param Request $request request to execute with client
  121. * @return [Response]
  122. */
  123. public function execute(Request_Client $client, Request $request, Response $response)
  124. {
  125. if ( ! $this->_cache instanceof Cache)
  126. return $client->execute_request($request, $response);
  127. // If this is a destructive request, by-pass cache completely
  128. if (in_array($request->method(), [
  129. HTTP_Request::POST,
  130. HTTP_Request::PUT,
  131. HTTP_Request::DELETE]))
  132. {
  133. // Kill existing caches for this request
  134. $this->invalidate_cache($request);
  135. $response = $client->execute_request($request, $response);
  136. $cache_control = HTTP_Header::create_cache_control([
  137. 'no-cache',
  138. 'must-revalidate'
  139. ]);
  140. // Ensure client respects destructive action
  141. return $response->headers('cache-control', $cache_control);
  142. }
  143. // Create the cache key
  144. $cache_key = $this->create_cache_key($request, $this->_cache_key_callback);
  145. // Try and return cached version
  146. if (($cached_response = $this->cache_response($cache_key, $request)) instanceof Response)
  147. return $cached_response;
  148. // Start request time
  149. $this->_request_time = time();
  150. // Execute the request with the Request client
  151. $response = $client->execute_request($request, $response);
  152. // Stop response time
  153. $this->_response_time = (time() - $this->_request_time);
  154. // Cache the response
  155. $this->cache_response($cache_key, $request, $response);
  156. $response->headers(HTTP_Cache::CACHE_STATUS_KEY,
  157. HTTP_Cache::CACHE_STATUS_MISS);
  158. return $response;
  159. }
  160. /**
  161. * Invalidate a cached response for the [Request] supplied.
  162. * This has the effect of deleting the response from the
  163. * [Cache] entry.
  164. *
  165. * @param Request $request Response to remove from cache
  166. * @return void
  167. */
  168. public function invalidate_cache(Request $request)
  169. {
  170. if (($cache = $this->cache()) instanceof Cache)
  171. {
  172. $cache->delete($this->create_cache_key($request, $this->_cache_key_callback));
  173. }
  174. return;
  175. }
  176. /**
  177. * Getter and setter for the internal caching engine,
  178. * used to cache responses if available and valid.
  179. *
  180. * @param KO7_Cache $cache engine to use for caching
  181. * @return KO7_Cache
  182. * @return KO7_Request_Client
  183. */
  184. public function cache(Cache $cache = NULL)
  185. {
  186. if ($cache === NULL)
  187. return $this->_cache;
  188. $this->_cache = $cache;
  189. return $this;
  190. }
  191. /**
  192. * Gets or sets the [Request_Client::allow_private_cache] setting.
  193. * If set to `TRUE`, the client will also cache cache-control directives
  194. * that have the `private` setting.
  195. *
  196. * @link http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.9
  197. * @param boolean $setting allow caching of privately marked responses
  198. * @return boolean
  199. * @return [Request_Client]
  200. */
  201. public function allow_private_cache($setting = NULL)
  202. {
  203. if ($setting === NULL)
  204. return $this->_allow_private_cache;
  205. $this->_allow_private_cache = (bool) $setting;
  206. return $this;
  207. }
  208. /**
  209. * Sets or gets the cache key generator callback for this caching
  210. * class. The cache key generator provides a unique hash based on the
  211. * `Request` object passed to it.
  212. *
  213. * The default generator is [HTTP_Cache::basic_cache_key_generator()], which
  214. * serializes the entire `HTTP_Request` into a unique sha1 hash. This will
  215. * provide basic caching for static and simple dynamic pages. More complex
  216. * algorithms can be defined and then passed into `HTTP_Cache` using this
  217. * method.
  218. *
  219. * // Get the cache key callback
  220. * $callback = $http_cache->cache_key_callback();
  221. *
  222. * // Set the cache key callback
  223. * $http_cache->cache_key_callback('Foo::cache_key');
  224. *
  225. * // Alternatively, in PHP 5.3 use a closure
  226. * $http_cache->cache_key_callback(function (Request $request) {
  227. * return sha1($request->render());
  228. * });
  229. *
  230. * @param callback $callback
  231. * @return mixed
  232. * @throws HTTP_Exception
  233. */
  234. public function cache_key_callback($callback = NULL)
  235. {
  236. if ($callback === NULL)
  237. return $this->_cache_key_callback;
  238. if ( ! is_callable($callback))
  239. throw new KO7_Exception('cache_key_callback must be callable!');
  240. $this->_cache_key_callback = $callback;
  241. return $this;
  242. }
  243. /**
  244. * Creates a cache key for the request to use for caching
  245. * [KO7_Response] returned by [Request::execute].
  246. *
  247. * This is the default cache key generating logic, but can be overridden
  248. * by setting [HTTP_Cache::cache_key_callback()].
  249. *
  250. * @param Request $request request to create key for
  251. * @param callback $callback optional callback to use instead of built-in method
  252. * @return string
  253. */
  254. public function create_cache_key(Request $request, $callback = FALSE)
  255. {
  256. if (is_callable($callback))
  257. return call_user_func($callback, $request);
  258. else
  259. return HTTP_Cache::basic_cache_key_generator($request);
  260. }
  261. /**
  262. * Controls whether the response can be cached. Uses HTTP
  263. * protocol to determine whether the response can be cached.
  264. *
  265. * @link http://www.w3.org/Protocols/rfc2616/rfc2616.html RFC 2616
  266. * @param Response $response The Response
  267. * @return boolean
  268. */
  269. public function set_cache(Response $response)
  270. {
  271. $headers = $response->headers()->getArrayCopy();
  272. if ($cache_control = Arr::get($headers, 'cache-control'))
  273. {
  274. // Parse the cache control
  275. $cache_control = HTTP_Header::parse_cache_control($cache_control);
  276. // If the no-cache or no-store directive is set, return
  277. if (array_intersect($cache_control, ['no-cache', 'no-store']))
  278. return FALSE;
  279. // Check for private cache and get out of here if invalid
  280. if ( ! $this->_allow_private_cache AND in_array('private', $cache_control))
  281. {
  282. if ( ! isset($cache_control['s-maxage']))
  283. return FALSE;
  284. // If there is a s-maxage directive we can use that
  285. $cache_control['max-age'] = $cache_control['s-maxage'];
  286. }
  287. // Check that max-age has been set and if it is valid for caching
  288. if (isset($cache_control['max-age']) AND $cache_control['max-age'] < 1)
  289. return FALSE;
  290. }
  291. if ($expires = Arr::get($headers, 'expires') AND ! isset($cache_control['max-age']))
  292. {
  293. // Can't cache things that have expired already
  294. if (strtotime($expires) <= time())
  295. return FALSE;
  296. }
  297. return TRUE;
  298. }
  299. /**
  300. * Caches a [Response] using the supplied [Cache]
  301. * and the key generated by [Request_Client::_create_cache_key].
  302. *
  303. * If not response is supplied, the cache will be checked for an existing
  304. * one that is available.
  305. *
  306. * @param string $key the cache key to use
  307. * @param Request $request the HTTP Request
  308. * @param Response $response the HTTP Response
  309. * @return mixed
  310. */
  311. public function cache_response($key, Request $request, Response $response = NULL)
  312. {
  313. if ( ! $this->_cache instanceof Cache)
  314. return FALSE;
  315. // Check for Pragma: no-cache
  316. if ($pragma = $request->headers('pragma'))
  317. {
  318. if ($pragma == 'no-cache')
  319. return FALSE;
  320. elseif (is_array($pragma) AND in_array('no-cache', $pragma))
  321. return FALSE;
  322. }
  323. // If there is no response, lookup an existing cached response
  324. if ($response === NULL)
  325. {
  326. $response = $this->_cache->get($key);
  327. if ( ! $response instanceof Response)
  328. return FALSE;
  329. // Do cache hit arithmetic, using fast arithmetic if available
  330. if ($this->_cache instanceof Cache_Arithmetic)
  331. {
  332. $hit_count = $this->_cache->increment(HTTP_Cache::CACHE_HIT_KEY.$key);
  333. }
  334. else
  335. {
  336. $hit_count = $this->_cache->get(HTTP_Cache::CACHE_HIT_KEY.$key);
  337. $this->_cache->set(HTTP_Cache::CACHE_HIT_KEY.$key, ++$hit_count);
  338. }
  339. // Update the header to have correct HIT status and count
  340. $response->headers(HTTP_Cache::CACHE_STATUS_KEY,
  341. HTTP_Cache::CACHE_STATUS_HIT)
  342. ->headers(HTTP_Cache::CACHE_HIT_KEY, $hit_count);
  343. return $response;
  344. }
  345. else
  346. {
  347. if (($ttl = $this->cache_lifetime($response)) === FALSE)
  348. return FALSE;
  349. $response->headers(HTTP_Cache::CACHE_STATUS_KEY,
  350. HTTP_Cache::CACHE_STATUS_SAVED);
  351. // Set the hit count to zero
  352. $this->_cache->set(HTTP_Cache::CACHE_HIT_KEY.$key, 0);
  353. return $this->_cache->set($key, $response, $ttl);
  354. }
  355. }
  356. /**
  357. * Calculates the total Time To Live based on the specification
  358. * RFC 2616 cache lifetime rules.
  359. *
  360. * @param Response $response Response to evaluate
  361. * @return mixed TTL value or false if the response should not be cached
  362. */
  363. public function cache_lifetime(Response $response)
  364. {
  365. // Get out of here if this cannot be cached
  366. if ( ! $this->set_cache($response))
  367. return FALSE;
  368. // Calculate apparent age
  369. if ($date = $response->headers('date'))
  370. {
  371. $apparent_age = max(0, $this->_response_time - strtotime($date));
  372. }
  373. else
  374. {
  375. $apparent_age = max(0, $this->_response_time);
  376. }
  377. // Calculate corrected received age
  378. if ($age = $response->headers('age'))
  379. {
  380. $corrected_received_age = max($apparent_age, intval($age));
  381. }
  382. else
  383. {
  384. $corrected_received_age = $apparent_age;
  385. }
  386. // Corrected initial age
  387. $corrected_initial_age = $corrected_received_age + $this->request_execution_time();
  388. // Resident time
  389. $resident_time = time() - $this->_response_time;
  390. // Current age
  391. $current_age = $corrected_initial_age + $resident_time;
  392. // Prepare the cache freshness lifetime
  393. $ttl = NULL;
  394. // Cache control overrides
  395. if ($cache_control = $response->headers('cache-control'))
  396. {
  397. // Parse the cache control header
  398. $cache_control = HTTP_Header::parse_cache_control($cache_control);
  399. if (isset($cache_control['max-age']))
  400. {
  401. $ttl = $cache_control['max-age'];
  402. }
  403. if (isset($cache_control['s-maxage']) AND isset($cache_control['private']) AND $this->_allow_private_cache)
  404. {
  405. $ttl = $cache_control['s-maxage'];
  406. }
  407. if (isset($cache_control['max-stale']) AND ! isset($cache_control['must-revalidate']))
  408. {
  409. $ttl = $current_age + $cache_control['max-stale'];
  410. }
  411. }
  412. // If we have a TTL at this point, return
  413. if ($ttl !== NULL)
  414. return $ttl;
  415. if ($expires = $response->headers('expires'))
  416. return strtotime($expires) - $current_age;
  417. return FALSE;
  418. }
  419. /**
  420. * Returns the duration of the last request execution.
  421. * Either returns the time of completed requests or
  422. * `FALSE` if the request hasn't finished executing, or
  423. * is yet to be run.
  424. *
  425. * @return mixed
  426. */
  427. public function request_execution_time()
  428. {
  429. if ($this->_request_time === NULL OR $this->_response_time === NULL)
  430. return FALSE;
  431. return $this->_response_time - $this->_request_time;
  432. }
  433. } // End KO7_HTTP_Cache