Elgg  Version 4.3
CacheHandler.php
Go to the documentation of this file.
1 <?php
2 
3 namespace Elgg\Application;
4 
7 use Elgg\Config;
11 
17 class CacheHandler {
18 
19  public static $extensions = [
20  'bmp' => 'image/bmp',
21  'css' => 'text/css',
22  'eot' => 'application/vnd.ms-fontobject',
23  'gif' => 'image/gif',
24  'html' => 'text/html',
25  'ico' => 'image/x-icon',
26  'jpeg' => 'image/jpeg',
27  'jpg' => 'image/jpeg',
28  'js' => 'application/javascript',
29  'json' => 'application/json',
30  'map' => 'application/json',
31  'otf' => 'application/font-otf',
32  'png' => 'image/png',
33  'svg' => 'image/svg+xml',
34  'swf' => 'application/x-shockwave-flash',
35  'tiff' => 'image/tiff',
36  'ttf' => 'application/font-ttf',
37  'webp' => 'image/webp',
38  'woff' => 'application/font-woff',
39  'woff2' => 'application/font-woff2',
40  'xml' => 'text/xml',
41  ];
42 
43  public static $utf8_content_types = [
44  'text/css',
45  'text/html',
46  'application/javascript',
47  'application/json',
48  'image/svg+xml',
49  'text/xml',
50  ];
51 
55  protected $config;
56 
60  protected $request;
61 
65  protected $simplecache;
66 
75  $this->config = $config;
76  $this->request = $request;
77  $this->simplecache = $simplecache;
78  }
79 
88  public function handleRequest(Request $request, Application $app) {
89  $parsed = $this->parsePath($request->getElggPath());
90  if (!$parsed) {
91  return $this->send403();
92  }
93 
94  $ts = $parsed['ts'];
95  $view = $parsed['view'];
96  $viewtype = $parsed['viewtype'];
97 
98  $content_type = $this->getContentType($view);
99  if (empty($content_type)) {
100  return $this->send403('Asset must have a valid file extension');
101  }
102 
103  $response = new Response();
104  if (in_array($content_type, self::$utf8_content_types)) {
105  $response->headers->set('Content-Type', "{$content_type};charset=utf-8", true);
106  } else {
107  $response->headers->set('Content-Type', $content_type, true);
108  }
109 
110  $response->headers->set('X-Content-Type-Options', 'nosniff', true);
111 
112  if (!$this->simplecache->isEnabled()) {
113  $app->bootCore();
114  if (!headers_sent()) {
115  header_remove('Cache-Control');
116  header_remove('Pragma');
117  header_remove('Expires');
118  }
119 
120  if (!$this->isCacheableView($view)) {
121  return $this->send403("Requested view ({$view}) is not an asset");
122  }
123 
125  if ($content === false) {
126  return $this->send403();
127  }
128 
129  $etag = '"' . md5($content) . '"';
130  $this->setRevalidateHeaders($etag, $response);
131  if ($this->is304($etag)) {
132  $response = new Response();
133  $response->setNotModified();
134 
135  return $response;
136  }
137 
138  return $response->setContent($content);
139  }
140 
141  $etag = "\"{$ts}\"";
142  if ($this->is304($etag)) {
143  $response = new Response();
144  $response->setNotModified();
145 
146  return $response;
147  }
148 
149  // trust the client but check for an existing cache file
150  $filename = $this->simplecache->getCachedAssetLocation($ts, $viewtype, $view);
151  if (!empty($filename)) {
152  $this->sendCacheHeaders($etag, $response);
153 
154  return new BinaryFileResponse($filename, 200, $response->headers->all());
155  }
156 
157  // the hard way
158  $app->bootCore();
159  header_remove('Cache-Control');
160  header_remove('Pragma');
161  header_remove('Expires');
162 
164  if (!$this->isCacheableView($view)) {
165  return $this->send403('Requested view is not an asset');
166  }
167 
168  if ((int) $this->config->lastcache === $ts) {
169  $this->sendCacheHeaders($etag, $response);
170 
172 
173  // store in simplecache for use later
174  $this->simplecache->cacheAsset($viewtype, $view, $content);
175  } else {
176  // if wrong timestamp, don't send HTTP cache
178  }
179 
180  return $response->setContent($content);
181  }
182 
190  public function parsePath($path) {
191  // no '..'
192  if (false !== strpos($path, '..')) {
193  return [];
194  }
195 
196  // only alphanumeric characters plus /, ., -, and _
197  if (preg_match('#[^a-zA-Z0-9/\.\-_]#', $path)) {
198  return [];
199  }
200 
201  // testing showed regex to be marginally faster than array / string functions over 100000 reps
202  // it won't make a difference in real life and regex is easier to read.
203  // <ts>/<viewtype>/<name/of/view.and.dots>.<type>
204  $matches = [];
205  if (!preg_match('#^/cache/([0-9]+)/([^/]+)/(.+)$#', $path, $matches)) {
206  return [];
207  }
208 
209  return [
210  'ts' => (int) $matches[1],
211  'viewtype' => $matches[2],
212  'view' => $matches[3],
213  ];
214  }
215 
223  protected function isCacheableView($view) {
224  $matches = [];
225  if (preg_match('~^languages/(.*)\.js$~', $view, $matches)) {
226  return in_array($matches[1], _elgg_services()->locale->getLanguageCodes());
227  }
228 
229  return _elgg_services()->views->isCacheableView($view);
230  }
231 
240  protected function sendCacheHeaders($etag, Response $response) {
241  $response->setSharedMaxAge(86400 * 30 * 6);
242  $response->setMaxAge(86400 * 30 * 6);
243  $response->headers->set('ETag', $etag);
244  }
245 
254  protected function setRevalidateHeaders($etag, Response $response) {
255  $response->headers->set('Cache-Control', "public, max-age=0, must-revalidate", true);
256  $response->headers->set('ETag', $etag);
257  }
258 
266  protected function is304($etag) {
267  $if_none_match = $this->request->headers->get('If-None-Match');
268  if ($if_none_match === null) {
269  return false;
270  }
271 
272  // strip -gzip and leading /W
273  $if_none_match = trim($if_none_match);
274  if (0 === strpos($if_none_match, 'W/')) {
275  $if_none_match = substr($if_none_match, 2);
276  }
277  $if_none_match = str_replace('-gzip', '', $if_none_match);
278 
279  return ($if_none_match === $etag);
280  }
281 
289  public function getContentType($view) {
290  $extension = $this->getViewFileType($view);
291 
292  return self::$extensions[$extension] ?? null;
293  }
294 
307  public function getViewFileType($view) {
308  $extension = (new \SplFileInfo($view))->getExtension();
309  if (isset(self::$extensions[$extension])) {
310  return $extension;
311  }
312 
313  $matches = [];
314  if (preg_match('~(?:^|/)(css|js)(?:$|/)~', $view, $matches)) {
315  return $matches[1];
316  }
317 
318  return 'unknown';
319  }
320 
330  protected function getProcessedView($view, $viewtype) {
331  $content = $this->renderView($view, $viewtype);
332  if ($content === false) {
333  return false;
334  }
335 
336  $hook_name = $this->simplecache->isEnabled() ? 'simplecache:generate' : 'cache:generate';
337  $hook_type = $this->getViewFileType($view);
338  $hook_params = [
339  'view' => $view,
340  'viewtype' => $viewtype,
341  'view_content' => $content,
342  ];
343  return _elgg_services()->hooks->trigger($hook_name, $hook_type, $hook_params, $content);
344  }
345 
354  protected function renderView($view, $viewtype) {
356 
357  $matches = [];
358  if ($viewtype === 'default' && preg_match("#^languages/(.*?)\\.js$#", $view, $matches)) {
359  $view = 'languages.js';
360  $vars = ['language' => $matches[1]];
361  } else {
362  $vars = [];
363  }
364 
365  if (!elgg_view_exists($view)) {
366  return false;
367  }
368 
369  // disable error reporting so we don't cache problems
370  $this->config->debug = null;
371 
372  return elgg_view($view, $vars);
373  }
374 
382  protected function send403($msg = 'Cache error: bad request') {
383  return new Response($msg, ELGG_HTTP_FORBIDDEN);
384  }
385 }
elgg_view_exists($view, $viewtype= '', $recurse=true)
Returns whether the specified view exists.
Definition: views.php:152
getElggPath()
Get the Request URI minus querystring.
Definition: Request.php:281
const ELGG_HTTP_FORBIDDEN
Definition: constants.php:82
Elgg HTTP request.
Definition: Request.php:17
parsePath($path)
Parse a request.
send403($msg= 'Cache error:bad request')
Send an error message to requestor.
getViewFileType($view)
Returns the type of output expected from the view.
getContentType($view)
Get the content type.
$path
Definition: details.php:68
Simplecache handler.
bootCore()
Bootstrap the Elgg engine, loads plugins, and calls initial system events.
if(!empty($avatar)&&!$avatar->isValid()) elseif(empty($avatar)) if(!$owner->saveIconFromUploadedFile('avatar')) if(!elgg_trigger_event('profileiconupdate', $owner->type, $owner)) $view
Definition: upload.php:39
__construct(Config $config, Request $request, SimpleCache $simplecache)
Constructor.
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.
$extensions
Load, boot, and implement a front controller for an Elgg application.
Definition: Application.php:48
$ts
CSRF security token view for use with secure forms.
Simple cache service.
Definition: SimpleCache.php:15
$content
Set robots.txt action.
Definition: set_robots.php:6
$filename
_elgg_services()
Get the global service provider.
Definition: elgglib.php:638
sendCacheHeaders($etag, Response $response)
Sets cache headers.
elgg_set_viewtype($viewtype= '')
Manually set the viewtype.
Definition: views.php:63
$vars['head']
Definition: html.php:24
getProcessedView($view, $viewtype)
Get the contents of a view for caching.
elgg_view($view, $vars=[], $viewtype= '')
Return a parsed view.
Definition: views.php:179
handleRequest(Request $request, Application $app)
Handle a request for a cached view.
$extension
Definition: default.php:25