2010-06-23 20:42:41 +01:00
|
|
|
<?php
|
|
|
|
|
|
|
|
/*
|
2011-01-15 13:29:43 +00:00
|
|
|
* This file is part of the Symfony package.
|
2010-06-23 20:42:41 +01:00
|
|
|
*
|
2011-03-06 11:40:06 +00:00
|
|
|
* (c) Fabien Potencier <fabien@symfony.com>
|
2010-06-23 20:42:41 +01:00
|
|
|
*
|
|
|
|
* This code is partially based on the Rack-Cache library by Ryan Tomayko,
|
|
|
|
* which is released under the MIT license.
|
|
|
|
*
|
2011-01-15 13:29:43 +00:00
|
|
|
* For the full copyright and license information, please view the LICENSE
|
|
|
|
* file that was distributed with this source code.
|
2010-06-23 20:42:41 +01:00
|
|
|
*/
|
|
|
|
|
2011-01-26 20:38:45 +00:00
|
|
|
namespace Symfony\Component\HttpKernel\HttpCache;
|
2011-01-15 13:29:43 +00:00
|
|
|
|
|
|
|
use Symfony\Component\HttpFoundation\Request;
|
|
|
|
use Symfony\Component\HttpFoundation\Response;
|
|
|
|
|
2010-06-23 20:42:41 +01:00
|
|
|
/**
|
|
|
|
* Store implements all the logic for storing cache metadata (Request and Response headers).
|
|
|
|
*
|
2011-03-06 11:40:06 +00:00
|
|
|
* @author Fabien Potencier <fabien@symfony.com>
|
2010-06-23 20:42:41 +01:00
|
|
|
*/
|
2011-01-31 13:10:53 +00:00
|
|
|
class Store implements StoreInterface
|
2010-06-23 20:42:41 +01:00
|
|
|
{
|
2012-11-14 22:35:13 +00:00
|
|
|
protected $root;
|
2011-03-23 18:47:16 +00:00
|
|
|
private $keyCache;
|
|
|
|
private $locks;
|
2010-06-23 20:42:41 +01:00
|
|
|
|
|
|
|
/**
|
|
|
|
* Constructor.
|
|
|
|
*
|
|
|
|
* @param string $root The path to the cache directory
|
|
|
|
*/
|
|
|
|
public function __construct($root)
|
|
|
|
{
|
|
|
|
$this->root = $root;
|
|
|
|
if (!is_dir($this->root)) {
|
2010-06-24 12:12:19 +01:00
|
|
|
mkdir($this->root, 0777, true);
|
2010-06-23 20:42:41 +01:00
|
|
|
}
|
|
|
|
$this->keyCache = new \SplObjectStorage();
|
|
|
|
$this->locks = array();
|
|
|
|
}
|
|
|
|
|
2011-01-31 13:10:53 +00:00
|
|
|
/**
|
|
|
|
* Cleanups storage.
|
|
|
|
*/
|
|
|
|
public function cleanup()
|
2010-06-23 20:42:41 +01:00
|
|
|
{
|
|
|
|
// unlock everything
|
|
|
|
foreach ($this->locks as $lock) {
|
2013-10-03 18:33:54 +01:00
|
|
|
!file_exists($lock) ?: @unlink($lock);
|
2010-06-23 20:42:41 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
$error = error_get_last();
|
|
|
|
if (1 === $error['type'] && false === headers_sent()) {
|
|
|
|
// send a 503
|
|
|
|
header('HTTP/1.0 503 Service Unavailable');
|
|
|
|
header('Retry-After: 10');
|
|
|
|
echo '503 Service Unavailable';
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Locks the cache for a given Request.
|
|
|
|
*
|
2010-07-27 14:33:28 +01:00
|
|
|
* @param Request $request A Request instance
|
2010-06-23 20:42:41 +01:00
|
|
|
*
|
|
|
|
* @return Boolean|string true if the lock is acquired, the path to the current lock otherwise
|
|
|
|
*/
|
|
|
|
public function lock(Request $request)
|
|
|
|
{
|
2012-08-30 05:05:27 +01:00
|
|
|
$path = $this->getPath($this->getCacheKey($request).'.lck');
|
|
|
|
if (!is_dir(dirname($path)) && false === @mkdir(dirname($path), 0777, true)) {
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
$lock = @fopen($path, 'x');
|
|
|
|
if (false !== $lock) {
|
2010-06-23 20:42:41 +01:00
|
|
|
fclose($lock);
|
|
|
|
|
|
|
|
$this->locks[] = $path;
|
|
|
|
|
|
|
|
return true;
|
|
|
|
}
|
2012-12-11 10:49:22 +00:00
|
|
|
|
2012-08-30 05:05:27 +01:00
|
|
|
return !file_exists($path) ?: $path;
|
2010-06-23 20:42:41 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Releases the lock for the given Request.
|
|
|
|
*
|
2010-07-27 14:33:28 +01:00
|
|
|
* @param Request $request A Request instance
|
2012-08-29 07:23:59 +01:00
|
|
|
*
|
|
|
|
* @return Boolean False if the lock file does not exist or cannot be unlocked, true otherwise
|
2010-06-23 20:42:41 +01:00
|
|
|
*/
|
|
|
|
public function unlock(Request $request)
|
|
|
|
{
|
2012-08-29 07:23:59 +01:00
|
|
|
$file = $this->getPath($this->getCacheKey($request).'.lck');
|
|
|
|
|
|
|
|
return is_file($file) ? @unlink($file) : false;
|
2012-08-30 05:05:27 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
public function isLocked(Request $request)
|
|
|
|
{
|
|
|
|
return is_file($this->getPath($this->getCacheKey($request).'.lck'));
|
2010-06-23 20:42:41 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Locates a cached Response for the Request provided.
|
|
|
|
*
|
2010-07-27 14:33:28 +01:00
|
|
|
* @param Request $request A Request instance
|
2010-06-23 20:42:41 +01:00
|
|
|
*
|
2010-07-27 14:33:28 +01:00
|
|
|
* @return Response|null A Response instance, or null if no cache entry was found
|
2010-06-23 20:42:41 +01:00
|
|
|
*/
|
|
|
|
public function lookup(Request $request)
|
|
|
|
{
|
|
|
|
$key = $this->getCacheKey($request);
|
|
|
|
|
|
|
|
if (!$entries = $this->getMetadata($key)) {
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
|
|
|
|
// find a cached entry that matches the request.
|
|
|
|
$match = null;
|
|
|
|
foreach ($entries as $entry) {
|
2012-04-19 10:41:27 +01:00
|
|
|
if ($this->requestsMatch(isset($entry[1]['vary'][0]) ? $entry[1]['vary'][0] : '', $request->headers->all(), $entry[0])) {
|
2010-06-23 20:42:41 +01:00
|
|
|
$match = $entry;
|
|
|
|
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if (null === $match) {
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
|
|
|
|
list($req, $headers) = $match;
|
2011-08-29 14:28:03 +01:00
|
|
|
if (is_file($body = $this->getPath($headers['x-content-digest'][0]))) {
|
2010-06-23 20:42:41 +01:00
|
|
|
return $this->restoreResponse($headers, $body);
|
|
|
|
}
|
2011-02-27 17:28:38 +00:00
|
|
|
|
|
|
|
// TODO the metaStore referenced an entity that doesn't exist in
|
|
|
|
// the entityStore. We definitely want to return nil but we should
|
|
|
|
// also purge the entry from the meta-store when this is detected.
|
|
|
|
return null;
|
2010-06-23 20:42:41 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Writes a cache entry to the store for the given Request and Response.
|
|
|
|
*
|
|
|
|
* Existing entries are read and any that match the response are removed. This
|
|
|
|
* method calls write with the new list of cache entries.
|
|
|
|
*
|
2010-07-27 14:33:28 +01:00
|
|
|
* @param Request $request A Request instance
|
|
|
|
* @param Response $response A Response instance
|
2010-06-23 20:42:41 +01:00
|
|
|
*
|
|
|
|
* @return string The key under which the response is stored
|
2012-12-16 12:02:54 +00:00
|
|
|
*
|
|
|
|
* @throws \RuntimeException
|
2010-06-23 20:42:41 +01:00
|
|
|
*/
|
|
|
|
public function write(Request $request, Response $response)
|
|
|
|
{
|
|
|
|
$key = $this->getCacheKey($request);
|
|
|
|
$storedEnv = $this->persistRequest($request);
|
|
|
|
|
|
|
|
// write the response body to the entity store if this is the original response
|
|
|
|
if (!$response->headers->has('X-Content-Digest')) {
|
2012-11-14 22:35:13 +00:00
|
|
|
$digest = $this->generateContentDigest($response);
|
2010-06-23 20:42:41 +01:00
|
|
|
|
|
|
|
if (false === $this->save($digest, $response->getContent())) {
|
2011-01-29 11:18:16 +00:00
|
|
|
throw new \RuntimeException('Unable to store the entity.');
|
2010-06-23 20:42:41 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
$response->headers->set('X-Content-Digest', $digest);
|
|
|
|
|
|
|
|
if (!$response->headers->has('Transfer-Encoding')) {
|
|
|
|
$response->headers->set('Content-Length', strlen($response->getContent()));
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// read existing cache entries, remove non-varying, and add this one to the list
|
|
|
|
$entries = array();
|
|
|
|
$vary = $response->headers->get('vary');
|
|
|
|
foreach ($this->getMetadata($key) as $entry) {
|
2012-04-20 12:29:15 +01:00
|
|
|
if (!isset($entry[1]['vary'][0])) {
|
2010-06-23 20:42:41 +01:00
|
|
|
$entry[1]['vary'] = array('');
|
|
|
|
}
|
|
|
|
|
|
|
|
if ($vary != $entry[1]['vary'][0] || !$this->requestsMatch($vary, $entry[0], $storedEnv)) {
|
|
|
|
$entries[] = $entry;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
$headers = $this->persistResponse($response);
|
|
|
|
unset($headers['age']);
|
|
|
|
|
|
|
|
array_unshift($entries, array($storedEnv, $headers));
|
|
|
|
|
|
|
|
if (false === $this->save($key, serialize($entries))) {
|
2011-01-29 11:18:16 +00:00
|
|
|
throw new \RuntimeException('Unable to store the metadata.');
|
2010-06-23 20:42:41 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
return $key;
|
|
|
|
}
|
|
|
|
|
2012-11-14 22:35:13 +00:00
|
|
|
/**
|
|
|
|
* Returns content digest for $response.
|
|
|
|
*
|
|
|
|
* @param Response $response
|
|
|
|
*
|
|
|
|
* @return string
|
|
|
|
*/
|
|
|
|
protected function generateContentDigest(Response $response)
|
|
|
|
{
|
|
|
|
return 'en'.sha1($response->getContent());
|
|
|
|
}
|
|
|
|
|
2010-06-23 20:42:41 +01:00
|
|
|
/**
|
|
|
|
* Invalidates all cache entries that match the request.
|
|
|
|
*
|
2010-07-27 14:33:28 +01:00
|
|
|
* @param Request $request A Request instance
|
2012-12-16 12:02:54 +00:00
|
|
|
*
|
|
|
|
* @throws \RuntimeException
|
2010-06-23 20:42:41 +01:00
|
|
|
*/
|
|
|
|
public function invalidate(Request $request)
|
|
|
|
{
|
|
|
|
$modified = false;
|
|
|
|
$key = $this->getCacheKey($request);
|
|
|
|
|
|
|
|
$entries = array();
|
|
|
|
foreach ($this->getMetadata($key) as $entry) {
|
|
|
|
$response = $this->restoreResponse($entry[1]);
|
|
|
|
if ($response->isFresh()) {
|
|
|
|
$response->expire();
|
|
|
|
$modified = true;
|
|
|
|
$entries[] = array($entry[0], $this->persistResponse($response));
|
|
|
|
} else {
|
|
|
|
$entries[] = $entry;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if ($modified) {
|
|
|
|
if (false === $this->save($key, serialize($entries))) {
|
|
|
|
throw new \RuntimeException('Unable to store the metadata.');
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Determines whether two Request HTTP header sets are non-varying based on
|
|
|
|
* the vary response header value provided.
|
|
|
|
*
|
|
|
|
* @param string $vary A Response vary header
|
|
|
|
* @param array $env1 A Request HTTP header array
|
|
|
|
* @param array $env2 A Request HTTP header array
|
|
|
|
*
|
2013-05-26 19:42:07 +01:00
|
|
|
* @return Boolean true if the two environments match, false otherwise
|
2010-06-23 20:42:41 +01:00
|
|
|
*/
|
2011-03-23 18:47:16 +00:00
|
|
|
private function requestsMatch($vary, $env1, $env2)
|
2010-06-23 20:42:41 +01:00
|
|
|
{
|
|
|
|
if (empty($vary)) {
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
|
|
|
foreach (preg_split('/[\s,]+/', $vary) as $header) {
|
changed Cache-Control default value behavior
The PHP native cache limiter feature has been disabled as this is now managed
by the HeaderBag class directly instead (see below.)
The HeaderBag class uses the following rules to define a sensible and
convervative default value for the Response 'Cache-Control' header:
* If no cache header is defined ('Cache-Control', 'ETag', 'Last-Modified',
and 'Expires'), 'Cache-Control' is set to 'no-cache';
* If 'Cache-Control' is empty, its value is set to "private, max-age=0,
must-revalidate";
* But if at least one 'Cache-Control' directive is set, and no 'public' or
'private' directives have been explicitely added, Symfony2 adds the
'private' directive automatically (except when 's-maxage' is set.)
So, remember to explicitly add the 'public' directive to 'Cache-Control' when
you want shared caches to store your application resources:
// The Response is private by default
$response->setEtag($etag);
$response->setLastModified($date);
$response->setMaxAge(10);
// Change the Response to be public
$response->setPublic();
// Set cache settings in one call
$response->setCache(array(
'etag' => $etag,
'last_modified' => $date,
'max_age' => 10,
'public' => true,
));
2010-11-10 09:48:22 +00:00
|
|
|
$key = strtr(strtolower($header), '_', '-');
|
2010-06-23 20:42:41 +01:00
|
|
|
$v1 = isset($env1[$key]) ? $env1[$key] : null;
|
|
|
|
$v2 = isset($env2[$key]) ? $env2[$key] : null;
|
|
|
|
if ($v1 !== $v2) {
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Gets all data associated with the given key.
|
|
|
|
*
|
|
|
|
* Use this method only if you know what you are doing.
|
|
|
|
*
|
|
|
|
* @param string $key The store key
|
|
|
|
*
|
|
|
|
* @return array An array of data associated with the key
|
|
|
|
*/
|
2011-03-23 18:47:16 +00:00
|
|
|
private function getMetadata($key)
|
2010-06-23 20:42:41 +01:00
|
|
|
{
|
|
|
|
if (false === $entries = $this->load($key)) {
|
|
|
|
return array();
|
|
|
|
}
|
|
|
|
|
|
|
|
return unserialize($entries);
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Purges data for the given URL.
|
|
|
|
*
|
|
|
|
* @param string $url A URL
|
2010-10-30 15:36:56 +01:00
|
|
|
*
|
|
|
|
* @return Boolean true if the URL exists and has been purged, false otherwise
|
2010-06-23 20:42:41 +01:00
|
|
|
*/
|
|
|
|
public function purge($url)
|
|
|
|
{
|
2011-08-29 14:28:03 +01:00
|
|
|
if (is_file($path = $this->getPath($this->getCacheKey(Request::create($url))))) {
|
2010-06-23 20:42:41 +01:00
|
|
|
unlink($path);
|
2010-10-30 15:36:56 +01:00
|
|
|
|
|
|
|
return true;
|
2010-06-23 20:42:41 +01:00
|
|
|
}
|
2010-10-30 15:36:56 +01:00
|
|
|
|
|
|
|
return false;
|
2010-06-23 20:42:41 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Loads data for the given key.
|
|
|
|
*
|
2012-05-15 21:19:31 +01:00
|
|
|
* @param string $key The store key
|
2010-06-23 20:42:41 +01:00
|
|
|
*
|
|
|
|
* @return string The data associated with the key
|
|
|
|
*/
|
2011-03-23 18:47:16 +00:00
|
|
|
private function load($key)
|
2010-06-23 20:42:41 +01:00
|
|
|
{
|
|
|
|
$path = $this->getPath($key);
|
|
|
|
|
2011-08-29 14:28:03 +01:00
|
|
|
return is_file($path) ? file_get_contents($path) : false;
|
2010-06-23 20:42:41 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Save data for the given key.
|
|
|
|
*
|
|
|
|
* @param string $key The store key
|
|
|
|
* @param string $data The data to store
|
2012-12-16 12:02:54 +00:00
|
|
|
*
|
|
|
|
* @return Boolean
|
2010-06-23 20:42:41 +01:00
|
|
|
*/
|
2011-03-23 18:47:16 +00:00
|
|
|
private function save($key, $data)
|
2010-06-23 20:42:41 +01:00
|
|
|
{
|
|
|
|
$path = $this->getPath($key);
|
2011-01-25 11:01:02 +00:00
|
|
|
if (!is_dir(dirname($path)) && false === @mkdir(dirname($path), 0777, true)) {
|
|
|
|
return false;
|
2010-06-23 20:42:41 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
$tmpFile = tempnam(dirname($path), basename($path));
|
|
|
|
if (false === $fp = @fopen($tmpFile, 'wb')) {
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
@fwrite($fp, $data);
|
|
|
|
@fclose($fp);
|
|
|
|
|
|
|
|
if ($data != file_get_contents($tmpFile)) {
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (false === @rename($tmpFile, $path)) {
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
2012-05-15 07:43:47 +01:00
|
|
|
@chmod($path, 0666 & ~umask());
|
2010-06-23 20:42:41 +01:00
|
|
|
}
|
|
|
|
|
2011-02-04 11:18:26 +00:00
|
|
|
public function getPath($key)
|
2010-06-23 20:42:41 +01:00
|
|
|
{
|
2011-01-25 11:43:59 +00:00
|
|
|
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);
|
2010-06-23 20:42:41 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Returns a cache key for the given Request.
|
|
|
|
*
|
2010-07-27 14:33:28 +01:00
|
|
|
* @param Request $request A Request instance
|
2010-06-23 20:42:41 +01:00
|
|
|
*
|
|
|
|
* @return string A key for the given Request
|
|
|
|
*/
|
2011-03-23 18:47:16 +00:00
|
|
|
private function getCacheKey(Request $request)
|
2010-06-23 20:42:41 +01:00
|
|
|
{
|
|
|
|
if (isset($this->keyCache[$request])) {
|
|
|
|
return $this->keyCache[$request];
|
|
|
|
}
|
|
|
|
|
|
|
|
return $this->keyCache[$request] = 'md'.sha1($request->getUri());
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Persists the Request HTTP headers.
|
|
|
|
*
|
2010-07-27 14:33:28 +01:00
|
|
|
* @param Request $request A Request instance
|
2010-06-23 20:42:41 +01:00
|
|
|
*
|
|
|
|
* @return array An array of HTTP headers
|
|
|
|
*/
|
2011-03-23 18:47:16 +00:00
|
|
|
private function persistRequest(Request $request)
|
2010-06-23 20:42:41 +01:00
|
|
|
{
|
|
|
|
return $request->headers->all();
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Persists the Response HTTP headers.
|
|
|
|
*
|
2010-07-27 14:33:28 +01:00
|
|
|
* @param Response $response A Response instance
|
2010-06-23 20:42:41 +01:00
|
|
|
*
|
|
|
|
* @return array An array of HTTP headers
|
|
|
|
*/
|
2011-03-23 18:47:16 +00:00
|
|
|
private function persistResponse(Response $response)
|
2010-06-23 20:42:41 +01:00
|
|
|
{
|
|
|
|
$headers = $response->headers->all();
|
|
|
|
$headers['X-Status'] = array($response->getStatusCode());
|
|
|
|
|
|
|
|
return $headers;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Restores a Response from the HTTP headers and body.
|
|
|
|
*
|
|
|
|
* @param array $headers An array of HTTP headers for the Response
|
|
|
|
* @param string $body The Response body
|
2012-12-16 12:02:54 +00:00
|
|
|
*
|
|
|
|
* @return Response
|
2010-06-23 20:42:41 +01:00
|
|
|
*/
|
2011-03-23 18:47:16 +00:00
|
|
|
private function restoreResponse($headers, $body = null)
|
2010-06-23 20:42:41 +01:00
|
|
|
{
|
|
|
|
$status = $headers['X-Status'][0];
|
|
|
|
unset($headers['X-Status']);
|
|
|
|
|
|
|
|
if (null !== $body) {
|
|
|
|
$headers['X-Body-File'] = array($body);
|
|
|
|
}
|
|
|
|
|
|
|
|
return new Response($body, $status, $headers);
|
|
|
|
}
|
|
|
|
}
|