1 | <?php |
||
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 Application */ |
||
51 | private $application; |
||
52 | |||
53 | /** @var Config */ |
||
54 | private $config; |
||
55 | |||
56 | /** @var Request */ |
||
57 | private $request; |
||
58 | |||
59 | /** |
||
60 | * Constructor |
||
61 | * |
||
62 | * @param Application $app Elgg Application |
||
63 | * @param Config $config Elgg configuration |
||
64 | * @param Request $request HTTP request |
||
65 | 6 | * @param bool $simplecache_enabled Is the simplecache enabled? |
|
66 | 6 | */ |
|
67 | 6 | public function __construct(Application $app, Config $config, Request $request, $simplecache_enabled) { |
|
|
|||
68 | 6 | $this->application = $app; |
|
69 | 6 | $this->config = $config; |
|
70 | $this->request = $request; |
||
71 | $this->simplecache_enabled = $simplecache_enabled; |
||
72 | } |
||
73 | |||
74 | /** |
||
75 | * Handle a request for a cached view |
||
76 | * |
||
77 | * @param array $path URL path |
||
78 | * @return Response (unprepared) |
||
79 | */ |
||
80 | public function handleRequest(Request $request) { |
||
81 | $config = $this->config; |
||
82 | |||
83 | $parsed = $this->parsePath($request->getElggPath()); |
||
84 | if (!$parsed) { |
||
85 | return $this->send403(); |
||
86 | } |
||
87 | |||
88 | $ts = $parsed['ts']; |
||
89 | $view = $parsed['view']; |
||
90 | $viewtype = $parsed['viewtype']; |
||
91 | |||
92 | $content_type = $this->getContentType($view); |
||
93 | if (empty($content_type)) { |
||
94 | return $this->send403("Asset must have a valid file extension"); |
||
95 | } |
||
96 | |||
97 | $response = Response::create(); |
||
98 | if (in_array($content_type, self::$utf8_content_types)) { |
||
99 | $response->headers->set('Content-Type', "$content_type;charset=utf-8", true); |
||
100 | } else { |
||
101 | $response->headers->set('Content-Type', $content_type, true); |
||
102 | } |
||
103 | |||
104 | if (!$this->simplecache_enabled) { |
||
105 | $this->application->bootCore(); |
||
106 | header_remove('Cache-Control'); |
||
107 | header_remove('Pragma'); |
||
108 | header_remove('Expires'); |
||
109 | |||
110 | if (!$this->isCacheableView($view)) { |
||
111 | return $this->send403("Requested view is not an asset"); |
||
112 | } |
||
113 | |||
114 | $content = $this->getProcessedView($view, $viewtype); |
||
115 | if ($content === false) { |
||
116 | return $this->send403(); |
||
117 | } |
||
118 | |||
119 | $etag = '"' . md5($content) . '"'; |
||
120 | $this->setRevalidateHeaders($etag, $response); |
||
121 | if ($this->is304($etag)) { |
||
122 | return Response::create()->setNotModified(); |
||
123 | } |
||
124 | |||
125 | return $response->setContent($content); |
||
126 | } |
||
127 | |||
128 | $etag = "\"$ts\""; |
||
129 | if ($this->is304($etag)) { |
||
130 | return Response::create()->setNotModified(); |
||
131 | } |
||
132 | |||
133 | // trust the client but check for an existing cache file |
||
134 | $filename = $config->cacheroot . "views_simplecache/$ts/$viewtype/$view"; |
||
135 | if (file_exists($filename)) { |
||
136 | $this->sendCacheHeaders($etag, $response); |
||
137 | return BinaryFileResponse::create($filename, 200, $response->headers->all()); |
||
138 | } |
||
139 | |||
140 | // the hard way |
||
141 | $this->application->bootCore(); |
||
142 | header_remove('Cache-Control'); |
||
143 | header_remove('Pragma'); |
||
144 | header_remove('Expires'); |
||
145 | |||
146 | elgg_set_viewtype($viewtype); |
||
147 | if (!$this->isCacheableView($view)) { |
||
148 | return $this->send403("Requested view is not an asset"); |
||
149 | } |
||
150 | |||
151 | $lastcache = (int) $config->lastcache; |
||
152 | |||
153 | $filename = $config->cacheroot . "views_simplecache/$lastcache/$viewtype/$view"; |
||
154 | |||
155 | if ($lastcache == $ts) { |
||
156 | $this->sendCacheHeaders($etag, $response); |
||
157 | |||
158 | $content = $this->getProcessedView($view, $viewtype); |
||
159 | |||
160 | $dir_name = dirname($filename); |
||
161 | if (!is_dir($dir_name)) { |
||
162 | // PHP and the server accessing the cache symlink may be a different user. And here |
||
163 | // it's safe to make everything readable anyway. |
||
164 | mkdir($dir_name, 0775, true); |
||
165 | } |
||
166 | |||
167 | file_put_contents($filename, $content); |
||
168 | chmod($filename, 0664); |
||
169 | 6 | } else { |
|
170 | // if wrong timestamp, don't send HTTP cache |
||
171 | 6 | $content = $this->getProcessedView($view, $viewtype); |
|
172 | 1 | } |
|
173 | |||
174 | return $response->setContent($content); |
||
175 | 5 | } |
|
176 | 1 | ||
177 | /** |
||
178 | * Parse a request |
||
179 | * |
||
180 | * @param string $path Request URL path |
||
181 | * @return array Cache parameters (empty array if failure) |
||
182 | 4 | */ |
|
183 | 3 | public function parsePath($path) { |
|
184 | // no '..' |
||
185 | if (false !== strpos($path, '..')) { |
||
186 | return []; |
||
187 | 1 | } |
|
188 | 1 | // only alphanumeric characters plus /, ., -, and _ |
|
189 | 1 | if (preg_match('#[^a-zA-Z0-9/\.\-_]#', $path)) { |
|
190 | return []; |
||
191 | } |
||
192 | |||
193 | // testing showed regex to be marginally faster than array / string functions over 100000 reps |
||
194 | // it won't make a difference in real life and regex is easier to read. |
||
195 | // <ts>/<viewtype>/<name/of/view.and.dots>.<type> |
||
196 | if (!preg_match('#^/cache/([0-9]+)/([^/]+)/(.+)$#', $path, $matches)) { |
||
197 | return []; |
||
198 | } |
||
199 | |||
200 | return [ |
||
201 | 'ts' => $matches[1], |
||
202 | 'viewtype' => $matches[2], |
||
203 | 'view' => $matches[3], |
||
204 | ]; |
||
205 | } |
||
206 | |||
207 | /** |
||
208 | * Is the view cacheable. Language views are handled specially. |
||
209 | * |
||
210 | * @param string $view View name |
||
211 | * |
||
212 | * @return bool |
||
213 | */ |
||
214 | protected function isCacheableView($view) { |
||
215 | if (preg_match('~^languages/(.*)\.js$~', $view, $m)) { |
||
216 | return in_array($m[1], _elgg_services()->translator->getAllLanguageCodes()); |
||
217 | } |
||
218 | return _elgg_services()->views->isCacheableView($view); |
||
219 | } |
||
220 | |||
221 | /** |
||
222 | * Send cache headers |
||
223 | * |
||
224 | * @param string $etag ETag value |
||
225 | * @return void |
||
226 | */ |
||
227 | protected function sendCacheHeaders($etag, Response $response) { |
||
232 | |||
233 | /** |
||
234 | * Send revalidate cache headers |
||
235 | * |
||
236 | * @param string $etag ETag value |
||
237 | * @return void |
||
238 | */ |
||
239 | protected function setRevalidateHeaders($etag, Response $response) { |
||
243 | |||
244 | /** |
||
245 | * Send a 304 and exit() if the ETag matches the request |
||
246 | * |
||
247 | * @param string $etag ETag value |
||
248 | * @return bool |
||
249 | */ |
||
250 | protected function is304($etag) { |
||
265 | |||
266 | /** |
||
267 | * Get the content type |
||
268 | * |
||
269 | * @param string $view The view name |
||
270 | * |
||
271 | * @return string|null |
||
272 | * @access private |
||
273 | */ |
||
274 | public function getContentType($view) { |
||
283 | |||
284 | /** |
||
285 | * Returns the type of output expected from the view. |
||
286 | * |
||
287 | * - view/name.extension returns "extension" if "extension" is valid |
||
288 | * - css/view return "css" |
||
289 | * - js/view return "js" |
||
290 | * - Otherwise, returns "unknown" |
||
291 | * |
||
292 | * @param string $view The view name |
||
293 | * @return string |
||
294 | * @access private |
||
295 | */ |
||
296 | public function getViewFileType($view) { |
||
310 | |||
311 | /** |
||
312 | * Get the contents of a view for caching |
||
313 | * |
||
314 | * @param string $view The view name |
||
315 | * @param string $viewtype The viewtype |
||
316 | * @return string|false |
||
317 | * @see CacheHandler::renderView() |
||
318 | */ |
||
319 | protected function getProcessedView($view, $viewtype) { |
||
338 | |||
339 | /** |
||
340 | * Render a view for caching. Language views are handled specially. |
||
341 | * |
||
342 | * @param string $view The view name |
||
343 | * @param string $viewtype The viewtype |
||
344 | * @return string|false |
||
345 | */ |
||
346 | protected function renderView($view, $viewtype) { |
||
365 | |||
366 | /** |
||
367 | * Send an error message to requestor |
||
368 | * |
||
369 | * @param string $msg Optional message text |
||
370 | * @return Response |
||
371 | */ |
||
372 | protected function send403($msg = 'Cache error: bad request') { |
||
375 | } |
||
376 | |||
377 |