CacheHandler.php 6.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279
  1. <?php
  2. namespace Elgg;
  3. /**
  4. * Simplecache handler
  5. *
  6. * @access private
  7. *
  8. * @package Elgg.Core
  9. */
  10. class CacheHandler {
  11. protected $config;
  12. /**
  13. * Constructor
  14. *
  15. * @param \stdClass $config Elgg config object
  16. */
  17. public function __construct($config) {
  18. $this->config = $config;
  19. }
  20. /**
  21. * Handle a request for a cached view
  22. *
  23. * @param array $get_vars $_GET variables
  24. * @param array $server_vars $_SERVER variables
  25. * @return void
  26. */
  27. public function handleRequest($get_vars, $server_vars) {
  28. if (empty($get_vars['request'])) {
  29. $this->send403();
  30. }
  31. $request = $this->parseRequestVar($get_vars['request']);
  32. if (!$request) {
  33. $this->send403();
  34. }
  35. $ts = $request['ts'];
  36. $view = $request['view'];
  37. $viewtype = $request['viewtype'];
  38. $this->sendContentType($view);
  39. // this may/may not have to connect to the DB
  40. $this->setupSimplecache();
  41. if (!$this->config->simplecache_enabled) {
  42. $this->loadEngine();
  43. if (!_elgg_is_view_cacheable($view)) {
  44. $this->send403();
  45. } else {
  46. echo $this->renderView($view, $viewtype);
  47. }
  48. exit;
  49. }
  50. $etag = "\"$ts\"";
  51. // If is the same ETag, content didn't change.
  52. if (isset($server_vars['HTTP_IF_NONE_MATCH'])) {
  53. // strip -gzip for #9427
  54. $if_none_match = str_replace('-gzip', '', trim($server_vars['HTTP_IF_NONE_MATCH']));
  55. if ($if_none_match === $etag) {
  56. header("HTTP/1.1 304 Not Modified");
  57. header("ETag: $etag");
  58. exit;
  59. }
  60. }
  61. $filename = $this->config->dataroot . 'views_simplecache/' . md5("$viewtype|$view");
  62. if (file_exists($filename)) {
  63. $this->sendCacheHeaders($etag);
  64. readfile($filename);
  65. exit;
  66. }
  67. $this->loadEngine();
  68. elgg_set_viewtype($viewtype);
  69. if (!_elgg_is_view_cacheable($view)) {
  70. $this->send403();
  71. }
  72. $cache_timestamp = (int)_elgg_services()->config->get('lastcache');
  73. if ($cache_timestamp == $ts) {
  74. $this->sendCacheHeaders($etag);
  75. $content = $this->getProcessedView($view, $viewtype);
  76. $dir_name = $this->config->dataroot . 'views_simplecache/';
  77. if (!is_dir($dir_name)) {
  78. mkdir($dir_name, 0700);
  79. }
  80. file_put_contents($filename, $content);
  81. } else {
  82. // if wrong timestamp, don't send HTTP cache
  83. $content = $this->renderView($view, $viewtype);
  84. }
  85. echo $content;
  86. exit;
  87. }
  88. /**
  89. * Parse a request
  90. *
  91. * @param string $request_var Request URL
  92. * @return array Cache parameters (empty array if failure)
  93. */
  94. public function parseRequestVar($request_var) {
  95. // no '..'
  96. if (false !== strpos($request_var, '..')) {
  97. return array();
  98. }
  99. // only alphanumeric characters plus /, ., -, and _
  100. if (preg_match('#[^a-zA-Z0-9/\.\-_]#', $request_var)) {
  101. return array();
  102. }
  103. // testing showed regex to be marginally faster than array / string functions over 100000 reps
  104. // it won't make a difference in real life and regex is easier to read.
  105. // <ts>/<viewtype>/<name/of/view.and.dots>.<type>
  106. if (!preg_match('#^/?([0-9]+)/([^/]+)/(.+)$#', $request_var, $matches)) {
  107. return array();
  108. }
  109. return array(
  110. 'ts' => $matches[1],
  111. 'viewtype' => $matches[2],
  112. 'view' => $matches[3],
  113. );
  114. }
  115. /**
  116. * Do a minimal engine load
  117. *
  118. * @return void
  119. */
  120. protected function setupSimplecache() {
  121. if (!empty($this->config->dataroot) && isset($this->config->simplecache_enabled)) {
  122. return;
  123. }
  124. $db_config = new Database\Config($this->config);
  125. $db = new Database($db_config, new Logger(new PluginHooksService()));
  126. try {
  127. $rows = $db->getData("
  128. SELECT `name`, `value`
  129. FROM {$db->getTablePrefix()}datalists
  130. WHERE `name` IN ('dataroot', 'simplecache_enabled')
  131. ");
  132. if (!$rows) {
  133. $this->send403('Cache error: unable to get the data root');
  134. }
  135. } catch (\DatabaseException $e) {
  136. if (0 === strpos($e->getMessage(), "Elgg couldn't connect")) {
  137. $this->send403('Cache error: unable to connect to database server');
  138. } else {
  139. $this->send403('Cache error: unable to connect to Elgg database');
  140. }
  141. exit; // unnecessary, but helps PhpStorm understand
  142. }
  143. foreach ($rows as $row) {
  144. $this->config->{$row->name} = $row->value;
  145. }
  146. if (empty($this->config->dataroot)) {
  147. $this->send403('Cache error: unable to get the data root');
  148. }
  149. }
  150. /**
  151. * Send cache headers
  152. *
  153. * @param string $etag ETag value
  154. * @return void
  155. */
  156. protected function sendCacheHeaders($etag) {
  157. header('Expires: ' . gmdate('D, d M Y H:i:s \G\M\T', strtotime("+6 months")), true);
  158. header("Pragma: public", true);
  159. header("Cache-Control: public", true);
  160. header("ETag: $etag");
  161. }
  162. /**
  163. * Send content type
  164. *
  165. * @param string $view The view name
  166. * @return void
  167. */
  168. protected function sendContentType($view) {
  169. $segments = explode('/', $view, 2);
  170. switch ($segments[0]) {
  171. case 'css':
  172. header("Content-Type: text/css;charset=utf-8");
  173. break;
  174. case 'js':
  175. header('Content-Type: text/javascript;charset=utf-8');
  176. break;
  177. default:
  178. header('Content-Type: text/html;charset=utf-8');
  179. }
  180. }
  181. /**
  182. * Get the contents of a view for caching
  183. *
  184. * @param string $view The view name
  185. * @param string $viewtype The viewtype
  186. * @return string
  187. * @see CacheHandler::renderView()
  188. */
  189. protected function getProcessedView($view, $viewtype) {
  190. $content = $this->renderView($view, $viewtype);
  191. $hook_type = _elgg_get_view_filetype($view);
  192. $hook_params = array(
  193. 'view' => $view,
  194. 'viewtype' => $viewtype,
  195. 'view_content' => $content,
  196. );
  197. return _elgg_services()->hooks->trigger('simplecache:generate', $hook_type, $hook_params, $content);
  198. }
  199. /**
  200. * Render a view for caching
  201. *
  202. * @param string $view The view name
  203. * @param string $viewtype The viewtype
  204. * @return string
  205. */
  206. protected function renderView($view, $viewtype) {
  207. elgg_set_viewtype($viewtype);
  208. if (!elgg_view_exists($view)) {
  209. $this->send403();
  210. }
  211. // disable error reporting so we don't cache problems
  212. _elgg_services()->config->set('debug', null);
  213. // @todo elgg_view() checks if the page set is done (isset($CONFIG->pagesetupdone)) and
  214. // triggers an event if it's not. Calling elgg_view() here breaks submenus
  215. // (at least) because the page setup hook is called before any
  216. // contexts can be correctly set (since this is called before page_handler()).
  217. // To avoid this, lie about $CONFIG->pagehandlerdone to force
  218. // the trigger correctly when the first view is actually being output.
  219. _elgg_services()->config->set('pagesetupdone', true);
  220. return elgg_view($view);
  221. }
  222. /**
  223. * Load the complete Elgg engine
  224. *
  225. * @return void
  226. */
  227. protected function loadEngine() {
  228. require_once dirname(dirname(dirname(__FILE__))) . "/start.php";
  229. }
  230. /**
  231. * Send an error message to requestor
  232. *
  233. * @param string $msg Optional message text
  234. * @return void
  235. */
  236. protected function send403($msg = 'Cache error: bad request') {
  237. header('HTTP/1.1 403 Forbidden');
  238. echo $msg;
  239. exit;
  240. }
  241. }