implicit conversion of array to boolean.
1 | <?php |
||
2 | namespace Elgg\Application; |
||
3 | |||
4 | use Elgg\Application; |
||
5 | use Elgg\Config; |
||
6 | use Elgg\Http\Request; |
||
7 | use Symfony\Component\HttpFoundation\BinaryFileResponse; |
||
8 | use Symfony\Component\HttpFoundation\Response; |
||
9 | use Symfony\Component\HttpFoundation\StreamedResponse; |
||
10 | |||
11 | /** |
||
12 | * Simplecache handler |
||
13 | * |
||
14 | * @access private |
||
15 | */ |
||
16 | class CacheHandler { |
||
17 | |||
18 | public static $extensions = [ |
||
19 | 'bmp' => "image/bmp", |
||
20 | 'css' => "text/css", |
||
21 | 'gif' => "image/gif", |
||
22 | 'html' => "text/html", |
||
23 | 'ico' => "image/x-icon", |
||
24 | 'jpeg' => "image/jpeg", |
||
25 | 'jpg' => "image/jpeg", |
||
26 | 'js' => "application/javascript", |
||
27 | 'json' => "application/json", |
||
28 | 'png' => "image/png", |
||
29 | 'svg' => "image/svg+xml", |
||
30 | 'swf' => "application/x-shockwave-flash", |
||
31 | 'tiff' => "image/tiff", |
||
32 | 'webp' => "image/webp", |
||
33 | 'xml' => "text/xml", |
||
34 | 'eot' => "application/vnd.ms-fontobject", |
||
35 | 'ttf' => "application/font-ttf", |
||
36 | 'woff' => "application/font-woff", |
||
37 | 'woff2' => "application/font-woff2", |
||
38 | 'otf' => "application/font-otf", |
||
39 | ]; |
||
40 | |||
41 | public static $utf8_content_types = [ |
||
42 | "text/css", |
||
43 | "text/html", |
||
44 | "application/javascript", |
||
45 | "application/json", |
||
46 | "image/svg+xml", |
||
47 | "text/xml", |
||
48 | ]; |
||
49 | |||
50 | /** @var Config */ |
||
51 | private $config; |
||
52 | |||
53 | /** @var Request */ |
||
54 | private $request; |
||
55 | |||
56 | /** @var bool */ |
||
57 | protected $simplecache_enabled; |
||
58 | |||
59 | /** |
||
60 | * Constructor |
||
61 | * |
||
62 | * @param Config $config Elgg configuration |
||
63 | * @param Request $request HTTP request |
||
64 | * @param bool $simplecache_enabled Is the simplecache enabled? |
||
65 | */ |
||
66 | 9 | public function __construct(Config $config, Request $request, $simplecache_enabled) { |
|
67 | 9 | $this->config = $config; |
|
68 | 9 | $this->request = $request; |
|
69 | 9 | $this->simplecache_enabled = $simplecache_enabled; |
|
70 | 9 | } |
|
71 | |||
72 | /** |
||
73 | * Handle a request for a cached view |
||
74 | * |
||
75 | * @param Request $request Elgg request |
||
76 | * @param Application $app Elgg application |
||
77 | * @return Response (unprepared) |
||
78 | */ |
||
79 | public function handleRequest(Request $request, Application $app) { |
||
80 | $config = $this->config; |
||
81 | |||
82 | $parsed = $this->parsePath($request->getElggPath()); |
||
83 | if (!$parsed) { |
||
0 ignored issues
–
show
|
|||
84 | return $this->send403(); |
||
85 | } |
||
86 | |||
87 | $ts = $parsed['ts']; |
||
88 | $view = $parsed['view']; |
||
89 | $viewtype = $parsed['viewtype']; |
||
90 | |||
91 | $content_type = $this->getContentType($view); |
||
92 | if (empty($content_type)) { |
||
93 | return $this->send403("Asset must have a valid file extension"); |
||
94 | } |
||
95 | |||
96 | $response = Response::create(); |
||
97 | if (in_array($content_type, self::$utf8_content_types)) { |
||
98 | $response->headers->set('Content-Type', "$content_type;charset=utf-8", true); |
||
99 | } else { |
||
100 | $response->headers->set('Content-Type', $content_type, true); |
||
101 | } |
||
102 | |||
103 | if (!$this->simplecache_enabled) { |
||
104 | $app->bootCore(); |
||
105 | header_remove('Cache-Control'); |
||
106 | header_remove('Pragma'); |
||
107 | header_remove('Expires'); |
||
108 | |||
109 | if (!$this->isCacheableView($view)) { |
||
110 | return $this->send403("Requested view ({$view}) is not an asset"); |
||
111 | } |
||
112 | |||
113 | $content = $this->getProcessedView($view, $viewtype); |
||
114 | if ($content === false) { |
||
115 | return $this->send403(); |
||
116 | } |
||
117 | |||
118 | $etag = '"' . md5($content) . '"'; |
||
119 | $this->setRevalidateHeaders($etag, $response); |
||
120 | if ($this->is304($etag)) { |
||
121 | return Response::create()->setNotModified(); |
||
122 | } |
||
123 | |||
124 | return $response->setContent($content); |
||
125 | } |
||
126 | |||
127 | $etag = "\"$ts\""; |
||
128 | if ($this->is304($etag)) { |
||
129 | return Response::create()->setNotModified(); |
||
130 | } |
||
131 | |||
132 | // trust the client but check for an existing cache file |
||
133 | $filename = $config->cacheroot . "views_simplecache/$ts/$viewtype/$view"; |
||
134 | if (file_exists($filename)) { |
||
135 | $this->sendCacheHeaders($etag, $response); |
||
136 | return BinaryFileResponse::create($filename, 200, $response->headers->all()); |
||
137 | } |
||
138 | |||
139 | // the hard way |
||
140 | $app->bootCore(); |
||
141 | header_remove('Cache-Control'); |
||
142 | header_remove('Pragma'); |
||
143 | header_remove('Expires'); |
||
144 | |||
145 | elgg_set_viewtype($viewtype); |
||
146 | if (!$this->isCacheableView($view)) { |
||
147 | return $this->send403("Requested view is not an asset"); |
||
148 | } |
||
149 | |||
150 | $lastcache = (int) $config->lastcache; |
||
151 | |||
152 | $filename = $config->cacheroot . "views_simplecache/$lastcache/$viewtype/$view"; |
||
153 | |||
154 | if ($lastcache == $ts) { |
||
155 | $this->sendCacheHeaders($etag, $response); |
||
156 | |||
157 | $content = $this->getProcessedView($view, $viewtype); |
||
158 | |||
159 | $dir_name = dirname($filename); |
||
160 | if (!is_dir($dir_name)) { |
||
161 | // PHP and the server accessing the cache symlink may be a different user. And here |
||
162 | // it's safe to make everything readable anyway. |
||
163 | mkdir($dir_name, 0775, true); |
||
164 | } |
||
165 | |||
166 | file_put_contents($filename, $content); |
||
167 | chmod($filename, 0664); |
||
168 | } else { |
||
169 | // if wrong timestamp, don't send HTTP cache |
||
170 | $content = $this->getProcessedView($view, $viewtype); |
||
171 | } |
||
172 | |||
173 | return $response->setContent($content); |
||
174 | } |
||
175 | |||
176 | /** |
||
177 | * Parse a request |
||
178 | * |
||
179 | * @param string $path Request URL path |
||
180 | * @return array Cache parameters (empty array if failure) |
||
181 | */ |
||
182 | 6 | public function parsePath($path) { |
|
183 | // no '..' |
||
184 | 6 | if (false !== strpos($path, '..')) { |
|
185 | 1 | return []; |
|
186 | } |
||
187 | // only alphanumeric characters plus /, ., -, and _ |
||
188 | 5 | if (preg_match('#[^a-zA-Z0-9/\.\-_]#', $path)) { |
|
189 | 1 | return []; |
|
190 | } |
||
191 | |||
192 | // testing showed regex to be marginally faster than array / string functions over 100000 reps |
||
193 | // it won't make a difference in real life and regex is easier to read. |
||
194 | // <ts>/<viewtype>/<name/of/view.and.dots>.<type> |
||
195 | 4 | if (!preg_match('#^/cache/([0-9]+)/([^/]+)/(.+)$#', $path, $matches)) { |
|
196 | 3 | return []; |
|
197 | } |
||
198 | |||
199 | return [ |
||
200 | 1 | 'ts' => $matches[1], |
|
201 | 1 | 'viewtype' => $matches[2], |
|
202 | 1 | 'view' => $matches[3], |
|
203 | ]; |
||
204 | } |
||
205 | |||
206 | /** |
||
207 | * Is the view cacheable. Language views are handled specially. |
||
208 | * |
||
209 | * @param string $view View name |
||
210 | * |
||
211 | * @return bool |
||
212 | */ |
||
213 | protected function isCacheableView($view) { |
||
214 | if (preg_match('~^languages/(.*)\.js$~', $view, $m)) { |
||
215 | return in_array($m[1], _elgg_services()->translator->getAllLanguageCodes()); |
||
216 | } |
||
217 | return _elgg_services()->views->isCacheableView($view); |
||
218 | } |
||
219 | |||
220 | /** |
||
221 | * Sets cache headers |
||
222 | * |
||
223 | * @param string $etag ETag value |
||
224 | * @param Response $response the response to set the headers on |
||
225 | * |
||
226 | * @return void |
||
227 | */ |
||
228 | protected function sendCacheHeaders($etag, Response $response) { |
||
229 | $response->setSharedMaxAge(86400 * 30 * 6); |
||
230 | $response->setMaxAge(86400 * 30 * 6); |
||
231 | $response->headers->set('ETag', $etag); |
||
232 | } |
||
233 | |||
234 | /** |
||
235 | * Set revalidate cache headers |
||
236 | * |
||
237 | * @param string $etag ETag value |
||
238 | * @param Response $response the response to set the headers on |
||
239 | * |
||
240 | * @return void |
||
241 | */ |
||
242 | protected function setRevalidateHeaders($etag, Response $response) { |
||
243 | $response->headers->set('Cache-Control', "public, max-age=0, must-revalidate", true); |
||
244 | $response->headers->set('ETag', $etag); |
||
245 | } |
||
246 | |||
247 | /** |
||
248 | * Send a 304 and exit() if the ETag matches the request |
||
249 | * |
||
250 | * @param string $etag ETag value |
||
251 | * @return bool |
||
252 | */ |
||
253 | protected function is304($etag) { |
||
254 | $if_none_match = $this->request->headers->get('If-None-Match'); |
||
255 | if ($if_none_match === null) { |
||
256 | return false; |
||
257 | } |
||
258 | |||
259 | // strip -gzip and leading /W |
||
260 | $if_none_match = trim($if_none_match); |
||
261 | if (0 === strpos($if_none_match, 'W/')) { |
||
262 | $if_none_match = substr($if_none_match, 2); |
||
263 | } |
||
264 | $if_none_match = str_replace('-gzip', '', $if_none_match); |
||
265 | |||
266 | return ($if_none_match === $etag); |
||
267 | } |
||
268 | |||
269 | /** |
||
270 | * Get the content type |
||
271 | * |
||
272 | * @param string $view The view name |
||
273 | * |
||
274 | * @return string|null |
||
275 | * @access private |
||
276 | */ |
||
277 | 1 | public function getContentType($view) { |
|
278 | 1 | $extension = $this->getViewFileType($view); |
|
279 | |||
280 | 1 | if (isset(self::$extensions[$extension])) { |
|
281 | 1 | return self::$extensions[$extension]; |
|
282 | } else { |
||
283 | return null; |
||
284 | } |
||
285 | } |
||
286 | |||
287 | /** |
||
288 | * Returns the type of output expected from the view. |
||
289 | * |
||
290 | * - view/name.extension returns "extension" if "extension" is valid |
||
291 | * - css/view return "css" |
||
292 | * - js/view return "js" |
||
293 | * - Otherwise, returns "unknown" |
||
294 | * |
||
295 | * @param string $view The view name |
||
296 | * @return string |
||
297 | * @access private |
||
298 | */ |
||
299 | 2 | public function getViewFileType($view) { |
|
300 | 2 | $extension = (new \SplFileInfo($view))->getExtension(); |
|
301 | 2 | $hasValidExtension = isset(self::$extensions[$extension]); |
|
302 | |||
303 | 2 | if ($hasValidExtension) { |
|
304 | 2 | return $extension; |
|
305 | } |
||
306 | |||
307 | if (preg_match('~(?:^|/)(css|js)(?:$|/)~', $view, $m)) { |
||
308 | return $m[1]; |
||
309 | } |
||
310 | |||
311 | return 'unknown'; |
||
312 | } |
||
313 | |||
314 | /** |
||
315 | * Get the contents of a view for caching |
||
316 | * |
||
317 | * @param string $view The view name |
||
318 | * @param string $viewtype The viewtype |
||
319 | * @return string|false |
||
320 | * @see CacheHandler::renderView() |
||
321 | */ |
||
322 | protected function getProcessedView($view, $viewtype) { |
||
323 | $content = $this->renderView($view, $viewtype); |
||
324 | if ($content === false) { |
||
325 | return false; |
||
326 | } |
||
327 | |||
328 | if ($this->simplecache_enabled) { |
||
329 | $hook_name = 'simplecache:generate'; |
||
330 | } else { |
||
331 | $hook_name = 'cache:generate'; |
||
332 | } |
||
333 | $hook_type = $this->getViewFileType($view); |
||
334 | $hook_params = [ |
||
335 | 'view' => $view, |
||
336 | 'viewtype' => $viewtype, |
||
337 | 'view_content' => $content, |
||
338 | ]; |
||
339 | return \_elgg_services()->hooks->trigger($hook_name, $hook_type, $hook_params, $content); |
||
340 | } |
||
341 | |||
342 | /** |
||
343 | * Render a view for caching. Language views are handled specially. |
||
344 | * |
||
345 | * @param string $view The view name |
||
346 | * @param string $viewtype The viewtype |
||
347 | * @return string|false |
||
348 | */ |
||
349 | protected function renderView($view, $viewtype) { |
||
350 | elgg_set_viewtype($viewtype); |
||
351 | |||
352 | if ($viewtype === 'default' && preg_match("#^languages/(.*?)\\.js$#", $view, $matches)) { |
||
353 | $view = "languages.js"; |
||
354 | $vars = ['language' => $matches[1]]; |
||
355 | } else { |
||
356 | $vars = []; |
||
357 | } |
||
358 | |||
359 | if (!elgg_view_exists($view)) { |
||
360 | return false; |
||
361 | } |
||
362 | |||
363 | // disable error reporting so we don't cache problems |
||
364 | $this->config->debug = null; |
||
365 | |||
366 | return elgg_view($view, $vars); |
||
367 | } |
||
368 | |||
369 | /** |
||
370 | * Send an error message to requestor |
||
371 | * |
||
372 | * @param string $msg Optional message text |
||
373 | * @return Response |
||
374 | */ |
||
375 | protected function send403($msg = 'Cache error: bad request') { |
||
376 | return Response::create($msg, 403); |
||
377 | } |
||
378 | } |
||
379 | |||
380 |
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.