vendor/symfony/src/Symfony/Component/HttpKernel/HttpCache/Store.php
changeset 0 7f95f8617b0b
equal deleted inserted replaced
-1:000000000000 0:7f95f8617b0b
       
     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 }