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