PageRenderTime 41ms CodeModel.GetById 15ms RepoModel.GetById 0ms app.codeStats 0ms

/sparkplug/plugs/sparkpagecache/sparkpagecache.php

https://code.google.com/p/sparkplug-framework/
PHP | 397 lines | 276 code | 68 blank | 53 comment | 54 complexity | 8b521ed72f7eac8f5ffdb43365eb75a5 MD5 | raw file
Possible License(s): GPL-3.0, LGPL-2.1
  1. <?php
  2. /*
  3. Copyright 2009-2012 Sam Weiss
  4. All Rights Reserved.
  5. This file is part of Spark/Plug.
  6. Spark/Plug is free software: you can redistribute it and/or modify
  7. it under the terms of the GNU General Public License as published by
  8. the Free Software Foundation, either version 3 of the License, or
  9. (at your option) any later version.
  10. This program is distributed in the hope that it will be useful,
  11. but WITHOUT ANY WARRANTY; without even the implied warranty of
  12. MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  13. GNU General Public License for more details.
  14. You should have received a copy of the GNU General Public License
  15. along with this program. If not, see <http://www.gnu.org/licenses/>.
  16. */
  17. if (!defined('spark/plug'))
  18. {
  19. header('HTTP/1.1 403 Forbidden');
  20. exit('<!DOCTYPE HTML PUBLIC "-//IETF//DTD HTML 2.0//EN"><html><head><title>403 Forbidden</title></head><body><h1>Forbidden</h1><p>You don\'t have permission to access the requested resource on this server.</p></body></html>');
  21. }
  22. // -----------------------------------------------------------------------------
  23. class SparkPageCache extends SparkApplication
  24. {
  25. private $_page_cache;
  26. private $_page_cache_active;
  27. private $_page_cache_static;
  28. private $_page_cache_reject_query_vars;
  29. private $_send_content_type;
  30. private $_send_last_modified;
  31. private $_send_etag;
  32. private $_expires_headers;
  33. private $_hasQueryVars;
  34. private $_cacheable;
  35. private $_ttl;
  36. private $_lmts;
  37. // --------------------------------------------------------------------------
  38. public function __construct($spark, $config = array())
  39. {
  40. parent::__construct($spark, $config);
  41. $cache_params = array_merge($this->config->get('cache', array()), $this->config->get('page_cache', array()));
  42. if (!empty($cache_params['active']) && method_exists($this, 'loadCacher'))
  43. {
  44. try
  45. {
  46. $this->_page_cache = $this->loadCacher($cache_params);
  47. }
  48. catch (Exception $e)
  49. {
  50. error_log('Could not load page cache: ' . $e->getMessage());
  51. }
  52. $this->_page_cache_active = true;
  53. $this->_page_cache_static = !empty($cache_params['static']);
  54. $this->_page_cache_reject_query_vars = isset($cache_params['reject_query_vars']) ? !empty($cache_params['reject_query_vars']) : true;
  55. $this->_send_content_type = !empty($cache_params['send_content_type']);
  56. $this->_send_last_modified = !empty($cache_params['send_last_modified']);
  57. $this->_send_etag = !empty($cache_params['send_etag']);
  58. $this->_expires_headers = @$cache_params['expires_headers'];
  59. }
  60. $this->_hasQueryVars = false;
  61. $this->_cacheable = NULL;
  62. $this->_ttl = NULL;
  63. $this->_lmts = NULL;
  64. // observe exception events so we don't cache error pages
  65. $this->observer->observe(array($this, 'handleException'), 'SparkApplication:run:exception');
  66. // observe cache flush requests
  67. $this->observer->observe(array($this, 'purgeCache'), 'SparkPageCache:request_flush');
  68. // observe cache disable requests
  69. $this->observer->observe(array($this, 'disableCache'), 'SparkPageCache:request_disable');
  70. // observe dispatch events so we can grab the params to check for query variables
  71. if ($this->_page_cache_reject_query_vars)
  72. {
  73. $this->observer->observe(array($this, 'checkQueryVars'), 'SparkApplication:dispatch:before');
  74. }
  75. }
  76. // --------------------------------------------------------------------------
  77. public function setDefaultTTL($ttl)
  78. {
  79. if ($this->_page_cache)
  80. {
  81. $this->_page_cache->setDefaultTimeout($ttl);
  82. }
  83. }
  84. // --------------------------------------------------------------------------
  85. public function getNameSpace()
  86. {
  87. return $this->_page_cache ? $this->_page_cache->getNameSpace() : '';
  88. }
  89. // --------------------------------------------------------------------------
  90. public function setNameSpace($namespace)
  91. {
  92. if ($this->_page_cache)
  93. {
  94. $this->_page_cache->setNameSpace($namespace);
  95. }
  96. }
  97. // --------------------------------------------------------------------------
  98. public function display($output, $contentType = 'text/html', $status = NULL, $headers = NULL)
  99. {
  100. $expDate = false;
  101. if ($this->_page_cache_active && $this->_page_cache && $this->isCacheable($ttl, $lmts))
  102. {
  103. if ($expDate = $this->checkExpires($contentType, $this->_expires_headers, $lifetime))
  104. {
  105. header('Expires: ' . $expDate);
  106. header('Cache-Control: max-age='.$lifetime*3600);
  107. }
  108. $preTag = '';
  109. if ($this->_send_content_type)
  110. {
  111. $preTag .= $contentType;
  112. }
  113. if ($this->_send_last_modified && isset($lmts))
  114. {
  115. $lmts = min($lmts, time());
  116. $preTag .= ';' . intval($lmts);
  117. header('Last-Modified: ' . gmstrftime('%a, %d %b %Y %H:%M:%S GMT', $lmts));
  118. }
  119. if ($this->_send_etag)
  120. {
  121. $etag = '"' . 'sparkplug-' . strlen($output) . '.' . crc32($output) . '"';
  122. $preTag .= ';' . $etag;
  123. header("ETag: $etag");
  124. }
  125. if (!empty($preTag))
  126. {
  127. $preTag .= ':';
  128. }
  129. if ($this->_page_cache_static)
  130. {
  131. $preTag = '';
  132. }
  133. $postTag = ($contentType === 'text/html') ? "\n<!-- served from page cache -->\n" : '';
  134. $this->_page_cache->set($this->generateCacheKey(), $preTag . $output . $postTag, $ttl);
  135. }
  136. if (!$expDate)
  137. {
  138. header('Cache-Control: max-age=0, no-cache, must-revalidate');
  139. }
  140. return parent::display($output, $contentType, $status, $headers);
  141. }
  142. // --------------------------------------------------------------------------
  143. public function setCacheable($cacheable, $ttl = NULL, $lmts = NULL)
  144. {
  145. // allow change from true to false, but not vice-versa
  146. if (!isset($this->_cacheable) || $this->_cacheable)
  147. {
  148. $this->_cacheable = $cacheable;
  149. $this->_ttl = $ttl;
  150. $this->_lmts = $lmts;
  151. }
  152. }
  153. // --------------------------------------------------------------------------
  154. public function disableCache($event = NULL)
  155. {
  156. $this->_page_cache_active = false;
  157. }
  158. // --------------------------------------------------------------------------
  159. public function checkQueryVars($event, $class, $method, $controller, $params)
  160. {
  161. $this->_hasQueryVars = !empty($params['qv']);
  162. }
  163. // --------------------------------------------------------------------------
  164. public function purgeCache($event = NULL)
  165. {
  166. if ($this->_page_cache)
  167. {
  168. $this->_page_cache->clear();
  169. }
  170. }
  171. // --------------------------------------------------------------------------
  172. public function handleException($event, $e)
  173. {
  174. // don't cache error pages as this can present a DOS vulnerability
  175. $this->setCacheable(false);
  176. }
  177. // --------------------------------------------------------------------------
  178. protected function runSafe()
  179. {
  180. // is the page cached?
  181. if ($this->_page_cache_active && $this->_page_cache && ($this->_requestMethod === SparkUtil::kRequestMethod_GET))
  182. {
  183. if (!$this->_page_cache_reject_query_vars || empty($_GET))
  184. {
  185. if ($page = $this->_page_cache->get($this->generateCacheKey()))
  186. {
  187. $this->sendCachedPage($page);
  188. return;
  189. }
  190. }
  191. }
  192. return parent::runSafe();
  193. }
  194. // --------------------------------------------------------------------------
  195. protected function runExit()
  196. {
  197. // do nothing - we don't want parent to send the SparkApplication:run:exit notification
  198. }
  199. // --------------------------------------------------------------------------
  200. private function isCacheable(&$ttl, &$lmts)
  201. {
  202. $ttl = $this->_ttl;
  203. $lmts = $this->_lmts;
  204. return ($this->_cacheable && ($this->_requestMethod === SparkUtil::kRequestMethod_GET) && !$this->_hasQueryVars);
  205. }
  206. // --------------------------------------------------------------------------
  207. private function sendCachedPage($page)
  208. {
  209. $contentType = 'text/html';
  210. if (!$this->_page_cache_static && ($this->_send_content_type || $this->_send_last_modified || $this->_send_etag))
  211. {
  212. if (($splitAt = strpos($page, ':')) !== false)
  213. {
  214. $preTag = substr($page, 0, $splitAt);
  215. $splits = explode(';', $preTag);
  216. $contentType = $splits[0];
  217. // send cache headers and decide if we can return a 304 status
  218. if ($this->_send_last_modified || $this->_send_etag)
  219. {
  220. if (isset($splits[2]))
  221. {
  222. if (is_numeric($splits[2]))
  223. {
  224. $lmts = $splits[2];
  225. $etag = $splits[1];
  226. }
  227. else
  228. {
  229. $lmts = $splits[1];
  230. $etag = $splits[2];
  231. }
  232. }
  233. elseif (isset($splits[1]))
  234. {
  235. if (is_numeric($splits[1]))
  236. {
  237. $lmts = $splits[1];
  238. }
  239. else
  240. {
  241. $etag = $splits[1];
  242. }
  243. }
  244. if (isset($lmts) && $this->_send_last_modified)
  245. {
  246. header('Last-Modified: ' . gmstrftime('%a, %d %b %Y %H:%M:%S GMT', $lmts));
  247. }
  248. if (!empty($etag) && $this->_send_etag)
  249. {
  250. header("ETag: $etag");
  251. }
  252. // can we send a "304 Not Modified"?
  253. //
  254. // prefer eTag because it is a more accurate indicator of page changes than a date
  255. if (!empty($etag) && ($match = isset($_SERVER['HTTP_IF_NONE_MATCH']) ? stripslashes($_SERVER['HTTP_IF_NONE_MATCH']) : ''))
  256. {
  257. if ($match === $etag)
  258. {
  259. while (@ob_end_clean());
  260. header('HTTP/1.1 304 Not Modified');
  261. if ($expDate = $this->checkExpires($contentType, $this->_expires_headers, $lifetime))
  262. {
  263. header('Expires: ' . $expDate);
  264. header('Cache-Control: max-age='.$lifetime*3600);
  265. }
  266. exit;
  267. }
  268. }
  269. elseif (isset($lmts) && ($match = isset($_SERVER['HTTP_IF_MODIFIED_SINCE']) ? stripslashes($_SERVER['HTTP_IF_MODIFIED_SINCE']) : ''))
  270. {
  271. if (@strtotime($match) >= intval($lmts))
  272. {
  273. while (@ob_end_clean());
  274. header('HTTP/1.1 304 Not Modified');
  275. if ($expDate = $this->checkExpires($contentType, $this->_expires_headers, $lifetime))
  276. {
  277. header('Expires: ' . $expDate);
  278. header('Cache-Control: max-age='.$lifetime*3600);
  279. }
  280. exit;
  281. }
  282. }
  283. }
  284. $page = substr($page, $splitAt + 1);
  285. }
  286. }
  287. if ($expDate = $this->checkExpires($contentType, $this->_expires_headers, $lifetime))
  288. {
  289. header('Expires: ' . $expDate);
  290. header('Cache-Control: max-age='.$lifetime*3600);
  291. }
  292. else
  293. {
  294. header('Cache-Control: max-age=0, no-cache, must-revalidate');
  295. }
  296. parent::display($page, $contentType);
  297. }
  298. // --------------------------------------------------------------------------
  299. private function checkExpires($contentType, $expiresList, &$lifetime)
  300. {
  301. if (!empty($expiresList))
  302. {
  303. foreach ($expiresList as $type => $ttl)
  304. {
  305. $type = '#^' . str_replace('*', '.*', $type) . '$#';
  306. if (preg_match($type, $contentType))
  307. {
  308. $lifetime = $ttl;
  309. return gmdate('D, d M Y H:i:s', strtotime("+ {$ttl} hours")) . ' GMT';
  310. }
  311. }
  312. }
  313. return false;
  314. }
  315. // --------------------------------------------------------------------------
  316. private function generateCacheKey($key = NULL)
  317. {
  318. // Whether or not the application suppresses caching of pages with query parameters,
  319. // we always strip them if present to avoid denial-of-service vulnerability.
  320. // This means that all pages with the same base url will share the same cached page,
  321. // regardless of query parameters.
  322. return rtrim(!empty($key) ? $key : preg_replace('/\?.*$/', '', SparkUtil::request_uri()), '/');
  323. }
  324. // --------------------------------------------------------------------------
  325. }