Elgg  Version 5.1
CacheHandler.php
Go to the documentation of this file.
1 <?php
2 
3 namespace Elgg\Application;
4 
7 use Elgg\Config;
12 
18 class CacheHandler {
19 
20  public static $extensions = [
21  'bmp' => 'image/bmp',
22  'css' => 'text/css',
23  'eot' => 'application/vnd.ms-fontobject',
24  'gif' => 'image/gif',
25  'html' => 'text/html',
26  'ico' => 'image/x-icon',
27  'jpeg' => 'image/jpeg',
28  'jpg' => 'image/jpeg',
29  'js' => 'application/javascript',
30  'json' => 'application/json',
31  'map' => 'application/json',
32  'otf' => 'application/font-otf',
33  'png' => 'image/png',
34  'svg' => 'image/svg+xml',
35  'swf' => 'application/x-shockwave-flash',
36  'tiff' => 'image/tiff',
37  'ttf' => 'application/font-ttf',
38  'webp' => 'image/webp',
39  'woff' => 'application/font-woff',
40  'woff2' => 'application/font-woff2',
41  'xml' => 'text/xml',
42  ];
43 
44  public static $utf8_content_types = [
45  'text/css',
46  'text/html',
47  'application/javascript',
48  'application/json',
49  'image/svg+xml',
50  'text/xml',
51  ];
52 
56  protected $config;
57 
61  protected $request;
62 
66  protected $simplecache;
67 
72 
82  $this->config = $config;
83  $this->request = $request;
84  $this->simplecache = $simplecache;
85 
86  $this->simplecache_enabled = $config->simplecache_enabled;
87  if (!$this->config->hasInitialValue('simplecache_enabled')) {
88  $db_value = $config_table->get('simplecache_enabled');
89  if (isset($db_value)) {
90  $this->simplecache_enabled = (bool) $db_value;
91  }
92  }
93  }
94 
103  public function handleRequest(Request $request, Application $app) {
104  $parsed = $this->parsePath($request->getElggPath());
105  if (!$parsed) {
106  return $this->send403();
107  }
108 
109  $ts = $parsed['ts'];
110  $view = $parsed['view'];
111  $viewtype = $parsed['viewtype'];
112 
113  $content_type = $this->getContentType($view);
114  if (empty($content_type)) {
115  return $this->send403('Asset must have a valid file extension');
116  }
117 
118  $response = new Response();
119  if (in_array($content_type, self::$utf8_content_types)) {
120  $response->headers->set('Content-Type', "{$content_type};charset=utf-8", true);
121  } else {
122  $response->headers->set('Content-Type', $content_type, true);
123  }
124 
125  $response->headers->set('X-Content-Type-Options', 'nosniff', true);
126 
127  if (!$this->simplecache_enabled) {
128  $app->bootCore();
129  if (!headers_sent()) {
130  header_remove('Cache-Control');
131  header_remove('Pragma');
132  header_remove('Expires');
133  }
134 
135  if (!$this->isCacheableView($view)) {
136  return $this->send403("Requested view ({$view}) is not an asset");
137  }
138 
140  if ($content === false) {
141  return $this->send403();
142  }
143 
144  $etag = '"' . md5($content) . '"';
145  $this->setRevalidateHeaders($etag, $response);
146  if ($this->is304($etag)) {
147  $response = new Response();
148  $response->setNotModified();
149 
150  return $response;
151  }
152 
153  return $response->setContent($content);
154  }
155 
156  $etag = "\"{$ts}\"";
157  if ($this->is304($etag)) {
158  $response = new Response();
159  $response->setNotModified();
160 
161  return $response;
162  }
163 
164  // trust the client but check for an existing cache file
165  $filename = $this->simplecache->getCachedAssetLocation($ts, $viewtype, $view);
166  if (!empty($filename)) {
167  $this->sendCacheHeaders($etag, $response);
168 
169  return new BinaryFileResponse($filename, 200, $response->headers->all());
170  }
171 
172  // the hard way
173  $app->bootCore();
174  header_remove('Cache-Control');
175  header_remove('Pragma');
176  header_remove('Expires');
177 
179  if (!$this->isCacheableView($view)) {
180  return $this->send403('Requested view is not an asset');
181  }
182 
183  if ((int) $this->config->lastcache === $ts) {
184  $this->sendCacheHeaders($etag, $response);
185 
187 
188  // store in simplecache for use later
189  $this->simplecache->cacheAsset($viewtype, $view, $content);
190  } else {
191  // if wrong timestamp, don't send HTTP cache
192  // also report that the resource has gone away
193  $response->setStatusCode(ELGG_HTTP_GONE);
194 
196  }
197 
198  return $response->setContent($content);
199  }
200 
208  public function parsePath($path) {
209  // no '..'
210  if (str_contains($path, '..')) {
211  return [];
212  }
213 
214  // only alphanumeric characters plus /, ., -, and _
215  if (preg_match('#[^a-zA-Z0-9/\.\-_]#', $path)) {
216  return [];
217  }
218 
219  // testing showed regex to be marginally faster than array / string functions over 100000 reps
220  // it won't make a difference in real life and regex is easier to read.
221  // <ts>/<viewtype>/<name/of/view.and.dots>.<type>
222  $matches = [];
223  if (!preg_match('#^/cache/([0-9]+)/([^/]+)/(.+)$#', $path, $matches)) {
224  return [];
225  }
226 
227  return [
228  'ts' => (int) $matches[1],
229  'viewtype' => $matches[2],
230  'view' => $matches[3],
231  ];
232  }
233 
241  protected function isCacheableView($view) {
242  $matches = [];
243  if (preg_match('~^languages/(.*)\.js$~', $view, $matches)) {
244  return in_array($matches[1], _elgg_services()->locale->getLanguageCodes());
245  }
246 
247  return _elgg_services()->views->isCacheableView($view);
248  }
249 
258  protected function sendCacheHeaders($etag, Response $response) {
259  $response->setSharedMaxAge(86400 * 30 * 6);
260  $response->setMaxAge(86400 * 30 * 6);
261  $response->headers->set('ETag', $etag);
262  }
263 
272  protected function setRevalidateHeaders($etag, Response $response) {
273  $response->headers->set('Cache-Control', 'public, max-age=0, must-revalidate', true);
274  $response->headers->set('ETag', $etag);
275  }
276 
284  protected function is304($etag) {
285  $if_none_match = $this->request->headers->get('If-None-Match');
286  if ($if_none_match === null) {
287  return false;
288  }
289 
290  // strip leading W/
291  $if_none_match = trim($if_none_match);
292  if (str_starts_with($if_none_match, 'W/')) {
293  $if_none_match = substr($if_none_match, 2);
294  }
295 
296  // strip -gzip
297  $if_none_match = str_replace('-gzip', '', $if_none_match);
298 
299  return ($if_none_match === $etag);
300  }
301 
309  public function getContentType($view) {
310  $extension = $this->getViewFileType($view);
311 
312  return self::$extensions[$extension] ?? null;
313  }
314 
327  public function getViewFileType($view) {
328  $extension = (new \SplFileInfo($view))->getExtension();
329  if (isset(self::$extensions[$extension])) {
330  return $extension;
331  }
332 
333  $matches = [];
334  if (preg_match('~(?:^|/)(css|js)(?:$|/)~', $view, $matches)) {
335  return $matches[1];
336  }
337 
338  return 'unknown';
339  }
340 
350  protected function getProcessedView($view, $viewtype) {
351  $content = $this->renderView($view, $viewtype);
352  if ($content === false) {
353  return false;
354  }
355 
356  $name = $this->simplecache_enabled ? 'simplecache:generate' : 'cache:generate';
357  $type = $this->getViewFileType($view);
358  $params = [
359  'view' => $view,
360  'viewtype' => $viewtype,
361  'view_content' => $content,
362  ];
363  return _elgg_services()->events->triggerResults($name, $type, $params, $content);
364  }
365 
374  protected function renderView($view, $viewtype) {
376 
377  $matches = [];
378  if ($viewtype === 'default' && preg_match('#^languages/(.*?)\\.js$#', $view, $matches)) {
379  $view = 'languages.js';
380  $vars = ['language' => $matches[1]];
381  } else {
382  $vars = [];
383  }
384 
385  if (!elgg_view_exists($view)) {
386  return false;
387  }
388 
389  // disable error reporting so we don't cache problems
390  $this->config->debug = null;
391 
392  return elgg_view($view, $vars);
393  }
394 
402  protected function send403($msg = 'Cache error: bad request') {
403  return new Response($msg, ELGG_HTTP_FORBIDDEN);
404  }
405 }
getElggPath()
Get the Request URI minus querystring.
Definition: Request.php:290
__construct(Config $config, Request $request, SimpleCache $simplecache, ConfigTable $config_table)
Constructor.
const ELGG_HTTP_FORBIDDEN
Definition: constants.php:67
Elgg HTTP request.
Definition: Request.php:17
$params
Saves global plugin settings.
Definition: save.php:13
parsePath($path)
Parse a request.
send403($msg= 'Cache error:bad request')
Send an error message to requestor.
if(!$user||!$user->canDelete()) $name
Definition: delete.php:22
$response
Definition: content.php:10
$type
Definition: delete.php:22
get(string $name)
Gets a configuration value.
getViewFileType($view)
Returns the type of output expected from the view.
getContentType($view)
Get the content type.
const ELGG_HTTP_GONE
Definition: constants.php:74
$path
Definition: details.php:70
elgg_view(string $view, array $vars=[], string $viewtype= '')
Return a parsed view.
Definition: views.php:177
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
is304($etag)
Send a 304 and exit() if the ETag matches the request.
renderView($view, $viewtype)
Render a view for caching.
elgg_set_viewtype(string $viewtype= '')
Manually set the viewtype.
Definition: views.php:63
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
$vars
Definition: theme.php:5
$content
Set robots.txt action.
Definition: set_robots.php:6
_elgg_services()
Get the global service provider.
Definition: elgglib.php:346
sendCacheHeaders($etag, Response $response)
Sets cache headers.
getProcessedView($view, $viewtype)
Get the contents of a view for caching.
Manipulates values in the dbprefix_config table.
Definition: ConfigTable.php:16
elgg_view_exists(string $view, string $viewtype= '', bool $recurse=true)
Returns whether the specified view exists.
Definition: views.php:152
handleRequest(Request $request, Application $app)
Handle a request for a cached view.
$extension
Definition: default.php:25