diff --git a/src/Symfony/Bundle/FrameworkBundle/EventListener/ResolveRedirectControllerSubscriber.php b/src/Symfony/Bundle/FrameworkBundle/EventListener/ResolveRedirectControllerSubscriber.php new file mode 100644 index 0000000000000..bf605b0a524d2 --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/EventListener/ResolveRedirectControllerSubscriber.php @@ -0,0 +1,62 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Bundle\FrameworkBundle\EventListener; + +use Symfony\Bundle\FrameworkBundle\Controller\RedirectController; +use Symfony\Component\EventDispatcher\EventSubscriberInterface; +use Symfony\Component\HttpKernel\Event\GetResponseEvent; +use Symfony\Component\HttpKernel\KernelEvents; + +/** + * Resolve the controller for requests containing the `_redirect_to` attributes. + * + * @author Samuel Roze + */ +class ResolveRedirectControllerSubscriber implements EventSubscriberInterface +{ + public static function getSubscribedEvents() + { + return array( + KernelEvents::REQUEST => array('onKernelRequest', 20), + ); + } + + public function onKernelRequest(GetResponseEvent $event) + { + $requestAttributes = $event->getRequest()->attributes; + + if (!$requestAttributes->has('_controller') && $redirectTo = $requestAttributes->get('_redirect_to')) { + if ($this->looksLikeUrl($redirectTo)) { + $requestAttributes->set('_controller', array(RedirectController::class, 'urlRedirectAction')); + $requestAttributes->set('path', $redirectTo); + } else { + $requestAttributes->set('_controller', array(RedirectController::class, 'redirectAction')); + $requestAttributes->set('route', $redirectTo); + } + + if (!$requestAttributes->has('permanent')) { + $requestAttributes->set('permanent', $requestAttributes->get('_redirect_permanent', false)); + } + } + } + + private function looksLikeUrl(string $urlOrRouteName): bool + { + foreach (array('/', 'http://', 'https://') as $pattern) { + if (0 === strpos($urlOrRouteName, $pattern)) { + return true; + } + } + + return false; + } +} diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/config/web.xml b/src/Symfony/Bundle/FrameworkBundle/Resources/config/web.xml index 0622c4196c104..a45971b5fcc49 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Resources/config/web.xml +++ b/src/Symfony/Bundle/FrameworkBundle/Resources/config/web.xml @@ -75,5 +75,9 @@ + + + + diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/EventListener/ResolveRedirectControllerSubscriberTest.php b/src/Symfony/Bundle/FrameworkBundle/Tests/EventListener/ResolveRedirectControllerSubscriberTest.php new file mode 100644 index 0000000000000..e4499dfb978f7 --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/EventListener/ResolveRedirectControllerSubscriberTest.php @@ -0,0 +1,91 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Bundle\FrameworkBundle\Tests\EventListener; + +use Symfony\Bundle\FrameworkBundle\Controller\RedirectController; +use Symfony\Bundle\FrameworkBundle\EventListener\ResolveRedirectControllerSubscriber; +use Symfony\Bundle\FrameworkBundle\Tests\TestCase; +use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\HttpKernel\Event\GetResponseEvent; +use Symfony\Component\HttpKernel\HttpKernelInterface; + +class ResolveRedirectControllerSubscriberTest extends TestCase +{ + /** + * @dataProvider provideRedirectionExamples + */ + public function testSetControllerForRedirectToRoute(Request $request, array $expectedAttributes) + { + $httpKernel = $this->getMockBuilder(HttpKernelInterface::class)->getMock(); + $subscriber = new ResolveRedirectControllerSubscriber(); + $subscriber->onKernelRequest(new GetResponseEvent($httpKernel, $request, HttpKernelInterface::MASTER_REQUEST)); + + foreach ($expectedAttributes as $name => $value) { + $this->assertEquals($value, $request->attributes->get($name)); + } + } + + public function provideRedirectionExamples() + { + // No redirection + yield array($this->requestWithAttributes(array( + '_controller' => 'AppBundle:Starting:format', + )), array( + '_controller' => 'AppBundle:Starting:format', + )); + + // Controller win over redirection + yield array($this->requestWithAttributes(array( + '_controller' => 'AppBundle:Starting:format', + '_redirect_to' => 'https://google.com', + )), array( + '_controller' => 'AppBundle:Starting:format', + )); + + // Redirection to URL + yield array($this->requestWithAttributes(array( + '_redirect_to' => 'https://google.com', + )), array( + '_controller' => array(RedirectController::class, 'urlRedirectAction'), + 'path' => 'https://google.com', + )); + + // Redirection to route + yield array($this->requestWithAttributes(array( + '_redirect_to' => 'route', + )), array( + '_controller' => array(RedirectController::class, 'redirectAction'), + 'route' => 'route', + )); + + // Permanent redirection to route + yield array($this->requestWithAttributes(array( + '_redirect_to' => 'route', + '_redirect_permanent' => true, + )), array( + '_controller' => array(RedirectController::class, 'redirectAction'), + 'route' => 'route', + 'permanent' => true, + )); + } + + private function requestWithAttributes(array $attributes): Request + { + $request = new Request(); + + foreach ($attributes as $name => $value) { + $request->attributes->set($name, $value); + } + + return $request; + } +} diff --git a/src/Symfony/Component/Routing/Loader/XmlFileLoader.php b/src/Symfony/Component/Routing/Loader/XmlFileLoader.php index 31d69ca6d8e8c..c523049b937c2 100644 --- a/src/Symfony/Component/Routing/Loader/XmlFileLoader.php +++ b/src/Symfony/Component/Routing/Loader/XmlFileLoader.php @@ -243,6 +243,15 @@ private function parseConfigs(\DOMElement $node, $path) $defaults['_controller'] = $controller; } + if ($redirectTo = $node->getAttribute('redirect-to')) { + if (isset($defaults['_controller'])) { + throw new \InvalidArgumentException(sprintf('The routing file "%s" must not specify both a controller and a redirection.', $path)); + } + + $defaults['_redirect_to'] = $redirectTo; + $defaults['_redirect_permanent'] = (bool) $node->getAttribute('redirect-permanent'); + } + return array($defaults, $requirements, $options, $condition); } diff --git a/src/Symfony/Component/Routing/Loader/YamlFileLoader.php b/src/Symfony/Component/Routing/Loader/YamlFileLoader.php index b3ed099f8149e..71e04724925c7 100644 --- a/src/Symfony/Component/Routing/Loader/YamlFileLoader.php +++ b/src/Symfony/Component/Routing/Loader/YamlFileLoader.php @@ -28,7 +28,7 @@ class YamlFileLoader extends FileLoader { private static $availableKeys = array( - 'resource', 'type', 'prefix', 'path', 'host', 'schemes', 'methods', 'defaults', 'requirements', 'options', 'condition', 'controller', 'name_prefix', + 'resource', 'type', 'prefix', 'path', 'host', 'schemes', 'methods', 'defaults', 'requirements', 'options', 'condition', 'controller', 'name_prefix', 'redirect_to', 'redirect_permanent', ); private $yamlParser; @@ -120,6 +120,15 @@ protected function parseRoute(RouteCollection $collection, $name, array $config, $defaults['_controller'] = $config['controller']; } + if (isset($config['redirect_to'])) { + if (isset($defaults['_controller'])) { + throw new \InvalidArgumentException(sprintf('The routing file "%s" must not specify both a controller and a redirection.', $path)); + } + + $defaults['_redirect_to'] = $config['redirect_to']; + $defaults['_redirect_permanent'] = $config['redirect_permanent'] ?? false; + } + $route = new Route($config['path'], $defaults, $requirements, $options, $host, $schemes, $methods, $condition); $collection->add($name, $route); diff --git a/src/Symfony/Component/Routing/Loader/schema/routing/routing-1.0.xsd b/src/Symfony/Component/Routing/Loader/schema/routing/routing-1.0.xsd index fd461154dfe44..01f1c427980c2 100644 --- a/src/Symfony/Component/Routing/Loader/schema/routing/routing-1.0.xsd +++ b/src/Symfony/Component/Routing/Loader/schema/routing/routing-1.0.xsd @@ -42,6 +42,8 @@ + + diff --git a/src/Symfony/Component/Routing/Tests/Fixtures/redirect_to/redirect_with_controller.xml b/src/Symfony/Component/Routing/Tests/Fixtures/redirect_to/redirect_with_controller.xml new file mode 100644 index 0000000000000..01557a469592c --- /dev/null +++ b/src/Symfony/Component/Routing/Tests/Fixtures/redirect_to/redirect_with_controller.xml @@ -0,0 +1,8 @@ + + + + + diff --git a/src/Symfony/Component/Routing/Tests/Fixtures/redirect_to/redirect_with_controller.yml b/src/Symfony/Component/Routing/Tests/Fixtures/redirect_to/redirect_with_controller.yml new file mode 100644 index 0000000000000..f1047394f9564 --- /dev/null +++ b/src/Symfony/Component/Routing/Tests/Fixtures/redirect_to/redirect_with_controller.yml @@ -0,0 +1,4 @@ +app_symfony: + path: /symfony + controller: App\Controller\Symfony + redirect_to: https://symfony.com diff --git a/src/Symfony/Component/Routing/Tests/Fixtures/redirect_to/routing.xml b/src/Symfony/Component/Routing/Tests/Fixtures/redirect_to/routing.xml new file mode 100644 index 0000000000000..880fff4c3bad9 --- /dev/null +++ b/src/Symfony/Component/Routing/Tests/Fixtures/redirect_to/routing.xml @@ -0,0 +1,10 @@ + + + + + + + diff --git a/src/Symfony/Component/Routing/Tests/Fixtures/redirect_to/routing.yml b/src/Symfony/Component/Routing/Tests/Fixtures/redirect_to/routing.yml new file mode 100644 index 0000000000000..ea1fa7f567b86 --- /dev/null +++ b/src/Symfony/Component/Routing/Tests/Fixtures/redirect_to/routing.yml @@ -0,0 +1,12 @@ +app_homepage: + path: / + controller: AppBundle:Homepage:show + +app_temporary_redirect: + path: /home + redirect_to: app_homepage + +app_permanent_redirect: + path: /permanent-home + redirect_to: / + redirect_permanent: true diff --git a/src/Symfony/Component/Routing/Tests/Loader/XmlFileLoaderTest.php b/src/Symfony/Component/Routing/Tests/Loader/XmlFileLoaderTest.php index e5353d7eba292..52efc35452e37 100644 --- a/src/Symfony/Component/Routing/Tests/Loader/XmlFileLoaderTest.php +++ b/src/Symfony/Component/Routing/Tests/Loader/XmlFileLoaderTest.php @@ -372,4 +372,31 @@ public function testImportRouteWithNamePrefix() $this->assertNotNull($routeCollection->get('api_app_blog')); $this->assertEquals('/api/blog', $routeCollection->get('api_app_blog')->getPath()); } + + public function testRedirections() + { + $loader = new XmlFileLoader(new FileLocator(array(__DIR__.'/../Fixtures/redirect_to'))); + $routeCollection = $loader->load('routing.xml'); + + $route = $routeCollection->get('app_homepage'); + $this->assertSame('AppBundle:Homepage:show', $route->getDefault('_controller')); + + $route = $routeCollection->get('app_temporary_redirect'); + $this->assertSame('app_homepage', $route->getDefault('_redirect_to')); + $this->assertFalse($route->getDefault('_redirect_permanent')); + + $route = $routeCollection->get('app_permanent_redirect'); + $this->assertSame('/', $route->getDefault('_redirect_to')); + $this->assertTrue($route->getDefault('_redirect_permanent')); + } + + /** + * @expectedException \InvalidArgumentException + * @expectedExceptionMessageRegExp /The routing file "[^"]*" must not specify both a controller and a redirection\./ + */ + public function testRedirectionCannotBeUsedWithController() + { + $loader = new XmlFileLoader(new FileLocator(array(__DIR__.'/../Fixtures/redirect_to'))); + $loader->load('redirect_with_controller.xml'); + } } diff --git a/src/Symfony/Component/Routing/Tests/Loader/YamlFileLoaderTest.php b/src/Symfony/Component/Routing/Tests/Loader/YamlFileLoaderTest.php index 5fa38f39d05a6..194e76db45ccb 100644 --- a/src/Symfony/Component/Routing/Tests/Loader/YamlFileLoaderTest.php +++ b/src/Symfony/Component/Routing/Tests/Loader/YamlFileLoaderTest.php @@ -193,4 +193,31 @@ public function testImportRouteWithNamePrefix() $this->assertNotNull($routeCollection->get('api_app_blog')); $this->assertEquals('/api/blog', $routeCollection->get('api_app_blog')->getPath()); } + + public function testRedirections() + { + $loader = new YamlFileLoader(new FileLocator(array(__DIR__.'/../Fixtures/redirect_to'))); + $routeCollection = $loader->load('routing.yml'); + + $route = $routeCollection->get('app_homepage'); + $this->assertSame('AppBundle:Homepage:show', $route->getDefault('_controller')); + + $route = $routeCollection->get('app_temporary_redirect'); + $this->assertSame('app_homepage', $route->getDefault('_redirect_to')); + $this->assertFalse($route->getDefault('_redirect_permanent')); + + $route = $routeCollection->get('app_permanent_redirect'); + $this->assertSame('/', $route->getDefault('_redirect_to')); + $this->assertTrue($route->getDefault('_redirect_permanent')); + } + + /** + * @expectedException \InvalidArgumentException + * @expectedExceptionMessageRegExp /The routing file "[^"]*" must not specify both a controller and a redirection\./ + */ + public function testRedirectionCannotBeUsedWithController() + { + $loader = new YamlFileLoader(new FileLocator(array(__DIR__.'/../Fixtures/redirect_to'))); + $loader->load('redirect_with_controller.yml'); + } }