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
|
|
|
*
|
|
|
|
* (c) Fabien Potencier <fabien.potencier@symfony-project.com>
|
|
|
|
*
|
|
|
|
* This code is partially based on the Rack-Cache library by Ryan Tomayko,
|
|
|
|
* which is released under the MIT license.
|
|
|
|
* (based on commit 02d2b48d75bcb63cf1c0c7149c077ad256542801)
|
|
|
|
*
|
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\HttpKernel\HttpKernelInterface;
|
|
|
|
use Symfony\Component\HttpFoundation\Request;
|
|
|
|
use Symfony\Component\HttpFoundation\Response;
|
|
|
|
|
2010-06-23 20:42:41 +01:00
|
|
|
/**
|
|
|
|
* Cache provides HTTP caching.
|
|
|
|
*
|
2010-10-17 12:45:15 +01:00
|
|
|
* @author Fabien Potencier <fabien.potencier@symfony-project.com>
|
2010-06-23 20:42:41 +01:00
|
|
|
*/
|
2011-01-26 20:38:45 +00:00
|
|
|
class HttpCache implements HttpKernelInterface
|
2010-06-23 20:42:41 +01:00
|
|
|
{
|
|
|
|
protected $kernel;
|
|
|
|
protected $traces;
|
|
|
|
protected $store;
|
2011-02-04 10:05:47 +00:00
|
|
|
protected $request;
|
2010-06-23 20:42:41 +01:00
|
|
|
protected $esi;
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Constructor.
|
|
|
|
*
|
|
|
|
* The available options are:
|
|
|
|
*
|
|
|
|
* * debug: If true, the traces are added as a HTTP header to ease debugging
|
|
|
|
*
|
|
|
|
* * default_ttl The number of seconds that a cache entry should be considered
|
|
|
|
* fresh when no explicit freshness information is provided in
|
|
|
|
* a response. Explicit Cache-Control or Expires headers
|
|
|
|
* override this value. (default: 0)
|
|
|
|
*
|
|
|
|
* * private_headers Set of request headers that trigger "private" cache-control behavior
|
|
|
|
* on responses that don't explicitly state whether the response is
|
|
|
|
* public or private via a Cache-Control directive. (default: Authorization and Cookie)
|
|
|
|
*
|
|
|
|
* * allow_reload Specifies whether the client can force a cache reload by including a
|
2010-10-30 19:46:39 +01:00
|
|
|
* Cache-Control "no-cache" directive in the request. Set it to ``true``
|
|
|
|
* for compliance with RFC 2616. (default: false)
|
2010-06-23 20:42:41 +01:00
|
|
|
*
|
|
|
|
* * allow_revalidate Specifies whether the client can force a cache revalidate by including
|
2010-10-30 19:46:39 +01:00
|
|
|
* a Cache-Control "max-age=0" directive in the request. Set it to ``true``
|
|
|
|
* for compliance with RFC 2616. (default: false)
|
2010-06-23 20:42:41 +01:00
|
|
|
*
|
|
|
|
* * stale_while_revalidate Specifies the default number of seconds (the granularity is the second as the
|
|
|
|
* Response TTL precision is a second) during which the cache can immediately return
|
|
|
|
* a stale response while it revalidates it in the background (default: 2).
|
2010-10-16 11:32:57 +01:00
|
|
|
* This setting is overridden by the stale-while-revalidate HTTP Cache-Control
|
2010-06-23 20:42:41 +01:00
|
|
|
* extension (see RFC 5861).
|
|
|
|
*
|
2010-10-16 11:32:57 +01:00
|
|
|
* * stale_if_error Specifies the default number of seconds (the granularity is the second) during which
|
2010-10-30 19:46:39 +01:00
|
|
|
* the cache can serve a stale response when an error is encountered (default: 60).
|
2010-10-16 11:32:57 +01:00
|
|
|
* This setting is overridden by the stale-if-error HTTP Cache-Control extension
|
2010-06-23 20:42:41 +01:00
|
|
|
* (see RFC 5861).
|
|
|
|
*
|
2010-07-27 14:33:28 +01:00
|
|
|
* @param HttpKernelInterface $kernel An HttpKernelInterface instance
|
2011-01-31 13:10:53 +00:00
|
|
|
* @param StoreInterface $store A Store instance
|
2010-07-27 14:33:28 +01:00
|
|
|
* @param Esi $esi An Esi instance
|
2010-06-23 20:42:41 +01:00
|
|
|
* @param array $options An array of options
|
|
|
|
*/
|
2011-01-31 13:10:53 +00:00
|
|
|
public function __construct(HttpKernelInterface $kernel, StoreInterface $store, Esi $esi = null, array $options = array())
|
2010-06-23 20:42:41 +01:00
|
|
|
{
|
|
|
|
$this->store = $store;
|
|
|
|
$this->kernel = $kernel;
|
|
|
|
|
|
|
|
// needed in case there is a fatal error because the backend is too slow to respond
|
2011-01-31 13:10:53 +00:00
|
|
|
register_shutdown_function(array($this->store, 'cleanup'));
|
2010-06-23 20:42:41 +01:00
|
|
|
|
|
|
|
$this->options = array_merge(array(
|
|
|
|
'debug' => false,
|
|
|
|
'default_ttl' => 0,
|
|
|
|
'private_headers' => array('Authorization', 'Cookie'),
|
|
|
|
'allow_reload' => false,
|
|
|
|
'allow_revalidate' => false,
|
|
|
|
'stale_while_revalidate' => 2,
|
|
|
|
'stale_if_error' => 60,
|
|
|
|
), $options);
|
|
|
|
$this->esi = $esi;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Returns an array of events that took place during processing of the last request.
|
|
|
|
*
|
|
|
|
* @return array An array of events
|
|
|
|
*/
|
|
|
|
public function getTraces()
|
|
|
|
{
|
|
|
|
return $this->traces;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Returns a log message for the events of the last request processing.
|
|
|
|
*
|
|
|
|
* @return string A log message
|
|
|
|
*/
|
|
|
|
public function getLog()
|
|
|
|
{
|
|
|
|
$log = array();
|
|
|
|
foreach ($this->traces as $request => $traces) {
|
|
|
|
$log[] = sprintf('%s: %s', $request, implode(', ', $traces));
|
|
|
|
}
|
|
|
|
|
|
|
|
return implode('; ', $log);
|
|
|
|
}
|
|
|
|
|
2011-02-04 10:05:47 +00:00
|
|
|
/**
|
|
|
|
* Gets the Request instance associated with the master request.
|
|
|
|
*
|
|
|
|
* @return Symfony\Component\HttpFoundation\Request A Request instance
|
|
|
|
*/
|
|
|
|
public function getRequest()
|
|
|
|
{
|
|
|
|
return $this->request;
|
|
|
|
}
|
|
|
|
|
2010-06-23 20:42:41 +01:00
|
|
|
/**
|
2010-11-06 14:13:23 +00:00
|
|
|
* {@inheritdoc}
|
2010-06-23 20:42:41 +01:00
|
|
|
*/
|
2010-11-06 14:13:23 +00:00
|
|
|
public function handle(Request $request, $type = HttpKernelInterface::MASTER_REQUEST, $catch = true)
|
2010-06-23 20:42:41 +01:00
|
|
|
{
|
2010-10-16 11:32:57 +01:00
|
|
|
// FIXME: catch exceptions and implement a 500 error page here? -> in Varnish, there is a built-in error page mechanism
|
2010-06-23 20:42:41 +01:00
|
|
|
if (HttpKernelInterface::MASTER_REQUEST === $type) {
|
|
|
|
$this->traces = array();
|
2011-02-04 10:05:47 +00:00
|
|
|
$this->request = $request;
|
2010-06-23 20:42:41 +01:00
|
|
|
}
|
|
|
|
|
2010-12-06 00:36:07 +00:00
|
|
|
$path = $request->getPathInfo();
|
|
|
|
if ($qs = $request->getQueryString()) {
|
|
|
|
$path .= '?'.$qs;
|
|
|
|
}
|
|
|
|
$this->traces[$request->getMethod().' '.$path] = array();
|
2010-06-23 20:42:41 +01:00
|
|
|
|
|
|
|
if (!$request->isMethodSafe($request)) {
|
2011-01-17 01:05:38 +00:00
|
|
|
$response = $this->invalidate($request, $catch);
|
2010-06-23 20:42:41 +01:00
|
|
|
} elseif ($request->headers->has('expect')) {
|
2011-01-17 01:05:38 +00:00
|
|
|
$response = $this->pass($request, $catch);
|
2010-06-23 20:42:41 +01:00
|
|
|
} else {
|
2011-01-17 01:05:38 +00:00
|
|
|
$response = $this->lookup($request, $catch);
|
2010-06-23 20:42:41 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
$response->isNotModified($request);
|
|
|
|
|
2011-01-23 07:16:18 +00:00
|
|
|
$this->restoreResponseBody($request, $response);
|
2010-06-23 20:42:41 +01:00
|
|
|
|
|
|
|
if (HttpKernelInterface::MASTER_REQUEST === $type && $this->options['debug']) {
|
|
|
|
$response->headers->set('X-Symfony-Cache', $this->getLog());
|
|
|
|
}
|
|
|
|
|
|
|
|
return $response;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Forwards the Request to the backend without storing the Response in the cache.
|
|
|
|
*
|
2010-07-27 14:33:28 +01:00
|
|
|
* @param Request $request A Request instance
|
2011-01-17 01:05:38 +00:00
|
|
|
* @param Boolean $catch whether to process exceptions
|
2010-06-23 20:42:41 +01:00
|
|
|
*
|
2010-07-27 14:33:28 +01:00
|
|
|
* @return Response A Response instance
|
2010-06-23 20:42:41 +01:00
|
|
|
*/
|
2011-01-17 01:05:38 +00:00
|
|
|
protected function pass(Request $request, $catch = false)
|
2010-06-23 20:42:41 +01:00
|
|
|
{
|
|
|
|
$this->record($request, 'pass');
|
|
|
|
|
2011-01-17 01:05:38 +00:00
|
|
|
return $this->forward($request, $catch);
|
2010-06-23 20:42:41 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Invalidates non-safe methods (like POST, PUT, and DELETE).
|
|
|
|
*
|
2010-07-27 14:33:28 +01:00
|
|
|
* @param Request $request A Request instance
|
2011-01-17 01:05:38 +00:00
|
|
|
* @param Boolean $catch whether to process exceptions
|
2010-06-23 20:42:41 +01:00
|
|
|
*
|
2010-07-27 14:33:28 +01:00
|
|
|
* @return Response A Response instance
|
2010-06-23 20:42:41 +01:00
|
|
|
*
|
|
|
|
* @see RFC2616 13.10
|
|
|
|
*/
|
2011-01-17 01:05:38 +00:00
|
|
|
protected function invalidate(Request $request, $catch = false)
|
2010-06-23 20:42:41 +01:00
|
|
|
{
|
2011-01-17 01:05:38 +00:00
|
|
|
$response = $this->pass($request, $catch);
|
2010-06-23 20:42:41 +01:00
|
|
|
|
|
|
|
// invalidate only when the response is successful
|
|
|
|
if ($response->isSuccessful() || $response->isRedirect()) {
|
|
|
|
try {
|
2011-01-17 01:05:38 +00:00
|
|
|
$this->store->invalidate($request, $catch);
|
2010-06-23 20:42:41 +01:00
|
|
|
|
|
|
|
$this->record($request, 'invalidate');
|
|
|
|
} catch (\Exception $e) {
|
|
|
|
$this->record($request, 'invalidate-failed');
|
|
|
|
|
|
|
|
if ($this->options['debug']) {
|
|
|
|
throw $e;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return $response;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Lookups a Response from the cache for the given Request.
|
|
|
|
*
|
|
|
|
* When a matching cache entry is found and is fresh, it uses it as the
|
|
|
|
* response without forwarding any request to the backend. When a matching
|
|
|
|
* cache entry is found but is stale, it attempts to "validate" the entry with
|
|
|
|
* the backend using conditional GET. When no matching cache entry is found,
|
|
|
|
* it triggers "miss" processing.
|
|
|
|
*
|
2010-07-27 14:33:28 +01:00
|
|
|
* @param Request $request A Request instance
|
2011-01-17 01:05:38 +00:00
|
|
|
* @param Boolean $catch whether to process exceptions
|
2010-06-23 20:42:41 +01:00
|
|
|
*
|
2010-07-27 14:33:28 +01:00
|
|
|
* @return Response A Response instance
|
2010-06-23 20:42:41 +01:00
|
|
|
*/
|
2011-01-17 01:05:38 +00:00
|
|
|
protected function lookup(Request $request, $catch = false)
|
2010-06-23 20:42:41 +01:00
|
|
|
{
|
2011-01-16 02:23:43 +00:00
|
|
|
// if allow_reload and no-cache Cache-Control, allow a cache reload
|
2010-06-23 20:42:41 +01:00
|
|
|
if ($this->options['allow_reload'] && $request->isNoCache()) {
|
|
|
|
$this->record($request, 'reload');
|
|
|
|
|
|
|
|
return $this->fetch($request);
|
|
|
|
}
|
|
|
|
|
|
|
|
try {
|
|
|
|
$entry = $this->store->lookup($request);
|
|
|
|
} catch (\Exception $e) {
|
|
|
|
$this->record($request, 'lookup-failed');
|
|
|
|
|
|
|
|
if ($this->options['debug']) {
|
|
|
|
throw $e;
|
|
|
|
}
|
|
|
|
|
2011-01-17 01:05:38 +00:00
|
|
|
return $this->pass($request, $catch);
|
2010-06-23 20:42:41 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
if (null === $entry) {
|
|
|
|
$this->record($request, 'miss');
|
|
|
|
|
2011-01-17 01:05:38 +00:00
|
|
|
return $this->fetch($request, $catch);
|
2010-06-23 20:42:41 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
if (!$this->isFreshEnough($request, $entry)) {
|
|
|
|
$this->record($request, 'stale');
|
|
|
|
|
|
|
|
return $this->validate($request, $entry);
|
|
|
|
}
|
|
|
|
|
|
|
|
$this->record($request, 'fresh');
|
|
|
|
|
|
|
|
$entry->headers->set('Age', $entry->getAge());
|
|
|
|
|
|
|
|
return $entry;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Validates that a cache entry is fresh.
|
|
|
|
*
|
|
|
|
* The original request is used as a template for a conditional
|
|
|
|
* GET request with the backend.
|
|
|
|
*
|
2010-07-27 14:33:28 +01:00
|
|
|
* @param Request $request A Request instance
|
|
|
|
* @param Response $entry A Response instance to validate
|
2010-06-23 20:42:41 +01:00
|
|
|
*
|
2010-07-27 14:33:28 +01:00
|
|
|
* @return Response A Response instance
|
2010-06-23 20:42:41 +01:00
|
|
|
*/
|
2011-01-16 02:23:43 +00:00
|
|
|
protected function validate(Request $request, Response $entry)
|
2010-06-23 20:42:41 +01:00
|
|
|
{
|
|
|
|
$subRequest = clone $request;
|
|
|
|
|
|
|
|
// send no head requests because we want content
|
|
|
|
$subRequest->setMethod('get');
|
|
|
|
|
|
|
|
// add our cached last-modified validator
|
|
|
|
$subRequest->headers->set('if_modified_since', $entry->headers->get('Last-Modified'));
|
|
|
|
|
|
|
|
// Add our cached etag validator to the environment.
|
|
|
|
// We keep the etags from the client to handle the case when the client
|
|
|
|
// has a different private valid entry which is not cached here.
|
|
|
|
$cachedEtags = array($entry->getEtag());
|
|
|
|
$requestEtags = $request->getEtags();
|
|
|
|
$etags = array_unique(array_merge($cachedEtags, $requestEtags));
|
|
|
|
$subRequest->headers->set('if_none_match', $etags ? implode(', ', $etags) : '');
|
|
|
|
|
|
|
|
$response = $this->forward($subRequest, false, $entry);
|
|
|
|
|
|
|
|
if (304 == $response->getStatusCode()) {
|
|
|
|
$this->record($request, 'valid');
|
|
|
|
|
|
|
|
// return the response and not the cache entry if the response is valid but not cached
|
|
|
|
$etag = $response->getEtag();
|
|
|
|
if ($etag && in_array($etag, $requestEtags) && !in_array($etag, $cachedEtags)) {
|
|
|
|
return $response;
|
|
|
|
}
|
|
|
|
|
|
|
|
$entry = clone $entry;
|
2010-11-23 08:42:19 +00:00
|
|
|
$entry->headers->remove('Date');
|
2010-06-23 20:42:41 +01:00
|
|
|
|
|
|
|
foreach (array('Date', 'Expires', 'Cache-Control', 'ETag', 'Last-Modified') as $name) {
|
|
|
|
if ($response->headers->has($name)) {
|
|
|
|
$entry->headers->set($name, $response->headers->get($name));
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
$response = $entry;
|
|
|
|
} else {
|
|
|
|
$this->record($request, 'invalid');
|
|
|
|
}
|
|
|
|
|
|
|
|
if ($response->isCacheable()) {
|
|
|
|
$this->store($request, $response);
|
|
|
|
}
|
|
|
|
|
|
|
|
return $response;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Forwards the Request to the backend and determines whether the response should be stored.
|
|
|
|
*
|
|
|
|
* This methods is trigered when the cache missed or a reload is required.
|
|
|
|
*
|
2010-07-27 14:33:28 +01:00
|
|
|
* @param Request $request A Request instance
|
2011-01-17 01:05:38 +00:00
|
|
|
* @param Boolean $catch whether to process exceptions
|
2010-06-23 20:42:41 +01:00
|
|
|
*
|
2010-07-27 14:33:28 +01:00
|
|
|
* @return Response A Response instance
|
2010-06-23 20:42:41 +01:00
|
|
|
*/
|
2011-01-17 01:05:38 +00:00
|
|
|
protected function fetch(Request $request, $catch = false)
|
2010-06-23 20:42:41 +01:00
|
|
|
{
|
|
|
|
$subRequest = clone $request;
|
|
|
|
|
|
|
|
// send no head requests because we want content
|
|
|
|
$subRequest->setMethod('get');
|
|
|
|
|
|
|
|
// avoid that the backend sends no content
|
2010-11-23 08:42:19 +00:00
|
|
|
$subRequest->headers->remove('if_modified_since');
|
|
|
|
$subRequest->headers->remove('if_none_match');
|
2010-06-23 20:42:41 +01:00
|
|
|
|
2011-01-17 01:05:38 +00:00
|
|
|
$response = $this->forward($subRequest, $catch);
|
2010-06-23 20:42:41 +01:00
|
|
|
|
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
|
|
|
if ($this->isPrivateRequest($request) && !$response->headers->hasCacheControlDirective('public')) {
|
2010-06-23 20:42:41 +01:00
|
|
|
$response->setPrivate(true);
|
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
|
|
|
} elseif ($this->options['default_ttl'] > 0 && null === $response->getTtl() && !$response->headers->getCacheControlDirective('must-revalidate')) {
|
2010-06-23 20:42:41 +01:00
|
|
|
$response->setTtl($this->options['default_ttl']);
|
|
|
|
}
|
|
|
|
|
|
|
|
if ($response->isCacheable()) {
|
|
|
|
$this->store($request, $response);
|
|
|
|
}
|
|
|
|
|
|
|
|
return $response;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Forwards the Request to the backend and returns the Response.
|
|
|
|
*
|
2010-07-27 14:33:28 +01:00
|
|
|
* @param Request $request A Request instance
|
2010-11-06 14:13:23 +00:00
|
|
|
* @param Boolean $catch Whether to catch exceptions or not
|
2010-07-27 14:33:28 +01:00
|
|
|
* @param Response $response A Response instance (the stale entry if present, null otherwise)
|
2010-06-23 20:42:41 +01:00
|
|
|
*
|
2010-07-27 14:33:28 +01:00
|
|
|
* @return Response A Response instance
|
2010-06-23 20:42:41 +01:00
|
|
|
*/
|
2010-11-06 14:13:23 +00:00
|
|
|
protected function forward(Request $request, $catch = false, Response $entry = null)
|
2010-06-23 20:42:41 +01:00
|
|
|
{
|
|
|
|
if ($this->esi) {
|
|
|
|
$this->esi->addSurrogateEsiCapability($request);
|
|
|
|
}
|
|
|
|
|
|
|
|
// always a "master" request (as the real master request can be in cache)
|
2010-11-06 14:13:23 +00:00
|
|
|
$response = $this->kernel->handle($request, HttpKernelInterface::MASTER_REQUEST, $catch);
|
2010-06-23 20:42:41 +01:00
|
|
|
// FIXME: we probably need to also catch exceptions if raw === true
|
|
|
|
|
|
|
|
// we don't implement the stale-if-error on Requests, which is nonetheless part of the RFC
|
|
|
|
if (null !== $entry && in_array($response->getStatusCode(), array(500, 502, 503, 504))) {
|
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
|
|
|
if (null === $age = $entry->headers->getCacheControlDirective('stale-if-error')) {
|
2010-06-23 20:42:41 +01:00
|
|
|
$age = $this->options['stale_if_error'];
|
|
|
|
}
|
|
|
|
|
|
|
|
if (abs($entry->getTtl()) < $age) {
|
|
|
|
$this->record($request, 'stale-if-error');
|
|
|
|
|
|
|
|
return $entry;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
$this->processResponseBody($request, $response);
|
|
|
|
|
|
|
|
return $response;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Checks whether the cache entry is "fresh enough" to satisfy the Request.
|
|
|
|
*
|
2010-07-27 14:33:28 +01:00
|
|
|
* @param Request $request A Request instance
|
|
|
|
* @param Response $entry A Response instance
|
2010-06-23 20:42:41 +01:00
|
|
|
*
|
|
|
|
* @return Boolean true if the cache entry if fresh enough, false otherwise
|
|
|
|
*/
|
|
|
|
protected function isFreshEnough(Request $request, Response $entry)
|
|
|
|
{
|
|
|
|
if (!$entry->isFresh()) {
|
|
|
|
return $this->lock($request, $entry);
|
|
|
|
}
|
|
|
|
|
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
|
|
|
if ($this->options['allow_revalidate'] && null !== $maxAge = $request->headers->getCacheControlDirective('max-age')) {
|
2010-06-23 20:42:41 +01:00
|
|
|
return $maxAge > 0 && $maxAge >= $entry->getAge();
|
|
|
|
}
|
|
|
|
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Locks a Request during the call to the backend.
|
|
|
|
*
|
2010-07-27 14:33:28 +01:00
|
|
|
* @param Request $request A Request instance
|
|
|
|
* @param Response $entry A Response instance
|
2010-06-23 20:42:41 +01:00
|
|
|
*
|
|
|
|
* @return Boolean true if the cache entry can be returned even if it is staled, false otherwise
|
|
|
|
*/
|
|
|
|
protected function lock(Request $request, Response $entry)
|
|
|
|
{
|
|
|
|
// try to acquire a lock to call the backend
|
|
|
|
$lock = $this->store->lock($request, $entry);
|
|
|
|
|
|
|
|
// there is already another process calling the backend
|
|
|
|
if (true !== $lock) {
|
|
|
|
// check if we can serve the stale entry
|
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
|
|
|
if (null === $age = $entry->headers->getCacheControlDirective('stale-while-revalidate')) {
|
2010-06-23 20:42:41 +01:00
|
|
|
$age = $this->options['stale_while_revalidate'];
|
|
|
|
}
|
|
|
|
|
|
|
|
if (abs($entry->getTtl()) < $age) {
|
|
|
|
$this->record($request, 'stale-while-revalidate');
|
|
|
|
|
|
|
|
// server the stale response while there is a revalidation
|
|
|
|
return true;
|
|
|
|
} else {
|
|
|
|
// wait for the lock to be released
|
|
|
|
$wait = 0;
|
2010-08-04 22:06:49 +01:00
|
|
|
while (file_exists($lock) && $wait < 5000000) {
|
2010-08-04 17:23:13 +01:00
|
|
|
usleep($wait += 50000);
|
2010-06-23 20:42:41 +01:00
|
|
|
}
|
|
|
|
|
2010-08-04 22:06:49 +01:00
|
|
|
if ($wait < 2000000) {
|
2010-06-23 20:42:41 +01:00
|
|
|
// replace the current entry with the fresh one
|
|
|
|
$new = $this->lookup($request);
|
|
|
|
$entry->headers = $new->headers;
|
|
|
|
$entry->setContent($new->getContent());
|
|
|
|
$entry->setStatusCode($new->getStatusCode());
|
|
|
|
$entry->setProtocolVersion($new->getProtocolVersion());
|
|
|
|
$entry->setCookies($new->getCookies());
|
|
|
|
|
|
|
|
return true;
|
|
|
|
} else {
|
|
|
|
// backend is slow as hell, send a 503 response (to avoid the dog pile effect)
|
|
|
|
$entry->setStatusCode(503);
|
|
|
|
$entry->setContent('503 Service Unavailable');
|
|
|
|
$entry->headers->set('Retry-After', 10);
|
|
|
|
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// we have the lock, call the backend
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Writes the Response to the cache.
|
|
|
|
*
|
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
|
|
|
*/
|
|
|
|
protected function store(Request $request, Response $response)
|
|
|
|
{
|
|
|
|
try {
|
|
|
|
$this->store->write($request, $response);
|
|
|
|
|
|
|
|
$this->record($request, 'store');
|
|
|
|
|
|
|
|
$response->headers->set('Age', $response->getAge());
|
|
|
|
} catch (\Exception $e) {
|
|
|
|
$this->record($request, 'store-failed');
|
|
|
|
|
|
|
|
if ($this->options['debug']) {
|
|
|
|
throw $e;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// now that the response is cached, release the lock
|
|
|
|
$this->store->unlock($request);
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Restores the Response body.
|
|
|
|
*
|
2010-07-27 14:33:28 +01:00
|
|
|
* @param Response $response A Response instance
|
2010-06-23 20:42:41 +01:00
|
|
|
*
|
2010-07-27 14:33:28 +01:00
|
|
|
* @return Response A Response instance
|
2010-06-23 20:42:41 +01:00
|
|
|
*/
|
2011-01-23 07:16:18 +00:00
|
|
|
protected function restoreResponseBody(Request $request, Response $response)
|
2010-06-23 20:42:41 +01:00
|
|
|
{
|
2011-01-23 07:16:18 +00:00
|
|
|
if ('head' === strtolower($request->getMethod())) {
|
|
|
|
$response->setContent('');
|
|
|
|
$response->headers->remove('X-Body-Eval');
|
|
|
|
$response->headers->remove('X-Body-File');
|
|
|
|
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2010-06-23 20:42:41 +01:00
|
|
|
if ($response->headers->has('X-Body-Eval')) {
|
|
|
|
ob_start();
|
|
|
|
|
|
|
|
if ($response->headers->has('X-Body-File')) {
|
|
|
|
include $response->headers->get('X-Body-File');
|
|
|
|
} else {
|
|
|
|
eval('; ?>'.$response->getContent().'<?php ;');
|
|
|
|
}
|
|
|
|
|
|
|
|
$response->setContent(ob_get_clean());
|
2010-11-23 08:42:19 +00:00
|
|
|
$response->headers->remove('X-Body-Eval');
|
2010-06-23 20:42:41 +01:00
|
|
|
} elseif ($response->headers->has('X-Body-File')) {
|
|
|
|
$response->setContent(file_get_contents($response->headers->get('X-Body-File')));
|
|
|
|
} else {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2010-11-23 08:42:19 +00:00
|
|
|
$response->headers->remove('X-Body-File');
|
2010-06-23 20:42:41 +01:00
|
|
|
|
|
|
|
if (!$response->headers->has('Transfer-Encoding')) {
|
|
|
|
$response->headers->set('Content-Length', strlen($response->getContent()));
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
protected function processResponseBody(Request $request, Response $response)
|
|
|
|
{
|
|
|
|
if (null !== $this->esi && $this->esi->needsEsiParsing($response)) {
|
|
|
|
$this->esi->process($request, $response);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Checks if the Request includes authorization or other sensitive information
|
|
|
|
* that should cause the Response to be considered private by default.
|
|
|
|
*
|
2010-07-27 14:33:28 +01:00
|
|
|
* @param Request $request A Request instance
|
2010-06-23 20:42:41 +01:00
|
|
|
*
|
|
|
|
* @return Boolean true if the Request is private, false otherwise
|
|
|
|
*/
|
|
|
|
protected function isPrivateRequest(Request $request)
|
|
|
|
{
|
|
|
|
foreach ($this->options['private_headers'] as $key) {
|
|
|
|
$key = strtolower(str_replace('HTTP_', '', $key));
|
|
|
|
|
|
|
|
if ('cookie' === $key) {
|
|
|
|
if (count($request->cookies->all())) {
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
} elseif ($request->headers->has($key)) {
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Records that an event took place.
|
|
|
|
*
|
|
|
|
* @param string $event The event name
|
|
|
|
*/
|
|
|
|
protected function record(Request $request, $event)
|
|
|
|
{
|
2010-12-06 00:36:07 +00:00
|
|
|
$path = $request->getPathInfo();
|
|
|
|
if ($qs = $request->getQueryString()) {
|
|
|
|
$path .= '?'.$qs;
|
|
|
|
}
|
|
|
|
$this->traces[$request->getMethod().' '.$path][] = $event;
|
2010-06-23 20:42:41 +01:00
|
|
|
}
|
|
|
|
}
|