From 323dc7fc421cef28361c7a02d8efca7422c78128 Mon Sep 17 00:00:00 2001 From: Sebastian Krebs Date: Sun, 13 Jan 2013 00:25:02 +0100 Subject: [PATCH 1/6] SSI-Proxy - Implement SSI rendering strategy - Implement SSI kernel proxy - Implement Listeer --- .../DependencyInjection/Configuration.php | 11 ++ .../FrameworkExtension.php | 16 ++ .../FrameworkBundle/Resources/config/ssi.xml | 23 +++ .../HttpKernel/EventListener/SsiListener.php | 53 ++++++ .../SsiRenderingStrategy.php | 87 ++++++++++ .../Component/HttpKernel/SsiKernelProxy.php | 153 ++++++++++++++++++ 6 files changed, 343 insertions(+) create mode 100644 src/Symfony/Bundle/FrameworkBundle/Resources/config/ssi.xml create mode 100644 src/Symfony/Component/HttpKernel/EventListener/SsiListener.php create mode 100644 src/Symfony/Component/HttpKernel/RenderingStrategy/SsiRenderingStrategy.php create mode 100644 src/Symfony/Component/HttpKernel/SsiKernelProxy.php diff --git a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Configuration.php b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Configuration.php index 885752b89da6b..f68b633dbd894 100644 --- a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Configuration.php +++ b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Configuration.php @@ -73,6 +73,7 @@ public function getConfigTreeBuilder() $this->addFormSection($rootNode); $this->addEsiSection($rootNode); + $this->addSsiSection($rootNode); $this->addRouterProxySection($rootNode); $this->addProfilerSection($rootNode); $this->addRouterSection($rootNode); @@ -130,6 +131,16 @@ private function addRouterProxySection(ArrayNodeDefinition $rootNode) ; } + private function addSsiSection (ArrayNodeDefinition $rootNode) { + $rootNode + ->children() + ->arrayNode('ssi') + ->info('ssi configuration') + ->canBeDisabled() + ->end() + ->end(); + } + private function addProfilerSection(ArrayNodeDefinition $rootNode) { $rootNode diff --git a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php index 958c2ce35065c..8000ca19c51a3 100644 --- a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php +++ b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php @@ -94,6 +94,10 @@ public function load(array $configs, ContainerBuilder $container) $this->registerEsiConfiguration($config['esi'], $loader); } + if (isset($config['ssi'])) { + $this->registerSsiConfiguration($config['ssi'], $loader); + } + if (isset($config['router_proxy'])) { $this->registerRouterProxyConfiguration($config['router_proxy'], $container, $loader); } @@ -188,6 +192,18 @@ private function registerEsiConfiguration(array $config, XmlFileLoader $loader) } } + /** + * Loads the SSI configuration. + * + * @param array $config An SSI configuration array + * @param XmlFileLoader $loader An XmlFileLoader instance + */ + private function registerSsiConfiguration (array $config, XmlFileLoader $loader) { + if (!empty($config['enabled'])) { + $loader->load('ssi.xml'); + } + } + /** * Loads the router proxy configuration. * 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 0000000000000..f76efec26dda1 --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Resources/config/ssi.xml @@ -0,0 +1,23 @@ + + + + + + Symfony\Component\HttpKernel\EventListener\SsiListener + Symfony\Component\HttpKernel\RenderingStrategy\SsiRenderingStrategy + + + + + + + + + + + + + + diff --git a/src/Symfony/Component/HttpKernel/EventListener/SsiListener.php b/src/Symfony/Component/HttpKernel/EventListener/SsiListener.php new file mode 100644 index 0000000000000..56c43a53279f0 --- /dev/null +++ b/src/Symfony/Component/HttpKernel/EventListener/SsiListener.php @@ -0,0 +1,53 @@ + + * + * 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\HttpFoundation\Response; +use Symfony\Component\HttpKernel\HttpKernelInterface; +use Symfony\Component\HttpKernel\Event\FilterResponseEvent; +use Symfony\Component\HttpKernel\KernelEvents; +use Symfony\Component\EventDispatcher\EventSubscriberInterface; + +/** + * SsiListener adds a Surrogate-Control HTTP header when the Response needs to be parsed for SSI. + * + * @author Sebastian Krebs + */ +class SsiListener implements EventSubscriberInterface +{ + /** + * Filters the Response. + * + * @param FilterResponseEvent $event A FilterResponseEvent instance + */ + public function onKernelResponse(FilterResponseEvent $event) + { + if (HttpKernelInterface::MASTER_REQUEST === $event->getRequestType()) { + $this->updateResponseHeader($event->getResponse()); + } + } + + public static function getSubscribedEvents() + { + return array( + KernelEvents::RESPONSE => 'onKernelResponse', + ); + } + + private function updateResponseHeader (Response $response) + { + if (false !== strpos($response->getContent(), '', + $uri, + $ignoreErrors ? ' fmt="?"' : '' + ); + + if (!empty($comment)) { + return sprintf("\n%s", $comment, $html); + } + + return $html; + } +} diff --git a/src/Symfony/Component/HttpKernel/SsiKernelProxy.php b/src/Symfony/Component/HttpKernel/SsiKernelProxy.php new file mode 100644 index 0000000000000..5bff7e2a6c7b6 --- /dev/null +++ b/src/Symfony/Component/HttpKernel/SsiKernelProxy.php @@ -0,0 +1,153 @@ + + */ +class SsiKernelProxy implements HttpKernelInterface, TerminableInterface +{ + private $kernel; + private $options = array( + 'pass_through' => false + ); + + public function __construct (HttpKernelInterface $kernel, array $options = array()) + { + $this->kernel = $kernel; + $this->options = array_merge($this->options, $options); + } + + /** + * Handles a Request to convert it to a Response. + * + * When $catch is true, the implementation must catch all exceptions + * and do its best to convert them to a Response instance. + * + * @param Request $request A Request instance + * @param integer $type The type of the request + * (one of HttpKernelInterface::MASTER_REQUEST or HttpKernelInterface::SUB_REQUEST) + * @param Boolean $catch Whether to catch exceptions or not + * + * @return Response A Response instance + * + * @throws \Exception When an Exception occurs during processing + * + * @api + */ + public function handle (Request $request, $type = self::MASTER_REQUEST, $catch = true) + { + $response = $this->kernel->handle($request, $type, $catch); + + if (!$this->options['pass_through'] && !$this->serverHasCapability($request) && $this->hasControlHeader($response)) { + $this->parse($request, $response); + } + + return $response; + } + + /** + * Terminates a request/response cycle. + * + * Should be called after sending the response and before shutting down the kernel. + * + * @param Request $request A Request instance + * @param Response $response A Response instance + * + * @api + */ + public function terminate (Request $request, Response $response) + { + if ($this->kernel instanceof TerminableInterface) { + $this->kernel->terminate($request, $response); + } + } + + private function parse (Request $request, Response $response) + { + $this->addCapabilityHeader($request); + + $content = preg_replace_callback('##', $this->createHandler($this, $request, $response), $response->getContent()); + $response->setContent($content); + + $this->removeControlHeader($response); + } + + private function createHandler (HttpKernelInterface $kernel, Request $request, Response $response) + { + return function ($attributes) use ($kernel, $request, $response) { + $options = array(); + preg_match_all('/(virtual|fmt)="([^"]*?)"/', $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.'); + } + + + $subRequest = Request::create($options['virtual'], 'GET', array(), $request->cookies->all(), array(), $request->server->all()); + + try { + $subResponse = $kernel->handle($subRequest, HttpKernelInterface::SUB_REQUEST, true); + + if (!$subResponse->isSuccessful()) { + throw new \RuntimeException(sprintf('Error when rendering "%s" (Status code is %s).', $subRequest->getUri(), $subResponse->getStatusCode())); + } + + if ($response->isCacheable() && $subResponse->isCacheable()) { + $maxAge = min($response->headers->getCacheControlDirective('max-age'), $subResponse->headers->getCacheControlDirective('max-age')); + $sMaxAge = min($response->headers->getCacheControlDirective('s-maxage'), $subResponse->headers->getCacheControlDirective('s-maxage')); + $response->setSharedMaxAge($sMaxAge); + $response->setMaxAge($maxAge); + } else { + $response->headers->set('Cache-Control', 'no-cache, must-revalidate'); + } + + return $subResponse->getContent(); + } catch (\Exception $e) { + + if (!isset($options['fmt']) || $options['fmt'] != '?') { + throw $e; + } + } + + return ''; + }; + } + + private function serverHasCapability (Request $request) + { + $value = $request->headers->get('Surrogate-Capability'); + return $value && strpos($value, 'SSI/1.0') !== false; + } + + private function hasControlHeader (Response $response) { + $value = $response->headers->get('Surrogate-Control'); + return $value && strpos($value, 'SSI/1.0') !== false; + } + + private function addCapabilityHeader (Request $request) + { + $current = $request->headers->get('Surrogate-Capability'); + $request->headers->set('Surrogate-Capability', ($current ? $current . ', ' : '') . 'symfony2="SSI/1.0"'); + } + + private function removeControlHeader (Response $response) + { + $current = $response->headers->get('Surrogate-Control'); + $new = array_filter(explode(',', $current), function ($control) { + return strpos($control, 'SSI/1.0') === false; + }); + if ($new) { + $response->headers->set('Surrogate-Control', implode(', ', $new)); + } else { + $response->headers->remove('Surrogate-Control'); + } + } +} From b22131aed32a566df143827f0e322c0663ad7a2e Mon Sep 17 00:00:00 2001 From: Sebastian Krebs Date: Tue, 22 Jan 2013 23:11:45 +0100 Subject: [PATCH 2/6] Refactoring into IncludeProxy-namespace with SSI-/ESI-IncludeStrategy --- .../FrameworkBundle/Resources/config/ssi.xml | 2 +- .../IncludeProxy/EsiIncludeStrategy.php | 73 +++++++++ .../HttpKernel/IncludeProxy/IncludeProxy.php | 127 +++++++++++++++ .../IncludeProxy/IncludeProxyInterface.php | 12 ++ .../IncludeProxy/IncludeStrategyInterface.php | 14 ++ .../IncludeProxy/SsiIncludeStrategy.php | 66 ++++++++ .../SsiRenderingStrategy.php | 11 +- .../Component/HttpKernel/SsiKernelProxy.php | 153 ------------------ 8 files changed, 299 insertions(+), 159 deletions(-) create mode 100644 src/Symfony/Component/HttpKernel/IncludeProxy/EsiIncludeStrategy.php create mode 100644 src/Symfony/Component/HttpKernel/IncludeProxy/IncludeProxy.php create mode 100644 src/Symfony/Component/HttpKernel/IncludeProxy/IncludeProxyInterface.php create mode 100644 src/Symfony/Component/HttpKernel/IncludeProxy/IncludeStrategyInterface.php create mode 100644 src/Symfony/Component/HttpKernel/IncludeProxy/SsiIncludeStrategy.php delete mode 100644 src/Symfony/Component/HttpKernel/SsiKernelProxy.php diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/config/ssi.xml b/src/Symfony/Bundle/FrameworkBundle/Resources/config/ssi.xml index f76efec26dda1..922d88ad4021b 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Resources/config/ssi.xml +++ b/src/Symfony/Bundle/FrameworkBundle/Resources/config/ssi.xml @@ -10,7 +10,7 @@ - + diff --git a/src/Symfony/Component/HttpKernel/IncludeProxy/EsiIncludeStrategy.php b/src/Symfony/Component/HttpKernel/IncludeProxy/EsiIncludeStrategy.php new file mode 100644 index 0000000000000..dda2127253e16 --- /dev/null +++ b/src/Symfony/Component/HttpKernel/IncludeProxy/EsiIncludeStrategy.php @@ -0,0 +1,73 @@ + + */ +class EsiIncludeStrategy implements IncludeStrategyInterface +{ + public function getName () + { + return 'ESI/1.0'; + } + + public function handle (HttpKernelInterface $kernel, Request $request, Response $response) + { + return preg_replace_callback('##', $this->createHandler($kernel, $request, $response), $response->getContent()); + } + + private function createHandler (HttpKernelInterface $kernel, Request $request, Response $response) + { + return function ($attributes) use ($kernel, $request, $response) { + $options = array(); + preg_match_all('/(src|onerror|alt)="([^"]*?)"/', $attributes[1], $matches, PREG_SET_ORDER); + foreach ($matches as $set) { + $options[$set[1]] = $set[2]; + } + + if (!isset($options['src'])) { + throw new \RuntimeException('Unable to process an ESI tag without a "src" attribute.'); + } + + + $subRequest = Request::create($options['src'], 'GET', array(), $request->cookies->all(), array(), $request->server->all()); + + try { + $subResponse = $kernel->handle($subRequest, HttpKernelInterface::SUB_REQUEST, true); + + if (!$subResponse->isSuccessful()) { + if (isset($options['alt']) && $options['alt']) { + $subRequest = Request::create($options['alt'], 'GET', array(), $request->cookies->all(), array(), $request->server->all()); + $subResponse = $kernel->handle($subRequest, HttpKernelInterface::SUB_REQUEST, true); + if (!$subResponse->isSuccessful()) { + throw new \RuntimeException(sprintf('Error when rendering "%s" (Status code is %s).', $subRequest->getUri(), $subResponse->getStatusCode())); + } + } else { + throw new \RuntimeException(sprintf('Error when rendering "%s" (Status code is %s).', $subRequest->getUri(), $subResponse->getStatusCode())); + } + } + + if ($response->isCacheable() && $subResponse->isCacheable()) { + $maxAge = min($response->headers->getCacheControlDirective('max-age'), $subResponse->headers->getCacheControlDirective('max-age')); + $sMaxAge = min($response->headers->getCacheControlDirective('s-maxage'), $subResponse->headers->getCacheControlDirective('s-maxage')); + $response->setSharedMaxAge($sMaxAge); + $response->setMaxAge($maxAge); + } else { + $response->headers->set('Cache-Control', 'no-cache, must-revalidate'); + } + + return $subResponse->getContent(); + } catch (\Exception $e) { + if (isset($options['onerror']) && 'continue' == $options['onerror']) { + throw $e; + } + } + + return ''; + }; + } +} diff --git a/src/Symfony/Component/HttpKernel/IncludeProxy/IncludeProxy.php b/src/Symfony/Component/HttpKernel/IncludeProxy/IncludeProxy.php new file mode 100644 index 0000000000000..4c5eeda2e3746 --- /dev/null +++ b/src/Symfony/Component/HttpKernel/IncludeProxy/IncludeProxy.php @@ -0,0 +1,127 @@ + + */ +class IncludeProxy implements IncludeProxyInterface +{ + private $kernel; + private $strategies; + private $options = array( + 'pass_through' => false + ); + + public function __construct (HttpKernelInterface $kernel, array $strategies = array(), array $options = array()) + { + $this->kernel = $kernel; + $this->options = array_merge($this->options, $options); + $this->strategies = $strategies; + } + + /** + * Handles a Request to convert it to a Response. + * + * When $catch is true, the implementation must catch all exceptions + * and do its best to convert them to a Response instance. + * + * @param Request $request A Request instance + * @param integer $type The type of the request + * (one of HttpKernelInterface::MASTER_REQUEST or HttpKernelInterface::SUB_REQUEST) + * @param Boolean $catch Whether to catch exceptions or not + * + * @return Response A Response instance + * + * @throws \Exception When an Exception occurs during processing + * + * @api + */ + public function handle (Request $request, $type = self::MASTER_REQUEST, $catch = true) + { + $that = $this; + $strategies = array_filter($this->strategies, function (IncludeStrategyInterface $strategy) use ($that, $request) { + if ($that->serverHasCapability($strategy->getName(), $request)) { + return false; + } else { + $that->addCapabilityHeader($strategy->getName(), $request); + return true; + } + }); + + $response = $this->kernel->handle($request, $type, $catch); + + if (!$this->options['pass_through']) { + foreach ($strategies as $strategy) { + /** @var $strategy IncludeStrategyInterface */ + if ($this->hasControlHeader($strategy->getName(), $response)) { + $this->parse($strategy, $request, $response); + } + } + } + + return $response; + } + + private function parse (IncludeStrategyInterface $strategy, Request $request, Response $response) + { + $content = $strategy->handle($this, $request, $response); + $response->setContent($content); + + $this->removeControlHeader($strategy->getName(), $response); + } + + /** + * Terminates a request/response cycle. + * + * Should be called after sending the response and before shutting down the kernel. + * + * @param Request $request A Request instance + * @param Response $response A Response instance + * + * @api + */ + public function terminate (Request $request, Response $response) + { + if ($this->kernel instanceof TerminableInterface) { + $this->kernel->terminate($request, $response); + } + } + + private function serverHasCapability ($name, Request $request) + { + $value = $request->headers->get('Surrogate-Capability'); + + return $value && strpos($value, $name) !== false; + } + + private function hasControlHeader ($name, Response $response) + { + $value = $response->headers->get('Surrogate-Control'); + + return $value && strpos($value, $name) !== false; + } + + private function addCapabilityHeader ($name, Request $request) + { + $current = $request->headers->get('Surrogate-Capability'); + $request->headers->set('Surrogate-Capability', ($current ? $current . ', ' : '') . sprintf('symfony2="%s"', $name)); + } + + private function removeControlHeader ($name, Response $response) + { + $current = $response->headers->get('Surrogate-Control'); + $new = array_filter(explode(',', $current), function ($control) use ($name) { + return strpos($control, $name) === false; + }); + if ($new) { + $response->headers->set('Surrogate-Control', implode(', ', $new)); + } else { + $response->headers->remove('Surrogate-Control'); + } + } +} diff --git a/src/Symfony/Component/HttpKernel/IncludeProxy/IncludeProxyInterface.php b/src/Symfony/Component/HttpKernel/IncludeProxy/IncludeProxyInterface.php new file mode 100644 index 0000000000000..c28c344462ad3 --- /dev/null +++ b/src/Symfony/Component/HttpKernel/IncludeProxy/IncludeProxyInterface.php @@ -0,0 +1,12 @@ + + */ +interface IncludeProxyInterface extends HttpKernelInterface,TerminableInterface +{ +} diff --git a/src/Symfony/Component/HttpKernel/IncludeProxy/IncludeStrategyInterface.php b/src/Symfony/Component/HttpKernel/IncludeProxy/IncludeStrategyInterface.php new file mode 100644 index 0000000000000..c391476d487bb --- /dev/null +++ b/src/Symfony/Component/HttpKernel/IncludeProxy/IncludeStrategyInterface.php @@ -0,0 +1,14 @@ + + */ +interface IncludeStrategyInterface { + public function getName (); + public function handle(HttpKernelInterface $kernel, Request $request, Response $response); +} diff --git a/src/Symfony/Component/HttpKernel/IncludeProxy/SsiIncludeStrategy.php b/src/Symfony/Component/HttpKernel/IncludeProxy/SsiIncludeStrategy.php new file mode 100644 index 0000000000000..96fccde47b6b2 --- /dev/null +++ b/src/Symfony/Component/HttpKernel/IncludeProxy/SsiIncludeStrategy.php @@ -0,0 +1,66 @@ + + */ +class SsiIncludeStrategy implements IncludeStrategyInterface +{ + public function getName () + { + return 'SSI/1.0'; + } + + public function handle (HttpKernelInterface $kernel, Request $request, Response $response) + { + return preg_replace_callback('##', $this->createHandler($kernel, $request, $response), $response->getContent()); + } + + private function createHandler (HttpKernelInterface $kernel, Request $request, Response $response) + { + return function ($attributes) use ($kernel, $request, $response) { + $options = array(); + preg_match_all('/(virtual|fmt)="([^"]*?)"/', $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.'); + } + + + $subRequest = Request::create($options['virtual'], 'GET', array(), $request->cookies->all(), array(), $request->server->all()); + + try { + $subResponse = $kernel->handle($subRequest, HttpKernelInterface::SUB_REQUEST, true); + + if (!$subResponse->isSuccessful()) { + throw new \RuntimeException(sprintf('Error when rendering "%s" (Status code is %s).', $subRequest->getUri(), $subResponse->getStatusCode())); + } + + if ($response->isCacheable() && $subResponse->isCacheable()) { + $maxAge = min($response->headers->getCacheControlDirective('max-age'), $subResponse->headers->getCacheControlDirective('max-age')); + $sMaxAge = min($response->headers->getCacheControlDirective('s-maxage'), $subResponse->headers->getCacheControlDirective('s-maxage')); + $response->setSharedMaxAge($sMaxAge); + $response->setMaxAge($maxAge); + } else { + $response->headers->set('Cache-Control', 'no-cache, must-revalidate'); + } + + return $subResponse->getContent(); + } catch (\Exception $e) { + + if (!isset($options['fmt']) || $options['fmt'] != '?') { + throw $e; + } + } + + return ''; + }; + } +} diff --git a/src/Symfony/Component/HttpKernel/RenderingStrategy/SsiRenderingStrategy.php b/src/Symfony/Component/HttpKernel/RenderingStrategy/SsiRenderingStrategy.php index 290b83098eb54..144bf43521a14 100644 --- a/src/Symfony/Component/HttpKernel/RenderingStrategy/SsiRenderingStrategy.php +++ b/src/Symfony/Component/HttpKernel/RenderingStrategy/SsiRenderingStrategy.php @@ -16,11 +16,11 @@ use Symfony\Component\HttpKernel\Controller\ControllerReference; /** - * Implements the ESI rendering strategy. + * Implements the SSI rendering strategy. * * @author Sebastian Krebs */ -class SsiRenderingStrategy extends GeneratorAwareRenderingStrategy +class SsiRenderingStrategy extends ProxyAwareRenderingStrategy { private $defaultStrategy; @@ -48,9 +48,11 @@ public function __construct(RenderingStrategyInterface $defaultStrategy) * * * comment: a comment to add when returning an esi:include tag */ - public function render($uri, Request $request = null, array $options = array()) + public function render($uri, Request $request, array $options = array()) { - if (null === $request) { + $value = $request->headers->get('Surrogate-Capability'); + + if ($value && strpos($value, 'SSI/1.0') !== false) { return $this->defaultStrategy->render($uri, $request, $options); } @@ -71,7 +73,6 @@ public function getName() return 'ssi'; } - private function renderIncludeTag ($uri, $ignoreErrors = true, $comment = '') { $html = sprintf('', $uri, diff --git a/src/Symfony/Component/HttpKernel/SsiKernelProxy.php b/src/Symfony/Component/HttpKernel/SsiKernelProxy.php deleted file mode 100644 index 5bff7e2a6c7b6..0000000000000 --- a/src/Symfony/Component/HttpKernel/SsiKernelProxy.php +++ /dev/null @@ -1,153 +0,0 @@ - - */ -class SsiKernelProxy implements HttpKernelInterface, TerminableInterface -{ - private $kernel; - private $options = array( - 'pass_through' => false - ); - - public function __construct (HttpKernelInterface $kernel, array $options = array()) - { - $this->kernel = $kernel; - $this->options = array_merge($this->options, $options); - } - - /** - * Handles a Request to convert it to a Response. - * - * When $catch is true, the implementation must catch all exceptions - * and do its best to convert them to a Response instance. - * - * @param Request $request A Request instance - * @param integer $type The type of the request - * (one of HttpKernelInterface::MASTER_REQUEST or HttpKernelInterface::SUB_REQUEST) - * @param Boolean $catch Whether to catch exceptions or not - * - * @return Response A Response instance - * - * @throws \Exception When an Exception occurs during processing - * - * @api - */ - public function handle (Request $request, $type = self::MASTER_REQUEST, $catch = true) - { - $response = $this->kernel->handle($request, $type, $catch); - - if (!$this->options['pass_through'] && !$this->serverHasCapability($request) && $this->hasControlHeader($response)) { - $this->parse($request, $response); - } - - return $response; - } - - /** - * Terminates a request/response cycle. - * - * Should be called after sending the response and before shutting down the kernel. - * - * @param Request $request A Request instance - * @param Response $response A Response instance - * - * @api - */ - public function terminate (Request $request, Response $response) - { - if ($this->kernel instanceof TerminableInterface) { - $this->kernel->terminate($request, $response); - } - } - - private function parse (Request $request, Response $response) - { - $this->addCapabilityHeader($request); - - $content = preg_replace_callback('##', $this->createHandler($this, $request, $response), $response->getContent()); - $response->setContent($content); - - $this->removeControlHeader($response); - } - - private function createHandler (HttpKernelInterface $kernel, Request $request, Response $response) - { - return function ($attributes) use ($kernel, $request, $response) { - $options = array(); - preg_match_all('/(virtual|fmt)="([^"]*?)"/', $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.'); - } - - - $subRequest = Request::create($options['virtual'], 'GET', array(), $request->cookies->all(), array(), $request->server->all()); - - try { - $subResponse = $kernel->handle($subRequest, HttpKernelInterface::SUB_REQUEST, true); - - if (!$subResponse->isSuccessful()) { - throw new \RuntimeException(sprintf('Error when rendering "%s" (Status code is %s).', $subRequest->getUri(), $subResponse->getStatusCode())); - } - - if ($response->isCacheable() && $subResponse->isCacheable()) { - $maxAge = min($response->headers->getCacheControlDirective('max-age'), $subResponse->headers->getCacheControlDirective('max-age')); - $sMaxAge = min($response->headers->getCacheControlDirective('s-maxage'), $subResponse->headers->getCacheControlDirective('s-maxage')); - $response->setSharedMaxAge($sMaxAge); - $response->setMaxAge($maxAge); - } else { - $response->headers->set('Cache-Control', 'no-cache, must-revalidate'); - } - - return $subResponse->getContent(); - } catch (\Exception $e) { - - if (!isset($options['fmt']) || $options['fmt'] != '?') { - throw $e; - } - } - - return ''; - }; - } - - private function serverHasCapability (Request $request) - { - $value = $request->headers->get('Surrogate-Capability'); - return $value && strpos($value, 'SSI/1.0') !== false; - } - - private function hasControlHeader (Response $response) { - $value = $response->headers->get('Surrogate-Control'); - return $value && strpos($value, 'SSI/1.0') !== false; - } - - private function addCapabilityHeader (Request $request) - { - $current = $request->headers->get('Surrogate-Capability'); - $request->headers->set('Surrogate-Capability', ($current ? $current . ', ' : '') . 'symfony2="SSI/1.0"'); - } - - private function removeControlHeader (Response $response) - { - $current = $response->headers->get('Surrogate-Control'); - $new = array_filter(explode(',', $current), function ($control) { - return strpos($control, 'SSI/1.0') === false; - }); - if ($new) { - $response->headers->set('Surrogate-Control', implode(', ', $new)); - } else { - $response->headers->remove('Surrogate-Control'); - } - } -} From 30042de8d3d5ac8b0eefd1441e5b39569ac56034 Mon Sep 17 00:00:00 2001 From: Sebastian Krebs Date: Wed, 23 Jan 2013 23:17:00 +0100 Subject: [PATCH 3/6] Remove ESI from HttpCache --- .../HttpKernel/HttpCache/HttpCache.php | 41 +------------------ 1 file changed, 1 insertion(+), 40 deletions(-) diff --git a/src/Symfony/Component/HttpKernel/HttpCache/HttpCache.php b/src/Symfony/Component/HttpKernel/HttpCache/HttpCache.php index 4b5b0c1574cd4..8d7a1c3de3d7e 100644 --- a/src/Symfony/Component/HttpKernel/HttpCache/HttpCache.php +++ b/src/Symfony/Component/HttpKernel/HttpCache/HttpCache.php @@ -33,8 +33,6 @@ class HttpCache implements HttpKernelInterface, TerminableInterface private $kernel; private $store; private $request; - private $esi; - private $esiCacheStrategy; private $traces; /** @@ -74,10 +72,9 @@ class HttpCache implements HttpKernelInterface, TerminableInterface * * @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 */ - public function __construct(HttpKernelInterface $kernel, StoreInterface $store, Esi $esi = null, array $options = array()) + public function __construct(HttpKernelInterface $kernel, StoreInterface $store, array $options = array()) { $this->store = $store; $this->kernel = $kernel; @@ -94,7 +91,6 @@ public function __construct(HttpKernelInterface $kernel, StoreInterface $store, 'stale_while_revalidate' => 2, 'stale_if_error' => 60, ), $options); - $this->esi = $esi; $this->traces = array(); } @@ -153,17 +149,6 @@ public function getKernel() return $this->kernel; } - - /** - * Gets the Esi instance - * - * @return Esi An Esi instance - */ - public function getEsi() - { - return $this->esi; - } - /** * {@inheritdoc} * @@ -175,9 +160,6 @@ public function handle(Request $request, $type = HttpKernelInterface::MASTER_REQ if (HttpKernelInterface::MASTER_REQUEST === $type) { $this->traces = array(); $this->request = $request; - if (null !== $this->esi) { - $this->esiCacheStrategy = $this->esi->createCacheStrategy(); - } } $path = $request->getPathInfo(); @@ -204,14 +186,6 @@ public function handle(Request $request, $type = HttpKernelInterface::MASTER_REQ $response->headers->set('X-Symfony-Cache', $this->getLog()); } - if (null !== $this->esi) { - $this->esiCacheStrategy->add($response); - - if (HttpKernelInterface::MASTER_REQUEST === $type) { - $this->esiCacheStrategy->update($response); - } - } - $response->prepare($request); return $response; @@ -444,10 +418,6 @@ protected function fetch(Request $request, $catch = false) */ protected function forward(Request $request, $catch = false, Response $entry = null) { - if ($this->esi) { - $this->esi->addSurrogateEsiCapability($request); - } - // always a "master" request (as the real master request can be in cache) $response = $this->kernel->handle($request, HttpKernelInterface::MASTER_REQUEST, $catch); // FIXME: we probably need to also catch exceptions if raw === true @@ -465,8 +435,6 @@ protected function forward(Request $request, $catch = false, Response $entry = n } } - $this->processResponseBody($request, $response); - return $response; } @@ -618,13 +586,6 @@ private function restoreResponseBody(Request $request, Response $response) $response->headers->remove('X-Body-File'); } - protected function processResponseBody(Request $request, Response $response) - { - if (null !== $this->esi && $this->esi->needsEsiParsing($response)) { - $this->esi->process($request, $response); - } - } - /** * Checks if the Request includes authorization or other sensitive information * that should cause the Response to be considered private by default. From 994556574ba591e9b54a2bc93d9eb6198d881658 Mon Sep 17 00:00:00 2001 From: Sebastian Krebs Date: Thu, 24 Jan 2013 20:15:06 +0100 Subject: [PATCH 4/6] Updated compatibility to symfony --- src/Symfony/Bundle/FrameworkBundle/HttpCache/HttpCache.php | 7 +------ .../Bundle/FrameworkBundle/Resources/config/ssi.xml | 3 ++- 2 files changed, 3 insertions(+), 7 deletions(-) diff --git a/src/Symfony/Bundle/FrameworkBundle/HttpCache/HttpCache.php b/src/Symfony/Bundle/FrameworkBundle/HttpCache/HttpCache.php index f07c994a5d713..051638362a698 100644 --- a/src/Symfony/Bundle/FrameworkBundle/HttpCache/HttpCache.php +++ b/src/Symfony/Bundle/FrameworkBundle/HttpCache/HttpCache.php @@ -39,7 +39,7 @@ public function __construct(HttpKernelInterface $kernel, $cacheDir = null) $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(), array_merge(array('debug' => $kernel->isDebug()), $this->getOptions())); } /** @@ -70,11 +70,6 @@ protected function getOptions() return array(); } - protected function createEsi() - { - return new Esi(); - } - protected function createStore() { return new Store($this->cacheDir ?: $this->kernel->getCacheDir().'/http_cache'); diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/config/ssi.xml b/src/Symfony/Bundle/FrameworkBundle/Resources/config/ssi.xml index 922d88ad4021b..710377adc2a55 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Resources/config/ssi.xml +++ b/src/Symfony/Bundle/FrameworkBundle/Resources/config/ssi.xml @@ -17,7 +17,8 @@ - + + %http_content_renderer.proxy_path% From 3ca14d346cbe4300829aa00532f859281bf9dc4a Mon Sep 17 00:00:00 2001 From: Sebastian Krebs Date: Thu, 24 Jan 2013 21:50:58 +0100 Subject: [PATCH 5/6] Extracted closure --- .../IncludeProxy/EsiIncludeStrategy.php | 51 ++---------- .../IncludeProxy/IncludeHandler.php | 83 +++++++++++++++++++ .../IncludeProxy/SsiIncludeStrategy.php | 44 ++-------- .../SsiRenderingStrategy.php | 2 +- 4 files changed, 98 insertions(+), 82 deletions(-) create mode 100644 src/Symfony/Component/HttpKernel/IncludeProxy/IncludeHandler.php diff --git a/src/Symfony/Component/HttpKernel/IncludeProxy/EsiIncludeStrategy.php b/src/Symfony/Component/HttpKernel/IncludeProxy/EsiIncludeStrategy.php index dda2127253e16..d5b7b594c2b07 100644 --- a/src/Symfony/Component/HttpKernel/IncludeProxy/EsiIncludeStrategy.php +++ b/src/Symfony/Component/HttpKernel/IncludeProxy/EsiIncludeStrategy.php @@ -17,57 +17,20 @@ public function getName () public function handle (HttpKernelInterface $kernel, Request $request, Response $response) { - return preg_replace_callback('##', $this->createHandler($kernel, $request, $response), $response->getContent()); - } - - private function createHandler (HttpKernelInterface $kernel, Request $request, Response $response) - { - return function ($attributes) use ($kernel, $request, $response) { + $extractor = function ($attributes) { $options = array(); preg_match_all('/(src|onerror|alt)="([^"]*?)"/', $attributes[1], $matches, PREG_SET_ORDER); foreach ($matches as $set) { $options[$set[1]] = $set[2]; } - if (!isset($options['src'])) { - throw new \RuntimeException('Unable to process an ESI tag without a "src" attribute.'); - } - - - $subRequest = Request::create($options['src'], 'GET', array(), $request->cookies->all(), array(), $request->server->all()); - - try { - $subResponse = $kernel->handle($subRequest, HttpKernelInterface::SUB_REQUEST, true); - - if (!$subResponse->isSuccessful()) { - if (isset($options['alt']) && $options['alt']) { - $subRequest = Request::create($options['alt'], 'GET', array(), $request->cookies->all(), array(), $request->server->all()); - $subResponse = $kernel->handle($subRequest, HttpKernelInterface::SUB_REQUEST, true); - if (!$subResponse->isSuccessful()) { - throw new \RuntimeException(sprintf('Error when rendering "%s" (Status code is %s).', $subRequest->getUri(), $subResponse->getStatusCode())); - } - } else { - throw new \RuntimeException(sprintf('Error when rendering "%s" (Status code is %s).', $subRequest->getUri(), $subResponse->getStatusCode())); - } - } - - if ($response->isCacheable() && $subResponse->isCacheable()) { - $maxAge = min($response->headers->getCacheControlDirective('max-age'), $subResponse->headers->getCacheControlDirective('max-age')); - $sMaxAge = min($response->headers->getCacheControlDirective('s-maxage'), $subResponse->headers->getCacheControlDirective('s-maxage')); - $response->setSharedMaxAge($sMaxAge); - $response->setMaxAge($maxAge); - } else { - $response->headers->set('Cache-Control', 'no-cache, must-revalidate'); - } - - return $subResponse->getContent(); - } catch (\Exception $e) { - if (isset($options['onerror']) && 'continue' == $options['onerror']) { - throw $e; - } - } + return array( + isset($options['src']) ? $options['virtual'] : null, + isset($options['alt']) ? $options['alt'] : null, + isset($options['onerror']) && $options['onerror'] == 'continue' + ); - return ''; }; + return preg_replace_callback('##', new IncludeHandler($kernel, $request, $response, $extractor), $response->getContent()); } } diff --git a/src/Symfony/Component/HttpKernel/IncludeProxy/IncludeHandler.php b/src/Symfony/Component/HttpKernel/IncludeProxy/IncludeHandler.php new file mode 100644 index 0000000000000..b7fa7e34ad078 --- /dev/null +++ b/src/Symfony/Component/HttpKernel/IncludeProxy/IncludeHandler.php @@ -0,0 +1,83 @@ + + * + * This is a helper class, that act as and replace the closure of the + * IncludeStrategy-implementations. + * + * @internal + */ +class IncludeHandler +{ + private $kernel; + private $request; + private $response; + private $extractor; + + public function __construct (HttpKernelInterface $kernel, Request $request, Response $response, $extractor) + { + $this->kernel = $kernel; + $this->request = $request; + $this->response = $response; + $this->extractor = $extractor; + } + + public function __invoke ($attributes) + { + list($source, $alternative, $ignoreErrors) = call_user_func($this->extractor, $attributes); + return $this->handle($source, $alternative, $ignoreErrors); + } + + private function handle ($source, $alternative, $ignoreErrors) + { + if (!$source) { + throw new \RuntimeException('Unable to process an include tag without a source attribute.'); + } + + try { + $subResponse = $this->request($source); + $this->updateHeaders($this->response, $subResponse); + return $subResponse->getContent(); + } catch (\Exception $e) { + if ($alternative) { + return $this->handle($alternative, null, $ignoreErrors); + } + + if ($ignoreErrors) { + throw $e; + } + } + + return ''; + } + + private function request ($source) + { + $subRequest = Request::create($source, 'GET', array(), $this->request->cookies->all(), array(), $this->request->server->all()); + + $response = $this->kernel->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; + } + + private function updateHeaders (Response $response, Response $subResponse) + { + if ($this->response->isCacheable() && $subResponse->isCacheable()) { + $maxAge = min($response->headers->getCacheControlDirective('max-age'), $subResponse->headers->getCacheControlDirective('max-age')); + $sMaxAge = min($response->headers->getCacheControlDirective('s-maxage'), $subResponse->headers->getCacheControlDirective('s-maxage')); + $response->setSharedMaxAge($sMaxAge); + $response->setMaxAge($maxAge); + } else { + $this->response->headers->set('Cache-Control', 'no-cache, must-revalidate'); + } + } +} diff --git a/src/Symfony/Component/HttpKernel/IncludeProxy/SsiIncludeStrategy.php b/src/Symfony/Component/HttpKernel/IncludeProxy/SsiIncludeStrategy.php index 96fccde47b6b2..ea7ca746746cd 100644 --- a/src/Symfony/Component/HttpKernel/IncludeProxy/SsiIncludeStrategy.php +++ b/src/Symfony/Component/HttpKernel/IncludeProxy/SsiIncludeStrategy.php @@ -17,50 +17,20 @@ public function getName () public function handle (HttpKernelInterface $kernel, Request $request, Response $response) { - return preg_replace_callback('##', $this->createHandler($kernel, $request, $response), $response->getContent()); - } - - private function createHandler (HttpKernelInterface $kernel, Request $request, Response $response) - { - return function ($attributes) use ($kernel, $request, $response) { + $extractor = function ($attributes) { $options = array(); preg_match_all('/(virtual|fmt)="([^"]*?)"/', $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.'); - } - - - $subRequest = Request::create($options['virtual'], 'GET', array(), $request->cookies->all(), array(), $request->server->all()); - - try { - $subResponse = $kernel->handle($subRequest, HttpKernelInterface::SUB_REQUEST, true); - - if (!$subResponse->isSuccessful()) { - throw new \RuntimeException(sprintf('Error when rendering "%s" (Status code is %s).', $subRequest->getUri(), $subResponse->getStatusCode())); - } - - if ($response->isCacheable() && $subResponse->isCacheable()) { - $maxAge = min($response->headers->getCacheControlDirective('max-age'), $subResponse->headers->getCacheControlDirective('max-age')); - $sMaxAge = min($response->headers->getCacheControlDirective('s-maxage'), $subResponse->headers->getCacheControlDirective('s-maxage')); - $response->setSharedMaxAge($sMaxAge); - $response->setMaxAge($maxAge); - } else { - $response->headers->set('Cache-Control', 'no-cache, must-revalidate'); - } - - return $subResponse->getContent(); - } catch (\Exception $e) { - - if (!isset($options['fmt']) || $options['fmt'] != '?') { - throw $e; - } - } + return array ( + isset($options['virtual']) ? $options['virtual'] : null, + null, + isset($options['fmt']) && $options['fmt'] == '?' + ); - return ''; }; + return preg_replace_callback('##', new IncludeHandler($kernel, $request, $response, $extractor), $response->getContent()); } } diff --git a/src/Symfony/Component/HttpKernel/RenderingStrategy/SsiRenderingStrategy.php b/src/Symfony/Component/HttpKernel/RenderingStrategy/SsiRenderingStrategy.php index 144bf43521a14..90bc45036c4b8 100644 --- a/src/Symfony/Component/HttpKernel/RenderingStrategy/SsiRenderingStrategy.php +++ b/src/Symfony/Component/HttpKernel/RenderingStrategy/SsiRenderingStrategy.php @@ -52,7 +52,7 @@ public function render($uri, Request $request, array $options = array()) { $value = $request->headers->get('Surrogate-Capability'); - if ($value && strpos($value, 'SSI/1.0') !== false) { + if (!$value || strpos($value, 'SSI/1.0') === false) { return $this->defaultStrategy->render($uri, $request, $options); } From 1d36a4eb6f54ce9356089864698457a95a7dac60 Mon Sep 17 00:00:00 2001 From: Sebastian Krebs Date: Thu, 24 Jan 2013 22:10:07 +0100 Subject: [PATCH 6/6] Remove interface --- .../HttpKernel/IncludeProxy/IncludeProxy.php | 2 +- .../IncludeProxy/IncludeProxyInterface.php | 12 ------------ 2 files changed, 1 insertion(+), 13 deletions(-) delete mode 100644 src/Symfony/Component/HttpKernel/IncludeProxy/IncludeProxyInterface.php diff --git a/src/Symfony/Component/HttpKernel/IncludeProxy/IncludeProxy.php b/src/Symfony/Component/HttpKernel/IncludeProxy/IncludeProxy.php index 4c5eeda2e3746..b750905c428fe 100644 --- a/src/Symfony/Component/HttpKernel/IncludeProxy/IncludeProxy.php +++ b/src/Symfony/Component/HttpKernel/IncludeProxy/IncludeProxy.php @@ -9,7 +9,7 @@ /** * @author Sebastian Krebs */ -class IncludeProxy implements IncludeProxyInterface +class IncludeProxy implements HttpKernelInterface, TerminableInterface { private $kernel; private $strategies; diff --git a/src/Symfony/Component/HttpKernel/IncludeProxy/IncludeProxyInterface.php b/src/Symfony/Component/HttpKernel/IncludeProxy/IncludeProxyInterface.php deleted file mode 100644 index c28c344462ad3..0000000000000 --- a/src/Symfony/Component/HttpKernel/IncludeProxy/IncludeProxyInterface.php +++ /dev/null @@ -1,12 +0,0 @@ - - */ -interface IncludeProxyInterface extends HttpKernelInterface,TerminableInterface -{ -}