Test Failed
Push — master ( 8c47c2...3acf9f )
by Steve
12:37
created

engine/classes/Elgg/Application/CacheHandler.php (1 issue)

Upgrade to new PHP Analysis Engine

These results are based on our legacy PHP analysis, consider migrating to our new PHP analysis engine instead. Learn more

1
<?php
2
namespace Elgg\Application;
3
4
use Elgg\Application;
5
use Elgg\Config;
6
7
8
/**
9
 * Simplecache handler
10
 *
11
 * @access private
12
 *
13
 * @package Elgg.Core
14
 */
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
		'json' => "application/json",
27
		'png' => "image/png",
28
		'svg' => "image/svg+xml",
29
		'swf' => "application/x-shockwave-flash",
30
		'tiff' => "image/tiff",
31
		'webp' => "image/webp",
32
		'xml' => "text/xml",
33
		'eot' => "application/vnd.ms-fontobject",
34
		'ttf' => "application/font-ttf",
35
		'woff' => "application/font-woff",
36
		'woff2' => "application/font-woff2",
37
		'otf' => "application/font-otf",
38
	];
39
40
	public static $utf8_content_types = [
41
		"text/css",
42
		"text/html",
43
		"application/javascript",
44
		"application/json",
45
		"image/svg+xml",
46
		"text/xml",
47
	];
48
49
	/** @var Application */
50
	private $application;
51
52
	/** @var Config */
53
	private $config;
54
55
	/** @var array */
56
	private $server_vars;
57
58
	/**
59
	 * Constructor
60
	 *
61
	 * @param Application $app         Elgg Application
62
	 * @param Config      $config      Elgg configuration
63
	 * @param array       $server_vars Server vars
64
	 */
65 6
	public function __construct(Application $app, Config $config, $server_vars) {
66 6
		$this->application = $app;
67 6
		$this->config = $config;
68 6
		$this->server_vars = $server_vars;
69 6
	}
70
71
	/**
72
	 * Handle a request for a cached view
73
	 *
74
	 * @param array $path URL path
75
	 * @return void
76
	 */
77
	public function handleRequest($path) {
78
		$config = $this->config;
79
		
80
		$request = $this->parsePath($path);
81
		if (!$request) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $request of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
82
			$this->send403();
83
		}
84
		
85
		$ts = $request['ts'];
86
		$view = $request['view'];
87
		$viewtype = $request['viewtype'];
88
89
		$content_type = $this->getContentType($view);
90
		if (empty($content_type)) {
91
			$this->send403("Asset must have a valid file extension");
92
		}
93
94
		if (in_array($content_type, self::$utf8_content_types)) {
95
			header("Content-Type: $content_type;charset=utf-8");
96
		} else {
97
			header("Content-Type: $content_type");
98
		}
99
100
		// we can't use $config->get yet. It fails before the core is booted
101
		if (!$config->get('simplecache_enabled')) {
102
			$this->application->bootCore();
103
104
			if (!$this->isCacheableView($view)) {
105
				$this->send403("Requested view is not an asset");
106
			} else {
107
				$content = $this->getProcessedView($view, $viewtype);
108
				$etag = '"' . md5($content) . '"';
109
				$this->sendRevalidateHeaders($etag);
110
				$this->handle304($etag);
111
112
				echo $content;
113
			}
114
			exit;
115
		}
116
117
		$etag = "\"$ts\"";
118
		$this->handle304($etag);
119
120
		// trust the client but check for an existing cache file
121
		$filename = $config->getCachePath() . "views_simplecache/$ts/$viewtype/$view";
122
		if (file_exists($filename)) {
123
			$this->sendCacheHeaders($etag);
124
			readfile($filename);
125
			exit;
126
		}
127
128
		// the hard way
129
		$this->application->bootCore();
130
131
		elgg_set_viewtype($viewtype);
132
		if (!$this->isCacheableView($view)) {
133
			$this->send403("Requested view is not an asset");
134
		}
135
136
		$lastcache = (int) $config->get('lastcache');
137
138
		$filename = $config->getCachePath() . "views_simplecache/$lastcache/$viewtype/$view";
139
140
		if ($lastcache == $ts) {
141
			$this->sendCacheHeaders($etag);
142
143
			$content = $this->getProcessedView($view, $viewtype);
144
145
			$dir_name = dirname($filename);
146
			if (!is_dir($dir_name)) {
147
				// PHP and the server accessing the cache symlink may be a different user. And here
148
				// it's safe to make everything readable anyway.
149
				mkdir($dir_name, 0775, true);
150
			}
151
152
			file_put_contents($filename, $content);
153
			chmod($filename, 0664);
154
		} else {
155
			// if wrong timestamp, don't send HTTP cache
156
			$content = $this->getProcessedView($view, $viewtype);
157
		}
158
159
		echo $content;
160
		exit;
161
	}
162
163
	/**
164
	 * Parse a request
165
	 *
166
	 * @param string $path Request URL path
167
	 * @return array Cache parameters (empty array if failure)
168
	 */
169 6
	public function parsePath($path) {
170
		// no '..'
171 6
		if (false !== strpos($path, '..')) {
172 1
			return [];
173
		}
174
		// only alphanumeric characters plus /, ., -, and _
175 5
		if (preg_match('#[^a-zA-Z0-9/\.\-_]#', $path)) {
176 1
			return [];
177
		}
178
179
		// testing showed regex to be marginally faster than array / string functions over 100000 reps
180
		// it won't make a difference in real life and regex is easier to read.
181
		// <ts>/<viewtype>/<name/of/view.and.dots>.<type>
182 4
		if (!preg_match('#^/cache/([0-9]+)/([^/]+)/(.+)$#', $path, $matches)) {
183 3
			return [];
184
		}
185
186
		return [
187 1
			'ts' => $matches[1],
188 1
			'viewtype' => $matches[2],
189 1
			'view' => $matches[3],
190
		];
191
	}
192
193
	/**
194
	 * Is the view cacheable. Language views are handled specially.
195
	 *
196
	 * @param string $view View name
197
	 *
198
	 * @return bool
199
	 */
200
	protected function isCacheableView($view) {
201
		if (preg_match('~^languages/(.*)\.js$~', $view, $m)) {
202
			return in_array($m[1],  _elgg_services()->translator->getAllLanguageCodes());
203
		}
204
		return _elgg_services()->views->isCacheableView($view);
205
	}
206
207
	/**
208
	 * Send cache headers
209
	 *
210
	 * @param string $etag ETag value
211
	 * @return void
212
	 */
213
	protected function sendCacheHeaders($etag) {
214
		header('Expires: ' . gmdate('D, d M Y H:i:s \G\M\T', strtotime("+6 months")), true);
215
		header("Pragma: public", true);
216
		header("Cache-Control: public", true);
217
		header("ETag: $etag");
218
	}
219
220
	/**
221
	 * Send revalidate cache headers
222
	 *
223
	 * @param string $etag ETag value
224
	 * @return void
225
	 */
226
	protected function sendRevalidateHeaders($etag) {
227
		header_remove('Expires');
228
		header("Pragma: public", true);
229
		header("Cache-Control: public, max-age=0, must-revalidate", true);
230
		header("ETag: $etag");
231
	}
232
233
	/**
234
	 * Send a 304 and exit() if the ETag matches the request
235
	 *
236
	 * @param string $etag ETag value
237
	 * @return void
238
	 */
239
	protected function handle304($etag) {
240
		if (!isset($this->server_vars['HTTP_IF_NONE_MATCH'])) {
241
			return;
242
		}
243
244
		// strip -gzip for #9427
245
		$if_none_match = str_replace('-gzip', '', trim($this->server_vars['HTTP_IF_NONE_MATCH']));
246
		if ($if_none_match === $etag) {
247
			header("HTTP/1.1 304 Not Modified");
248
			exit;
249
		}
250
	}
251
252
	/**
253
	 * Get the content type
254
	 *
255
	 * @param string $view The view name
256
	 *
257
	 * @return string|null
258
	 */
259
	protected function getContentType($view) {
260
		$extension = $this->getViewFileType($view);
261
		
262
		if (isset(self::$extensions[$extension])) {
263
			return self::$extensions[$extension];
264
		} else {
265
			return null;
266
		}
267
	}
268
	
269
	/**
270
	 * Returns the type of output expected from the view.
271
	 *
272
	 *  - view/name.extension returns "extension" if "extension" is valid
273
	 *  - css/view return "css"
274
	 *  - js/view return "js"
275
	 *  - Otherwise, returns "unknown"
276
	 *
277
	 * @param string $view The view name
278
	 * @return string
279
	 */
280
	private function getViewFileType($view) {
281
		$extension = (new \SplFileInfo($view))->getExtension();
282
		$hasValidExtension = isset(self::$extensions[$extension]);
283
284
		if ($hasValidExtension) {
285
			return $extension;
286
		}
287
		
288
		if (preg_match('~(?:^|/)(css|js)(?:$|/)~', $view, $m)) {
289
			return $m[1];
290
		}
291
		
292
		return 'unknown';
293
	}
294
295
	/**
296
	 * Get the contents of a view for caching
297
	 *
298
	 * @param string $view     The view name
299
	 * @param string $viewtype The viewtype
300
	 * @return string
301
	 * @see CacheHandler::renderView()
302
	 */
303
	protected function getProcessedView($view, $viewtype) {
304
		$content = $this->renderView($view, $viewtype);
305
306
		if ($this->config->getVolatile('simplecache_enabled')) {
307
			$hook_name = 'simplecache:generate';
308
		} else {
309
			$hook_name = 'cache:generate';
310
		}
311
		$hook_type = $this->getViewFileType($view);
312
		$hook_params = [
313
			'view' => $view,
314
			'viewtype' => $viewtype,
315
			'view_content' => $content,
316
		];
317
		return \_elgg_services()->hooks->trigger($hook_name, $hook_type, $hook_params, $content);
318
	}
319
320
	/**
321
	 * Render a view for caching. Language views are handled specially.
322
	 *
323
	 * @param string $view     The view name
324
	 * @param string $viewtype The viewtype
325
	 * @return string
326
	 */
327
	protected function renderView($view, $viewtype) {
328
		elgg_set_viewtype($viewtype);
329
330
		if ($viewtype === 'default' && preg_match("#^languages/(.*?)\\.js$#", $view, $matches)) {
331
			$view = "languages.js";
332
			$vars = ['language' => $matches[1]];
333
		} else {
334
			$vars = [];
335
		}
336
337
		if (!elgg_view_exists($view)) {
338
			$this->send403();
339
		}
340
341
		// disable error reporting so we don't cache problems
342
		$this->config->set('debug', null);
343
344
		return elgg_view($view, $vars);
345
	}
346
347
	/**
348
	 * Send an error message to requestor
349
	 *
350
	 * @param string $msg Optional message text
351
	 * @return void
352
	 */
353
	protected function send403($msg = 'Cache error: bad request') {
354
		header('HTTP/1.1 403 Forbidden');
355
		echo $msg;
356
		exit;
357
	}
358
}
359
360