diff --git a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Configuration.php b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Configuration.php index c9a4482b0b..ec6c1f0a28 100644 --- a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Configuration.php +++ b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Configuration.php @@ -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 diff --git a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php index f9146c242e..49121e716e 100644 --- a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php +++ b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php @@ -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. * diff --git a/src/Symfony/Bundle/FrameworkBundle/HttpCache/HttpCache.php b/src/Symfony/Bundle/FrameworkBundle/HttpCache/HttpCache.php index 2e5a2312dd..bc5a0cc82f 100644 --- a/src/Symfony/Bundle/FrameworkBundle/HttpCache/HttpCache.php +++ b/src/Symfony/Bundle/FrameworkBundle/HttpCache/HttpCache.php @@ -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(); diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/config/fragment_renderer.xml b/src/Symfony/Bundle/FrameworkBundle/Resources/config/fragment_renderer.xml index a1beee30a1..06003d684f 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Resources/config/fragment_renderer.xml +++ b/src/Symfony/Bundle/FrameworkBundle/Resources/config/fragment_renderer.xml @@ -10,6 +10,7 @@ Symfony\Bundle\FrameworkBundle\Fragment\ContainerAwareHIncludeFragmentRenderer Symfony\Component\HttpKernel\Fragment\EsiFragmentRenderer + Symfony\Component\HttpKernel\Fragment\SsiFragmentRenderer /_fragment @@ -41,5 +42,17 @@ %fragment.path% + + + + + + + %fragment.path% + + + + + diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/config/ssi.xml b/src/Symfony/Bundle/FrameworkBundle/Resources/config/ssi.xml new file mode 100644 index 0000000000..fd2fdd776e --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Resources/config/ssi.xml @@ -0,0 +1,20 @@ + + + + + + Symfony\Component\HttpKernel\HttpCache\Ssi + Symfony\Component\HttpKernel\EventListener\SurrogateListener + + + + + + + + + + + diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/ConfigurationTest.php b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/ConfigurationTest.php index 794ca1922c..05f74ad4c8 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/ConfigurationTest.php +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/ConfigurationTest.php @@ -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', diff --git a/src/Symfony/Component/HttpKernel/EventListener/EsiListener.php b/src/Symfony/Component/HttpKernel/EventListener/EsiListener.php index 686778afc4..6380169017 100644 --- a/src/Symfony/Component/HttpKernel/EventListener/EsiListener.php +++ b/src/Symfony/Component/HttpKernel/EventListener/EsiListener.php @@ -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 + * + * @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', - ); - } } diff --git a/src/Symfony/Component/HttpKernel/EventListener/SurrogateListener.php b/src/Symfony/Component/HttpKernel/EventListener/SurrogateListener.php new file mode 100644 index 0000000000..00f4fbf24e --- /dev/null +++ b/src/Symfony/Component/HttpKernel/EventListener/SurrogateListener.php @@ -0,0 +1,58 @@ + + * + * 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 + */ +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', + ); + } +} diff --git a/src/Symfony/Component/HttpKernel/Fragment/AbstractSurrogateFragmentRenderer.php b/src/Symfony/Component/HttpKernel/Fragment/AbstractSurrogateFragmentRenderer.php new file mode 100644 index 0000000000..69e43a371f --- /dev/null +++ b/src/Symfony/Component/HttpKernel/Fragment/AbstractSurrogateFragmentRenderer.php @@ -0,0 +1,79 @@ + + * + * 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 + */ +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); + } +} diff --git a/src/Symfony/Component/HttpKernel/Fragment/EsiFragmentRenderer.php b/src/Symfony/Component/HttpKernel/Fragment/EsiFragmentRenderer.php index 620c71a878..a4570e3beb 100644 --- a/src/Symfony/Component/HttpKernel/Fragment/EsiFragmentRenderer.php +++ b/src/Symfony/Component/HttpKernel/Fragment/EsiFragmentRenderer.php @@ -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 */ -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} */ diff --git a/src/Symfony/Component/HttpKernel/Fragment/SsiFragmentRenderer.php b/src/Symfony/Component/HttpKernel/Fragment/SsiFragmentRenderer.php new file mode 100644 index 0000000000..1d524391b9 --- /dev/null +++ b/src/Symfony/Component/HttpKernel/Fragment/SsiFragmentRenderer.php @@ -0,0 +1,59 @@ + + * + * 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 + */ +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; + } +} diff --git a/src/Symfony/Component/HttpKernel/HttpCache/Esi.php b/src/Symfony/Component/HttpKernel/HttpCache/Esi.php index 3ba9ecd818..70d8e11773 100644 --- a/src/Symfony/Component/HttpKernel/HttpCache/Esi.php +++ b/src/Symfony/Component/HttpKernel/HttpCache/Esi.php @@ -26,7 +26,7 @@ use Symfony\Component\HttpKernel\HttpKernelInterface; * * @author Fabien Potencier */ -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('esi->handle($this, \'%s\', \'%s\', %s) ?>'."\n", + return sprintf('surrogate->handle($this, \'%s\', \'%s\', %s) ?>'."\n", $options['src'], isset($options['alt']) ? $options['alt'] : null, isset($options['onerror']) && 'continue' == $options['onerror'] ? 'true' : 'false' diff --git a/src/Symfony/Component/HttpKernel/HttpCache/EsiResponseCacheStrategy.php b/src/Symfony/Component/HttpKernel/HttpCache/EsiResponseCacheStrategy.php index 6384af9660..1bef147595 100644 --- a/src/Symfony/Component/HttpKernel/HttpCache/EsiResponseCacheStrategy.php +++ b/src/Symfony/Component/HttpKernel/HttpCache/EsiResponseCacheStrategy.php @@ -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 + * + * @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); - } } diff --git a/src/Symfony/Component/HttpKernel/HttpCache/EsiResponseCacheStrategyInterface.php b/src/Symfony/Component/HttpKernel/HttpCache/EsiResponseCacheStrategyInterface.php index 0fb8a12436..03df0575a5 100644 --- a/src/Symfony/Component/HttpKernel/HttpCache/EsiResponseCacheStrategyInterface.php +++ b/src/Symfony/Component/HttpKernel/HttpCache/EsiResponseCacheStrategyInterface.php @@ -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 + * + * @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); } diff --git a/src/Symfony/Component/HttpKernel/HttpCache/HttpCache.php b/src/Symfony/Component/HttpKernel/HttpCache/HttpCache.php index 84a10f7d49..3c5eb065d1 100644 --- a/src/Symfony/Component/HttpKernel/HttpCache/HttpCache.php +++ b/src/Symfony/Component/HttpKernel/HttpCache/HttpCache.php @@ -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(); @@ -72,16 +72,16 @@ class HttpCache implements HttpKernelInterface, TerminableInterface * This setting is overridden by the stale-if-error HTTP Cache-Control extension * (see RFC 5861). * - * @param HttpKernelInterface $kernel An HttpKernelInterface instance - * @param StoreInterface $store A Store instance - * @param Esi $esi An Esi instance - * @param array $options An array of options + * @param HttpKernelInterface $kernel An HttpKernelInterface instance + * @param StoreInterface $store A Store 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); } } diff --git a/src/Symfony/Component/HttpKernel/HttpCache/ResponseCacheStrategy.php b/src/Symfony/Component/HttpKernel/HttpCache/ResponseCacheStrategy.php new file mode 100644 index 0000000000..d22dff1e5f --- /dev/null +++ b/src/Symfony/Component/HttpKernel/HttpCache/ResponseCacheStrategy.php @@ -0,0 +1,85 @@ + + * + * 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 + */ +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); + } +} diff --git a/src/Symfony/Component/HttpKernel/HttpCache/ResponseCacheStrategyInterface.php b/src/Symfony/Component/HttpKernel/HttpCache/ResponseCacheStrategyInterface.php new file mode 100644 index 0000000000..d70c2e06ec --- /dev/null +++ b/src/Symfony/Component/HttpKernel/HttpCache/ResponseCacheStrategyInterface.php @@ -0,0 +1,41 @@ + + * + * 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 + */ +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); +} diff --git a/src/Symfony/Component/HttpKernel/HttpCache/Ssi.php b/src/Symfony/Component/HttpKernel/HttpCache/Ssi.php new file mode 100644 index 0000000000..6f9d102b95 --- /dev/null +++ b/src/Symfony/Component/HttpKernel/HttpCache/Ssi.php @@ -0,0 +1,197 @@ + + * + * 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 + */ +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(), '', $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('', ''), $content); + $content = preg_replace_callback('##', 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('surrogate->handle($this, \'%s\', \'%s\', %s) ?>' . "\n", + $options['virtual'], + '', + 'false' + ); + } +} diff --git a/src/Symfony/Component/HttpKernel/HttpCache/SurrogateInterface.php b/src/Symfony/Component/HttpKernel/HttpCache/SurrogateInterface.php new file mode 100644 index 0000000000..c26304dfdf --- /dev/null +++ b/src/Symfony/Component/HttpKernel/HttpCache/SurrogateInterface.php @@ -0,0 +1,103 @@ + + * + * 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); +} diff --git a/src/Symfony/Component/HttpKernel/Tests/EventListener/EsiListenerTest.php b/src/Symfony/Component/HttpKernel/Tests/EventListener/EsiListenerTest.php index 56f68535f1..9b0517d03e 100644 --- a/src/Symfony/Component/HttpKernel/Tests/EventListener/EsiListenerTest.php +++ b/src/Symfony/Component/HttpKernel/Tests/EventListener/EsiListenerTest.php @@ -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 '); - $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 '); - $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); diff --git a/src/Symfony/Component/HttpKernel/Tests/HttpCache/EsiTest.php b/src/Symfony/Component/HttpKernel/Tests/HttpCache/EsiTest.php index c509706389..3133e1506a 100644 --- a/src/Symfony/Component/HttpKernel/Tests/HttpCache/EsiTest.php +++ b/src/Symfony/Component/HttpKernel/Tests/HttpCache/EsiTest.php @@ -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->process($request, $response); - $this->assertEquals('foo esi->handle($this, \'...\', \'alt\', true) ?>'."\n", $response->getContent()); + $this->assertEquals('foo surrogate->handle($this, \'...\', \'alt\', true) ?>'."\n", $response->getContent()); $this->assertEquals('ESI', $response->headers->get('x-body-eval')); $response = new Response('foo '); $esi->process($request, $response); - $this->assertEquals('foo esi->handle($this, \'...\', \'\', false) ?>'."\n", $response->getContent()); + $this->assertEquals('foo surrogate->handle($this, \'...\', \'\', false) ?>'."\n", $response->getContent()); $response = new Response('foo '); $esi->process($request, $response); - $this->assertEquals('foo esi->handle($this, \'...\', \'\', false) ?>'."\n", $response->getContent()); + $this->assertEquals('foo surrogate->handle($this, \'...\', \'\', false) ?>'."\n", $response->getContent()); } public function testProcessEscapesPhpTags() diff --git a/src/Symfony/Component/HttpKernel/Tests/HttpCache/SsiTest.php b/src/Symfony/Component/HttpKernel/Tests/HttpCache/SsiTest.php new file mode 100644 index 0000000000..225911e52f --- /dev/null +++ b/src/Symfony/Component/HttpKernel/Tests/HttpCache/SsiTest.php @@ -0,0 +1,209 @@ + + * + * 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 '); + $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('', $ssi->renderIncludeTag('/', '/alt', true)); + $this->assertEquals('', $ssi->renderIncludeTag('/', '/alt', false)); + $this->assertEquals('', $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 '); + $ssi->process($request, $response); + + $this->assertEquals('foo 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 <%= "lala" %>'); + $ssi->process($request, $response); + + $this->assertEquals('foo php die("foo"); ?>= "lala" %>', $response->getContent()); + } + + /** + * @expectedException \RuntimeException + */ + public function testProcessWhenNoSrcInAnEsi() + { + $ssi = new Ssi(); + + $request = Request::create('/'); + $response = new Response('foo '); + $ssi->process($request, $response); + } + + public function testProcessRemoveSurrogateControlHeader() + { + $ssi = new Ssi(); + + $request = Request::create('/'); + $response = new Response('foo '); + $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; + } +}