d20bfaa1b0c4c28ed18b47f14064ae80335af85a
[yaffs-website] / vendor / symfony / http-kernel / HttpCache / Store.php
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
20 /**
21  * Store implements all the logic for storing cache metadata (Request and Response headers).
22  *
23  * @author Fabien Potencier <fabien@symfony.com>
24  */
25 class Store implements StoreInterface
26 {
27     protected $root;
28     private $keyCache;
29     private $locks;
30
31     /**
32      * @param string $root The path to the cache directory
33      *
34      * @throws \RuntimeException
35      */
36     public function __construct($root)
37     {
38         $this->root = $root;
39         if (!file_exists($this->root) && !@mkdir($this->root, 0777, true) && !is_dir($this->root)) {
40             throw new \RuntimeException(sprintf('Unable to create the store directory (%s).', $this->root));
41         }
42         $this->keyCache = new \SplObjectStorage();
43         $this->locks = array();
44     }
45
46     /**
47      * Cleanups storage.
48      */
49     public function cleanup()
50     {
51         // unlock everything
52         foreach ($this->locks as $lock) {
53             flock($lock, LOCK_UN);
54             fclose($lock);
55         }
56
57         $this->locks = array();
58     }
59
60     /**
61      * Tries to lock the cache for a given Request, without blocking.
62      *
63      * @return bool|string true if the lock is acquired, the path to the current lock otherwise
64      */
65     public function lock(Request $request)
66     {
67         $key = $this->getCacheKey($request);
68
69         if (!isset($this->locks[$key])) {
70             $path = $this->getPath($key);
71             if (!file_exists(dirname($path)) && false === @mkdir(dirname($path), 0777, true) && !is_dir(dirname($path))) {
72                 return $path;
73             }
74             $h = fopen($path, 'cb');
75             if (!flock($h, LOCK_EX | LOCK_NB)) {
76                 fclose($h);
77
78                 return $path;
79             }
80
81             $this->locks[$key] = $h;
82         }
83
84         return true;
85     }
86
87     /**
88      * Releases the lock for the given Request.
89      *
90      * @return bool False if the lock file does not exist or cannot be unlocked, true otherwise
91      */
92     public function unlock(Request $request)
93     {
94         $key = $this->getCacheKey($request);
95
96         if (isset($this->locks[$key])) {
97             flock($this->locks[$key], LOCK_UN);
98             fclose($this->locks[$key]);
99             unset($this->locks[$key]);
100
101             return true;
102         }
103
104         return false;
105     }
106
107     public function isLocked(Request $request)
108     {
109         $key = $this->getCacheKey($request);
110
111         if (isset($this->locks[$key])) {
112             return true; // shortcut if lock held by this process
113         }
114
115         if (!file_exists($path = $this->getPath($key))) {
116             return false;
117         }
118
119         $h = fopen($path, 'rb');
120         flock($h, LOCK_EX | LOCK_NB, $wouldBlock);
121         flock($h, LOCK_UN); // release the lock we just acquired
122         fclose($h);
123
124         return (bool) $wouldBlock;
125     }
126
127     /**
128      * Locates a cached Response for the Request provided.
129      *
130      * @return Response|null A Response instance, or null if no cache entry was found
131      */
132     public function lookup(Request $request)
133     {
134         $key = $this->getCacheKey($request);
135
136         if (!$entries = $this->getMetadata($key)) {
137             return;
138         }
139
140         // find a cached entry that matches the request.
141         $match = null;
142         foreach ($entries as $entry) {
143             if ($this->requestsMatch(isset($entry[1]['vary'][0]) ? implode(', ', $entry[1]['vary']) : '', $request->headers->all(), $entry[0])) {
144                 $match = $entry;
145
146                 break;
147             }
148         }
149
150         if (null === $match) {
151             return;
152         }
153
154         $headers = $match[1];
155         if (file_exists($body = $this->getPath($headers['x-content-digest'][0]))) {
156             return $this->restoreResponse($headers, $body);
157         }
158
159         // TODO the metaStore referenced an entity that doesn't exist in
160         // the entityStore. We definitely want to return nil but we should
161         // also purge the entry from the meta-store when this is detected.
162     }
163
164     /**
165      * Writes a cache entry to the store for the given Request and Response.
166      *
167      * Existing entries are read and any that match the response are removed. This
168      * method calls write with the new list of cache entries.
169      *
170      * @return string The key under which the response is stored
171      *
172      * @throws \RuntimeException
173      */
174     public function write(Request $request, Response $response)
175     {
176         $key = $this->getCacheKey($request);
177         $storedEnv = $this->persistRequest($request);
178
179         // write the response body to the entity store if this is the original response
180         if (!$response->headers->has('X-Content-Digest')) {
181             $digest = $this->generateContentDigest($response);
182
183             if (false === $this->save($digest, $response->getContent())) {
184                 throw new \RuntimeException('Unable to store the entity.');
185             }
186
187             $response->headers->set('X-Content-Digest', $digest);
188
189             if (!$response->headers->has('Transfer-Encoding')) {
190                 $response->headers->set('Content-Length', strlen($response->getContent()));
191             }
192         }
193
194         // read existing cache entries, remove non-varying, and add this one to the list
195         $entries = array();
196         $vary = $response->headers->get('vary');
197         foreach ($this->getMetadata($key) as $entry) {
198             if (!isset($entry[1]['vary'][0])) {
199                 $entry[1]['vary'] = array('');
200             }
201
202             if ($entry[1]['vary'][0] != $vary || !$this->requestsMatch($vary, $entry[0], $storedEnv)) {
203                 $entries[] = $entry;
204             }
205         }
206
207         $headers = $this->persistResponse($response);
208         unset($headers['age']);
209
210         array_unshift($entries, array($storedEnv, $headers));
211
212         if (false === $this->save($key, serialize($entries))) {
213             throw new \RuntimeException('Unable to store the metadata.');
214         }
215
216         return $key;
217     }
218
219     /**
220      * Returns content digest for $response.
221      *
222      * @return string
223      */
224     protected function generateContentDigest(Response $response)
225     {
226         return 'en'.hash('sha256', $response->getContent());
227     }
228
229     /**
230      * Invalidates all cache entries that match the request.
231      *
232      * @throws \RuntimeException
233      */
234     public function invalidate(Request $request)
235     {
236         $modified = false;
237         $key = $this->getCacheKey($request);
238
239         $entries = array();
240         foreach ($this->getMetadata($key) as $entry) {
241             $response = $this->restoreResponse($entry[1]);
242             if ($response->isFresh()) {
243                 $response->expire();
244                 $modified = true;
245                 $entries[] = array($entry[0], $this->persistResponse($response));
246             } else {
247                 $entries[] = $entry;
248             }
249         }
250
251         if ($modified && false === $this->save($key, serialize($entries))) {
252             throw new \RuntimeException('Unable to store the metadata.');
253         }
254     }
255
256     /**
257      * Determines whether two Request HTTP header sets are non-varying based on
258      * the vary response header value provided.
259      *
260      * @param string $vary A Response vary header
261      * @param array  $env1 A Request HTTP header array
262      * @param array  $env2 A Request HTTP header array
263      *
264      * @return bool true if the two environments match, false otherwise
265      */
266     private function requestsMatch($vary, $env1, $env2)
267     {
268         if (empty($vary)) {
269             return true;
270         }
271
272         foreach (preg_split('/[\s,]+/', $vary) as $header) {
273             $key = str_replace('_', '-', strtolower($header));
274             $v1 = isset($env1[$key]) ? $env1[$key] : null;
275             $v2 = isset($env2[$key]) ? $env2[$key] : null;
276             if ($v1 !== $v2) {
277                 return false;
278             }
279         }
280
281         return true;
282     }
283
284     /**
285      * Gets all data associated with the given key.
286      *
287      * Use this method only if you know what you are doing.
288      *
289      * @param string $key The store key
290      *
291      * @return array An array of data associated with the key
292      */
293     private function getMetadata($key)
294     {
295         if (!$entries = $this->load($key)) {
296             return array();
297         }
298
299         return unserialize($entries);
300     }
301
302     /**
303      * Purges data for the given URL.
304      *
305      * This method purges both the HTTP and the HTTPS version of the cache entry.
306      *
307      * @param string $url A URL
308      *
309      * @return bool true if the URL exists with either HTTP or HTTPS scheme and has been purged, false otherwise
310      */
311     public function purge($url)
312     {
313         $http = preg_replace('#^https:#', 'http:', $url);
314         $https = preg_replace('#^http:#', 'https:', $url);
315
316         $purgedHttp = $this->doPurge($http);
317         $purgedHttps = $this->doPurge($https);
318
319         return $purgedHttp || $purgedHttps;
320     }
321
322     /**
323      * Purges data for the given URL.
324      *
325      * @param string $url A URL
326      *
327      * @return bool true if the URL exists and has been purged, false otherwise
328      */
329     private function doPurge($url)
330     {
331         $key = $this->getCacheKey(Request::create($url));
332         if (isset($this->locks[$key])) {
333             flock($this->locks[$key], LOCK_UN);
334             fclose($this->locks[$key]);
335             unset($this->locks[$key]);
336         }
337
338         if (file_exists($path = $this->getPath($key))) {
339             unlink($path);
340
341             return true;
342         }
343
344         return false;
345     }
346
347     /**
348      * Loads data for the given key.
349      *
350      * @param string $key The store key
351      *
352      * @return string The data associated with the key
353      */
354     private function load($key)
355     {
356         $path = $this->getPath($key);
357
358         return file_exists($path) ? file_get_contents($path) : false;
359     }
360
361     /**
362      * Save data for the given key.
363      *
364      * @param string $key  The store key
365      * @param string $data The data to store
366      *
367      * @return bool
368      */
369     private function save($key, $data)
370     {
371         $path = $this->getPath($key);
372
373         if (isset($this->locks[$key])) {
374             $fp = $this->locks[$key];
375             @ftruncate($fp, 0);
376             @fseek($fp, 0);
377             $len = @fwrite($fp, $data);
378             if (strlen($data) !== $len) {
379                 @ftruncate($fp, 0);
380
381                 return false;
382             }
383         } else {
384             if (!file_exists(dirname($path)) && false === @mkdir(dirname($path), 0777, true) && !is_dir(dirname($path))) {
385                 return false;
386             }
387
388             $tmpFile = tempnam(dirname($path), basename($path));
389             if (false === $fp = @fopen($tmpFile, 'wb')) {
390                 @unlink($tmpFile);
391
392                 return false;
393             }
394             @fwrite($fp, $data);
395             @fclose($fp);
396
397             if ($data != file_get_contents($tmpFile)) {
398                 @unlink($tmpFile);
399
400                 return false;
401             }
402
403             if (false === @rename($tmpFile, $path)) {
404                 @unlink($tmpFile);
405
406                 return false;
407             }
408         }
409
410         @chmod($path, 0666 & ~umask());
411     }
412
413     public function getPath($key)
414     {
415         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);
416     }
417
418     /**
419      * Generates a cache key for the given Request.
420      *
421      * This method should return a key that must only depend on a
422      * normalized version of the request URI.
423      *
424      * If the same URI can have more than one representation, based on some
425      * headers, use a Vary header to indicate them, and each representation will
426      * be stored independently under the same cache key.
427      *
428      * @return string A key for the given Request
429      */
430     protected function generateCacheKey(Request $request)
431     {
432         return 'md'.hash('sha256', $request->getUri());
433     }
434
435     /**
436      * Returns a cache key for the given Request.
437      *
438      * @return string A key for the given Request
439      */
440     private function getCacheKey(Request $request)
441     {
442         if (isset($this->keyCache[$request])) {
443             return $this->keyCache[$request];
444         }
445
446         return $this->keyCache[$request] = $this->generateCacheKey($request);
447     }
448
449     /**
450      * Persists the Request HTTP headers.
451      *
452      * @return array An array of HTTP headers
453      */
454     private function persistRequest(Request $request)
455     {
456         return $request->headers->all();
457     }
458
459     /**
460      * Persists the Response HTTP headers.
461      *
462      * @return array An array of HTTP headers
463      */
464     private function persistResponse(Response $response)
465     {
466         $headers = $response->headers->all();
467         $headers['X-Status'] = array($response->getStatusCode());
468
469         return $headers;
470     }
471
472     /**
473      * Restores a Response from the HTTP headers and body.
474      *
475      * @param array  $headers An array of HTTP headers for the Response
476      * @param string $body    The Response body
477      *
478      * @return Response
479      */
480     private function restoreResponse($headers, $body = null)
481     {
482         $status = $headers['X-Status'][0];
483         unset($headers['X-Status']);
484
485         if (null !== $body) {
486             $headers['X-Body-File'] = array($body);
487         }
488
489         return new Response($body, $status, $headers);
490     }
491 }