Elgg  Version 2.3
CacheHandler.php
Go to the documentation of this file.
1 <?php
2 namespace Elgg\Application;
3 
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  // PHP and the server accessing the cache symlink may be a different user. And here
150  // it's safe to make everything readable anyway.
151  mkdir($dir_name, 0775, true);
152  }
153 
154  file_put_contents($filename, $content);
155  chmod($filename, 0664);
156  } else {
157  // if wrong timestamp, don't send HTTP cache
158  $content = $this->renderView($view, $viewtype);
159  }
160 
161  echo $content;
162  exit;
163  }
164 
171  public function parsePath($path) {
172  // no '..'
173  if (false !== strpos($path, '..')) {
174  return array();
175  }
176  // only alphanumeric characters plus /, ., -, and _
177  if (preg_match('#[^a-zA-Z0-9/\.\-_]#', $path)) {
178  return array();
179  }
180 
181  // testing showed regex to be marginally faster than array / string functions over 100000 reps
182  // it won't make a difference in real life and regex is easier to read.
183  // <ts>/<viewtype>/<name/of/view.and.dots>.<type>
184  if (!preg_match('#^/cache/([0-9]+)/([^/]+)/(.+)$#', $path, $matches)) {
185  return array();
186  }
187 
188  return array(
189  'ts' => $matches[1],
190  'viewtype' => $matches[2],
191  'view' => $matches[3],
192  );
193  }
194 
202  protected function isCacheableView($view) {
203  if (preg_match('~^languages/(.*)\.js$~', $view, $m)) {
204  return in_array($m[1], _elgg_services()->translator->getAllLanguageCodes());
205  }
206  return _elgg_services()->views->isCacheableView($view);
207  }
208 
214  protected function setupSimplecache() {
215  // we can't use Elgg\Config::get yet. It fails before the core is booted
216  $config = $this->config;
217  $config->loadSettingsFile();
218 
219  if ($config->getVolatile('cacheroot') && $config->getVolatile('simplecache_enabled') !== null) {
220  // we can work with these...
221  return;
222  }
223 
224  $db = $this->application->getDb();
225 
226  try {
227  $rows = $db->getData("
228  SELECT `name`, `value`
229  FROM {$db->prefix}datalists
230  WHERE `name` IN ('dataroot', 'simplecache_enabled')
231  ");
232  if (!$rows) {
233  $this->send403('Cache error: unable to get the data root');
234  }
235  } catch (\DatabaseException $e) {
236  if (0 === strpos($e->getMessage(), "Elgg couldn't connect")) {
237  $this->send403('Cache error: unable to connect to database server');
238  } else {
239  $this->send403('Cache error: unable to connect to Elgg database');
240  }
241  exit; // unnecessary, but helps PhpStorm understand
242  }
243 
244  foreach ($rows as $row) {
245  $config->set($row->name, $row->value);
246  }
247 
248  if (!$config->getVolatile('cacheroot')) {
249  $dataroot = $config->getVolatile('dataroot');
250  if (!$dataroot) {
251  $this->send403('Cache error: unable to get the cache root');
252  }
253  $config->set('cacheroot', $dataroot);
254  }
255  }
256 
263  protected function sendCacheHeaders($etag) {
264  header('Expires: ' . gmdate('D, d M Y H:i:s \G\M\T', strtotime("+6 months")), true);
265  header("Pragma: public", true);
266  header("Cache-Control: public", true);
267  header("ETag: $etag");
268  }
269 
276  protected function sendRevalidateHeaders($etag) {
277  header_remove('Expires');
278  header("Pragma: public", true);
279  header("Cache-Control: public, max-age=0, must-revalidate", true);
280  header("ETag: $etag");
281  }
282 
289  protected function handle304($etag) {
290  if (!isset($this->server_vars['HTTP_IF_NONE_MATCH'])) {
291  return;
292  }
293 
294  // strip -gzip for #9427
295  $if_none_match = str_replace('-gzip', '', trim($this->server_vars['HTTP_IF_NONE_MATCH']));
296  if ($if_none_match === $etag) {
297  header("HTTP/1.1 304 Not Modified");
298  exit;
299  }
300  }
301 
309  protected function getContentType($view) {
310  $extension = $this->getViewFileType($view);
311 
312  if (isset(self::$extensions[$extension])) {
314  } else {
315  return null;
316  }
317  }
318 
330  private function getViewFileType($view) {
331  $extension = (new \SplFileInfo($view))->getExtension();
332  $hasValidExtension = isset(self::$extensions[$extension]);
333 
334  if ($hasValidExtension) {
335  return $extension;
336  }
337 
338  if (preg_match('~(?:^|/)(css|js)(?:$|/)~', $view, $m)) {
339  return $m[1];
340  }
341 
342  return 'unknown';
343  }
344 
353  protected function getProcessedView($view, $viewtype) {
354  $content = $this->renderView($view, $viewtype);
355 
356  $hook_type = $this->getViewFileType($view);
357  $hook_params = array(
358  'view' => $view,
359  'viewtype' => $viewtype,
360  'view_content' => $content,
361  );
362  return \_elgg_services()->hooks->trigger('simplecache:generate', $hook_type, $hook_params, $content);
363  }
364 
372  protected function renderView($view, $viewtype) {
373  elgg_set_viewtype($viewtype);
374 
375  if ($viewtype === 'default' && preg_match("#^languages/(.*?)\\.js$#", $view, $matches)) {
376  $view = "languages.js";
377  $vars = ['language' => $matches[1]];
378  } else {
379  $vars = [];
380  }
381 
382  if (!elgg_view_exists($view)) {
383  $this->send403();
384  }
385 
386  // disable error reporting so we don't cache problems
387  $this->config->set('debug', null);
388 
389  // @todo elgg_view() checks if the page set is done (isset($GLOBALS['_ELGG']->pagesetupdone)) and
390  // triggers an event if it's not. Calling elgg_view() here breaks submenus
391  // (at least) because the page setup hook is called before any
392  // contexts can be correctly set (since this is called before page_handler()).
393  // To avoid this, lie about $CONFIG->pagehandlerdone to force
394  // the trigger correctly when the first view is actually being output.
395  $GLOBALS['_ELGG']->pagesetupdone = true;
396 
397  return elgg_view($view, $vars);
398  }
399 
406  protected function send403($msg = 'Cache error: bad request') {
407  header('HTTP/1.1 403 Forbidden');
408  echo $msg;
409  exit;
410  }
411 }
412 
elgg_view_exists($view, $viewtype= '', $recurse=true)
Returns whether the specified view exists.
Definition: views.php:293
$extensions
Definition: summary.php:41
$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
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:74
elgg echo
Translates a string.
Definition: languages.js:48
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:336
clearfix elgg elgg elgg elgg page header
Definition: admin.css.php:127
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
http free of to any person obtaining a copy of this software and associated documentation to deal in the Software without including without limitation the rights to use
Definition: MIT-LICENSE.txt:5
setupSimplecache()
Do a minimal engine load.
$extension
Definition: default.php:23