|
0
|
1 |
<?php |
|
|
2 |
|
|
|
3 |
/* |
|
|
4 |
* This file is part of the Symfony package. |
|
|
5 |
* |
|
|
6 |
* (c) Fabien Potencier <fabien@symfony.com> |
|
|
7 |
* |
|
|
8 |
* This code is partially based on the Rack-Cache library by Ryan Tomayko, |
|
|
9 |
* which is released under the MIT license. |
|
|
10 |
* |
|
|
11 |
* For the full copyright and license information, please view the LICENSE |
|
|
12 |
* file that was distributed with this source code. |
|
|
13 |
*/ |
|
|
14 |
|
|
|
15 |
namespace Symfony\Component\HttpKernel\HttpCache; |
|
|
16 |
|
|
|
17 |
use Symfony\Component\HttpFoundation\Request; |
|
|
18 |
use Symfony\Component\HttpFoundation\Response; |
|
|
19 |
use Symfony\Component\HttpFoundation\HeaderBag; |
|
|
20 |
|
|
|
21 |
/** |
|
|
22 |
* Store implements all the logic for storing cache metadata (Request and Response headers). |
|
|
23 |
* |
|
|
24 |
* @author Fabien Potencier <fabien@symfony.com> |
|
|
25 |
*/ |
|
|
26 |
class Store implements StoreInterface |
|
|
27 |
{ |
|
|
28 |
private $root; |
|
|
29 |
private $keyCache; |
|
|
30 |
private $locks; |
|
|
31 |
|
|
|
32 |
/** |
|
|
33 |
* Constructor. |
|
|
34 |
* |
|
|
35 |
* @param string $root The path to the cache directory |
|
|
36 |
*/ |
|
|
37 |
public function __construct($root) |
|
|
38 |
{ |
|
|
39 |
$this->root = $root; |
|
|
40 |
if (!is_dir($this->root)) { |
|
|
41 |
mkdir($this->root, 0777, true); |
|
|
42 |
} |
|
|
43 |
$this->keyCache = new \SplObjectStorage(); |
|
|
44 |
$this->locks = array(); |
|
|
45 |
} |
|
|
46 |
|
|
|
47 |
/** |
|
|
48 |
* Cleanups storage. |
|
|
49 |
*/ |
|
|
50 |
public function cleanup() |
|
|
51 |
{ |
|
|
52 |
// unlock everything |
|
|
53 |
foreach ($this->locks as $lock) { |
|
|
54 |
@unlink($lock); |
|
|
55 |
} |
|
|
56 |
|
|
|
57 |
$error = error_get_last(); |
|
|
58 |
if (1 === $error['type'] && false === headers_sent()) { |
|
|
59 |
// send a 503 |
|
|
60 |
header('HTTP/1.0 503 Service Unavailable'); |
|
|
61 |
header('Retry-After: 10'); |
|
|
62 |
echo '503 Service Unavailable'; |
|
|
63 |
} |
|
|
64 |
} |
|
|
65 |
|
|
|
66 |
/** |
|
|
67 |
* Locks the cache for a given Request. |
|
|
68 |
* |
|
|
69 |
* @param Request $request A Request instance |
|
|
70 |
* |
|
|
71 |
* @return Boolean|string true if the lock is acquired, the path to the current lock otherwise |
|
|
72 |
*/ |
|
|
73 |
public function lock(Request $request) |
|
|
74 |
{ |
|
|
75 |
if (false !== $lock = @fopen($path = $this->getPath($this->getCacheKey($request).'.lck'), 'x')) { |
|
|
76 |
fclose($lock); |
|
|
77 |
|
|
|
78 |
$this->locks[] = $path; |
|
|
79 |
|
|
|
80 |
return true; |
|
|
81 |
} |
|
|
82 |
|
|
|
83 |
return $path; |
|
|
84 |
} |
|
|
85 |
|
|
|
86 |
/** |
|
|
87 |
* Releases the lock for the given Request. |
|
|
88 |
* |
|
|
89 |
* @param Request $request A Request instance |
|
|
90 |
*/ |
|
|
91 |
public function unlock(Request $request) |
|
|
92 |
{ |
|
|
93 |
return @unlink($this->getPath($this->getCacheKey($request).'.lck')); |
|
|
94 |
} |
|
|
95 |
|
|
|
96 |
/** |
|
|
97 |
* Locates a cached Response for the Request provided. |
|
|
98 |
* |
|
|
99 |
* @param Request $request A Request instance |
|
|
100 |
* |
|
|
101 |
* @return Response|null A Response instance, or null if no cache entry was found |
|
|
102 |
*/ |
|
|
103 |
public function lookup(Request $request) |
|
|
104 |
{ |
|
|
105 |
$key = $this->getCacheKey($request); |
|
|
106 |
|
|
|
107 |
if (!$entries = $this->getMetadata($key)) { |
|
|
108 |
return null; |
|
|
109 |
} |
|
|
110 |
|
|
|
111 |
// find a cached entry that matches the request. |
|
|
112 |
$match = null; |
|
|
113 |
foreach ($entries as $entry) { |
|
|
114 |
if ($this->requestsMatch(isset($entry[1]['vary']) ? $entry[1]['vary'][0] : '', $request->headers->all(), $entry[0])) { |
|
|
115 |
$match = $entry; |
|
|
116 |
|
|
|
117 |
break; |
|
|
118 |
} |
|
|
119 |
} |
|
|
120 |
|
|
|
121 |
if (null === $match) { |
|
|
122 |
return null; |
|
|
123 |
} |
|
|
124 |
|
|
|
125 |
list($req, $headers) = $match; |
|
|
126 |
if (file_exists($body = $this->getPath($headers['x-content-digest'][0]))) { |
|
|
127 |
return $this->restoreResponse($headers, $body); |
|
|
128 |
} |
|
|
129 |
|
|
|
130 |
// TODO the metaStore referenced an entity that doesn't exist in |
|
|
131 |
// the entityStore. We definitely want to return nil but we should |
|
|
132 |
// also purge the entry from the meta-store when this is detected. |
|
|
133 |
return null; |
|
|
134 |
} |
|
|
135 |
|
|
|
136 |
/** |
|
|
137 |
* Writes a cache entry to the store for the given Request and Response. |
|
|
138 |
* |
|
|
139 |
* Existing entries are read and any that match the response are removed. This |
|
|
140 |
* method calls write with the new list of cache entries. |
|
|
141 |
* |
|
|
142 |
* @param Request $request A Request instance |
|
|
143 |
* @param Response $response A Response instance |
|
|
144 |
* |
|
|
145 |
* @return string The key under which the response is stored |
|
|
146 |
*/ |
|
|
147 |
public function write(Request $request, Response $response) |
|
|
148 |
{ |
|
|
149 |
$key = $this->getCacheKey($request); |
|
|
150 |
$storedEnv = $this->persistRequest($request); |
|
|
151 |
|
|
|
152 |
// write the response body to the entity store if this is the original response |
|
|
153 |
if (!$response->headers->has('X-Content-Digest')) { |
|
|
154 |
$digest = 'en'.sha1($response->getContent()); |
|
|
155 |
|
|
|
156 |
if (false === $this->save($digest, $response->getContent())) { |
|
|
157 |
throw new \RuntimeException('Unable to store the entity.'); |
|
|
158 |
} |
|
|
159 |
|
|
|
160 |
$response->headers->set('X-Content-Digest', $digest); |
|
|
161 |
|
|
|
162 |
if (!$response->headers->has('Transfer-Encoding')) { |
|
|
163 |
$response->headers->set('Content-Length', strlen($response->getContent())); |
|
|
164 |
} |
|
|
165 |
} |
|
|
166 |
|
|
|
167 |
// read existing cache entries, remove non-varying, and add this one to the list |
|
|
168 |
$entries = array(); |
|
|
169 |
$vary = $response->headers->get('vary'); |
|
|
170 |
foreach ($this->getMetadata($key) as $entry) { |
|
|
171 |
if (!isset($entry[1]['vary'])) { |
|
|
172 |
$entry[1]['vary'] = array(''); |
|
|
173 |
} |
|
|
174 |
|
|
|
175 |
if ($vary != $entry[1]['vary'][0] || !$this->requestsMatch($vary, $entry[0], $storedEnv)) { |
|
|
176 |
$entries[] = $entry; |
|
|
177 |
} |
|
|
178 |
} |
|
|
179 |
|
|
|
180 |
$headers = $this->persistResponse($response); |
|
|
181 |
unset($headers['age']); |
|
|
182 |
|
|
|
183 |
array_unshift($entries, array($storedEnv, $headers)); |
|
|
184 |
|
|
|
185 |
if (false === $this->save($key, serialize($entries))) { |
|
|
186 |
throw new \RuntimeException('Unable to store the metadata.'); |
|
|
187 |
} |
|
|
188 |
|
|
|
189 |
return $key; |
|
|
190 |
} |
|
|
191 |
|
|
|
192 |
/** |
|
|
193 |
* Invalidates all cache entries that match the request. |
|
|
194 |
* |
|
|
195 |
* @param Request $request A Request instance |
|
|
196 |
*/ |
|
|
197 |
public function invalidate(Request $request) |
|
|
198 |
{ |
|
|
199 |
$modified = false; |
|
|
200 |
$key = $this->getCacheKey($request); |
|
|
201 |
|
|
|
202 |
$entries = array(); |
|
|
203 |
foreach ($this->getMetadata($key) as $entry) { |
|
|
204 |
$response = $this->restoreResponse($entry[1]); |
|
|
205 |
if ($response->isFresh()) { |
|
|
206 |
$response->expire(); |
|
|
207 |
$modified = true; |
|
|
208 |
$entries[] = array($entry[0], $this->persistResponse($response)); |
|
|
209 |
} else { |
|
|
210 |
$entries[] = $entry; |
|
|
211 |
} |
|
|
212 |
} |
|
|
213 |
|
|
|
214 |
if ($modified) { |
|
|
215 |
if (false === $this->save($key, serialize($entries))) { |
|
|
216 |
throw new \RuntimeException('Unable to store the metadata.'); |
|
|
217 |
} |
|
|
218 |
} |
|
|
219 |
|
|
|
220 |
// As per the RFC, invalidate Location and Content-Location URLs if present |
|
|
221 |
foreach (array('Location', 'Content-Location') as $header) { |
|
|
222 |
if ($uri = $request->headers->get($header)) { |
|
|
223 |
$subRequest = Request::create($uri, 'get', array(), array(), array(), $request->server->all()); |
|
|
224 |
|
|
|
225 |
$this->invalidate($subRequest); |
|
|
226 |
} |
|
|
227 |
} |
|
|
228 |
} |
|
|
229 |
|
|
|
230 |
/** |
|
|
231 |
* Determines whether two Request HTTP header sets are non-varying based on |
|
|
232 |
* the vary response header value provided. |
|
|
233 |
* |
|
|
234 |
* @param string $vary A Response vary header |
|
|
235 |
* @param array $env1 A Request HTTP header array |
|
|
236 |
* @param array $env2 A Request HTTP header array |
|
|
237 |
* |
|
|
238 |
* @return Boolean true if the the two environments match, false otherwise |
|
|
239 |
*/ |
|
|
240 |
private function requestsMatch($vary, $env1, $env2) |
|
|
241 |
{ |
|
|
242 |
if (empty($vary)) { |
|
|
243 |
return true; |
|
|
244 |
} |
|
|
245 |
|
|
|
246 |
foreach (preg_split('/[\s,]+/', $vary) as $header) { |
|
|
247 |
$key = strtr(strtolower($header), '_', '-'); |
|
|
248 |
$v1 = isset($env1[$key]) ? $env1[$key] : null; |
|
|
249 |
$v2 = isset($env2[$key]) ? $env2[$key] : null; |
|
|
250 |
if ($v1 !== $v2) { |
|
|
251 |
return false; |
|
|
252 |
} |
|
|
253 |
} |
|
|
254 |
|
|
|
255 |
return true; |
|
|
256 |
} |
|
|
257 |
|
|
|
258 |
/** |
|
|
259 |
* Gets all data associated with the given key. |
|
|
260 |
* |
|
|
261 |
* Use this method only if you know what you are doing. |
|
|
262 |
* |
|
|
263 |
* @param string $key The store key |
|
|
264 |
* |
|
|
265 |
* @return array An array of data associated with the key |
|
|
266 |
*/ |
|
|
267 |
private function getMetadata($key) |
|
|
268 |
{ |
|
|
269 |
if (false === $entries = $this->load($key)) { |
|
|
270 |
return array(); |
|
|
271 |
} |
|
|
272 |
|
|
|
273 |
return unserialize($entries); |
|
|
274 |
} |
|
|
275 |
|
|
|
276 |
/** |
|
|
277 |
* Purges data for the given URL. |
|
|
278 |
* |
|
|
279 |
* @param string $url A URL |
|
|
280 |
* |
|
|
281 |
* @return Boolean true if the URL exists and has been purged, false otherwise |
|
|
282 |
*/ |
|
|
283 |
public function purge($url) |
|
|
284 |
{ |
|
|
285 |
if (file_exists($path = $this->getPath($this->getCacheKey(Request::create($url))))) { |
|
|
286 |
unlink($path); |
|
|
287 |
|
|
|
288 |
return true; |
|
|
289 |
} |
|
|
290 |
|
|
|
291 |
return false; |
|
|
292 |
} |
|
|
293 |
|
|
|
294 |
/** |
|
|
295 |
* Loads data for the given key. |
|
|
296 |
* |
|
|
297 |
* @param string $key The store key |
|
|
298 |
* |
|
|
299 |
* @return string The data associated with the key |
|
|
300 |
*/ |
|
|
301 |
private function load($key) |
|
|
302 |
{ |
|
|
303 |
$path = $this->getPath($key); |
|
|
304 |
|
|
|
305 |
return file_exists($path) ? file_get_contents($path) : false; |
|
|
306 |
} |
|
|
307 |
|
|
|
308 |
/** |
|
|
309 |
* Save data for the given key. |
|
|
310 |
* |
|
|
311 |
* @param string $key The store key |
|
|
312 |
* @param string $data The data to store |
|
|
313 |
*/ |
|
|
314 |
private function save($key, $data) |
|
|
315 |
{ |
|
|
316 |
$path = $this->getPath($key); |
|
|
317 |
if (!is_dir(dirname($path)) && false === @mkdir(dirname($path), 0777, true)) { |
|
|
318 |
return false; |
|
|
319 |
} |
|
|
320 |
|
|
|
321 |
$tmpFile = tempnam(dirname($path), basename($path)); |
|
|
322 |
if (false === $fp = @fopen($tmpFile, 'wb')) { |
|
|
323 |
return false; |
|
|
324 |
} |
|
|
325 |
@fwrite($fp, $data); |
|
|
326 |
@fclose($fp); |
|
|
327 |
|
|
|
328 |
if ($data != file_get_contents($tmpFile)) { |
|
|
329 |
return false; |
|
|
330 |
} |
|
|
331 |
|
|
|
332 |
if (false === @rename($tmpFile, $path)) { |
|
|
333 |
return false; |
|
|
334 |
} |
|
|
335 |
|
|
|
336 |
chmod($path, 0644); |
|
|
337 |
} |
|
|
338 |
|
|
|
339 |
public function getPath($key) |
|
|
340 |
{ |
|
|
341 |
return $this->root.DIRECTORY_SEPARATOR.substr($key, 0, 2).DIRECTORY_SEPARATOR.substr($key, 2, 2).DIRECTORY_SEPARATOR.substr($key, 4, 2).DIRECTORY_SEPARATOR.substr($key, 6); |
|
|
342 |
} |
|
|
343 |
|
|
|
344 |
/** |
|
|
345 |
* Returns a cache key for the given Request. |
|
|
346 |
* |
|
|
347 |
* @param Request $request A Request instance |
|
|
348 |
* |
|
|
349 |
* @return string A key for the given Request |
|
|
350 |
*/ |
|
|
351 |
private function getCacheKey(Request $request) |
|
|
352 |
{ |
|
|
353 |
if (isset($this->keyCache[$request])) { |
|
|
354 |
return $this->keyCache[$request]; |
|
|
355 |
} |
|
|
356 |
|
|
|
357 |
return $this->keyCache[$request] = 'md'.sha1($request->getUri()); |
|
|
358 |
} |
|
|
359 |
|
|
|
360 |
/** |
|
|
361 |
* Persists the Request HTTP headers. |
|
|
362 |
* |
|
|
363 |
* @param Request $request A Request instance |
|
|
364 |
* |
|
|
365 |
* @return array An array of HTTP headers |
|
|
366 |
*/ |
|
|
367 |
private function persistRequest(Request $request) |
|
|
368 |
{ |
|
|
369 |
return $request->headers->all(); |
|
|
370 |
} |
|
|
371 |
|
|
|
372 |
/** |
|
|
373 |
* Persists the Response HTTP headers. |
|
|
374 |
* |
|
|
375 |
* @param Response $response A Response instance |
|
|
376 |
* |
|
|
377 |
* @return array An array of HTTP headers |
|
|
378 |
*/ |
|
|
379 |
private function persistResponse(Response $response) |
|
|
380 |
{ |
|
|
381 |
$headers = $response->headers->all(); |
|
|
382 |
$headers['X-Status'] = array($response->getStatusCode()); |
|
|
383 |
|
|
|
384 |
return $headers; |
|
|
385 |
} |
|
|
386 |
|
|
|
387 |
/** |
|
|
388 |
* Restores a Response from the HTTP headers and body. |
|
|
389 |
* |
|
|
390 |
* @param array $headers An array of HTTP headers for the Response |
|
|
391 |
* @param string $body The Response body |
|
|
392 |
*/ |
|
|
393 |
private function restoreResponse($headers, $body = null) |
|
|
394 |
{ |
|
|
395 |
$status = $headers['X-Status'][0]; |
|
|
396 |
unset($headers['X-Status']); |
|
|
397 |
|
|
|
398 |
if (null !== $body) { |
|
|
399 |
$headers['X-Body-File'] = array($body); |
|
|
400 |
} |
|
|
401 |
|
|
|
402 |
return new Response($body, $status, $headers); |
|
|
403 |
} |
|
|
404 |
} |