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
|
|||
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 |
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.