Elgg  Version 3.0
CacheHandler.php
Go to the documentation of this file.
1 <?php
2 
3 namespace Elgg\Application;
4 
6 use Elgg\Config;
11 
17 class CacheHandler {
18 
19  public static $extensions = [
20  'bmp' => "image/bmp",
21  'css' => "text/css",
22  'gif' => "image/gif",
23  'html' => "text/html",
24  'ico' => "image/x-icon",
25  'jpeg' => "image/jpeg",
26  'jpg' => "image/jpeg",
27  'js' => "application/javascript",
28  'json' => "application/json",
29  'png' => "image/png",
30  'svg' => "image/svg+xml",
31  'swf' => "application/x-shockwave-flash",
32  'tiff' => "image/tiff",
33  'webp' => "image/webp",
34  'xml' => "text/xml",
35  'eot' => "application/vnd.ms-fontobject",
36  'ttf' => "application/font-ttf",
37  'woff' => "application/font-woff",
38  'woff2' => "application/font-woff2",
39  'otf' => "application/font-otf",
40  ];
41 
42  public static $utf8_content_types = [
43  "text/css",
44  "text/html",
45  "application/javascript",
46  "application/json",
47  "image/svg+xml",
48  "text/xml",
49  ];
50 
52  private $config;
53 
55  private $request;
56 
59 
67  public function __construct(Config $config, Request $request, $simplecache_enabled) {
68  $this->config = $config;
69  $this->request = $request;
71  }
72 
80  public function handleRequest(Request $request, Application $app) {
81  $config = $this->config;
82 
83  $parsed = $this->parsePath($request->getElggPath());
84  if (!$parsed) {
85  return $this->send403();
86  }
87 
88  $ts = $parsed['ts'];
89  $view = $parsed['view'];
90  $viewtype = $parsed['viewtype'];
91 
92  $content_type = $this->getContentType($view);
93  if (empty($content_type)) {
94  return $this->send403("Asset must have a valid file extension");
95  }
96 
97  $response = Response::create();
98  if (in_array($content_type, self::$utf8_content_types)) {
99  $response->headers->set('Content-Type', "$content_type;charset=utf-8", true);
100  } else {
101  $response->headers->set('Content-Type', $content_type, true);
102  }
103 
104  if (!$this->simplecache_enabled) {
105  $app->bootCore();
106  if (!headers_sent()) {
107  header_remove('Cache-Control');
108  header_remove('Pragma');
109  header_remove('Expires');
110  }
111 
112  if (!$this->isCacheableView($view)) {
113  return $this->send403("Requested view ({$view}) is not an asset");
114  }
115 
117  if ($content === false) {
118  return $this->send403();
119  }
120 
121  $etag = '"' . md5($content) . '"';
122  $this->setRevalidateHeaders($etag, $response);
123  if ($this->is304($etag)) {
124  return Response::create()->setNotModified();
125  }
126 
127  return $response->setContent($content);
128  }
129 
130  $etag = "\"$ts\"";
131  if ($this->is304($etag)) {
132  return Response::create()->setNotModified();
133  }
134 
135  // trust the client but check for an existing cache file
136  $filename = $config->assetroot . "$ts/$viewtype/$view";
137  if (file_exists($filename)) {
138  $this->sendCacheHeaders($etag, $response);
139  return BinaryFileResponse::create($filename, 200, $response->headers->all());
140  }
141 
142  // the hard way
143  $app->bootCore();
144  header_remove('Cache-Control');
145  header_remove('Pragma');
146  header_remove('Expires');
147 
149  if (!$this->isCacheableView($view)) {
150  return $this->send403("Requested view is not an asset");
151  }
152 
153  $lastcache = (int) $config->lastcache;
154 
155  $filename = $config->assetroot . "$lastcache/$viewtype/$view";
156 
157  if ($lastcache == $ts) {
158  $this->sendCacheHeaders($etag, $response);
159 
161 
162  $dir_name = dirname($filename);
163  if (!is_dir($dir_name)) {
164  // PHP and the server accessing the cache symlink may be a different user. And here
165  // it's safe to make everything readable anyway.
166  mkdir($dir_name, 0775, true);
167  }
168 
169  file_put_contents($filename, $content);
170  chmod($filename, 0664);
171  } else {
172  // if wrong timestamp, don't send HTTP cache
174  }
175 
176  return $response->setContent($content);
177  }
178 
185  public function parsePath($path) {
186  // no '..'
187  if (false !== strpos($path, '..')) {
188  return [];
189  }
190  // only alphanumeric characters plus /, ., -, and _
191  if (preg_match('#[^a-zA-Z0-9/\.\-_]#', $path)) {
192  return [];
193  }
194 
195  // testing showed regex to be marginally faster than array / string functions over 100000 reps
196  // it won't make a difference in real life and regex is easier to read.
197  // <ts>/<viewtype>/<name/of/view.and.dots>.<type>
198  if (!preg_match('#^/cache/([0-9]+)/([^/]+)/(.+)$#', $path, $matches)) {
199  return [];
200  }
201 
202  return [
203  'ts' => $matches[1],
204  'viewtype' => $matches[2],
205  'view' => $matches[3],
206  ];
207  }
208 
216  protected function isCacheableView($view) {
217  if (preg_match('~^languages/(.*)\.js$~', $view, $m)) {
218  return in_array($m[1], _elgg_services()->localeService->getLanguageCodes());
219  }
220  return _elgg_services()->views->isCacheableView($view);
221  }
222 
231  protected function sendCacheHeaders($etag, Response $response) {
232  $response->setSharedMaxAge(86400 * 30 * 6);
233  $response->setMaxAge(86400 * 30 * 6);
234  $response->headers->set('ETag', $etag);
235  }
236 
245  protected function setRevalidateHeaders($etag, Response $response) {
246  $response->headers->set('Cache-Control', "public, max-age=0, must-revalidate", true);
247  $response->headers->set('ETag', $etag);
248  }
249 
256  protected function is304($etag) {
257  $if_none_match = $this->request->headers->get('If-None-Match');
258  if ($if_none_match === null) {
259  return false;
260  }
261 
262  // strip -gzip and leading /W
263  $if_none_match = trim($if_none_match);
264  if (0 === strpos($if_none_match, 'W/')) {
265  $if_none_match = substr($if_none_match, 2);
266  }
267  $if_none_match = str_replace('-gzip', '', $if_none_match);
268 
269  return ($if_none_match === $etag);
270  }
271 
279  public function getContentType($view) {
280  $extension = $this->getViewFileType($view);
281 
282  if (isset(self::$extensions[$extension])) {
284  } else {
285  return null;
286  }
287  }
288 
300  public function getViewFileType($view) {
301  $extension = (new \SplFileInfo($view))->getExtension();
302  $hasValidExtension = isset(self::$extensions[$extension]);
303 
304  if ($hasValidExtension) {
305  return $extension;
306  }
307 
308  if (preg_match('~(?:^|/)(css|js)(?:$|/)~', $view, $m)) {
309  return $m[1];
310  }
311 
312  return 'unknown';
313  }
314 
323  protected function getProcessedView($view, $viewtype) {
324  $content = $this->renderView($view, $viewtype);
325  if ($content === false) {
326  return false;
327  }
328 
329  if ($this->simplecache_enabled) {
330  $hook_name = 'simplecache:generate';
331  } else {
332  $hook_name = 'cache:generate';
333  }
334  $hook_type = $this->getViewFileType($view);
335  $hook_params = [
336  'view' => $view,
337  'viewtype' => $viewtype,
338  'view_content' => $content,
339  ];
340  return \_elgg_services()->hooks->trigger($hook_name, $hook_type, $hook_params, $content);
341  }
342 
350  protected function renderView($view, $viewtype) {
352 
353  if ($viewtype === 'default' && preg_match("#^languages/(.*?)\\.js$#", $view, $matches)) {
354  $view = "languages.js";
355  $vars = ['language' => $matches[1]];
356  } else {
357  $vars = [];
358  }
359 
360  if (!elgg_view_exists($view)) {
361  return false;
362  }
363 
364  // disable error reporting so we don't cache problems
365  $this->config->debug = null;
366 
367  return elgg_view($view, $vars);
368  }
369 
376  protected function send403($msg = 'Cache error: bad request') {
377  return Response::create($msg, 403);
378  }
379 }
elgg_view_exists($view, $viewtype= '', $recurse=true)
Returns whether the specified view exists.
Definition: views.php:205
getElggPath()
Get the Request URI minus querystring.
Definition: Request.php:252
Elgg HTTP request.
Definition: Request.php:17
$CONFIG simplecache_enabled
Is simplecache enabled?
Definition: config.php:89
parsePath($path)
Parse a request.
send403($msg= 'Cache error:bad request')
Send an error message to requestor.
$extensions
if(!$enabled) if(PHP_SAPI!== 'cli')
Interates through each element of an array and calls callback a function.
$path
Definition: details.php:89
getViewFileType($view)
Returns the type of output expected from the view.
getContentType($view)
Get the content type.
Simplecache handler.
if(!$owner||!$owner->canEdit()) if(!$owner->hasIcon('master')) if(!$owner->saveIconFromElggFile($owner->getIcon('master'), 'icon', $coords)) $view
Definition: crop.php:30
bootCore()
Bootstrap the Elgg engine, loads plugins, and calls initial system events.
is304($etag)
Send a 304 and exit() if the ETag matches the request.
renderView($view, $viewtype)
Render a view for caching.
setRevalidateHeaders($etag, Response $response)
Set revalidate cache headers.
$viewtype
Definition: default.php:11
isCacheableView($view)
Is the view cacheable.
Load, boot, and implement a front controller for an Elgg application.
Definition: Application.php:50
$content
Set robots.txt action.
Definition: set_robots.php:6
$vars['type']
Definition: save.php:11
$filename
_elgg_services()
Get the global service provider.
Definition: elgglib.php:1292
sendCacheHeaders($etag, Response $response)
Sets cache headers.
elgg_set_viewtype($viewtype= '')
Manually set the viewtype.
Definition: views.php:65
getProcessedView($view, $viewtype)
Get the contents of a view for caching.
__construct(Config $config, Request $request, $simplecache_enabled)
Constructor.
elgg_view($view, $vars=[], $viewtype= '')
Return a parsed view.
Definition: views.php:246
handleRequest(Request $request, Application $app)
Handle a request for a cached view.
$extension
Definition: default.php:28