diff --git a/UPGRADE-2.2.md b/UPGRADE-2.2.md index 33ed8c5700c23..5ce66de41a289 100644 --- a/UPGRADE-2.2.md +++ b/UPGRADE-2.2.md @@ -14,11 +14,9 @@ After: ``` - {% render url('post_list', { 'limit': 2 }), { 'alt': 'BlogBundle:Post:error' } %} + {% render controller('BlogBundle:Post:list', { 'limit': 2 }), { 'alt': 'BlogBundle:Post:error' } %} ``` - where `post_list` is the route name for the `BlogBundle:Post:list` controller. - ### HttpFoundation * The MongoDbSessionHandler default field names and timestamp type have changed. @@ -537,7 +535,12 @@ render($view['router']->generate('post_list', array('limit' => 2)), array('alt' => 'BlogBundle:Post:error')) ?> ``` - where `post_list` is the route name for the `BlogBundle:Post:list` controller. + where `post_list` is the route name for the `BlogBundle:Post:list` + controller, or if you don't want to create a route: + + ``` + render(new ControllerReference('BlogBundle:Post:list', array('limit' => 2)), array('alt' => 'BlogBundle:Post:error')) ?> + ``` #### Configuration diff --git a/src/Symfony/Bridge/Twig/CHANGELOG.md b/src/Symfony/Bridge/Twig/CHANGELOG.md index 420a99b61ff8b..343c7743a3ca2 100644 --- a/src/Symfony/Bridge/Twig/CHANGELOG.md +++ b/src/Symfony/Bridge/Twig/CHANGELOG.md @@ -4,8 +4,10 @@ CHANGELOG 2.2.0 ----- - * [BC BREAK] restricted the `render` tag to only accept URIs as reference (the signature changed) - * added a render function to render a request + * added a `controller` function to help generating controller references + * added a `render_esi` and a `render_hinclude` function + * [BC BREAK] restricted the `render` tag to only accept URIs or ControllerReference instances (the signature changed) + * added a `render` function to render a request * The `app` global variable is now injected even when using the twig service directly. * Added an optional parameter to the `path` and `url` function which allows to generate relative paths (e.g. "../parent-file") and scheme-relative URLs (e.g. "//example.com/dir/file"). diff --git a/src/Symfony/Bridge/Twig/Extension/HttpKernelExtension.php b/src/Symfony/Bridge/Twig/Extension/HttpKernelExtension.php index bdf882ef4ac41..2e406fc1d94d3 100644 --- a/src/Symfony/Bridge/Twig/Extension/HttpKernelExtension.php +++ b/src/Symfony/Bridge/Twig/Extension/HttpKernelExtension.php @@ -11,83 +11,65 @@ namespace Symfony\Bridge\Twig\Extension; -use Symfony\Component\HttpFoundation\Request; -use Symfony\Component\HttpKernel\KernelEvents; -use Symfony\Component\HttpKernel\HttpKernelInterface; -use Symfony\Component\HttpKernel\Event\GetResponseEvent; -use Symfony\Component\EventDispatcher\EventSubscriberInterface; +use Symfony\Component\HttpKernel\HttpContentRenderer; +use Symfony\Component\HttpKernel\Controller\ControllerReference; /** * Provides integration with the HttpKernel component. * * @author Fabien Potencier */ -class HttpKernelExtension extends \Twig_Extension implements EventSubscriberInterface +class HttpKernelExtension extends \Twig_Extension { - private $kernel; - private $request; + private $renderer; /** * Constructor. * - * @param HttpKernelInterface $kernel A HttpKernelInterface install + * @param HttpContentRenderer $kernel A HttpContentRenderer instance */ - public function __construct(HttpKernelInterface $kernel) + public function __construct(HttpContentRenderer $renderer) { - $this->kernel = $kernel; + $this->renderer = $renderer; } public function getFunctions() { return array( - 'render' => new \Twig_Function_Method($this, 'render', array('needs_environment' => true, 'is_safe' => array('html'))), + 'render' => new \Twig_Function_Method($this, 'render', array('is_safe' => array('html'))), + 'render_*' => new \Twig_Function_Method($this, 'renderStrategy', array('is_safe' => array('html'))), + 'controller' => new \Twig_Function_Method($this, 'controller'), ); } /** * Renders a URI. * - * @param \Twig_Environment $twig A \Twig_Environment instance - * @param string $uri The URI to render + * @param string $uri A URI + * @param array $options An array of options * * @return string The Response content * - * @throws \RuntimeException + * @see Symfony\Component\HttpKernel\HttpContentRenderer::render() */ - public function render(\Twig_Environment $twig, $uri) + public function render($uri, $options = array()) { - if (null !== $this->request) { - $cookies = $this->request->cookies->all(); - $server = $this->request->server->all(); - } else { - $cookies = array(); - $server = array(); - } + $options = $this->renderer->fixOptions($options); - $subRequest = Request::create($uri, 'get', array(), $cookies, array(), $server); - if (null !== $this->request && $this->request->getSession()) { - $subRequest->setSession($this->request->getSession()); - } + $strategy = isset($options['strategy']) ? $options['strategy'] : 'default'; + unset($options['strategy']); - $response = $this->kernel->handle($subRequest, HttpKernelInterface::SUB_REQUEST, false); - - if (!$response->isSuccessful()) { - throw new \RuntimeException(sprintf('Error when rendering "%s" (Status code is %s).', $subRequest->getUri(), $response->getStatusCode())); - } - - return $response->getContent(); + return $this->renderer->render($uri, $strategy, $options); } - public function onKernelRequest(GetResponseEvent $event) + public function renderStrategy($strategy, $uri, $options = array()) { - $this->request = $event->getRequest(); + return $this->renderer->render($uri, $strategy, $options); } - public static function getSubscribedEvents() + public function controller($controller, $attributes = array(), $query = array()) { - return array( - KernelEvents::REQUEST => array('onKernelRequest'), - ); + return new ControllerReference($controller, $attributes, $query); } public function getName() diff --git a/src/Symfony/Bridge/Twig/Tests/Extension/HttpKernelExtensionTest.php b/src/Symfony/Bridge/Twig/Tests/Extension/HttpKernelExtensionTest.php index f5390d31be1d9..182c42d07715e 100644 --- a/src/Symfony/Bridge/Twig/Tests/Extension/HttpKernelExtensionTest.php +++ b/src/Symfony/Bridge/Twig/Tests/Extension/HttpKernelExtensionTest.php @@ -13,8 +13,7 @@ use Symfony\Bridge\Twig\Extension\HttpKernelExtension; use Symfony\Bridge\Twig\Tests\TestCase; -use Symfony\Component\HttpFoundation\Response; -use Symfony\Component\HttpKernel\HttpKernelInterface; +use Symfony\Component\HttpKernel\HttpContentRenderer; class HttpKernelExtensionTest extends TestCase { @@ -31,7 +30,7 @@ protected function setUp() public function testRenderWithoutMasterRequest() { - $kernel = $this->getKernel($this->returnValue(new Response('foo'))); + $kernel = $this->getHttpContentRenderer($this->returnValue('foo')); $this->assertEquals('foo', $this->renderTemplate($kernel)); } @@ -41,7 +40,7 @@ public function testRenderWithoutMasterRequest() */ public function testRenderWithError() { - $kernel = $this->getKernel($this->throwException(new \Exception('foo'))); + $kernel = $this->getHttpContentRenderer($this->throwException(new \Exception('foo'))); $loader = new \Twig_Loader_Array(array('index' => '{{ render("foo") }}')); $twig = new \Twig_Environment($loader, array('debug' => true, 'cache' => false)); @@ -50,23 +49,20 @@ public function testRenderWithError() $this->renderTemplate($kernel); } - protected function getKernel($return) + protected function getHttpContentRenderer($return) { - $kernel = $this->getMock('Symfony\Component\HttpKernel\HttpKernelInterface'); - $kernel - ->expects($this->once()) - ->method('handle') - ->will($return) - ; + $strategy = $this->getMock('Symfony\\Component\\HttpKernel\\RenderingStrategy\\RenderingStrategyInterface'); + $strategy->expects($this->once())->method('getName')->will($this->returnValue('default')); + $strategy->expects($this->once())->method('render')->will($return); - return $kernel; + return new HttpContentRenderer(array($strategy)); } - protected function renderTemplate(HttpKernelInterface $kernel, $template = '{{ render("foo") }}') + protected function renderTemplate(HttpContentRenderer $renderer, $template = '{{ render("foo") }}') { $loader = new \Twig_Loader_Array(array('index' => $template)); $twig = new \Twig_Environment($loader, array('debug' => true, 'cache' => false)); - $twig->addExtension(new HttpKernelExtension($kernel)); + $twig->addExtension(new HttpKernelExtension($renderer)); return $twig->render('index'); } diff --git a/src/Symfony/Bundle/FrameworkBundle/CHANGELOG.md b/src/Symfony/Bundle/FrameworkBundle/CHANGELOG.md index af67c70ea27e5..5797f3f19f892 100644 --- a/src/Symfony/Bundle/FrameworkBundle/CHANGELOG.md +++ b/src/Symfony/Bundle/FrameworkBundle/CHANGELOG.md @@ -4,11 +4,16 @@ CHANGELOG 2.2.0 ----- - * [BC BREAK] restricted the `Symfony\Bundle\FrameworkBundle\HttpKernel::render()` method to only accept URIs as reference + * added a new `uri_signer` service to help sign URIs + * deprecated `Symfony\Bundle\FrameworkBundle\HttpKernel::render()` and `Symfony\Bundle\FrameworkBundle\HttpKernel::forward()` + * deprecated the `Symfony\Bundle\FrameworkBundle\HttpKernel` class in favor of `Symfony\Component\HttpKernel\DependencyInjection\ContainerAwareHttpKernel` + * added support for adding new HTTP content rendering strategies (like ESI and Hinclude) + in the DIC via the `kernel.content_renderer_strategy` tag + * [BC BREAK] restricted the `Symfony\Bundle\FrameworkBundle\HttpKernel::render()` method to only accept URIs or ControllerReference instances * `Symfony\Bundle\FrameworkBundle\HttpKernel::render()` method signature changed and the first argument - must now be a URI (the `generateInternalUri()` method was removed) - * The internal routes have been removed (`Resources/config/routing/internal.xml`) - * The `render` method of the `actions` templating helper signature and arguments changed: + must now be a URI or a ControllerReference instance (the `generateInternalUri()` method was removed) + * The internal routes (`Resources/config/routing/internal.xml`) have been replaced with a new proxy route (`Resources/config/routing/proxy.xml`) + * The `render` method of the `actions` templating helper signature and arguments changed * replaced Symfony\Bundle\FrameworkBundle\Controller\TraceableControllerResolver by Symfony\Component\HttpKernel\Controller\TraceableControllerResolver * replaced Symfony\Component\HttpKernel\Debug\ContainerAwareTraceableEventDispatcher by Symfony\Component\HttpKernel\Debug\TraceableEventDispatcher * added Client::enableProfiler() diff --git a/src/Symfony/Bundle/FrameworkBundle/Controller/Controller.php b/src/Symfony/Bundle/FrameworkBundle/Controller/Controller.php index a19030e11b4de..0196058642d0a 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Controller/Controller.php +++ b/src/Symfony/Bundle/FrameworkBundle/Controller/Controller.php @@ -59,7 +59,10 @@ public function generateUrl($route, $parameters = array(), $referenceType = UrlG */ public function forward($controller, array $path = array(), array $query = array()) { - return $this->container->get('http_kernel')->forward($controller, $path, $query); + $path['_controller'] = $controller; + $subRequest = $this->container->get('request')->duplicate($query, null, $path); + + return $this->container->get('http_kernel')->handle($subRequest, HttpKernelInterface::SUB_REQUEST); } /** diff --git a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Compiler/HttpRenderingStrategyPass.php b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Compiler/HttpRenderingStrategyPass.php new file mode 100644 index 0000000000000..3bb16f28c7036 --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Compiler/HttpRenderingStrategyPass.php @@ -0,0 +1,45 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Bundle\FrameworkBundle\DependencyInjection\Compiler; + +use Symfony\Component\DependencyInjection\Reference; +use Symfony\Component\DependencyInjection\ContainerBuilder; +use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface; + +/** + * Adds services tagged kernel.content_renderer_strategy as HTTP content rendering strategies. + * + * @author Fabien Potencier + */ +class HttpRenderingStrategyPass implements CompilerPassInterface +{ + public function process(ContainerBuilder $container) + { + if (false === $container->hasDefinition('http_content_renderer')) { + return; + } + + $definition = $container->getDefinition('http_content_renderer'); + foreach (array_keys($container->findTaggedServiceIds('kernel.content_renderer_strategy')) as $id) { + // We must assume that the class value has been correctly filled, even if the service is created by a factory + $class = $container->getDefinition($id)->getClass(); + + $refClass = new \ReflectionClass($class); + $interface = 'Symfony\Component\HttpKernel\RenderingStrategy\RenderingStrategyInterface'; + if (!$refClass->implementsInterface($interface)) { + throw new \InvalidArgumentException(sprintf('Service "%s" must implement interface "%s".', $id, $interface)); + } + + $definition->addMethodCall('addStrategy', array(new Reference($id))); + } + } +} diff --git a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php index 6c87519ea0263..bdf1720eadf85 100644 --- a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php +++ b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php @@ -41,6 +41,7 @@ public function load(array $configs, ContainerBuilder $container) $loader->load('web.xml'); $loader->load('services.xml'); + $loader->load('content_generator.xml'); // A translator must always be registered (as support is included by // default in the Form component). If disabled, an identity translator diff --git a/src/Symfony/Bundle/FrameworkBundle/FrameworkBundle.php b/src/Symfony/Bundle/FrameworkBundle/FrameworkBundle.php index aec13c4aab3b2..c4caaea4e988a 100644 --- a/src/Symfony/Bundle/FrameworkBundle/FrameworkBundle.php +++ b/src/Symfony/Bundle/FrameworkBundle/FrameworkBundle.php @@ -25,6 +25,7 @@ use Symfony\Bundle\FrameworkBundle\DependencyInjection\Compiler\CompilerDebugDumpPass; use Symfony\Bundle\FrameworkBundle\DependencyInjection\Compiler\TranslationExtractorPass; use Symfony\Bundle\FrameworkBundle\DependencyInjection\Compiler\TranslationDumperPass; +use Symfony\Bundle\FrameworkBundle\DependencyInjection\Compiler\HttpRenderingStrategyPass; use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\DependencyInjection\Compiler\PassConfig; use Symfony\Component\DependencyInjection\Scope; @@ -65,6 +66,7 @@ public function build(ContainerBuilder $container) $container->addCompilerPass(new AddCacheClearerPass()); $container->addCompilerPass(new TranslationExtractorPass()); $container->addCompilerPass(new TranslationDumperPass()); + $container->addCompilerPass(new HttpRenderingStrategyPass(), PassConfig::TYPE_AFTER_REMOVING); if ($container->getParameter('kernel.debug')) { $container->addCompilerPass(new ContainerBuilderDebugDumpPass(), PassConfig::TYPE_AFTER_REMOVING); diff --git a/src/Symfony/Bundle/FrameworkBundle/HttpKernel.php b/src/Symfony/Bundle/FrameworkBundle/HttpKernel.php index 4f2d2a19ecfb1..71a677cebeb0e 100644 --- a/src/Symfony/Bundle/FrameworkBundle/HttpKernel.php +++ b/src/Symfony/Bundle/FrameworkBundle/HttpKernel.php @@ -11,54 +11,20 @@ namespace Symfony\Bundle\FrameworkBundle; -use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; -use Symfony\Component\HttpFoundation\StreamedResponse; use Symfony\Component\HttpKernel\HttpKernelInterface; -use Symfony\Component\HttpKernel\Controller\ControllerResolverInterface; -use Symfony\Component\DependencyInjection\ContainerInterface; -use Symfony\Component\HttpKernel\HttpKernel as BaseHttpKernel; -use Symfony\Component\EventDispatcher\EventDispatcherInterface; +use Symfony\Component\HttpKernel\DependencyInjection\ContainerAwareHttpKernel; /** * This HttpKernel is used to manage scope changes of the DI container. * * @author Fabien Potencier * @author Johannes M. Schmitt + * + * @deprecated This class is deprecated in 2.2 and will be removed in 2.3 */ -class HttpKernel extends BaseHttpKernel +class HttpKernel extends ContainerAwareHttpKernel { - protected $container; - - private $esiSupport; - - public function __construct(EventDispatcherInterface $dispatcher, ContainerInterface $container, ControllerResolverInterface $controllerResolver) - { - parent::__construct($dispatcher, $controllerResolver); - - $this->container = $container; - } - - public function handle(Request $request, $type = HttpKernelInterface::MASTER_REQUEST, $catch = true) - { - $request->headers->set('X-Php-Ob-Level', ob_get_level()); - - $this->container->enterScope('request'); - $this->container->set('request', $request, 'request'); - - try { - $response = parent::handle($request, $type, $catch); - } catch (\Exception $e) { - $this->container->leaveScope('request'); - - throw $e; - } - - $this->container->leaveScope('request'); - - return $response; - } - /** * Forwards the request to another controller. * @@ -67,9 +33,13 @@ public function handle(Request $request, $type = HttpKernelInterface::MASTER_REQ * @param array $query An array of request query parameters * * @return Response A Response instance + * + * @deprecated in 2.2, will be removed in 2.3 */ public function forward($controller, array $attributes = array(), array $query = array()) { + trigger_error('forward() is deprecated since version 2.2 and will be removed in 2.3.', E_USER_DEPRECATED); + $attributes['_controller'] = $controller; $subRequest = $this->container->get('request')->duplicate($query, null, $attributes); @@ -96,97 +66,18 @@ public function forward($controller, array $attributes = array(), array $query = * * @throws \RuntimeException * @throws \Exception + * + * @deprecated in 2.2, will be removed in 2.3 (use Symfony\Component\HttpKernel\HttpContentRenderer::render() instead) */ public function render($uri, array $options = array()) { - $request = $this->container->get('request'); - - $options = array_merge(array( - 'ignore_errors' => !$this->container->getParameter('kernel.debug'), - 'alt' => null, - 'standalone' => false, - 'comment' => '', - 'default' => null, - ), $options); - - if (null === $this->esiSupport) { - $this->esiSupport = $this->container->has('esi') && $this->container->get('esi')->hasSurrogateEsiCapability($request); - } + trigger_error('render() is deprecated since version 2.2 and will be removed in 2.3. Use Symfony\Component\HttpKernel\HttpContentRenderer::render() instead.', E_USER_DEPRECATED); - if ($this->esiSupport && (true === $options['standalone'] || 'esi' === $options['standalone'])) { - return $this->container->get('esi')->renderIncludeTag($uri, $options['alt'], $options['ignore_errors'], $options['comment']); - } + $options = $this->renderer->fixOptions($options); - if ('js' === $options['standalone']) { - $defaultContent = null; + $strategy = isset($options['strategy']) ? $options['strategy'] : 'default'; + unset($options['strategy']); - $templating = $this->container->get('templating'); - - if ($options['default']) { - if ($templating->exists($options['default'])) { - $defaultContent = $templating->render($options['default']); - } else { - $defaultContent = $options['default']; - } - } elseif ($template = $this->container->getParameter('templating.hinclude.default_template')) { - $defaultContent = $templating->render($template); - } - - return $this->renderHIncludeTag($uri, $defaultContent); - } - - $subRequest = Request::create($uri, 'get', array(), $request->cookies->all(), array(), $request->server->all()); - if ($session = $request->getSession()) { - $subRequest->setSession($session); - } - - $level = ob_get_level(); - try { - $response = $this->handle($subRequest, HttpKernelInterface::SUB_REQUEST, false); - - if (!$response->isSuccessful()) { - throw new \RuntimeException(sprintf('Error when rendering "%s" (Status code is %s).', $request->getUri(), $response->getStatusCode())); - } - - if (!$response instanceof StreamedResponse) { - return $response->getContent(); - } - - $response->sendContent(); - } catch (\Exception $e) { - if ($options['alt']) { - $alt = $options['alt']; - unset($options['alt']); - - return $this->render($alt, $options); - } - - if (!$options['ignore_errors']) { - throw $e; - } - - // let's clean up the output buffers that were created by the sub-request - while (ob_get_level() > $level) { - ob_get_clean(); - } - } - } - - /** - * Renders an HInclude tag. - * - * @param string $uri A URI - * @param string $defaultContent Default content - * - * @return string - */ - public function renderHIncludeTag($uri, $defaultContent = null) - { - return sprintf('%s', $uri, $defaultContent); - } - - public function hasEsiSupport() - { - return $this->esiSupport; + $this->container->get('http_content_renderer')->render($uri, $strategy, $options); } } diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/config/content_generator.xml b/src/Symfony/Bundle/FrameworkBundle/Resources/config/content_generator.xml new file mode 100644 index 0000000000000..332cf0de38804 --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Resources/config/content_generator.xml @@ -0,0 +1,43 @@ + + + + + + Symfony\Component\HttpKernel\HttpContentRenderer + Symfony\Component\HttpKernel\RenderingStrategy\DefaultRenderingStrategy + Symfony\Component\HttpKernel\RenderingStrategy\HIncludeRenderingStrategy + + Symfony\Component\HttpKernel\EventListener\RouterProxyListener + + + + + + + %kernel.debug% + + + + + + + + + + + + + %http_content_renderer.strategy.hinclude.global_template% + + + + + + + + + + + diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/config/esi.xml b/src/Symfony/Bundle/FrameworkBundle/Resources/config/esi.xml index 3038f40e97b46..0c4a271863a8d 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Resources/config/esi.xml +++ b/src/Symfony/Bundle/FrameworkBundle/Resources/config/esi.xml @@ -7,6 +7,7 @@ Symfony\Component\HttpKernel\HttpCache\Esi Symfony\Component\HttpKernel\EventListener\EsiListener + Symfony\Component\HttpKernel\RenderingStrategy\EsiRenderingStrategy @@ -16,5 +17,12 @@ + + + + + + + diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/config/routing/proxy.xml b/src/Symfony/Bundle/FrameworkBundle/Resources/config/routing/proxy.xml new file mode 100644 index 0000000000000..21ca146126f52 --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Resources/config/routing/proxy.xml @@ -0,0 +1,8 @@ + + + + + + diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/config/services.xml b/src/Symfony/Bundle/FrameworkBundle/Resources/config/services.xml index 36f01f09e4218..fbddc0e07fbfd 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Resources/config/services.xml +++ b/src/Symfony/Bundle/FrameworkBundle/Resources/config/services.xml @@ -11,6 +11,7 @@ Symfony\Component\HttpKernel\CacheWarmer\CacheWarmerAggregate Symfony\Component\HttpKernel\CacheClearer\ChainCacheClearer Symfony\Component\HttpKernel\Config\FileLocator + Symfony\Component\HttpKernel\UriSigner @@ -51,5 +52,9 @@ %kernel.root_dir%/Resources + + + %kernel.secret% + diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/config/templating_php.xml b/src/Symfony/Bundle/FrameworkBundle/Resources/config/templating_php.xml index ea1795455f80b..f93c7c7db4c21 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Resources/config/templating_php.xml +++ b/src/Symfony/Bundle/FrameworkBundle/Resources/config/templating_php.xml @@ -81,7 +81,7 @@ - + diff --git a/src/Symfony/Bundle/FrameworkBundle/Templating/Helper/ActionsHelper.php b/src/Symfony/Bundle/FrameworkBundle/Templating/Helper/ActionsHelper.php index a4fdc3510ea99..abce1d10bf8f5 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Templating/Helper/ActionsHelper.php +++ b/src/Symfony/Bundle/FrameworkBundle/Templating/Helper/ActionsHelper.php @@ -12,7 +12,8 @@ namespace Symfony\Bundle\FrameworkBundle\Templating\Helper; use Symfony\Component\Templating\Helper\Helper; -use Symfony\Bundle\FrameworkBundle\HttpKernel; +use Symfony\Component\HttpKernel\HttpContentRenderer; +use Symfony\Component\HttpKernel\Controller\ControllerReference; /** * ActionsHelper manages action inclusions. @@ -21,16 +22,16 @@ */ class ActionsHelper extends Helper { - protected $kernel; + private $renderer; /** * Constructor. * - * @param HttpKernel $kernel A HttpKernel instance + * @param HttpContentRenderer $kernel A HttpContentRenderer instance */ - public function __construct(HttpKernel $kernel) + public function __construct(HttpContentRenderer $renderer) { - $this->kernel = $kernel; + $this->renderer = $renderer; } /** @@ -41,11 +42,21 @@ public function __construct(HttpKernel $kernel) * * @return string * - * @see Symfony\Bundle\FrameworkBundle\HttpKernel::render() + * @see Symfony\Component\HttpKernel\HttpContentRenderer::render() */ public function render($uri, array $options = array()) { - return $this->kernel->render($uri, $options); + $options = $this->renderer->fixOptions($options); + + $strategy = isset($options['strategy']) ? $options['strategy'] : 'default'; + unset($options['strategy']); + + return $this->renderer->render($uri, $strategy, $options); + } + + public function controller($controller, $attributes = array(), $query = array()) + { + return new ControllerReference($controller, $attributes, $query); } /** diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Compiler/HttpRenderingStrategyPassTest.php b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Compiler/HttpRenderingStrategyPassTest.php new file mode 100644 index 0000000000000..4fe461fec4bf8 --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Compiler/HttpRenderingStrategyPassTest.php @@ -0,0 +1,105 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Bundle\FrameworkBundle\Tests\DependencyInjection\Compiler; + +use Symfony\Component\DependencyInjection\ContainerBuilder; +use Symfony\Component\DependencyInjection\Definition; +use Symfony\Component\DependencyInjection\Reference; +use Symfony\Component\HttpFoundation\Request; +use Symfony\Bundle\FrameworkBundle\DependencyInjection\Compiler\HttpRenderingStrategyPass; + +class HttpRenderingStrategyPassTest extends \PHPUnit_Framework_TestCase +{ + /** + * Tests that content rendering not implementing RenderingStrategyInterface + * trigger an exception. + * + * @expectedException \InvalidArgumentException + */ + public function testContentRendererWithoutInterface() + { + // one service, not implementing any interface + $services = array( + 'my_content_renderer' => array(), + ); + + $definition = $this->getMock('Symfony\Component\DependencyInjection\Definition'); + $definition->expects($this->atLeastOnce()) + ->method('getClass') + ->will($this->returnValue('stdClass')); + + $builder = $this->getMock('Symfony\Component\DependencyInjection\ContainerBuilder'); + $builder->expects($this->any()) + ->method('hasDefinition') + ->will($this->returnValue(true)); + + // We don't test kernel.content_renderer_strategy here + $builder->expects($this->atLeastOnce()) + ->method('findTaggedServiceIds') + ->will($this->returnValue($services)); + + $builder->expects($this->atLeastOnce()) + ->method('getDefinition') + ->will($this->returnValue($definition)); + + $pass = new HttpRenderingStrategyPass(); + $pass->process($builder); + } + + public function testValidContentRenderer() + { + $services = array( + 'my_content_renderer' => array(), + ); + + $renderer = $this->getMock('Symfony\Component\DependencyInjection\Definition'); + $renderer + ->expects($this->once()) + ->method('addMethodCall') + ->with('addStrategy', array(new Reference('my_content_renderer'))) + ; + + $definition = $this->getMock('Symfony\Component\DependencyInjection\Definition'); + $definition->expects($this->atLeastOnce()) + ->method('getClass') + ->will($this->returnValue('Symfony\Bundle\FrameworkBundle\Tests\DependencyInjection\Compiler\RenderingStrategyService')); + + $builder = $this->getMock('Symfony\Component\DependencyInjection\ContainerBuilder'); + $builder->expects($this->any()) + ->method('hasDefinition') + ->will($this->returnValue(true)); + + // We don't test kernel.content_renderer_strategy here + $builder->expects($this->atLeastOnce()) + ->method('findTaggedServiceIds') + ->will($this->returnValue($services)); + + $builder->expects($this->atLeastOnce()) + ->method('getDefinition') + ->will($this->onConsecutiveCalls($renderer, $definition)); + + $pass = new HttpRenderingStrategyPass(); + $pass->process($builder); + } +} + +class RenderingStrategyService implements \Symfony\Component\HttpKernel\RenderingStrategy\RenderingStrategyInterface +{ + public function render($uri, Request $request = null, array $options = array()) + { + } + + public function getName() + { + return 'test'; + } +} diff --git a/src/Symfony/Bundle/TwigBundle/Resources/config/twig.xml b/src/Symfony/Bundle/TwigBundle/Resources/config/twig.xml index c12c0898b1323..2a106d6c3b22e 100644 --- a/src/Symfony/Bundle/TwigBundle/Resources/config/twig.xml +++ b/src/Symfony/Bundle/TwigBundle/Resources/config/twig.xml @@ -90,8 +90,7 @@ - - + diff --git a/src/Symfony/Component/HttpFoundation/CHANGELOG.md b/src/Symfony/Component/HttpFoundation/CHANGELOG.md index b81fe51fd4c26..252eac5e3fb53 100644 --- a/src/Symfony/Component/HttpFoundation/CHANGELOG.md +++ b/src/Symfony/Component/HttpFoundation/CHANGELOG.md @@ -4,6 +4,8 @@ CHANGELOG 2.2.0 ----- + * added Request::getTrustedProxies() + * deprecated Request::isProxyTrusted() * [BC BREAK] JsonResponse does not change a top level array to an object when the array is empty anymore in setData() * added a IpUtils class to check if an IP belongs to a CIDR * added Request::getRealMethod() to get the "real" HTTP method (getMethod() returns the "intended" HTTP method) diff --git a/src/Symfony/Component/HttpFoundation/Request.php b/src/Symfony/Component/HttpFoundation/Request.php index 376e0612e5916..0815b46eae4bd 100644 --- a/src/Symfony/Component/HttpFoundation/Request.php +++ b/src/Symfony/Component/HttpFoundation/Request.php @@ -486,6 +486,16 @@ public static function setTrustedProxies(array $proxies) self::$trustProxy = $proxies ? true : false; } + /** + * Gets the list of trusted proxies. + * + * @return array An array of trusted proxies. + */ + public static function getTrustedProxies() + { + return self::$trustedProxies; + } + /** * Sets the name for trusted headers. * @@ -517,6 +527,8 @@ public static function setTrustedHeaderName($key, $value) * false otherwise. * * @return boolean + * + * @deprecated Deprecated since version 2.2, to be removed in 2.3. Use getTrustedProxies instead. */ public static function isProxyTrusted() { @@ -914,8 +926,7 @@ public function getSchemeAndHttpHost() */ public function getUri() { - $qs = $this->getQueryString(); - if (null !== $qs) { + if (null !== $qs = $this->getQueryString()) { $qs = '?'.$qs; } diff --git a/src/Symfony/Component/HttpKernel/CHANGELOG.md b/src/Symfony/Component/HttpKernel/CHANGELOG.md index 75b5d9321a7fc..15ddbf199d791 100644 --- a/src/Symfony/Component/HttpKernel/CHANGELOG.md +++ b/src/Symfony/Component/HttpKernel/CHANGELOG.md @@ -4,6 +4,11 @@ CHANGELOG 2.2.0 ----- + * added Symfony\Component\HttpKernel\UriSigner + * added Symfony\Component\HttpKernel\HttpContentRenderer and rendering strategies (in Symfony\Component\HttpKernel\RenderingStrategy) + * added Symfony\Component\HttpKernel\EventListener\RouterProxyListener + * added Symfony\Component\HttpKernel\DependencyInjection\ContainerAwareHttpKernel + * added ControllerReference to create reference of Controllers (used in the HttpContentRenderer class) * [BC BREAK] renamed TimeDataCollector::getTotalTime() to TimeDataCollector::getDuration() * updated the MemoryDataCollector to include the memory used in the diff --git a/src/Symfony/Component/HttpKernel/Controller/ControllerReference.php b/src/Symfony/Component/HttpKernel/Controller/ControllerReference.php new file mode 100644 index 0000000000000..905e89f5dc00b --- /dev/null +++ b/src/Symfony/Component/HttpKernel/Controller/ControllerReference.php @@ -0,0 +1,45 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\HttpKernel\Controller; + +/** + * Acts as a marker and a data holder for a Controller. + * + * Some methods in Symfony accept both a URI (as a string) or a controller as + * an argument. In the latter case, instead of passing an array representing + * the controller, you can use an instance of this class. + * + * @author Fabien Potencier + * + * @see Symfony\Component\HttpKernel\HttpContentRenderer + * @see Symfony\Component\HttpKernel\RenderingStrategy\RenderingStrategyInterface + */ +class ControllerReference +{ + public $controller; + public $attributes = array(); + public $query = array(); + + /** + * Constructor. + * + * @param string $controller The controller name + * @param array $attributes An array of parameters to add to the Request attributes + * @param array $query An array of parameters to add to the Request query string + */ + public function __construct($controller, array $attributes = array(), array $query = array()) + { + $this->controller = $controller; + $this->attributes = $attributes; + $this->query = $query; + } +} diff --git a/src/Symfony/Component/HttpKernel/DependencyInjection/ContainerAwareHttpKernel.php b/src/Symfony/Component/HttpKernel/DependencyInjection/ContainerAwareHttpKernel.php new file mode 100644 index 0000000000000..20b4a5e75e9ad --- /dev/null +++ b/src/Symfony/Component/HttpKernel/DependencyInjection/ContainerAwareHttpKernel.php @@ -0,0 +1,68 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\HttpKernel\DependencyInjection; + +use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\HttpFoundation\Response; +use Symfony\Component\HttpKernel\HttpKernelInterface; +use Symfony\Component\HttpKernel\HttpKernel; +use Symfony\Component\HttpKernel\Controller\ControllerResolverInterface; +use Symfony\Component\EventDispatcher\EventDispatcherInterface; +use Symfony\Component\DependencyInjection\ContainerInterface; + +/** + * This HttpKernel is used to manage scope changes of the DI container. + * + * @author Fabien Potencier + * @author Johannes M. Schmitt + */ +class ContainerAwareHttpKernel extends HttpKernel +{ + protected $container; + + /** + * Constructor. + * + * @param EventDispatcherInterface $dispatcher An EventDispatcherInterface instance + * @param ContainerInterface $container A ContainerInterface instance + * @param ControllerResolverInterface $controllerResolver A ControllerResolverInterface instance + */ + public function __construct(EventDispatcherInterface $dispatcher, ContainerInterface $container, ControllerResolverInterface $controllerResolver) + { + parent::__construct($dispatcher, $controllerResolver); + + $this->container = $container; + } + + /** + * {@inheritdoc} + */ + public function handle(Request $request, $type = HttpKernelInterface::MASTER_REQUEST, $catch = true) + { + $request->headers->set('X-Php-Ob-Level', ob_get_level()); + + $this->container->enterScope('request'); + $this->container->set('request', $request, 'request'); + + try { + $response = parent::handle($request, $type, $catch); + } catch (\Exception $e) { + $this->container->leaveScope('request'); + + throw $e; + } + + $this->container->leaveScope('request'); + + return $response; + } +} diff --git a/src/Symfony/Component/HttpKernel/EventListener/RouterProxyListener.php b/src/Symfony/Component/HttpKernel/EventListener/RouterProxyListener.php new file mode 100644 index 0000000000000..b88350c20fa32 --- /dev/null +++ b/src/Symfony/Component/HttpKernel/EventListener/RouterProxyListener.php @@ -0,0 +1,98 @@ + + * + * 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\Request; +use Symfony\Component\HttpFoundation\IpUtils; +use Symfony\Component\HttpKernel\Event\GetResponseEvent; +use Symfony\Component\HttpKernel\KernelEvents; +use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException; +use Symfony\Component\HttpKernel\UriSigner; +use Symfony\Component\EventDispatcher\EventSubscriberInterface; + +/** + * Proxies URIs when the current route name is "_proxy". + * + * If the request does not come from a trusted IP, it throws an + * AccessDeniedHttpException exception. + * + * @author Fabien Potencier + */ +class RouterProxyListener implements EventSubscriberInterface +{ + private $signer; + + public function __construct(UriSigner $signer) + { + $this->signer = $signer; + } + + /** + * Fixes request attributes when the route is '_proxy'. + * + * @param GetResponseEvent $event A GetResponseEvent instance + * + * @throws AccessDeniedHttpException if the request does not come from a trusted IP. + */ + public function onKernelRequest(GetResponseEvent $event) + { + $request = $event->getRequest(); + + if ('_proxy' !== $request->attributes->get('_route')) { + return; + } + + $this->validateRequest($request); + + parse_str($request->query->get('path', ''), $attributes); + $request->attributes->add($attributes); + $request->attributes->set('_route_params', array_replace($request->attributes->get('_route_params', array()), $attributes)); + $request->query->remove('path'); + } + + protected function validateRequest(Request $request) + { + // is the Request safe? + if (!$request->isMethodSafe()) { + throw new AccessDeniedHttpException(); + } + + // does the Request come from a trusted IP? + $trustedIps = array_merge($this->getLocalIpAddresses(), $request->getTrustedProxies()); + $remoteAddress = $request->server->get('REMOTE_ADDR'); + foreach ($trustedIps as $ip) { + if (IpUtils::checkIp($remoteAddress, $ip)) { + return; + } + } + + // is the Request signed? + // we cannot use $request->getUri() here as we want to work with the original URI (no query string reordering) + if ($this->signer->check($request->getSchemeAndHttpHost().$request->getBaseUrl().$request->getPathInfo().(null !== ($qs = $request->server->get('QUERY_STRING')) ? '?'.$qs : ''))) { + return; + } + + throw new AccessDeniedHttpException(); + } + + protected function getLocalIpAddresses() + { + return array('127.0.0.1', 'fe80::1', '::1'); + } + + public static function getSubscribedEvents() + { + return array( + KernelEvents::REQUEST => array(array('onKernelRequest', 16)), + ); + } +} diff --git a/src/Symfony/Component/HttpKernel/HttpContentRenderer.php b/src/Symfony/Component/HttpKernel/HttpContentRenderer.php new file mode 100644 index 0000000000000..4849b8f6b5ec4 --- /dev/null +++ b/src/Symfony/Component/HttpKernel/HttpContentRenderer.php @@ -0,0 +1,141 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\HttpKernel; + +use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\HttpKernel\Controller\ControllerReference; +use Symfony\Component\HttpKernel\Event\GetResponseEvent; +use Symfony\Component\HttpKernel\Event\FilterResponseEvent; +use Symfony\Component\HttpKernel\RenderingStrategy\RenderingStrategyInterface; +use Symfony\Component\EventDispatcher\EventSubscriberInterface; + +/** + * Renders a URI using different strategies. + * + * @author Fabien Potencier + */ +class HttpContentRenderer implements EventSubscriberInterface +{ + private $debug; + private $strategies; + private $requests; + + /** + * Constructor. + * + * @param RenderingStrategyInterface[] $strategies An array of RenderingStrategyInterface instances + * @param Boolean $debug Whether the debug mode is enabled or not + */ + public function __construct(array $strategies = array(), $debug = false) + { + $this->strategies = array(); + foreach ($strategies as $strategy) { + $this->addStrategy($strategy); + } + $this->debug = $debug; + $this->requests = array(); + } + + /** + * Adds a rendering strategy. + * + * @param RenderingStrategyInterface $strategy A RenderingStrategyInterface instance + */ + public function addStrategy(RenderingStrategyInterface $strategy) + { + $this->strategies[$strategy->getName()] = $strategy; + } + + /** + * Stores the Request object. + * + * @param GetResponseEvent $event A GetResponseEvent instance + */ + public function onKernelRequest(GetResponseEvent $event) + { + array_unshift($this->requests, $event->getRequest()); + } + + /** + * Removes the most recent Request object. + * + * @param FilterResponseEvent $event A FilterResponseEvent instance + */ + public function onKernelResponse(FilterResponseEvent $event) + { + array_shift($this->requests); + } + + /** + * Renders a URI and returns the Response content. + * + * When the Response is a StreamedResponse, the content is streamed immediately + * instead of being returned. + * + * Available options: + * + * * ignore_errors: true to return an empty string in case of an error + * + * @param string|ControllerReference $uri A URI as a string or a ControllerReference instance + * @param string $strategy The strategy to use for the rendering + * @param array $options An array of options + * + * @return string|null The Response content or null when the Response is streamed + * + * @throws \InvalidArgumentException when the strategy does not exist + */ + public function render($uri, $strategy = 'default', array $options = array()) + { + if (!isset($options['ignore_errors'])) { + $options['ignore_errors'] = !$this->debug; + } + + if (!isset($this->strategies[$strategy])) { + throw new \InvalidArgumentException(sprintf('The "%s" rendering strategy does not exist.', $strategy)); + } + + return $this->strategies[$strategy]->render($uri, $this->requests ? $this->requests[0] : null, $options); + } + + public static function getSubscribedEvents() + { + return array( + KernelEvents::REQUEST => 'onKernelRequest', + KernelEvents::RESPONSE => 'onKernelResponse', + ); + } + + // to be removed in 2.3 + public function fixOptions(array $options) + { + // support for the standalone option is @deprecated in 2.2 and replaced with the strategy option + if (isset($options['standalone'])) { + trigger_error('The "standalone" option is deprecated in version 2.2 and replaced with the "strategy" option.', E_USER_DEPRECATED); + + // support for the true value is @deprecated in 2.2, will be removed in 2.3 + if (true === $options['standalone']) { + trigger_error('The "true" value for the "standalone" option is deprecated in version 2.2 and replaced with the "esi" value.', E_USER_DEPRECATED); + + $options['standalone'] = 'esi'; + } elseif ('js' === $options['standalone']) { + trigger_error('The "js" value for the "standalone" option is deprecated in version 2.2 and replaced with the "hinclude" value.', E_USER_DEPRECATED); + + $options['standalone'] = 'hinclude'; + } + + $options['strategy'] = $options['standalone']; + unset($options['standalone']); + } + + return $options; + } +} diff --git a/src/Symfony/Component/HttpKernel/RenderingStrategy/DefaultRenderingStrategy.php b/src/Symfony/Component/HttpKernel/RenderingStrategy/DefaultRenderingStrategy.php new file mode 100644 index 0000000000000..c0820b8504b3d --- /dev/null +++ b/src/Symfony/Component/HttpKernel/RenderingStrategy/DefaultRenderingStrategy.php @@ -0,0 +1,117 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\HttpKernel\RenderingStrategy; + +use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\HttpKernel\HttpKernelInterface; +use Symfony\Component\HttpKernel\Controller\ControllerReference; + +/** + * Implements the default rendering strategy where the Request is rendered by the current HTTP kernel. + * + * @author Fabien Potencier + */ +class DefaultRenderingStrategy extends GeneratorAwareRenderingStrategy +{ + private $kernel; + + /** + * Constructor. + * + * @param HttpKernelInterface $kernel A HttpKernelInterface instance + */ + public function __construct(HttpKernelInterface $kernel) + { + $this->kernel = $kernel; + } + + /** + * {@inheritdoc} + * + * Additional available options: + * + * * alt: an alternative URI to render in case of an error + */ + public function render($uri, Request $request = null, array $options = array()) + { + if ($uri instanceof ControllerReference) { + $uri = $this->generateProxyUri($uri, $request); + } + + $subRequest = $this->createSubRequest($uri, $request); + + $level = ob_get_level(); + try { + return $this->handle($subRequest); + } catch (\Exception $e) { + // let's clean up the output buffers that were created by the sub-request + while (ob_get_level() > $level) { + ob_get_clean(); + } + + if (isset($options['alt'])) { + $alt = $options['alt']; + unset($options['alt']); + + return $this->render($alt, $request, $options); + } + + if (!isset($options['ignore_errors']) || !$options['ignore_errors']) { + throw $e; + } + } + } + + protected function handle(Request $request) + { + $response = $this->kernel->handle($request, HttpKernelInterface::SUB_REQUEST, false); + + if (!$response->isSuccessful()) { + throw new \RuntimeException(sprintf('Error when rendering "%s" (Status code is %s).', $request->getUri(), $response->getStatusCode())); + } + + if (!$response instanceof StreamedResponse) { + return $response->getContent(); + } + + $response->sendContent(); + } + + protected function createSubRequest($uri, Request $request = null) + { + if (null !== $request) { + $cookies = $request->cookies->all(); + $server = $request->server->all(); + + // the sub-request is internal + $server['REMOTE_ADDR'] = '127.0.0.1'; + } else { + $cookies = array(); + $server = array(); + } + + $subRequest = Request::create($uri, 'get', array(), $cookies, array(), $server); + if (null !== $request && $session = $request->getSession()) { + $subRequest->setSession($session); + } + + return $subRequest; + } + + /** + * {@inheritdoc} + */ + public function getName() + { + return 'default'; + } +} diff --git a/src/Symfony/Component/HttpKernel/RenderingStrategy/EsiRenderingStrategy.php b/src/Symfony/Component/HttpKernel/RenderingStrategy/EsiRenderingStrategy.php new file mode 100644 index 0000000000000..f77669f484c62 --- /dev/null +++ b/src/Symfony/Component/HttpKernel/RenderingStrategy/EsiRenderingStrategy.php @@ -0,0 +1,82 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\HttpKernel\RenderingStrategy; + +use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\HttpKernel\Controller\ControllerReference; +use Symfony\Component\HttpKernel\HttpCache\Esi; + +/** + * Implements the ESI rendering strategy. + * + * @author Fabien Potencier + */ +class EsiRenderingStrategy extends GeneratorAwareRenderingStrategy +{ + private $esi; + private $defaultStrategy; + + /** + * Constructor. + * + * The "fallback" strategy when ESI is not available should always be an + * instance of DefaultRenderingStrategy (or a class you are using for the + * default strategy). + * + * @param Esi $esi An Esi instance + * @param RenderingStrategyInterface $defaultStrategy The default strategy to use when ESI is not supported + */ + public function __construct(Esi $esi, RenderingStrategyInterface $defaultStrategy) + { + $this->esi = $esi; + $this->defaultStrategy = $defaultStrategy; + } + + /** + * {@inheritdoc} + * + * Note that if the current Request has no ESI capability, this method + * falls back to use the default 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 = null, array $options = array()) + { + if (null === $request || !$this->esi->hasSurrogateEsiCapability($request)) { + return $this->defaultStrategy->render($uri, $request, $options); + } + + if ($uri instanceof ControllerReference) { + $uri = $this->generateProxyUri($uri, $request); + } + + $alt = isset($options['alt']) ? $options['alt'] : null; + if ($alt instanceof ControllerReference) { + $alt = $this->generateProxyUri($alt, $request); + } + + return $this->esi->renderIncludeTag($uri, $alt, isset($options['ignore_errors']) ? $options['ignore_errors'] : false, isset($options['comment']) ? $options['comment'] : ''); + } + + /** + * {@inheritdoc} + */ + public function getName() + { + return 'esi'; + } +} diff --git a/src/Symfony/Component/HttpKernel/RenderingStrategy/GeneratorAwareRenderingStrategy.php b/src/Symfony/Component/HttpKernel/RenderingStrategy/GeneratorAwareRenderingStrategy.php new file mode 100644 index 0000000000000..a5ba272f81e9b --- /dev/null +++ b/src/Symfony/Component/HttpKernel/RenderingStrategy/GeneratorAwareRenderingStrategy.php @@ -0,0 +1,84 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\HttpKernel\RenderingStrategy; + +use Symfony\Component\HttpKernel\Controller\ControllerReference; +use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\Routing\Generator\UrlGeneratorInterface; +use Symfony\Component\Routing\Exception\RouteNotFoundException; + +/** + * Adds the possibility to generate a proxy URI for a given Controller. + * + * @author Fabien Potencier + */ +abstract class GeneratorAwareRenderingStrategy implements RenderingStrategyInterface +{ + protected $generator; + + /** + * Sets a URL generator to use for proxy URIs generation. + * + * @param UrlGeneratorInterface $generator An UrlGeneratorInterface instance + */ + public function setUrlGenerator(UrlGeneratorInterface $generator) + { + $this->generator = $generator; + } + + /** + * Generates a proxy URI for a given controller. + * + * This method only works when using the Symfony Routing component and + * if a "_proxy" route is defined with a {_controller} and {_format} + * placeholders. + * + * @param ControllerReference $reference A ControllerReference instance + * @param Request $request A Request instance + * + * @return string A proxy URI + * + * @throws \LogicException when the _proxy route is not available + * @throws \LogicException when there is no registered route generator + */ + protected function generateProxyUri(ControllerReference $reference, Request $request = null) + { + if (null === $this->generator) { + throw new \LogicException('Unable to generate a proxy URL as there is no registered route generator.'); + } + + if (isset($reference->attributes['_format'])) { + $format = $reference->attributes['_format']; + unset($reference->attributes['_format']); + } elseif (null !== $request) { + $format = $request->getRequestFormat(); + } else { + $format = 'html'; + } + + try { + $uri = $this->generator->generate('_proxy', array('_controller' => $reference->controller, '_format' => $format), UrlGeneratorInterface::ABSOLUTE_URL); + } catch (RouteNotFoundException $e) { + throw new \LogicException('Unable to generate a proxy URL as the "_proxy" route is not registered.', 0, $e); + } + + if ($path = http_build_query($reference->attributes, '', '&')) { + $reference->query['path'] = $path; + } + + if ($qs = http_build_query($reference->query, '', '&')) { + $uri .= '?'.$qs; + } + + return $uri; + } +} diff --git a/src/Symfony/Component/HttpKernel/RenderingStrategy/HIncludeRenderingStrategy.php b/src/Symfony/Component/HttpKernel/RenderingStrategy/HIncludeRenderingStrategy.php new file mode 100644 index 0000000000000..82abf3f71f248 --- /dev/null +++ b/src/Symfony/Component/HttpKernel/RenderingStrategy/HIncludeRenderingStrategy.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\RenderingStrategy; + +use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\Templating\EngineInterface; +use Symfony\Component\HttpKernel\Controller\ControllerReference; +use Symfony\Component\HttpKernel\UriSigner; + +/** + * Implements the Hinclude rendering strategy. + * + * @author Fabien Potencier + */ +class HIncludeRenderingStrategy extends GeneratorAwareRenderingStrategy +{ + private $templating; + private $globalDefaultTemplate; + private $signer; + + /** + * Constructor. + * + * @param EngineInterface|\Twig_Environment $templating An EngineInterface or a \Twig_Environment instance + * @param UriSigner $signer A UriSigner instance + * @param string $globalDefaultTemplate The global default content (it can be a template name or the content) + */ + public function __construct($templating = null, UriSigner $signer = null, $globalDefaultTemplate = null) + { + if (null !== $templating && !$templating instanceof EngineInterface && !$templating instanceof \Twig_Environment) { + throw new \InvalidArgumentException('The hinclude rendering strategy needs an instance of \Twig_Environment or Symfony\Component\Templating\EngineInterface'); + } + + $this->templating = $templating; + $this->globalDefaultTemplate = $globalDefaultTemplate; + $this->signer = $signer; + } + + /** + * {@inheritdoc} + * + * Additional available options: + * + * * default: The default content (it can be a template name or the content) + */ + public function render($uri, Request $request = null, array $options = array()) + { + if ($uri instanceof ControllerReference) { + if (null === $this->signer) { + throw new \LogicException('You must use a proper URI when using the Hinclude rendering strategy or set a URL signer.'); + } + + $uri = $this->signer->sign($this->generateProxyUri($uri, $request)); + } + + $template = isset($options['default']) ? $options['default'] : $this->globalDefaultTemplate; + if (null !== $this->templating && $this->templateExists($template)) { + $content = $this->templating->render($template); + } else { + $content = $template; + } + + return sprintf('%s', $uri, $content); + } + + private function templateExists($template) + { + if ($this->templating instanceof EngineInterface) { + return $this->templating->exists($template); + } + + $loader = $this->templating->getLoader(); + if ($loader instanceof \Twig_ExistsLoaderInterface) { + return $loader->exists($template); + } + + try { + $loader->getSource($template); + + return true; + } catch (\Twig_Error_Loader $e) { + } + + return false; + } + + /** + * {@inheritdoc} + */ + public function getName() + { + return 'hinclude'; + } +} diff --git a/src/Symfony/Component/HttpKernel/RenderingStrategy/RenderingStrategyInterface.php b/src/Symfony/Component/HttpKernel/RenderingStrategy/RenderingStrategyInterface.php new file mode 100644 index 0000000000000..36419c3792f94 --- /dev/null +++ b/src/Symfony/Component/HttpKernel/RenderingStrategy/RenderingStrategyInterface.php @@ -0,0 +1,46 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\HttpKernel\RenderingStrategy; + +use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\HttpKernel\Controller\ControllerReference; + +/** + * Interface implemented by all rendering strategies. + * + * @author Fabien Potencier + * + * @see Symfony\Component\HttpKernel\HttpContentRenderer + */ +interface RenderingStrategyInterface +{ + /** + * Renders a URI and returns the Response content. + * + * When the Response is a StreamedResponse, the content is streamed immediately + * instead of being returned. + * + * @param string|ControllerReference $uri A URI as a string or a ControllerReference instance + * @param Request $request A Request instance + * @param array $options An array of options + * + * @return string|null The Response content or null when the Response is streamed + */ + public function render($uri, Request $request = null, array $options = array()); + + /** + * Gets the name of the strategy. + * + * @return string The strategy name + */ + public function getName(); +} diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/HttpKernelTest.php b/src/Symfony/Component/HttpKernel/Tests/DependencyInjection/ContainerAwareHttpKernelTest.php similarity index 66% rename from src/Symfony/Bundle/FrameworkBundle/Tests/HttpKernelTest.php rename to src/Symfony/Component/HttpKernel/Tests/DependencyInjection/ContainerAwareHttpKernelTest.php index c7587f6b4b941..80d5ffa61a664 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/HttpKernelTest.php +++ b/src/Symfony/Component/HttpKernel/Tests/DependencyInjection/ContainerAwareHttpKernelTest.php @@ -9,16 +9,31 @@ * file that was distributed with this source code. */ -namespace Symfony\Bundle\FrameworkBundle\Tests; +namespace Symfony\Component\HttpKernel\Tests; use Symfony\Component\HttpKernel\HttpKernelInterface; +use Symfony\Component\HttpKernel\DependencyInjection\ContainerAwareHttpKernel; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpFoundation\Request; -use Symfony\Bundle\FrameworkBundle\HttpKernel; use Symfony\Component\EventDispatcher\EventDispatcher; -class HttpKernelTest extends \PHPUnit_Framework_TestCase +class ContainerAwareHttpKernelTest extends \PHPUnit_Framework_TestCase { + protected function setUp() + { + if (!class_exists('Symfony\Component\DependencyInjection\Container')) { + $this->markTestSkipped('The "DependencyInjection" component is not available'); + } + + if (!class_exists('Symfony\Component\EventDispatcher\EventDispatcher')) { + $this->markTestSkipped('The "EventDispatcher" component is not available'); + } + + if (!class_exists('Symfony\Component\HttpFoundation\Request')) { + $this->markTestSkipped('The "HttpFoundation" component is not available'); + } + } + /** * @dataProvider getProviderTypes */ @@ -46,7 +61,7 @@ public function testHandle($type) $dispatcher = new EventDispatcher(); $resolver = $this->getMock('Symfony\\Component\\HttpKernel\\Controller\\ControllerResolverInterface'); - $kernel = new HttpKernel($dispatcher, $container, $resolver); + $kernel = new ContainerAwareHttpKernel($dispatcher, $container, $resolver); $controller = function() use ($expected) { return $expected; @@ -93,7 +108,7 @@ public function testHandleRestoresThePreviousRequestOnException($type) $dispatcher = new EventDispatcher(); $resolver = $this->getMock('Symfony\\Component\\HttpKernel\\Controller\\ControllerResolverInterface'); - $kernel = new HttpKernel($dispatcher, $container, $resolver); + $kernel = new ContainerAwareHttpKernel($dispatcher, $container, $resolver); $controller = function() use ($expected) { throw $expected; @@ -123,53 +138,4 @@ public function getProviderTypes() array(HttpKernelInterface::SUB_REQUEST), ); } - - public function testExceptionInSubRequestsDoesNotMangleOutputBuffers() - { - $request = new Request(); - - $container = $this->getMock('Symfony\\Component\\DependencyInjection\\ContainerInterface'); - $container - ->expects($this->at(0)) - ->method('get') - ->with($this->equalTo('request')) - ->will($this->returnValue($request)) - ; - $container - ->expects($this->at(1)) - ->method('getParameter') - ->with($this->equalTo('kernel.debug')) - ->will($this->returnValue(false)) - ; - $container - ->expects($this->at(2)) - ->method('has') - ->with($this->equalTo('esi')) - ->will($this->returnValue(false)) - ; - - $dispatcher = new EventDispatcher(); - $resolver = $this->getMock('Symfony\\Component\\HttpKernel\\Controller\\ControllerResolverInterface'); - $resolver->expects($this->once()) - ->method('getController') - ->will($this->returnValue(function () { - ob_start(); - echo 'bar'; - throw new \RuntimeException(); - })); - $resolver->expects($this->once()) - ->method('getArguments') - ->will($this->returnValue(array())); - - $kernel = new HttpKernel($dispatcher, $container, $resolver); - - // simulate a main request with output buffering - ob_start(); - echo 'Foo'; - - // simulate a sub-request with output buffering and an exception - $kernel->render('/'); - - $this->assertEquals('Foo', ob_get_clean()); - } } diff --git a/src/Symfony/Component/HttpKernel/Tests/EventListener/RouterProxyListenerTest.php b/src/Symfony/Component/HttpKernel/Tests/EventListener/RouterProxyListenerTest.php new file mode 100644 index 0000000000000..32b750f9ce670 --- /dev/null +++ b/src/Symfony/Component/HttpKernel/Tests/EventListener/RouterProxyListenerTest.php @@ -0,0 +1,119 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\HttpKernel\Tests\EventListener; + +use Symfony\Component\HttpKernel\EventListener\RouterProxyListener; +use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\HttpKernel\HttpKernelInterface; +use Symfony\Component\HttpKernel\Event\GetResponseEvent; +use Symfony\Component\HttpKernel\UriSigner; + +class RouterProxyListenerTest extends \PHPUnit_Framework_TestCase +{ + protected function setUp() + { + if (!class_exists('Symfony\Component\EventDispatcher\EventDispatcher')) { + $this->markTestSkipped('The "EventDispatcher" component is not available'); + } + } + + public function testOnlyTrigerredOnProxyRoute() + { + $request = Request::create('http://example.com/foo?path=foo%3D=bar'); + + $listener = new RouterProxyListener(new UriSigner('foo')); + $event = $this->createGetResponseEvent($request, 'foobar'); + + $expected = $request->attributes->all(); + + $listener->onKernelRequest($event); + + $this->assertEquals($expected, $request->attributes->all()); + $this->assertTrue($request->query->has('path')); + } + + /** + * @expectedException \Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException + */ + public function testAccessDeniedWithNonSafeMethods() + { + $request = Request::create('http://example.com/foo', 'POST'); + + $listener = new RouterProxyListener(new UriSigner('foo')); + $event = $this->createGetResponseEvent($request); + + $listener->onKernelRequest($event); + } + + /** + * @expectedException \Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException + */ + public function testAccessDeniedWithNonLocalIps() + { + $request = Request::create('http://example.com/foo', 'GET', array(), array(), array(), array('REMOTE_ADDR' => '10.0.0.1')); + + $listener = new RouterProxyListener(new UriSigner('foo')); + $event = $this->createGetResponseEvent($request); + + $listener->onKernelRequest($event); + } + + /** + * @expectedException \Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException + */ + public function testAccessDeniedWithWrongSignature() + { + $request = Request::create('http://example.com/foo', 'GET', array(), array(), array(), array('REMOTE_ADDR' => '10.0.0.1')); + + $listener = new RouterProxyListener(new UriSigner('foo')); + $event = $this->createGetResponseEvent($request); + + $listener->onKernelRequest($event); + } + + public function testWithSignatureAndNoPath() + { + $signer = new UriSigner('foo'); + $request = Request::create($signer->sign('http://example.com/foo'), 'GET', array(), array(), array(), array('REMOTE_ADDR' => '10.0.0.1')); + + $listener = new RouterProxyListener($signer); + $event = $this->createGetResponseEvent($request); + + $listener->onKernelRequest($event); + + $this->assertEquals(array('foo' => 'foo'), $request->attributes->get('_route_params')); + $this->assertFalse($request->query->has('path')); + } + + public function testWithSignatureAndPath() + { + $signer = new UriSigner('foo'); + $request = Request::create($signer->sign('http://example.com/foo?path=bar%3Dbar'), 'GET', array(), array(), array(), array('REMOTE_ADDR' => '10.0.0.1')); + + $listener = new RouterProxyListener($signer); + $event = $this->createGetResponseEvent($request); + + $listener->onKernelRequest($event); + + $this->assertEquals(array('foo' => 'foo', 'bar' => 'bar'), $request->attributes->get('_route_params')); + $this->assertFalse($request->query->has('path')); + } + + private function createGetResponseEvent(Request $request, $route = '_proxy') + { + $kernel = $this->getMock('Symfony\Component\HttpKernel\HttpKernelInterface'); + $request->attributes->set('_route', $route); + $request->attributes->set('_route_params', array('foo' => 'foo')); + + return new GetResponseEvent($kernel, $request, HttpKernelInterface::MASTER_REQUEST); + } +} diff --git a/src/Symfony/Component/HttpKernel/Tests/HttpContentRendererTest.php b/src/Symfony/Component/HttpKernel/Tests/HttpContentRendererTest.php new file mode 100644 index 0000000000000..ff0d4cbdb0776 --- /dev/null +++ b/src/Symfony/Component/HttpKernel/Tests/HttpContentRendererTest.php @@ -0,0 +1,75 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\HttpKernel\Tests; + +use Symfony\Component\HttpKernel\HttpContentRenderer; + +class HttpContentRendererTest extends \PHPUnit_Framework_TestCase +{ + protected function setUp() + { + if (!class_exists('Symfony\Component\EventDispatcher\EventDispatcher')) { + $this->markTestSkipped('The "EventDispatcher" component is not available'); + } + } + + /** + * @expectedException \InvalidArgumentException + */ + public function testRenderWhenStrategyDoesNotExist() + { + $renderer = new HttpContentRenderer(); + $renderer->render('/', 'foo'); + } + + public function testRender() + { + $strategy = $this->getMock('Symfony\Component\HttpKernel\RenderingStrategy\RenderingStrategyInterface'); + $strategy + ->expects($this->any()) + ->method('getName') + ->will($this->returnValue('foo')) + ; + $strategy + ->expects($this->any()) + ->method('render') + ->with('/', null, array('foo' => 'foo', 'ignore_errors' => true)) + ->will($this->returnValue('foo')) + ; + + $renderer = new HttpContentRenderer(); + $renderer->addStrategy($strategy); + + $this->assertEquals('foo', $renderer->render('/', 'foo', array('foo' => 'foo'))); + } + + /** + * @dataProvider getFixOptionsData + */ + public function testFixOptions($expected, $options) + { + $renderer = new HttpContentRenderer(); + + set_error_handler(function ($errorNumber, $message, $file, $line, $context) { return $errorNumber & E_USER_DEPRECATED; }); + $this->assertEquals($expected, $renderer->fixOptions($options)); + restore_error_handler(); + } + + public function getFixOptionsData() + { + return array( + array(array('strategy' => 'esi'), array('standalone' => true)), + array(array('strategy' => 'esi'), array('standalone' => 'esi')), + array(array('strategy' => 'hinclude'), array('standalone' => 'js')), + ); + } +} diff --git a/src/Symfony/Component/HttpKernel/Tests/RenderingStrategy/AbstractRenderingStrategyTest.php b/src/Symfony/Component/HttpKernel/Tests/RenderingStrategy/AbstractRenderingStrategyTest.php new file mode 100644 index 0000000000000..ae3a07f2cce10 --- /dev/null +++ b/src/Symfony/Component/HttpKernel/Tests/RenderingStrategy/AbstractRenderingStrategyTest.php @@ -0,0 +1,29 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\HttpKernel\Tests\RenderingStrategy; + +abstract class AbstractRenderingStrategyTest extends \PHPUnit_Framework_TestCase +{ + protected function getUrlGenerator() + { + $generator = $this->getMock('Symfony\Component\Routing\Generator\UrlGeneratorInterface'); + $generator + ->expects($this->any()) + ->method('generate') + ->will($this->returnCallback(function ($name, $parameters, $referenceType) { + return '/'.$parameters['_controller'].'.'.$parameters['_format']; + })) + ; + + return $generator; + } +} diff --git a/src/Symfony/Component/HttpKernel/Tests/RenderingStrategy/DefaultRenderingStrategyTest.php b/src/Symfony/Component/HttpKernel/Tests/RenderingStrategy/DefaultRenderingStrategyTest.php new file mode 100644 index 0000000000000..3c55c5905ce22 --- /dev/null +++ b/src/Symfony/Component/HttpKernel/Tests/RenderingStrategy/DefaultRenderingStrategyTest.php @@ -0,0 +1,118 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\HttpKernel\Tests\RenderingStrategy; + +use Symfony\Component\HttpKernel\Controller\ControllerReference; +use Symfony\Component\HttpKernel\HttpKernel; +use Symfony\Component\HttpKernel\RenderingStrategy\DefaultRenderingStrategy; +use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\HttpFoundation\Response; +use Symfony\Component\EventDispatcher\EventDispatcher; + +class DefaultRenderingStrategyTest extends AbstractRenderingStrategyTest +{ + protected function setUp() + { + if (!class_exists('Symfony\Component\EventDispatcher\EventDispatcher')) { + $this->markTestSkipped('The "EventDispatcher" component is not available'); + } + + if (!class_exists('Symfony\Component\HttpFoundation\Request')) { + $this->markTestSkipped('The "HttpFoundation" component is not available'); + } + } + + public function testRender() + { + $strategy = new DefaultRenderingStrategy($this->getKernel($this->returnValue(new Response('foo')))); + + $this->assertEquals('foo', $strategy->render('/')); + } + + public function testRenderWithControllerReference() + { + $strategy = new DefaultRenderingStrategy($this->getKernel($this->returnValue(new Response('foo')))); + $strategy->setUrlGenerator($this->getUrlGenerator()); + + $this->assertEquals('foo', $strategy->render(new ControllerReference('main_controller', array(), array()))); + } + + /** + * @expectedException \RuntimeException + */ + public function testRenderExceptionNoIgnoreErrors() + { + $strategy = new DefaultRenderingStrategy($this->getKernel($this->throwException(new \RuntimeException('foo')))); + + $this->assertEquals('foo', $strategy->render('/')); + } + + public function testRenderExceptionIgnoreErrors() + { + $strategy = new DefaultRenderingStrategy($this->getKernel($this->throwException(new \RuntimeException('foo')))); + + $this->assertNull($strategy->render('/', null, array('ignore_errors' => true))); + } + + public function testRenderExceptionIgnoreErrorsWithAlt() + { + $strategy = new DefaultRenderingStrategy($this->getKernel($this->onConsecutiveCalls( + $this->throwException(new \RuntimeException('foo')), + $this->returnValue(new Response('bar')) + ))); + + $this->assertEquals('bar', $strategy->render('/', null, array('ignore_errors' => true, 'alt' => '/foo'))); + } + + private function getKernel($returnValue) + { + $kernel = $this->getMock('Symfony\Component\HttpKernel\HttpKernelInterface'); + $kernel + ->expects($this->any()) + ->method('handle') + ->will($returnValue) + ; + + return $kernel; + } + + public function testExceptionInSubRequestsDoesNotMangleOutputBuffers() + { + $resolver = $this->getMock('Symfony\\Component\\HttpKernel\\Controller\\ControllerResolverInterface'); + $resolver + ->expects($this->once()) + ->method('getController') + ->will($this->returnValue(function () { + ob_start(); + echo 'bar'; + throw new \RuntimeException(); + })) + ; + $resolver + ->expects($this->once()) + ->method('getArguments') + ->will($this->returnValue(array())) + ; + + $kernel = new HttpKernel(new EventDispatcher(), $resolver); + $renderer = new DefaultRenderingStrategy($kernel); + + // simulate a main request with output buffering + ob_start(); + echo 'Foo'; + + // simulate a sub-request with output buffering and an exception + $renderer->render('/', Request::create('/'), array('ignore_errors' => true)); + + $this->assertEquals('Foo', ob_get_clean()); + } +} diff --git a/src/Symfony/Component/HttpKernel/Tests/RenderingStrategy/EsiRenderingStrategyTest.php b/src/Symfony/Component/HttpKernel/Tests/RenderingStrategy/EsiRenderingStrategyTest.php new file mode 100644 index 0000000000000..513e30039c037 --- /dev/null +++ b/src/Symfony/Component/HttpKernel/Tests/RenderingStrategy/EsiRenderingStrategyTest.php @@ -0,0 +1,68 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\HttpKernel\Tests\RenderingStrategy; + +use Symfony\Component\HttpKernel\Controller\ControllerReference; +use Symfony\Component\HttpKernel\RenderingStrategy\EsiRenderingStrategy; +use Symfony\Component\HttpKernel\HttpCache\Esi; +use Symfony\Component\HttpFoundation\Request; + +class EsiRenderingStrategyTest extends AbstractRenderingStrategyTest +{ + protected function setUp() + { + if (!class_exists('Symfony\Component\HttpFoundation\Request')) { + $this->markTestSkipped('The "HttpFoundation" component is not available'); + } + + if (!interface_exists('Symfony\Component\Routing\Generator\UrlGeneratorInterface')) { + $this->markTestSkipped('The "Routing" component is not available'); + } + } + + public function testRenderFallbackToDefaultStrategyIfNoRequest() + { + $strategy = new EsiRenderingStrategy(new Esi(), $this->getDefaultStrategy(true)); + $strategy->render('/'); + } + + public function testRenderFallbackToDefaultStrategyIfEsiNotSupported() + { + $strategy = new EsiRenderingStrategy(new Esi(), $this->getDefaultStrategy(true)); + $strategy->render('/', Request::create('/')); + } + + public function testRender() + { + $strategy = new EsiRenderingStrategy(new Esi(), $this->getDefaultStrategy()); + $strategy->setUrlGenerator($this->getUrlGenerator()); + + $request = Request::create('/'); + $request->headers->set('Surrogate-Capability', 'ESI/1.0'); + + $this->assertEquals('', $strategy->render('/', $request)); + $this->assertEquals("\n", $strategy->render('/', $request, array('comment' => 'This is a comment'))); + $this->assertEquals('', $strategy->render('/', $request, array('alt' => 'foo'))); + $this->assertEquals('', $strategy->render(new ControllerReference('main_controller', array(), array()), $request, array('alt' => new ControllerReference('alt_controller', array(), array())))); + } + + private function getDefaultStrategy($called = false) + { + $default = $this->getMockBuilder('Symfony\Component\HttpKernel\RenderingStrategy\DefaultRenderingStrategy')->disableOriginalConstructor()->getMock(); + + if ($called) { + $default->expects($this->once())->method('render'); + } + + return $default; + } +} diff --git a/src/Symfony/Component/HttpKernel/Tests/RenderingStrategy/GeneratorAwareRenderingStrategyTest.php b/src/Symfony/Component/HttpKernel/Tests/RenderingStrategy/GeneratorAwareRenderingStrategyTest.php new file mode 100644 index 0000000000000..387ab3e2a0f20 --- /dev/null +++ b/src/Symfony/Component/HttpKernel/Tests/RenderingStrategy/GeneratorAwareRenderingStrategyTest.php @@ -0,0 +1,101 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\HttpKernel\Tests\RenderingStrategy; + +use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\HttpKernel\Controller\ControllerReference; +use Symfony\Component\HttpKernel\RenderingStrategy\GeneratorAwareRenderingStrategy; +use Symfony\Component\Routing\Generator\UrlGeneratorInterface; +use Symfony\Component\Routing\Exception\RouteNotFoundException; + +class GeneratorAwareRenderingStrategyTest extends AbstractRenderingStrategyTest +{ + protected function setUp() + { + if (!interface_exists('Symfony\Component\Routing\Generator\UrlGeneratorInterface')) { + $this->markTestSkipped('The "Routing" component is not available'); + } + } + + /** + * @expectedException \LogicException + */ + public function testGenerateProxyUriWithNoGenerator() + { + $strategy = new Strategy(); + $strategy->doGenerateProxyUri(new ControllerReference('controller', array(), array())); + } + + /** + * @expectedException \LogicException + */ + public function testGenerateProxyUriWhenRouteNotFound() + { + $generator = $this->getMock('Symfony\Component\Routing\Generator\UrlGeneratorInterface'); + $generator + ->expects($this->once()) + ->method('generate') + ->will($this->throwException(new RouteNotFoundException())) + ; + + $strategy = new Strategy(); + $strategy->setUrlGenerator($generator); + $strategy->doGenerateProxyUri(new ControllerReference('controller', array(), array())); + } + + /** + * @dataProvider getGeneratorProxyUriData + */ + public function testGenerateProxyUri($uri, $controller) + { + $this->assertEquals($uri, $this->getStrategy()->doGenerateProxyUri($controller)); + } + + public function getGeneratorProxyUriData() + { + return array( + array('/controller.html', new ControllerReference('controller', array(), array())), + array('/controller.xml', new ControllerReference('controller', array('_format' => 'xml'), array())), + array('/controller.json?path=foo%3Dfoo', new ControllerReference('controller', array('foo' => 'foo', '_format' => 'json'), array())), + array('/controller.html?bar=bar&path=foo%3Dfoo', new ControllerReference('controller', array('foo' => 'foo'), array('bar' => 'bar'))), + array('/controller.html?foo=foo', new ControllerReference('controller', array(), array('foo' => 'foo'))), + ); + } + + public function testGenerateProxyUriWithARequest() + { + $request = Request::create('/'); + $request->attributes->set('_format', 'json'); + $controller = new ControllerReference('controller', array(), array()); + + $this->assertEquals('/controller.json', $this->getStrategy()->doGenerateProxyUri($controller, $request)); + } + + private function getStrategy() + { + $strategy = new Strategy(); + $strategy->setUrlGenerator($this->getUrlGenerator()); + + return $strategy; + } +} + +class Strategy extends GeneratorAwareRenderingStrategy +{ + public function render($uri, Request $request = null, array $options = array()) {} + public function getName() {} + + public function doGenerateProxyUri(ControllerReference $reference, Request $request = null) + { + return parent::generateProxyUri($reference, $request); + } +} diff --git a/src/Symfony/Component/HttpKernel/Tests/RenderingStrategy/HIncludeRenderingStrategyTest.php b/src/Symfony/Component/HttpKernel/Tests/RenderingStrategy/HIncludeRenderingStrategyTest.php new file mode 100644 index 0000000000000..ecc99665f8c70 --- /dev/null +++ b/src/Symfony/Component/HttpKernel/Tests/RenderingStrategy/HIncludeRenderingStrategyTest.php @@ -0,0 +1,71 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\HttpKernel\Tests\RenderingStrategy; + +use Symfony\Component\HttpKernel\Controller\ControllerReference; +use Symfony\Component\HttpKernel\RenderingStrategy\HIncludeRenderingStrategy; +use Symfony\Component\HttpKernel\UriSigner; +use Symfony\Component\HttpFoundation\Request; + +class HIncludeRenderingStrategyTest extends AbstractRenderingStrategyTest +{ + protected function setUp() + { + if (!class_exists('Symfony\Component\HttpFoundation\Request')) { + $this->markTestSkipped('The "HttpFoundation" component is not available'); + } + + if (!interface_exists('Symfony\Component\Routing\Generator\UrlGeneratorInterface')) { + $this->markTestSkipped('The "Routing" component is not available'); + } + } + + /** + * @expectedException \LogicException + */ + public function testRenderExceptionWhenControllerAndNoSigner() + { + $strategy = new HIncludeRenderingStrategy(); + $strategy->render(new ControllerReference('main_controller', array(), array())); + } + + public function testRenderWithControllerAndSigner() + { + $strategy = new HIncludeRenderingStrategy(null, new UriSigner('foo')); + $strategy->setUrlGenerator($this->getUrlGenerator()); + $this->assertEquals('', $strategy->render(new ControllerReference('main_controller', array(), array()))); + } + + public function testRenderWithUri() + { + $strategy = new HIncludeRenderingStrategy(); + $this->assertEquals('', $strategy->render('/foo')); + + $strategy = new HIncludeRenderingStrategy(null, new UriSigner('foo')); + $this->assertEquals('', $strategy->render('/foo')); + } + + public function testRenderWhithDefault() + { + // only default + $strategy = new HIncludeRenderingStrategy(); + $this->assertEquals('default', $strategy->render('/foo', null, array('default' => 'default'))); + + // only global default + $strategy = new HIncludeRenderingStrategy(null, null, 'global_default'); + $this->assertEquals('global_default', $strategy->render('/foo', null, array())); + + // global default and default + $strategy = new HIncludeRenderingStrategy(null, null, 'global_default'); + $this->assertEquals('default', $strategy->render('/foo', null, array('default' => 'default'))); + } +} diff --git a/src/Symfony/Component/HttpKernel/Tests/UriSignerTest.php b/src/Symfony/Component/HttpKernel/Tests/UriSignerTest.php new file mode 100644 index 0000000000000..8ffc2bfbbd872 --- /dev/null +++ b/src/Symfony/Component/HttpKernel/Tests/UriSignerTest.php @@ -0,0 +1,37 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\HttpKernel\Tests; + +use Symfony\Component\HttpKernel\UriSigner; + +class UriSignerTest extends \PHPUnit_Framework_TestCase +{ + public function testSign() + { + $signer = new UriSigner('foobar'); + + $this->assertContains('?_hash=', $signer->sign('http://example.com/foo')); + $this->assertContains('&_hash=', $signer->sign('http://example.com/foo?foo=bar')); + } + + public function testCheck() + { + $signer = new UriSigner('foobar'); + + $this->assertFalse($signer->check('http://example.com/foo?_hash=foo')); + $this->assertFalse($signer->check('http://example.com/foo?foo=bar&_hash=foo')); + $this->assertFalse($signer->check('http://example.com/foo?foo=bar&_hash=foo&bar=foo')); + + $this->assertTrue($signer->check($signer->sign('http://example.com/foo'))); + $this->assertTrue($signer->check($signer->sign('http://example.com/foo?foo=bar'))); + } +} diff --git a/src/Symfony/Component/HttpKernel/UriSigner.php b/src/Symfony/Component/HttpKernel/UriSigner.php new file mode 100644 index 0000000000000..45825fe246052 --- /dev/null +++ b/src/Symfony/Component/HttpKernel/UriSigner.php @@ -0,0 +1,75 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\HttpKernel; + +/** + * Signs URIs. + * + * @author Fabien Potencier + */ +class UriSigner +{ + private $secret; + + /** + * Constructor. + * + * @param string $secret A secret + */ + public function __construct($secret) + { + $this->secret = $secret; + } + + /** + * Signs a URI. + * + * The given URI is signed by adding a _hash query string parameter + * which value depends on the URI and the secret. + * + * @param string $uri A URI to sign + * + * @return string The signed URI + */ + public function sign($uri) + { + return $uri.(false === (strpos($uri, '?')) ? '?' : '&').'_hash='.$this->computeHash($uri); + } + + /** + * Checks that a URI contains the correct hash. + * + * The _hash query string parameter must be the last one + * (as it is generated that way by the sign() method, it should + * never be a problem). + * + * @param string $uri A signed URI + * + * @return Boolean True if the URI is signed correctly, false otherwise + */ + public function check($uri) + { + if (!preg_match('/(\?|&)_hash=(.+?)$/', $uri, $matches, PREG_OFFSET_CAPTURE)) { + return false; + } + + // the naked URI is the URI without the _hash parameter (we need to keep the ? if there is some other parameters after) + $nakedUri = substr($uri, 0, $matches[0][1]).substr($uri, $matches[0][1] + strlen($matches[0][0])); + + return $this->computeHash($nakedUri) === $matches[2][0]; + } + + private function computeHash($uri) + { + return urlencode(base64_encode(hash_hmac('sha1', $uri, $this->secret, true))); + } +}