Elgg  Version 2.2
 All Classes Namespaces Files Functions Variables Pages
CacheHandler.php
Go to the documentation of this file.
1 <?php
2 namespace Elgg\Application;
3 
5 use Elgg\Config;
6 
7 
15 class CacheHandler {
16 
17  public static $extensions = [
18  'bmp' => "image/bmp",
19  'css' => "text/css",
20  'gif' => "image/gif",
21  'html' => "text/html",
22  'ico' => "image/x-icon",
23  'jpeg' => "image/jpeg",
24  'jpg' => "image/jpeg",
25  'js' => "application/javascript",
26  'png' => "image/png",
27  'svg' => "image/svg+xml",
28  'swf' => "application/x-shockwave-flash",
29  'tiff' => "image/tiff",
30  'webp' => "image/webp",
31  'xml' => "text/xml",
32  'eot' => "application/vnd.ms-fontobject",
33  'ttf' => "application/font-ttf",
34  'woff' => "application/font-woff",
35  'woff2' => "application/font-woff2",
36  'otf' => "application/font-otf",
37  ];
38 
39  public static $utf8_content_types = [
40  "text/css",
41  "text/html",
42  "application/javascript",
43  "image/svg+xml",
44  "text/xml",
45  ];
46 
48  private $application;
49 
51  private $config;
52 
54  private $server_vars;
55 
63  public function __construct(Application $app, Config $config, $server_vars) {
64  $this->application = $app;
65  $this->config = $config;
66  $this->server_vars = $server_vars;
67  }
68 
75  public function handleRequest($path) {
76  $config = $this->config;
77 
78  $request = $this->parsePath($path);
79  if (!$request) {
80  $this->send403();
81  }
82 
83  $ts = $request['ts'];
84  $view = $request['view'];
85  $viewtype = $request['viewtype'];
86 
87  $content_type = $this->getContentType($view);
88  if (empty($content_type)) {
89  $this->send403("Asset must have a valid file extension");
90  }
91 
92  if (in_array($content_type, self::$utf8_content_types)) {
93  header("Content-Type: $content_type;charset=utf-8");
94  } else {
95  header("Content-Type: $content_type");
96  }
97 
98  // this may/may not have to connect to the DB
99  $this->setupSimplecache();
100 
101  // we can't use $config->get yet. It fails before the core is booted
102  if (!$config->getVolatile('simplecache_enabled')) {
103 
104  $this->application->bootCore();
105 
106  if (!$this->isCacheableView($view)) {
107  $this->send403("Requested view is not an asset");
108  } else {
109  $content = $this->renderView($view, $viewtype);
110  $etag = '"' . md5($content) . '"';
111  $this->sendRevalidateHeaders($etag);
112  $this->handle304($etag);
113 
114  echo $content;
115  }
116  exit;
117  }
118 
119  $etag = "\"$ts\"";
120  $this->handle304($etag);
121 
122  // trust the client but check for an existing cache file
123  $filename = $config->getVolatile('cacheroot') . "views_simplecache/$ts/$viewtype/$view";
124  if (file_exists($filename)) {
125  $this->sendCacheHeaders($etag);
126  readfile($filename);
127  exit;
128  }
129 
130  // the hard way
131  $this->application->bootCore();
132 
133  elgg_set_viewtype($viewtype);
134  if (!$this->isCacheableView($view)) {
135  $this->send403("Requested view is not an asset");
136  }
137 
138  $lastcache = (int)$config->get('lastcache');
139 
140  $filename = $config->getVolatile('cacheroot') . "views_simplecache/$lastcache/$viewtype/$view";
141 
142  if ($lastcache == $ts) {
143  $this->sendCacheHeaders($etag);
144 
145  $content = $this->getProcessedView($view, $viewtype);
146 
147  $dir_name = dirname($filename);
148  if (!is_dir($dir_name)) {
149  mkdir($dir_name, 0700, true);
150  }
151 
152  file_put_contents($filename, $content);
153  } else {
154  // if wrong timestamp, don't send HTTP cache
155  $content = $this->renderView($view, $viewtype);
156  }
157 
158  echo $content;
159  exit;
160  }
161 
168  public function parsePath($path) {
169  // no '..'
170  if (false !== strpos($path, '..')) {
171  return array();
172  }
173  // only alphanumeric characters plus /, ., -, and _
174  if (preg_match('#[^a-zA-Z0-9/\.\-_]#', $path)) {
175  return array();
176  }
177 
178  // testing showed regex to be marginally faster than array / string functions over 100000 reps
179  // it won't make a difference in real life and regex is easier to read.
180  // <ts>/<viewtype>/<name/of/view.and.dots>.<type>
181  if (!preg_match('#^/cache/([0-9]+)/([^/]+)/(.+)$#', $path, $matches)) {
182  return array();
183  }
184 
185  return array(
186  'ts' => $matches[1],
187  'viewtype' => $matches[2],
188  'view' => $matches[3],
189  );
190  }
191 
199  protected function isCacheableView($view) {
200  if (preg_match('~^languages/(.*)\.js$~', $view, $m)) {
201  return in_array($m[1], _elgg_services()->translator->getAllLanguageCodes());
202  }
203  return _elgg_services()->views->isCacheableView($view);
204  }
205 
211  protected function setupSimplecache() {
212  // we can't use Elgg\Config::get yet. It fails before the core is booted
213  $config = $this->config;
214  $config->loadSettingsFile();
215 
216  if ($config->getVolatile('cacheroot') && $config->getVolatile('simplecache_enabled') !== null) {
217  // we can work with these...
218  return;
219  }
220 
221  $db = $this->application->getDb();
222 
223  try {
224  $rows = $db->getData("
225  SELECT `name`, `value`
226  FROM {$db->getTablePrefix()}datalists
227  WHERE `name` IN ('dataroot', 'simplecache_enabled')
228  ");
229  if (!$rows) {
230  $this->send403('Cache error: unable to get the data root');
231  }
232  } catch (\DatabaseException $e) {
233  if (0 === strpos($e->getMessage(), "Elgg couldn't connect")) {
234  $this->send403('Cache error: unable to connect to database server');
235  } else {
236  $this->send403('Cache error: unable to connect to Elgg database');
237  }
238  exit; // unnecessary, but helps PhpStorm understand
239  }
240 
241  foreach ($rows as $row) {
242  $config->set($row->name, $row->value);
243  }
244 
245  if (!$config->getVolatile('cacheroot')) {
246  $dataroot = $config->getVolatile('dataroot');
247  if (!$dataroot) {
248  $this->send403('Cache error: unable to get the cache root');
249  }
250  $config->set('cacheroot', $dataroot);
251  }
252  }
253 
260  protected function sendCacheHeaders($etag) {
261  header('Expires: ' . gmdate('D, d M Y H:i:s \G\M\T', strtotime("+6 months")), true);
262  header("Pragma: public", true);
263  header("Cache-Control: public", true);
264  header("ETag: $etag");
265  }
266 
273  protected function sendRevalidateHeaders($etag) {
274  header_remove('Expires');
275  header("Pragma: public", true);
276  header("Cache-Control: public, max-age=0, must-revalidate", true);
277  header("ETag: $etag");
278  }
279 
286  protected function handle304($etag) {
287  if (!isset($this->server_vars['HTTP_IF_NONE_MATCH'])) {
288  return;
289  }
290 
291  // strip -gzip for #9427
292  $if_none_match = str_replace('-gzip', '', trim($this->server_vars['HTTP_IF_NONE_MATCH']));
293  if ($if_none_match === $etag) {
294  header("HTTP/1.1 304 Not Modified");
295  exit;
296  }
297  }
298 
306  protected function getContentType($view) {
307  $extension = $this->getViewFileType($view);
308 
309  if (isset(self::$extensions[$extension])) {
311  } else {
312  return null;
313  }
314  }
315 
327  private function getViewFileType($view) {
328  $extension = (new \SplFileInfo($view))->getExtension();
329  $hasValidExtension = isset(self::$extensions[$extension]);
330 
331  if ($hasValidExtension) {
332  return $extension;
333  }
334 
335  if (preg_match('~(?:^|/)(css|js)(?:$|/)~', $view, $m)) {
336  return $m[1];
337  }
338 
339  return 'unknown';
340  }
341 
350  protected function getProcessedView($view, $viewtype) {
351  $content = $this->renderView($view, $viewtype);
352 
353  $hook_type = $this->getViewFileType($view);
354  $hook_params = array(
355  'view' => $view,
356  'viewtype' => $viewtype,
357  'view_content' => $content,
358  );
359  return \_elgg_services()->hooks->trigger('simplecache:generate', $hook_type, $hook_params, $content);
360  }
361 
369  protected function renderView($view, $viewtype) {
370  elgg_set_viewtype($viewtype);
371 
372  if ($viewtype === 'default' && preg_match("#^languages/(.*?)\\.js$#", $view, $matches)) {
373  $view = "languages.js";
374  $vars = ['language' => $matches[1]];
375  } else {
376  $vars = [];
377  }
378 
379  if (!elgg_view_exists($view)) {
380  $this->send403();
381  }
382 
383  // disable error reporting so we don't cache problems
384  $this->config->set('debug', null);
385 
386  // @todo elgg_view() checks if the page set is done (isset($GLOBALS['_ELGG']->pagesetupdone)) and
387  // triggers an event if it's not. Calling elgg_view() here breaks submenus
388  // (at least) because the page setup hook is called before any
389  // contexts can be correctly set (since this is called before page_handler()).
390  // To avoid this, lie about $CONFIG->pagehandlerdone to force
391  // the trigger correctly when the first view is actually being output.
392  $GLOBALS['_ELGG']->pagesetupdone = true;
393 
394  return elgg_view($view, $vars);
395  }
396 
403  protected function send403($msg = 'Cache error: bad request') {
404  header('HTTP/1.1 403 Forbidden');
405  echo $msg;
406  exit;
407  }
408 }
409 
elgg_view_exists($view, $viewtype= '', $recurse=true)
Returns whether the specified view exists.
Definition: views.php:299
$view
Definition: crop.php:34
$m
Definition: metadata.php:11
parsePath($path)
Parse a request.
send403($msg= 'Cache error:bad request')
Send an error message to requestor.
sendRevalidateHeaders($etag)
Send revalidate cache headers.
$e
Definition: metadata.php:12
$extensions
sendCacheHeaders($etag)
Send cache headers.
$path
Definition: details.php:88
Access to configuration values.
Definition: Config.php:11
$vars['entity']
getContentType($view)
Get the content type.
__construct(Application $app, Config $config, $server_vars)
Constructor.
elgg_set_viewtype($viewtype="")
Manually set the viewtype.
Definition: views.php:73
renderView($view, $viewtype)
Render a view for caching.
$dataroot
elgg_view($view, $vars=array(), $ignore1=false, $ignore2=false, $viewtype= '')
Return a parsed view.
Definition: views.php:342
handle304($etag)
Send a 304 and exit() if the ETag matches the request.
isCacheableView($view)
Is the view cacheable.
_elgg_services(\Elgg\Di\ServiceProvider $services=null)
Get the global service provider.
Definition: autoloader.php:17
$content
Set robots.txt action.
Definition: set_robots.php:6
handleRequest($path)
Handle a request for a cached view.
$filename
$row
getProcessedView($view, $viewtype)
Get the contents of a view for caching.
$rows
exit
Definition: autoloader.php:34
setupSimplecache()
Do a minimal engine load.
$extension
Definition: default.php:23