CacheHandler::loadEngine()   A
last analyzed

Complexity

Conditions 1
Paths 1

Size

Total Lines 3

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 2

Importance

Changes 0
Metric Value
cc 1
nc 1
nop 0
dl 0
loc 3
ccs 0
cts 3
cp 0
crap 2
rs 10
c 0
b 0
f 0
1
<?php
2
namespace Elgg;
3
4
/**
5
 * Simplecache handler
6
 *
7
 * @access private
8
 *
9
 * @package Elgg.Core
10
 */
11
class CacheHandler {
12
13
	protected $config;
14
15
	/**
16
	 * Constructor
17
	 *
18
	 * @param \stdClass $config Elgg config object
19
	 */
20 6
	public function __construct($config) {
21 6
		$this->config = $config;
22 6
	}
23
24
	/**
25
	 * Handle a request for a cached view
26
	 *
27
	 * @param array $get_vars    $_GET variables
28
	 * @param array $server_vars $_SERVER variables
29
	 * @return void
30
	 */
31
	public function handleRequest($get_vars, $server_vars) {
32
		if (empty($get_vars['request'])) {
33
			$this->send403();
34
		}
35
		$request = $this->parseRequestVar($get_vars['request']);
36
		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...
37
			$this->send403();
38
		}
39
		$ts = $request['ts'];
40
		$view = $request['view'];
41
		$viewtype = $request['viewtype'];
42
43
		$this->sendContentType($view);
44
45
		// this may/may not have to connect to the DB
46
		$this->setupSimplecache();
47
48
		if (!$this->config->simplecache_enabled) {
49
			$this->loadEngine();
50
			if (!_elgg_is_view_cacheable($view)) {
51
				$this->send403();
52
			} else {
53
				echo $this->renderView($view, $viewtype);
54
			}
55
			exit;
56
		}
57
58
		$etag = "\"$ts\"";
59
		// If is the same ETag, content didn't change.
60 View Code Duplication
		if (isset($server_vars['HTTP_IF_NONE_MATCH'])) {
61
			// strip -gzip for #9427
62
			$if_none_match = str_replace('-gzip', '', trim($server_vars['HTTP_IF_NONE_MATCH']));
63
			if ($if_none_match === $etag) {
64
				header("HTTP/1.1 304 Not Modified");
65
				header("ETag: $etag");
66
				exit;
67
			}
68
		}
69
70
		$filename = $this->config->dataroot . 'views_simplecache/' . md5("$viewtype|$view");
71
		if (file_exists($filename)) {
72
			$this->sendCacheHeaders($etag);
73
			readfile($filename);
74
			exit;
75
		}
76
77
		$this->loadEngine();
78
79
		elgg_set_viewtype($viewtype);
80
		if (!_elgg_is_view_cacheable($view)) {
81
			$this->send403();
82
		}
83
84
		$cache_timestamp = (int)_elgg_services()->config->get('lastcache');
85
86
		if ($cache_timestamp == $ts) {
87
			$this->sendCacheHeaders($etag);
88
89
			$content = $this->getProcessedView($view, $viewtype);
90
91
			$dir_name = $this->config->dataroot . 'views_simplecache/';
92
			if (!is_dir($dir_name)) {
93
				mkdir($dir_name, 0700);
94
			}
95
96
			file_put_contents($filename, $content);
97
		} else {
98
			// if wrong timestamp, don't send HTTP cache
99
			$content = $this->renderView($view, $viewtype);
100
		}
101
102
		echo $content;
103
		exit;
104
	}
105
106
	/**
107
	 * Parse a request
108
	 *
109
	 * @param string $request_var Request URL
110
	 * @return array Cache parameters (empty array if failure)
111
	 */
112 6
	public function parseRequestVar($request_var) {
113
		// no '..'
114 6
		if (false !== strpos($request_var, '..')) {
115 1
			return array();
116
		}
117
		// only alphanumeric characters plus /, ., -, and _
118 5
		if (preg_match('#[^a-zA-Z0-9/\.\-_]#', $request_var)) {
119 1
			return array();
120
		}
121
122
		// testing showed regex to be marginally faster than array / string functions over 100000 reps
123
		// it won't make a difference in real life and regex is easier to read.
124
		// <ts>/<viewtype>/<name/of/view.and.dots>.<type>
125 4
		if (!preg_match('#^/?([0-9]+)/([^/]+)/(.+)$#', $request_var, $matches)) {
126 2
			return array();
127
		}
128
129
		return array(
130 2
			'ts' => $matches[1],
131 2
			'viewtype' => $matches[2],
132 2
			'view' => $matches[3],
133 2
		);
134
	}
135
136
	/**
137
	 * Do a minimal engine load
138
	 *
139
	 * @return void
140
	 */
141
	protected function setupSimplecache() {
142
		if (!empty($this->config->dataroot) && isset($this->config->simplecache_enabled)) {
143
			return;
144
		}
145
146
		$db_config = new Database\Config($this->config);
147
		$db = new Database($db_config, new Logger(new PluginHooksService()));
148
149
		try {
150
			$rows = $db->getData("
151
				SELECT `name`, `value`
152
				FROM {$db->getTablePrefix()}datalists
153
				WHERE `name` IN ('dataroot', 'simplecache_enabled')
154
			");
155
			if (!$rows) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $rows 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...
156
				$this->send403('Cache error: unable to get the data root');
157
			}
158
		} catch (\DatabaseException $e) {
159
			if (0 === strpos($e->getMessage(), "Elgg couldn't connect")) {
160
				$this->send403('Cache error: unable to connect to database server');
161
			} else {
162
				$this->send403('Cache error: unable to connect to Elgg database');
163
			}
164
			exit; // unnecessary, but helps PhpStorm understand
165
		}
166
167
		foreach ($rows as $row) {
168
			$this->config->{$row->name} = $row->value;
169
		}
170
171
		if (empty($this->config->dataroot)) {
172
			$this->send403('Cache error: unable to get the data root');
173
		}
174
	}
175
176
	/**
177
	 * Send cache headers
178
	 *
179
	 * @param string $etag ETag value
180
	 * @return void
181
	 */
182
	protected function sendCacheHeaders($etag) {
183
		header('Expires: ' . gmdate('D, d M Y H:i:s \G\M\T', strtotime("+6 months")), true);
184
		header("Pragma: public", true);
185
		header("Cache-Control: public", true);
186
		header("ETag: $etag");
187
	}
188
189
	/**
190
	 * Send content type
191
	 *
192
	 * @param string $view The view name
193
	 * @return void
194
	 */
195
	protected function sendContentType($view) {
196
		$segments = explode('/', $view, 2);
197 View Code Duplication
		switch ($segments[0]) {
198
			case 'css':
199
				header("Content-Type: text/css;charset=utf-8");
200
				break;
201
			case 'js':
202
				header('Content-Type: text/javascript;charset=utf-8');
203
				break;
204
			default:
205
				header('Content-Type: text/html;charset=utf-8');
206
		}
207
	}
208
209
	/**
210
	 * Get the contents of a view for caching
211
	 *
212
	 * @param string $view     The view name
213
	 * @param string $viewtype The viewtype
214
	 * @return string
215
	 * @see CacheHandler::renderView()
216
	 */
217
	protected function getProcessedView($view, $viewtype) {
218
		$content = $this->renderView($view, $viewtype);
219
220
		$hook_type = _elgg_get_view_filetype($view);
221
		$hook_params = array(
222
			'view' => $view,
223
			'viewtype' => $viewtype,
224
			'view_content' => $content,
225
		);
226
		return _elgg_services()->hooks->trigger('simplecache:generate', $hook_type, $hook_params, $content);
227
	}
228
229
	/**
230
	 * Render a view for caching
231
	 *
232
	 * @param string $view     The view name
233
	 * @param string $viewtype The viewtype
234
	 * @return string
235
	 */
236
	protected function renderView($view, $viewtype) {
237
		elgg_set_viewtype($viewtype);
238
239
		if (!elgg_view_exists($view)) {
240
			$this->send403();
241
		}
242
243
		// disable error reporting so we don't cache problems
244
		_elgg_services()->config->set('debug', null);
245
246
		// @todo elgg_view() checks if the page set is done (isset($CONFIG->pagesetupdone)) and
247
		// triggers an event if it's not. Calling elgg_view() here breaks submenus
248
		// (at least) because the page setup hook is called before any
249
		// contexts can be correctly set (since this is called before page_handler()).
250
		// To avoid this, lie about $CONFIG->pagehandlerdone to force
251
		// the trigger correctly when the first view is actually being output.
252
		_elgg_services()->config->set('pagesetupdone', true);
253
254
		return elgg_view($view);
255
	}
256
257
	/**
258
	 * Load the complete Elgg engine
259
	 *
260
	 * @return void
261
	 */
262
	protected function loadEngine() {
263
		require_once dirname(dirname(dirname(__FILE__))) . "/start.php";
264
	}
265
266
	/**
267
	 * Send an error message to requestor
268
	 *
269
	 * @param string $msg Optional message text
270
	 * @return void
271
	 */
272
	protected function send403($msg = 'Cache error: bad request') {
273
		header('HTTP/1.1 403 Forbidden');
274
		echo $msg;
275
		exit;
276
	}
277
}
278
279