SSI support

This commit is contained in:
Sebastian Krebs 2014-03-23 01:04:57 +01:00
parent 5febbb27f5
commit 06cea083b4
22 changed files with 1019 additions and 207 deletions

View File

@ -81,6 +81,7 @@ class Configuration implements ConfigurationInterface
$this->addCsrfSection($rootNode);
$this->addFormSection($rootNode);
$this->addEsiSection($rootNode);
$this->addSsiSection($rootNode);
$this->addFragmentsSection($rootNode);
$this->addProfilerSection($rootNode);
$this->addRouterSection($rootNode);
@ -148,6 +149,17 @@ class Configuration implements ConfigurationInterface
;
}
private function addSsiSection(ArrayNodeDefinition $rootNode)
{
$rootNode
->children()
->arrayNode('ssi')
->info('ssi configuration')
->canBeEnabled()
->end()
->end();
}
private function addFragmentsSection(ArrayNodeDefinition $rootNode)
{
$rootNode

View File

@ -122,6 +122,7 @@ class FrameworkExtension extends Extension
$this->registerValidationConfiguration($config['validation'], $container, $loader);
$this->registerEsiConfiguration($config['esi'], $container, $loader);
$this->registerSsiConfiguration($config['ssi'], $container, $loader);
$this->registerFragmentsConfiguration($config['fragments'], $container, $loader);
$this->registerProfilerConfiguration($config['profiler'], $container, $loader);
$this->registerTranslatorConfiguration($config['translator'], $container);
@ -208,6 +209,22 @@ class FrameworkExtension extends Extension
$loader->load('esi.xml');
}
/**
* Loads the SSI configuration.
*
* @param array $config An SSI configuration array
* @param ContainerBuilder $container A ContainerBuilder instance
* @param XmlFileLoader $loader An XmlFileLoader instance
*/
private function registerSsiConfiguration(array $config, ContainerBuilder $container, XmlFileLoader $loader)
{
if (!$this->isConfigEnabled($container, $config)) {
return;
}
$loader->load('ssi.xml');
}
/**
* Loads the fragments configuration.
*

View File

@ -39,7 +39,7 @@ abstract class HttpCache extends BaseHttpCache
$this->kernel = $kernel;
$this->cacheDir = $cacheDir;
parent::__construct($kernel, $this->createStore(), $this->createEsi(), array_merge(array('debug' => $kernel->isDebug()), $this->getOptions()));
parent::__construct($kernel, $this->createStore(), $this->createSurrogate(), array_merge(array('debug' => $kernel->isDebug()), $this->getOptions()));
}
/**
@ -55,7 +55,7 @@ abstract class HttpCache extends BaseHttpCache
{
$this->getKernel()->boot();
$this->getKernel()->getContainer()->set('cache', $this);
$this->getKernel()->getContainer()->set('esi', $this->getEsi());
$this->getKernel()->getContainer()->set($this->getSurrogate()->getName(), $this->getSurrogate());
return parent::forward($request, $raw, $entry);
}
@ -70,6 +70,18 @@ abstract class HttpCache extends BaseHttpCache
return array();
}
protected function createSurrogate()
{
return $this->createEsi();
}
/**
* Creates new ESI instance
*
* @return Esi
*
* @deprecated Deprecated since version 2.6, to be removed in 3.0. Use createSurrogate() instead
*/
protected function createEsi()
{
return new Esi();

View File

@ -10,6 +10,7 @@
<parameter key="fragment.renderer.hinclude.class">Symfony\Bundle\FrameworkBundle\Fragment\ContainerAwareHIncludeFragmentRenderer</parameter>
<parameter key="fragment.renderer.hinclude.global_template"></parameter>
<parameter key="fragment.renderer.esi.class">Symfony\Component\HttpKernel\Fragment\EsiFragmentRenderer</parameter>
<parameter key="fragment.renderer.ssi.class">Symfony\Component\HttpKernel\Fragment\SsiFragmentRenderer</parameter>
<parameter key="fragment.path">/_fragment</parameter>
</parameters>
@ -41,5 +42,17 @@
<argument type="service" id="fragment.renderer.inline" />
<call method="setFragmentPath"><argument>%fragment.path%</argument></call>
</service>
<service id="fragment.renderer.ssi" class="%fragment.renderer.ssi.class%">
<tag name="kernel.fragment_renderer" />
<argument type="service" id="ssi" on-invalid="null" />
<argument type="service" id="fragment.renderer.inline" />
<call method="setFragmentPath">
<argument>%fragment.path%</argument>
</call>
<call method="setUriSigner">
<argument type="service" id="uri_signer" />
</call>
</service>
</services>
</container>

View File

@ -0,0 +1,20 @@
<?xml version="1.0" ?>
<container xmlns="http://symfony.com/schema/dic/services"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://symfony.com/schema/dic/services http://symfony.com/schema/dic/services/services-1.0.xsd">
<parameters>
<parameter key="ssi.class">Symfony\Component\HttpKernel\HttpCache\Ssi</parameter>
<parameter key="ssi_listener.class">Symfony\Component\HttpKernel\EventListener\SurrogateListener</parameter>
</parameters>
<services>
<service id="ssi" class="%ssi.class%" />
<service id="ssi_listener" class="%ssi_listener.class%">
<tag name="kernel.event_subscriber" />
<argument type="service" id="ssi" on-invalid="ignore" />
</service>
</services>
</container>

View File

@ -105,6 +105,7 @@ class ConfigurationTest extends \PHPUnit_Framework_TestCase
'field_name' => '_token',
),
'esi' => array('enabled' => false),
'ssi' => array('enabled' => false),
'fragments' => array(
'enabled' => false,
'path' => '/_fragment',

View File

@ -11,48 +11,13 @@
namespace Symfony\Component\HttpKernel\EventListener;
use Symfony\Component\HttpKernel\Event\FilterResponseEvent;
use Symfony\Component\HttpKernel\KernelEvents;
use Symfony\Component\HttpKernel\HttpCache\Esi;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
/**
* EsiListener adds a Surrogate-Control HTTP header when the Response needs to be parsed for ESI.
*
* @author Fabien Potencier <fabien@symfony.com>
*
* @deprecated Deprecated since version 2.6, to be removed in 3.0. Use SurrogateListener instead
*/
class EsiListener implements EventSubscriberInterface
class EsiListener extends SurrogateListener
{
private $esi;
/**
* Constructor.
*
* @param Esi $esi An ESI instance
*/
public function __construct(Esi $esi = null)
{
$this->esi = $esi;
}
/**
* Filters the Response.
*
* @param FilterResponseEvent $event A FilterResponseEvent instance
*/
public function onKernelResponse(FilterResponseEvent $event)
{
if (!$event->isMasterRequest() || null === $this->esi) {
return;
}
$this->esi->addSurrogateControl($event->getResponse());
}
public static function getSubscribedEvents()
{
return array(
KernelEvents::RESPONSE => 'onKernelResponse',
);
}
}

View File

@ -0,0 +1,58 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\HttpKernel\EventListener;
use Symfony\Component\HttpKernel\Event\FilterResponseEvent;
use Symfony\Component\HttpKernel\HttpCache\SurrogateInterface;
use Symfony\Component\HttpKernel\KernelEvents;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
/**
* SurrogateListener adds a Surrogate-Control HTTP header when the Response needs to be parsed for Surrogates
*
* @author Fabien Potencier <fabien@symfony.com>
*/
class SurrogateListener implements EventSubscriberInterface
{
private $surrogate;
/**
* Constructor.
*
* @param SurrogateInterface $surrogate An SurrogateInterface instance
*/
public function __construct(SurrogateInterface $surrogate = null)
{
$this->surrogate = $surrogate;
}
/**
* Filters the Response.
*
* @param FilterResponseEvent $event A FilterResponseEvent instance
*/
public function onKernelResponse(FilterResponseEvent $event)
{
if (!$event->isMasterRequest() || null === $this->surrogate) {
return;
}
$this->surrogate->addSurrogateControl($event->getResponse());
}
public static function getSubscribedEvents()
{
return array(
KernelEvents::RESPONSE => 'onKernelResponse',
);
}
}

View File

@ -0,0 +1,79 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\HttpKernel\Fragment;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\Controller\ControllerReference;
use Symfony\Component\HttpKernel\HttpCache\SurrogateInterface;
/**
* Implements Surrogate rendering strategy.
*
* @author Fabien Potencier <fabien@symfony.com>
*/
abstract class AbstractSurrogateFragmentRenderer extends RoutableFragmentRenderer
{
private $surrogate;
private $inlineStrategy;
/**
* Constructor.
*
* The "fallback" strategy when surrogate is not available should always be an
* instance of InlineFragmentRenderer.
*
* @param SurrogateInterface $surrogate An Surrogate instance
* @param FragmentRendererInterface $inlineStrategy The inline strategy to use when the surrogate is not supported
*/
public function __construct(SurrogateInterface $surrogate = null, FragmentRendererInterface $inlineStrategy)
{
$this->surrogate = $surrogate;
$this->inlineStrategy = $inlineStrategy;
}
/**
* {@inheritdoc}
*
* Note that if the current Request has no surrogate capability, this method
* falls back to use the inline rendering strategy.
*
* Additional available options:
*
* * alt: an alternative URI to render in case of an error
* * comment: a comment to add when returning the surrogate tag
*
* Note, that not all surrogate strategies support all options. For now
* 'alt' and 'comment' are only supported by ESI.
*
* @see Symfony\Component\HttpKernel\HttpCache\SurrogateInterface
*/
public function render($uri, Request $request, array $options = array())
{
if (!$this->surrogate || !$this->surrogate->hasSurrogateCapability($request)) {
return $this->inlineStrategy->render($uri, $request, $options);
}
if ($uri instanceof ControllerReference) {
$uri = $this->generateFragmentUri($uri, $request);
}
$alt = isset($options['alt']) ? $options['alt'] : null;
if ($alt instanceof ControllerReference) {
$alt = $this->generateFragmentUri($alt, $request);
}
$tag = $this->surrogate->renderIncludeTag($uri, $alt, isset($options['ignore_errors']) ? $options['ignore_errors'] : false, isset($options['comment']) ? $options['comment'] : '');
return new Response($tag);
}
}

View File

@ -11,69 +11,13 @@
namespace Symfony\Component\HttpKernel\Fragment;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\Controller\ControllerReference;
use Symfony\Component\HttpKernel\HttpCache\Esi;
/**
* Implements the ESI rendering strategy.
*
* @author Fabien Potencier <fabien@symfony.com>
*/
class EsiFragmentRenderer extends RoutableFragmentRenderer
class EsiFragmentRenderer extends AbstractSurrogateFragmentRenderer
{
private $esi;
private $inlineStrategy;
/**
* Constructor.
*
* The "fallback" strategy when ESI is not available should always be an
* instance of InlineFragmentRenderer.
*
* @param Esi $esi An Esi instance
* @param FragmentRendererInterface $inlineStrategy The inline strategy to use when ESI is not supported
*/
public function __construct(Esi $esi = null, InlineFragmentRenderer $inlineStrategy)
{
$this->esi = $esi;
$this->inlineStrategy = $inlineStrategy;
}
/**
* {@inheritdoc}
*
* Note that if the current Request has no ESI capability, this method
* falls back to use the inline rendering strategy.
*
* Additional available options:
*
* * alt: an alternative URI to render in case of an error
* * comment: a comment to add when returning an esi:include tag
*
* @see Symfony\Component\HttpKernel\HttpCache\ESI
*/
public function render($uri, Request $request, array $options = array())
{
if (!$this->esi || !$this->esi->hasSurrogateEsiCapability($request)) {
return $this->inlineStrategy->render($uri, $request, $options);
}
if ($uri instanceof ControllerReference) {
$uri = $this->generateFragmentUri($uri, $request);
}
$alt = isset($options['alt']) ? $options['alt'] : null;
if ($alt instanceof ControllerReference) {
$alt = $this->generateFragmentUri($alt, $request);
}
$tag = $this->esi->renderIncludeTag($uri, $alt, isset($options['ignore_errors']) ? $options['ignore_errors'] : false, isset($options['comment']) ? $options['comment'] : '');
return new Response($tag);
}
/**
* {@inheritdoc}
*/

View File

@ -0,0 +1,59 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\HttpKernel\Fragment;
use Symfony\Component\HttpKernel\Controller\ControllerReference;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpKernel\UriSigner;
/**
* Implements the ESI rendering strategy.
*
* @author Sebastian Krebs <krebs.seb@gmail.com>
*/
class SsiFragmentRenderer extends AbstractSurrogateFragmentRenderer
{
/** @var UriSigner */
private $signer;
/**
* Set uri signer
*
* @param UriSigner $signer
*/
public function setUriSigner(UriSigner $signer)
{
$this->signer = $signer;
}
/**
* {@inheritdoc}
*/
public function getName()
{
return 'ssi';
}
/**
* {@inheritdoc}
*/
protected function generateFragmentUri(ControllerReference $reference, Request $request, $absolute = false, $strict = true)
{
$uri = parent::generateFragmentUri($reference, $request, $absolute, $strict);
if ($this->signer) {
$uri = $this->signer->sign($uri);
}
return $uri;
}
}

View File

@ -26,7 +26,7 @@ use Symfony\Component\HttpKernel\HttpKernelInterface;
*
* @author Fabien Potencier <fabien@symfony.com>
*/
class Esi
class Esi implements SurrogateInterface
{
private $contentTypes;
@ -41,10 +41,15 @@ class Esi
$this->contentTypes = $contentTypes;
}
public function getName()
{
return 'esi';
}
/**
* Returns a new cache strategy instance.
*
* @return EsiResponseCacheStrategyInterface A EsiResponseCacheStrategyInterface instance
* @return ResponseCacheStrategyInterface A ResponseCacheStrategyInterface instance
*/
public function createCacheStrategy()
{
@ -58,6 +63,20 @@ class Esi
*
* @return bool true if one surrogate has ESI/1.0 capability, false otherwise
*/
public function hasSurrogateCapability(Request $request)
{
return $this->hasSurrogateEsiCapability($request);
}
/**
* Checks that at least one surrogate has ESI/1.0 capability.
*
* @param Request $request A Request instance
*
* @return bool true if one surrogate has ESI/1.0 capability, false otherwise
*
* @deprecated Deprecated since version 2.6, to be removed in 3.0. Use hasSurrogateCapability() instead
*/
public function hasSurrogateEsiCapability(Request $request)
{
if (null === $value = $request->headers->get('Surrogate-Capability')) {
@ -72,6 +91,18 @@ class Esi
*
* @param Request $request A Request instance
*/
public function addSurrogateCapability(Request $request)
{
$this->addSurrogateEsiCapability($request);
}
/**
* Adds ESI/1.0 capability to the given Request.
*
* @param Request $request A Request instance
*
* @deprecated Deprecated since version 2.6, to be removed in 3.0. Use addSurrogateCapability() instead
*/
public function addSurrogateEsiCapability(Request $request)
{
$current = $request->headers->get('Surrogate-Capability');
@ -101,6 +132,20 @@ class Esi
*
* @return bool true if the Response needs to be parsed, false otherwise
*/
public function needsParsing(Response $response)
{
return $this->needsEsiParsing($response);
}
/**
* Checks that the Response needs to be parsed for ESI tags.
*
* @param Response $response A Response instance
*
* @return bool true if the Response needs to be parsed, false otherwise
*
* @deprecated Deprecated since version 2.6, to be removed in 3.0. Use needsParsing() instead
*/
public function needsEsiParsing(Response $response)
{
if (!$control = $response->headers->get('Surrogate-Control')) {
@ -236,7 +281,7 @@ class Esi
throw new \RuntimeException('Unable to process an ESI tag without a "src" attribute.');
}
return sprintf('<?php echo $this->esi->handle($this, \'%s\', \'%s\', %s) ?>'."\n",
return sprintf('<?php echo $this->surrogate->handle($this, \'%s\', \'%s\', %s) ?>'."\n",
$options['src'],
isset($options['alt']) ? $options['alt'] : null,
isset($options['onerror']) && 'continue' == $options['onerror'] ? 'true' : 'false'

View File

@ -15,8 +15,6 @@
namespace Symfony\Component\HttpKernel\HttpCache;
use Symfony\Component\HttpFoundation\Response;
/**
* EsiResponseCacheStrategy knows how to compute the Response cache HTTP header
* based on the different ESI response cache headers.
@ -25,61 +23,9 @@ use Symfony\Component\HttpFoundation\Response;
* or force validation if one of the ESI has validation cache strategy.
*
* @author Fabien Potencier <fabien@symfony.com>
*
* @deprecated Deprecated since version 2.6, to be removed in 3.0. Use ResponseCacheStrategy instead
*/
class EsiResponseCacheStrategy implements EsiResponseCacheStrategyInterface
class EsiResponseCacheStrategy extends ResponseCacheStrategy implements EsiResponseCacheStrategyInterface
{
private $cacheable = true;
private $embeddedResponses = 0;
private $ttls = array();
private $maxAges = array();
/**
* {@inheritdoc}
*/
public function add(Response $response)
{
if ($response->isValidateable()) {
$this->cacheable = false;
} else {
$this->ttls[] = $response->getTtl();
$this->maxAges[] = $response->getMaxAge();
}
$this->embeddedResponses++;
}
/**
* {@inheritdoc}
*/
public function update(Response $response)
{
// if we have no embedded Response, do nothing
if (0 === $this->embeddedResponses) {
return;
}
// Remove validation related headers in order to avoid browsers using
// their own cache, because some of the response content comes from
// at least one embedded response (which likely has a different caching strategy).
if ($response->isValidateable()) {
$response->setEtag(null);
$response->setLastModified(null);
$this->cacheable = false;
}
if (!$this->cacheable) {
$response->headers->set('Cache-Control', 'no-cache, must-revalidate');
return;
}
$this->ttls[] = $response->getTtl();
$this->maxAges[] = $response->getMaxAge();
if (null !== $maxAge = min($this->maxAges)) {
$response->setSharedMaxAge($maxAge);
$response->headers->set('Age', $maxAge - min($this->ttls));
}
$response->setMaxAge(0);
}
}

View File

@ -15,27 +15,14 @@
namespace Symfony\Component\HttpKernel\HttpCache;
use Symfony\Component\HttpFoundation\Response;
/**
* EsiResponseCacheStrategyInterface implementations know how to compute the
* Response cache HTTP header based on the different ESI response cache headers.
* ResponseCacheStrategyInterface implementations know how to compute the
* Response cache HTTP header based on the different response cache headers.
*
* @author Fabien Potencier <fabien@symfony.com>
*
* @deprecated Deprecated since version 2.6, to be removed in 3.0. Use ResponseCacheStrategyInterface instead
*/
interface EsiResponseCacheStrategyInterface
interface EsiResponseCacheStrategyInterface extends ResponseCacheStrategyInterface
{
/**
* Adds a Response.
*
* @param Response $response
*/
public function add(Response $response);
/**
* Updates the Response HTTP headers based on the embedded Responses.
*
* @param Response $response
*/
public function update(Response $response);
}

View File

@ -32,8 +32,8 @@ class HttpCache implements HttpKernelInterface, TerminableInterface
private $kernel;
private $store;
private $request;
private $esi;
private $esiCacheStrategy;
private $surrogate;
private $surrogateCacheStrategy;
private $options = array();
private $traces = array();
@ -74,14 +74,14 @@ class HttpCache implements HttpKernelInterface, TerminableInterface
*
* @param HttpKernelInterface $kernel An HttpKernelInterface instance
* @param StoreInterface $store A Store instance
* @param Esi $esi An Esi instance
* @param SurrogateInterface $surrogate A SurrogateInterface instance
* @param array $options An array of options
*/
public function __construct(HttpKernelInterface $kernel, StoreInterface $store, Esi $esi = null, array $options = array())
public function __construct(HttpKernelInterface $kernel, StoreInterface $store, SurrogateInterface $surrogate = null, array $options = array())
{
$this->store = $store;
$this->kernel = $kernel;
$this->esi = $esi;
$this->surrogate = $surrogate;
// needed in case there is a fatal error because the backend is too slow to respond
register_shutdown_function(array($this->store, 'cleanup'));
@ -153,14 +153,33 @@ class HttpCache implements HttpKernelInterface, TerminableInterface
}
/**
* Gets the Surrogate instance
*
* @throws \LogicException
* @return SurrogateInterface A Surrogate instance
*/
public function getSurrogate()
{
return $this->getEsi();
}
/**
* Gets the Esi instance
*
* @throws \LogicException
* @return Esi An Esi instance
*
* @deprecated Deprecated since version 2.6, to be removed in 3.0. Use getSurrogate() instead
*/
public function getEsi()
{
return $this->esi;
if (!$this->surrogate instanceof Esi) {
throw new \LogicException('This instance of HttpCache was not set up to use ESI as surrogate handler. You must overwrite and use createSurrogate');
}
return $this->surrogate;
}
/**
@ -174,8 +193,8 @@ class HttpCache implements HttpKernelInterface, TerminableInterface
if (HttpKernelInterface::MASTER_REQUEST === $type) {
$this->traces = array();
$this->request = $request;
if (null !== $this->esi) {
$this->esiCacheStrategy = $this->esi->createCacheStrategy();
if (null !== $this->surrogate) {
$this->surrogateCacheStrategy = $this->surrogate->createCacheStrategy();
}
}
@ -201,11 +220,11 @@ class HttpCache implements HttpKernelInterface, TerminableInterface
$response->headers->set('X-Symfony-Cache', $this->getLog());
}
if (null !== $this->esi) {
if (null !== $this->surrogate) {
if (HttpKernelInterface::MASTER_REQUEST === $type) {
$this->esiCacheStrategy->update($response);
$this->surrogateCacheStrategy->update($response);
} else {
$this->esiCacheStrategy->add($response);
$this->surrogateCacheStrategy->add($response);
}
}
@ -446,8 +465,8 @@ class HttpCache implements HttpKernelInterface, TerminableInterface
*/
protected function forward(Request $request, $catch = false, Response $entry = null)
{
if ($this->esi) {
$this->esi->addSurrogateEsiCapability($request);
if ($this->surrogate) {
$this->surrogate->addSurrogateCapability($request);
}
// modify the X-Forwarded-For header if needed
@ -640,8 +659,8 @@ class HttpCache implements HttpKernelInterface, TerminableInterface
protected function processResponseBody(Request $request, Response $response)
{
if (null !== $this->esi && $this->esi->needsEsiParsing($response)) {
$this->esi->process($request, $response);
if (null !== $this->surrogate && $this->surrogate->needsParsing($response)) {
$this->surrogate->process($request, $response);
}
}

View File

@ -0,0 +1,85 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.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)
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\HttpKernel\HttpCache;
use Symfony\Component\HttpFoundation\Response;
/**
* ResponseCacheStrategy knows how to compute the Response cache HTTP header
* based on the different response cache headers.
*
* This implementation changes the master response TTL to the smallest TTL received
* or force validation if one of the surrogates has validation cache strategy.
*
* @author Fabien Potencier <fabien@symfony.com>
*/
class ResponseCacheStrategy implements ResponseCacheStrategyInterface
{
private $cacheable = true;
private $embeddedResponses = 0;
private $ttls = array();
private $maxAges = array();
/**
* {@inheritdoc}
*/
public function add(Response $response)
{
if ($response->isValidateable()) {
$this->cacheable = false;
} else {
$this->ttls[] = $response->getTtl();
$this->maxAges[] = $response->getMaxAge();
}
$this->embeddedResponses++;
}
/**
* {@inheritdoc}
*/
public function update(Response $response)
{
// if we have no embedded Response, do nothing
if (0 === $this->embeddedResponses) {
return;
}
// Remove validation related headers in order to avoid browsers using
// their own cache, because some of the response content comes from
// at least one embedded response (which likely has a different caching strategy).
if ($response->isValidateable()) {
$response->setEtag(null);
$response->setLastModified(null);
$this->cacheable = false;
}
if (!$this->cacheable) {
$response->headers->set('Cache-Control', 'no-cache, must-revalidate');
return;
}
$this->ttls[] = $response->getTtl();
$this->maxAges[] = $response->getMaxAge();
if (null !== $maxAge = min($this->maxAges)) {
$response->setSharedMaxAge($maxAge);
$response->headers->set('Age', $maxAge - min($this->ttls));
}
$response->setMaxAge(0);
}
}

View File

@ -0,0 +1,41 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.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)
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\HttpKernel\HttpCache;
use Symfony\Component\HttpFoundation\Response;
/**
* ResponseCacheStrategyInterface implementations know how to compute the
* Response cache HTTP header based on the different response cache headers.
*
* @author Fabien Potencier <fabien@symfony.com>
*/
interface ResponseCacheStrategyInterface
{
/**
* Adds a Response.
*
* @param Response $response
*/
public function add(Response $response);
/**
* Updates the Response HTTP headers based on the embedded Responses.
*
* @param Response $response
*/
public function update(Response $response);
}

View File

@ -0,0 +1,197 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\HttpKernel\HttpCache;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\HttpKernelInterface;
/**
* Ssi implements the SSI capabilities to Request and Response instances.
*
* @author Sebastian Krebs <krebs.seb@gmail.com>
*/
class Ssi implements SurrogateInterface
{
private $contentTypes;
/**
* Constructor.
*
* @param array $contentTypes An array of content-type that should be parsed for SSI information.
* (default: text/html, text/xml, application/xhtml+xml, and application/xml)
*/
public function __construct(array $contentTypes = array('text/html', 'text/xml', 'application/xhtml+xml', 'application/xml'))
{
$this->contentTypes = $contentTypes;
}
/**
* {@inheritdoc}
*/
public function getName()
{
return 'ssi';
}
/**
* {@inheritdoc}
*/
public function createCacheStrategy()
{
return new ResponseCacheStrategy();
}
/**
* {@inheritdoc}
*/
public function hasSurrogateCapability(Request $request)
{
if (null === $value = $request->headers->get('Surrogate-Capability')) {
return false;
}
return false !== strpos($value, 'SSI/1.0');
}
/**
* {@inheritdoc}
*/
public function addSurrogateCapability(Request $request)
{
$current = $request->headers->get('Surrogate-Capability');
$new = 'symfony2="SSI/1.0"';
$request->headers->set('Surrogate-Capability', $current ? $current . ', ' . $new : $new);
}
/**
* {@inheritdoc}
*/
public function addSurrogateControl(Response $response)
{
if (false !== strpos($response->getContent(), '<!--#include')) {
$response->headers->set('Surrogate-Control', 'content="SSI/1.0"');
}
}
/**
* {@inheritdoc}
*/
public function needsParsing(Response $response)
{
if (!$control = $response->headers->get('Surrogate-Control')) {
return false;
}
return (bool) preg_match('#content="[^"]*SSI/1.0[^"]*"#', $control);
}
/**
* {@inheritdoc}
*/
public function renderIncludeTag($uri, $alt = null, $ignoreErrors = true, $comment = '')
{
return sprintf('<!--#include virtual="%s" -->', $uri);
}
/**
* {@inheritdoc}
*/
public function process(Request $request, Response $response)
{
$this->request = $request;
$type = $response->headers->get('Content-Type');
if (empty($type)) {
$type = 'text/html';
}
$parts = explode(';', $type);
if (!in_array($parts[0], $this->contentTypes)) {
return $response;
}
// we don't use a proper XML parser here as we can have SSI tags in a plain text response
$content = $response->getContent();
$content = str_replace(array('<?', '<%'), array('<?php echo "<?"; ?>', '<?php echo "<%"; ?>'), $content);
$content = preg_replace_callback('#<!--\#include\s+(.*?)\s*-->#', array($this, 'handleIncludeTag'), $content);
$response->setContent($content);
$response->headers->set('X-Body-Eval', 'SSI');
// remove SSI/1.0 from the Surrogate-Control header
if ($response->headers->has('Surrogate-Control')) {
$value = $response->headers->get('Surrogate-Control');
if ('content="SSI/1.0"' == $value) {
$response->headers->remove('Surrogate-Control');
} elseif (preg_match('#,\s*content="SSI/1.0"#', $value)) {
$response->headers->set('Surrogate-Control', preg_replace('#,\s*content="SSI/1.0"#', '', $value));
} elseif (preg_match('#content="SSI/1.0",\s*#', $value)) {
$response->headers->set('Surrogate-Control', preg_replace('#content="SSI/1.0",\s*#', '', $value));
}
}
}
/**
* {@inheritdoc}
*/
public function handle(HttpCache $cache, $uri, $alt, $ignoreErrors)
{
$subRequest = Request::create($uri, 'get', array(), $cache->getRequest()->cookies->all(), array(), $cache->getRequest()->server->all());
try {
$response = $cache->handle($subRequest, HttpKernelInterface::SUB_REQUEST, true);
if (!$response->isSuccessful()) {
throw new \RuntimeException(sprintf('Error when rendering "%s" (Status code is %s).', $subRequest->getUri(), $response->getStatusCode()));
}
return $response->getContent();
} catch (\Exception $e) {
if ($alt) {
return $this->handle($cache, $alt, '', $ignoreErrors);
}
if (!$ignoreErrors) {
throw $e;
}
}
}
/**
* Handles an SSI include tag (called internally).
*
* @param array $attributes An array containing the attributes.
*
* @return string The response content for the include.
*
* @throws \RuntimeException
*/
private function handleIncludeTag($attributes)
{
$options = array();
preg_match_all('/(virtual)="([^"]*?)"/', $attributes[1], $matches, PREG_SET_ORDER);
foreach ($matches as $set) {
$options[$set[1]] = $set[2];
}
if (!isset($options['virtual'])) {
throw new \RuntimeException('Unable to process an SSI tag without a "virtual" attribute.');
}
return sprintf('<?php echo $this->surrogate->handle($this, \'%s\', \'%s\', %s) ?>' . "\n",
$options['virtual'],
'',
'false'
);
}
}

View File

@ -0,0 +1,103 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\HttpKernel\HttpCache;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
interface SurrogateInterface
{
/**
* Returns surrogate name
*
* @return string
*/
public function getName();
/**
* Returns a new cache strategy instance.
*
* @return ResponseCacheStrategyInterface A ResponseCacheStrategyInterface instance
*/
public function createCacheStrategy();
/**
* Checks that at least one surrogate has Surrogate capability.
*
* @param Request $request A Request instance
*
* @return bool true if one surrogate has Surrogate capability, false otherwise
*/
public function hasSurrogateCapability(Request $request);
/**
* Adds Surrogate-capability to the given Request.
*
* @param Request $request A Request instance
*/
public function addSurrogateCapability(Request $request);
/**
* Adds HTTP headers to specify that the Response needs to be parsed for Surrogate.
*
* This method only adds an Surrogate HTTP header if the Response has some Surrogate tags.
*
* @param Response $response A Response instance
*/
public function addSurrogateControl(Response $response);
/**
* Checks that the Response needs to be parsed for Surrogate tags.
*
* @param Response $response A Response instance
*
* @return bool true if the Response needs to be parsed, false otherwise
*/
public function needsParsing(Response $response);
/**
* Renders a Surrogate tag.
*
* @param string $uri A URI
* @param string $alt An alternate URI
* @param bool $ignoreErrors Whether to ignore errors or not
* @param string $comment A comment to add as an esi:include tag
*
* @return string
*/
public function renderIncludeTag($uri, $alt = null, $ignoreErrors = true, $comment = '');
/**
* Replaces a Response Surrogate tags with the included resource content.
*
* @param Request $request A Request instance
* @param Response $response A Response instance
*
* @return Response
*/
public function process(Request $request, Response $response);
/**
* Handles a Surrogate from the cache.
*
* @param HttpCache $cache An HttpCache instance
* @param string $uri The main URI
* @param string $alt An alternative URI
* @param bool $ignoreErrors Whether to ignore errors or not
*
* @return string
*
* @throws \RuntimeException
* @throws \Exception
*/
public function handle(HttpCache $cache, $uri, $alt, $ignoreErrors);
}

View File

@ -12,7 +12,7 @@
namespace Symfony\Component\HttpKernel\Tests\EventListener;
use Symfony\Component\HttpKernel\HttpCache\Esi;
use Symfony\Component\HttpKernel\EventListener\EsiListener;
use Symfony\Component\HttpKernel\EventListener\SurrogateListener;
use Symfony\Component\HttpKernel\Event\FilterResponseEvent;
use Symfony\Component\HttpKernel\KernelEvents;
use Symfony\Component\HttpKernel\HttpKernelInterface;
@ -27,7 +27,7 @@ class EsiListenerTest extends \PHPUnit_Framework_TestCase
$dispatcher = new EventDispatcher();
$kernel = $this->getMock('Symfony\Component\HttpKernel\HttpKernelInterface');
$response = new Response('foo <esi:include src="" />');
$listener = new EsiListener(new Esi());
$listener = new SurrogateListener(new Esi());
$dispatcher->addListener(KernelEvents::RESPONSE, array($listener, 'onKernelResponse'));
$event = new FilterResponseEvent($kernel, new Request(), HttpKernelInterface::SUB_REQUEST, $response);
@ -41,7 +41,7 @@ class EsiListenerTest extends \PHPUnit_Framework_TestCase
$dispatcher = new EventDispatcher();
$kernel = $this->getMock('Symfony\Component\HttpKernel\HttpKernelInterface');
$response = new Response('foo <esi:include src="" />');
$listener = new EsiListener(new Esi());
$listener = new SurrogateListener(new Esi());
$dispatcher->addListener(KernelEvents::RESPONSE, array($listener, 'onKernelResponse'));
$event = new FilterResponseEvent($kernel, new Request(), HttpKernelInterface::MASTER_REQUEST, $response);
@ -55,7 +55,7 @@ class EsiListenerTest extends \PHPUnit_Framework_TestCase
$dispatcher = new EventDispatcher();
$kernel = $this->getMock('Symfony\Component\HttpKernel\HttpKernelInterface');
$response = new Response('foo');
$listener = new EsiListener(new Esi());
$listener = new SurrogateListener(new Esi());
$dispatcher->addListener(KernelEvents::RESPONSE, array($listener, 'onKernelResponse'));
$event = new FilterResponseEvent($kernel, new Request(), HttpKernelInterface::MASTER_REQUEST, $response);

View File

@ -23,14 +23,14 @@ class EsiTest extends \PHPUnit_Framework_TestCase
$request = Request::create('/');
$request->headers->set('Surrogate-Capability', 'abc="ESI/1.0"');
$this->assertTrue($esi->hasSurrogateEsiCapability($request));
$this->assertTrue($esi->hasSurrogateCapability($request));
$request = Request::create('/');
$request->headers->set('Surrogate-Capability', 'foobar');
$this->assertFalse($esi->hasSurrogateEsiCapability($request));
$this->assertFalse($esi->hasSurrogateCapability($request));
$request = Request::create('/');
$this->assertFalse($esi->hasSurrogateEsiCapability($request));
$this->assertFalse($esi->hasSurrogateCapability($request));
}
public function testAddSurrogateEsiCapability()
@ -38,10 +38,10 @@ class EsiTest extends \PHPUnit_Framework_TestCase
$esi = new Esi();
$request = Request::create('/');
$esi->addSurrogateEsiCapability($request);
$esi->addSurrogateCapability($request);
$this->assertEquals('symfony2="ESI/1.0"', $request->headers->get('Surrogate-Capability'));
$esi->addSurrogateEsiCapability($request);
$esi->addSurrogateCapability($request);
$this->assertEquals('symfony2="ESI/1.0", symfony2="ESI/1.0"', $request->headers->get('Surrogate-Capability'));
}
@ -64,10 +64,10 @@ class EsiTest extends \PHPUnit_Framework_TestCase
$response = new Response();
$response->headers->set('Surrogate-Control', 'content="ESI/1.0"');
$this->assertTrue($esi->needsEsiParsing($response));
$this->assertTrue($esi->needsParsing($response));
$response = new Response();
$this->assertFalse($esi->needsEsiParsing($response));
$this->assertFalse($esi->needsParsing($response));
}
public function testRenderIncludeTag()
@ -100,18 +100,18 @@ class EsiTest extends \PHPUnit_Framework_TestCase
$response = new Response('foo <esi:comment text="some comment" /><esi:include src="..." alt="alt" onerror="continue" />');
$esi->process($request, $response);
$this->assertEquals('foo <?php echo $this->esi->handle($this, \'...\', \'alt\', true) ?>'."\n", $response->getContent());
$this->assertEquals('foo <?php echo $this->surrogate->handle($this, \'...\', \'alt\', true) ?>'."\n", $response->getContent());
$this->assertEquals('ESI', $response->headers->get('x-body-eval'));
$response = new Response('foo <esi:include src="..." />');
$esi->process($request, $response);
$this->assertEquals('foo <?php echo $this->esi->handle($this, \'...\', \'\', false) ?>'."\n", $response->getContent());
$this->assertEquals('foo <?php echo $this->surrogate->handle($this, \'...\', \'\', false) ?>'."\n", $response->getContent());
$response = new Response('foo <esi:include src="..."></esi:include>');
$esi->process($request, $response);
$this->assertEquals('foo <?php echo $this->esi->handle($this, \'...\', \'\', false) ?>'."\n", $response->getContent());
$this->assertEquals('foo <?php echo $this->surrogate->handle($this, \'...\', \'\', false) ?>'."\n", $response->getContent());
}
public function testProcessEscapesPhpTags()

View File

@ -0,0 +1,209 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\HttpKernel\Tests\HttpCache;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\HttpCache\Ssi;
class SsiTest extends \PHPUnit_Framework_TestCase
{
public function testHasSurrogateEsiCapability()
{
$ssi = new Ssi();
$request = Request::create('/');
$request->headers->set('Surrogate-Capability', 'abc="SSI/1.0"');
$this->assertTrue($ssi->hasSurrogateCapability($request));
$request = Request::create('/');
$request->headers->set('Surrogate-Capability', 'foobar');
$this->assertFalse($ssi->hasSurrogateCapability($request));
$request = Request::create('/');
$this->assertFalse($ssi->hasSurrogateCapability($request));
}
public function testAddSurrogateEsiCapability()
{
$ssi = new Ssi();
$request = Request::create('/');
$ssi->addSurrogateCapability($request);
$this->assertEquals('symfony2="SSI/1.0"', $request->headers->get('Surrogate-Capability'));
$ssi->addSurrogateCapability($request);
$this->assertEquals('symfony2="SSI/1.0", symfony2="SSI/1.0"', $request->headers->get('Surrogate-Capability'));
}
public function testAddSurrogateControl()
{
$ssi = new Ssi();
$response = new Response('foo <!--#include virtual="" -->');
$ssi->addSurrogateControl($response);
$this->assertEquals('content="SSI/1.0"', $response->headers->get('Surrogate-Control'));
$response = new Response('foo');
$ssi->addSurrogateControl($response);
$this->assertEquals('', $response->headers->get('Surrogate-Control'));
}
public function testNeedsEsiParsing()
{
$ssi = new Ssi();
$response = new Response();
$response->headers->set('Surrogate-Control', 'content="SSI/1.0"');
$this->assertTrue($ssi->needsParsing($response));
$response = new Response();
$this->assertFalse($ssi->needsParsing($response));
}
public function testRenderIncludeTag()
{
$ssi = new Ssi();
$this->assertEquals('<!--#include virtual="/" -->', $ssi->renderIncludeTag('/', '/alt', true));
$this->assertEquals('<!--#include virtual="/" -->', $ssi->renderIncludeTag('/', '/alt', false));
$this->assertEquals('<!--#include virtual="/" -->', $ssi->renderIncludeTag('/'));
}
public function testProcessDoesNothingIfContentTypeIsNotHtml()
{
$ssi = new Ssi();
$request = Request::create('/');
$response = new Response();
$response->headers->set('Content-Type', 'text/plain');
$ssi->process($request, $response);
$this->assertFalse($response->headers->has('x-body-eval'));
}
public function testProcess()
{
$ssi = new Ssi();
$request = Request::create('/');
$response = new Response('foo <!--#include virtual="..." -->');
$ssi->process($request, $response);
$this->assertEquals('foo <?php echo $this->surrogate->handle($this, \'...\', \'\', false) ?>'."\n", $response->getContent());
$this->assertEquals('SSI', $response->headers->get('x-body-eval'));
}
public function testProcessEscapesPhpTags()
{
$ssi = new Ssi();
$request = Request::create('/');
$response = new Response('foo <?php die("foo"); ?><%= "lala" %>');
$ssi->process($request, $response);
$this->assertEquals('foo <?php echo "<?"; ?>php die("foo"); ?><?php echo "<%"; ?>= "lala" %>', $response->getContent());
}
/**
* @expectedException \RuntimeException
*/
public function testProcessWhenNoSrcInAnEsi()
{
$ssi = new Ssi();
$request = Request::create('/');
$response = new Response('foo <!--#include -->');
$ssi->process($request, $response);
}
public function testProcessRemoveSurrogateControlHeader()
{
$ssi = new Ssi();
$request = Request::create('/');
$response = new Response('foo <!--#include virtual="..." -->');
$response->headers->set('Surrogate-Control', 'content="SSI/1.0"');
$ssi->process($request, $response);
$this->assertEquals('SSI', $response->headers->get('x-body-eval'));
$response->headers->set('Surrogate-Control', 'no-store, content="SSI/1.0"');
$ssi->process($request, $response);
$this->assertEquals('SSI', $response->headers->get('x-body-eval'));
$this->assertEquals('no-store', $response->headers->get('surrogate-control'));
$response->headers->set('Surrogate-Control', 'content="SSI/1.0", no-store');
$ssi->process($request, $response);
$this->assertEquals('SSI', $response->headers->get('x-body-eval'));
$this->assertEquals('no-store', $response->headers->get('surrogate-control'));
}
public function testHandle()
{
$ssi = new Ssi();
$cache = $this->getCache(Request::create('/'), new Response('foo'));
$this->assertEquals('foo', $ssi->handle($cache, '/', '/alt', true));
}
/**
* @expectedException \RuntimeException
*/
public function testHandleWhenResponseIsNot200()
{
$ssi = new Ssi();
$response = new Response('foo');
$response->setStatusCode(404);
$cache = $this->getCache(Request::create('/'), $response);
$ssi->handle($cache, '/', '/alt', false);
}
public function testHandleWhenResponseIsNot200AndErrorsAreIgnored()
{
$ssi = new Ssi();
$response = new Response('foo');
$response->setStatusCode(404);
$cache = $this->getCache(Request::create('/'), $response);
$this->assertEquals('', $ssi->handle($cache, '/', '/alt', true));
}
public function testHandleWhenResponseIsNot200AndAltIsPresent()
{
$ssi = new Ssi();
$response1 = new Response('foo');
$response1->setStatusCode(404);
$response2 = new Response('bar');
$cache = $this->getCache(Request::create('/'), array($response1, $response2));
$this->assertEquals('bar', $ssi->handle($cache, '/', '/alt', false));
}
protected function getCache($request, $response)
{
$cache = $this->getMock('Symfony\Component\HttpKernel\HttpCache\HttpCache', array('getRequest', 'handle'), array(), '', false);
$cache->expects($this->any())
->method('getRequest')
->will($this->returnValue($request))
;
if (is_array($response)) {
$cache->expects($this->any())
->method('handle')
->will(call_user_func_array(array($this, 'onConsecutiveCalls'), $response))
;
} else {
$cache->expects($this->any())
->method('handle')
->will($this->returnValue($response))
;
}
return $cache;
}
}