Elgg  Version 1.12
CacheHandler.php
Go to the documentation of this file.
1 <?php
2 namespace Elgg;
3 
11 class CacheHandler {
12 
13  protected $config;
14 
20  public function __construct($config) {
21  $this->config = $config;
22  }
23 
31  public function handleRequest($get_vars, $server_vars) {
32  if (empty($get_vars['request'])) {
33  $this->send403();
34  }
35  $request = $this->parseRequestVar($get_vars['request']);
36  if (!$request) {
37  $this->send403();
38  }
39  $ts = $request['ts'];
40  $view = $request['view'];
41  $viewtype = $request['viewtype'];
42 
43  $this->sendContentType($view);
44 
45  // this may/may not have to connect to the DB
46  $this->setupSimplecache();
47 
48  if (!$this->config->simplecache_enabled) {
49  $this->loadEngine();
51  $this->send403();
52  } else {
53  echo $this->renderView($view, $viewtype);
54  }
55  exit;
56  }
57 
58  $etag = "\"$ts\"";
59  // If is the same ETag, content didn't change.
60  if (isset($server_vars['HTTP_IF_NONE_MATCH'])) {
61  // strip -gzip for #9427
62  $if_none_match = str_replace('-gzip', '', trim($server_vars['HTTP_IF_NONE_MATCH']));
63  if ($if_none_match === $etag) {
64  header("HTTP/1.1 304 Not Modified");
65  header("ETag: $etag");
66  exit;
67  }
68  }
69 
70  $filename = $this->config->dataroot . 'views_simplecache/' . md5("$viewtype|$view");
71  if (file_exists($filename)) {
72  $this->sendCacheHeaders($etag);
73  readfile($filename);
74  exit;
75  }
76 
77  $this->loadEngine();
78 
81  $this->send403();
82  }
83 
84  $cache_timestamp = (int)_elgg_services()->config->get('lastcache');
85 
86  if ($cache_timestamp == $ts) {
87  $this->sendCacheHeaders($etag);
88 
89  $content = $this->getProcessedView($view, $viewtype);
90 
91  $dir_name = $this->config->dataroot . 'views_simplecache/';
92  if (!is_dir($dir_name)) {
93  mkdir($dir_name, 0700);
94  }
95 
96  file_put_contents($filename, $content);
97  } else {
98  // if wrong timestamp, don't send HTTP cache
99  $content = $this->renderView($view, $viewtype);
100  }
101 
102  echo $content;
103  exit;
104  }
105 
112  public function parseRequestVar($request_var) {
113  // no '..'
114  if (false !== strpos($request_var, '..')) {
115  return array();
116  }
117  // only alphanumeric characters plus /, ., -, and _
118  if (preg_match('#[^a-zA-Z0-9/\.\-_]#', $request_var)) {
119  return array();
120  }
121 
122  // testing showed regex to be marginally faster than array / string functions over 100000 reps
123  // it won't make a difference in real life and regex is easier to read.
124  // <ts>/<viewtype>/<name/of/view.and.dots>.<type>
125  if (!preg_match('#^/?([0-9]+)/([^/]+)/(.+)$#', $request_var, $matches)) {
126  return array();
127  }
128 
129  return array(
130  'ts' => $matches[1],
131  'viewtype' => $matches[2],
132  'view' => $matches[3],
133  );
134  }
135 
141  protected function setupSimplecache() {
142  if (!empty($this->config->dataroot) && isset($this->config->simplecache_enabled)) {
143  return;
144  }
145 
146  $db_config = new Database\Config($this->config);
147  $db = new Database($db_config, new Logger(new PluginHooksService()));
148 
149  try {
150  $rows = $db->getData("
151  SELECT `name`, `value`
152  FROM {$db->getTablePrefix()}datalists
153  WHERE `name` IN ('dataroot', 'simplecache_enabled')
154  ");
155  if (!$rows) {
156  $this->send403('Cache error: unable to get the data root');
157  }
158  } catch (\DatabaseException $e) {
159  if (0 === strpos($e->getMessage(), "Elgg couldn't connect")) {
160  $this->send403('Cache error: unable to connect to database server');
161  } else {
162  $this->send403('Cache error: unable to connect to Elgg database');
163  }
164  exit; // unnecessary, but helps PhpStorm understand
165  }
166 
167  foreach ($rows as $row) {
168  $this->config->{$row->name} = $row->value;
169  }
170 
171  if (empty($this->config->dataroot)) {
172  $this->send403('Cache error: unable to get the data root');
173  }
174  }
175 
182  protected function sendCacheHeaders($etag) {
183  header('Expires: ' . gmdate('D, d M Y H:i:s \G\M\T', strtotime("+6 months")), true);
184  header("Pragma: public", true);
185  header("Cache-Control: public", true);
186  header("ETag: $etag");
187  }
188 
195  protected function sendContentType($view) {
196  $segments = explode('/', $view, 2);
197  switch ($segments[0]) {
198  case 'css':
199  header("Content-Type: text/css;charset=utf-8");
200  break;
201  case 'js':
202  header('Content-Type: text/javascript;charset=utf-8');
203  break;
204  default:
205  header('Content-Type: text/html;charset=utf-8');
206  }
207  }
208 
217  protected function getProcessedView($view, $viewtype) {
218  $content = $this->renderView($view, $viewtype);
219 
220  $hook_type = _elgg_get_view_filetype($view);
221  $hook_params = array(
222  'view' => $view,
223  'viewtype' => $viewtype,
224  'view_content' => $content,
225  );
226  return _elgg_services()->hooks->trigger('simplecache:generate', $hook_type, $hook_params, $content);
227  }
228 
236  protected function renderView($view, $viewtype) {
238 
239  if (!elgg_view_exists($view)) {
240  $this->send403();
241  }
242 
243  // disable error reporting so we don't cache problems
244  _elgg_services()->config->set('debug', null);
245 
246  // @todo elgg_view() checks if the page set is done (isset($CONFIG->pagesetupdone)) and
247  // triggers an event if it's not. Calling elgg_view() here breaks submenus
248  // (at least) because the page setup hook is called before any
249  // contexts can be correctly set (since this is called before page_handler()).
250  // To avoid this, lie about $CONFIG->pagehandlerdone to force
251  // the trigger correctly when the first view is actually being output.
252  _elgg_services()->config->set('pagesetupdone', true);
253 
254  return elgg_view($view);
255  }
256 
262  protected function loadEngine() {
263  require_once dirname(dirname(dirname(__FILE__))) . "/start.php";
264  }
265 
272  protected function send403($msg = 'Cache error: bad request') {
273  header('HTTP/1.1 403 Forbidden');
274  echo $msg;
275  exit;
276  }
277 }
278 
getProcessedView($view, $viewtype)
Get the contents of a view for caching.
$view
Definition: crop.php:68
handleRequest($get_vars, $server_vars)
Handle a request for a cached view.
$e
Definition: metadata.php:12
elgg_view_exists($view, $viewtype= '', $recurse=true)
Returns whether the specified view exists.
Definition: views.php:304
parseRequestVar($request_var)
Parse a request.
exit
Definition: reorder.php:12
setupSimplecache()
Do a minimal engine load.
_elgg_get_view_filetype($view)
Returns the type of output expected from the view.
Definition: cache.php:148
__construct($config)
Constructor.
Save menu items.
elgg_set_viewtype($viewtype="")
Manually set the viewtype.
Definition: views.php:70
renderView($view, $viewtype)
Render a view for caching.
_elgg_services()
Definition: autoloader.php:14
_elgg_is_view_cacheable($view)
Check whether a view is registered as cacheable.
Definition: views.php:253
sendCacheHeaders($etag)
Send cache headers.
elgg echo
Translates a string.
Definition: languages.js:48
elgg_view($view, $vars=array(), $bypass=false, $ignored=false, $viewtype= '')
Return a parsed view.
Definition: views.php:340
send403($msg= 'Cache error:bad request')
Send an error message to requestor.
$content
Set robots.txt action.
Definition: set_robots.php:6
sendContentType($view)
Send content type.
$filename
Definition: crop.php:23
$row
$rows
clearfix elgg elgg elgg elgg page header
Definition: admin.php:127
$request
$viewtype
Definition: start.php:86
loadEngine()
Load the complete Elgg engine.