Browse Source

[Routing] added support for _scheme requirement

The _scheme requirement can be used to force routes to always match one given scheme
and to always be generated with the given scheme.

So, if _scheme is set to https, URL generation will force an absolute URL if the
current scheme is http. And if you request the URL with http, you will be redirected
to the https URL.
Fabien Potencier 14 years ago
parent
commit
07aae98495

+ 26 - 6
src/Symfony/Bundle/FrameworkBundle/Controller/RedirectController.php

@@ -51,23 +51,43 @@ class RedirectController extends ContainerAware
     /**
      * Redirects to a URL.
      *
-     * It expects a url path parameter.
      * By default, the response status code is 301.
      *
-     * If the url is empty, the status code will be 410.
-     * If the permanent path parameter is set, the status code will be 302.
+     * If the path is empty, the status code will be 410.
+     * If the permanent flag is set, the status code will be 302.
      *
-     * @param string  $url       The url to redirect to
+     * @param string  $path      The path to redirect to
      * @param Boolean $permanent Whether the redirect is permanent or not
+     * @param Boolean $scheme    The URL scheme (null to keep the current one)
+     * @param integer $httpPort  The HTTP port
+     * @param integer $httpsPort The HTTPS port
      *
      * @return Response A Response instance
      */
-    public function urlRedirectAction($url, $permanent = false)
+    public function urlRedirectAction($path, $permanent = false, $scheme = null, $httpPort = 80, $httpsPort = 443)
     {
-        if (!$url) {
+        if (!$path) {
             return new Response(null, 410);
         }
 
+        $request = $this->container->get('request');
+        if (null === $scheme) {
+            $scheme = $request->getScheme();
+        }
+        $qs = $request->getQueryString();
+        if ($qs) {
+            $qs = '?'.$qs;
+        }
+
+        $port = '';
+        if ('http' === $scheme && 80 != $httpPort) {
+            $port = ':'.$httpPort;
+        } elseif ('https' === $scheme && 443 != $httpPort) {
+            $port = ':'.$httpsPort;
+        }
+
+        $url = $scheme.'://'.$request->getHttpHost().$port.$request->getBaseUrl().$path.$qs;
+
         return new RedirectResponse($url, $permanent ? 301 : 302);
     }
 }

+ 2 - 0
src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Configuration.php

@@ -131,6 +131,8 @@ class Configuration implements ConfigurationInterface
                         ->scalarNode('cache_warmer')->defaultFalse()->end()
                         ->scalarNode('resource')->isRequired()->end()
                         ->scalarNode('type')->end()
+                        ->scalarNode('http_port')->defaultValue(80)->end()
+                        ->scalarNode('https_port')->defaultValue(443)->end()
                     ->end()
                 ->end()
             ->end()

+ 4 - 0
src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php

@@ -256,6 +256,10 @@ class FrameworkExtension extends Extension
             $container->setAlias('router', 'router.cached');
         }
 
+        $def = $container->getDefinition('request_listener');
+        $def->setArgument(2, $config['http_port']);
+        $def->setArgument(3, $config['https_port']);
+
         $this->addClassesToCompile(array(
             'Symfony\\Component\\Routing\\RouterInterface',
             'Symfony\\Component\\Routing\\Matcher\\UrlMatcherInterface',

+ 14 - 9
src/Symfony/Bundle/FrameworkBundle/RequestListener.php

@@ -29,14 +29,18 @@ use Symfony\Component\Routing\RouterInterface;
  */
 class RequestListener
 {
-    protected $router;
-    protected $logger;
-    protected $container;
+    private $router;
+    private $logger;
+    private $container;
+    private $httpPort;
+    private $httpsPort;
 
-    public function __construct(ContainerInterface $container, RouterInterface $router, LoggerInterface $logger = null)
+    public function __construct(ContainerInterface $container, RouterInterface $router, $httpPort = 80, $httpsPort = 443, LoggerInterface $logger = null)
     {
         $this->container = $container;
         $this->router = $router;
+        $this->httpPort = $httpPort;
+        $this->httpsPort = $httpsPort;
         $this->logger = $logger;
     }
 
@@ -73,11 +77,12 @@ class RequestListener
             // set the context even if the parsing does not need to be done
             // to have correct link generation
             $this->router->setContext(array(
-                'base_url'  => $request->getBaseUrl(),
-                'method'    => $request->getMethod(),
-                'host'      => $request->getHost(),
-                'port'      => $request->getPort(),
-                'is_secure' => $request->isSecure(),
+                'base_url'   => $request->getBaseUrl(),
+                'method'     => $request->getMethod(),
+                'host'       => $request->getHost(),
+                'scheme'     => $request->getScheme(),
+                'http_port'  => $this->httpPort,
+                'https_port' => $this->httpsPort,
             ));
         }
 

+ 2 - 0
src/Symfony/Bundle/FrameworkBundle/Resources/config/web.xml

@@ -31,6 +31,8 @@
             <tag name="monolog.logger" channel="request" />
             <argument type="service" id="service_container" />
             <argument type="service" id="router" />
+            <argument /> <!-- HTTP port -->
+            <argument /> <!-- HTTPS port -->
             <argument type="service" id="logger" on-invalid="ignore" />
         </service>
 

+ 14 - 2
src/Symfony/Bundle/FrameworkBundle/Routing/RedirectableUrlMatcher.php

@@ -19,12 +19,24 @@ use Symfony\Component\Routing\Matcher\RedirectableUrlMatcherInterface;
  */
 class RedirectableUrlMatcher extends UrlMatcher implements RedirectableUrlMatcherInterface
 {
-    public function redirect($pathinfo, $route)
+    /**
+     * Redirects the user to another URL.
+     *
+     * @param string  $path   The path info to redirect to.
+     * @param string  $route  The route that matched
+     * @param string  $scheme The URL scheme (null to keep the current one)
+     *
+     * @return array An array of parameters
+     */
+    public function redirect($path, $route, $scheme = null)
     {
         return array(
             '_controller' => 'Symfony\\Bundle\\FrameworkBundle\\Controller\\RedirectController::urlRedirectAction',
-            'url'         => $this->context['base_url'].$pathinfo,
+            'path'        => $path,
             'permanent'   => true,
+            'scheme'      => $scheme,
+            'httpPort'    => isset($this->context['http_port']) ? $this->context['http_port'] : 80,
+            'httpsPort'   => isset($this->context['https_port']) ? $this->context['https_port'] : 443,
             '_route'      => $route,
         );
     }

+ 16 - 7
src/Symfony/Component/Routing/Generator/UrlGenerator.php

@@ -129,14 +129,23 @@ class UrlGenerator implements UrlGeneratorInterface
 
         $url = (isset($this->context['base_url']) ? $this->context['base_url'] : '').$url;
 
-        if ($absolute && isset($this->context['host'])) {
-            $isSecure = (isset($this->context['is_secure']) && $this->context['is_secure']);
-            $port = isset($this->context['port']) ? $this->context['port'] : 80;
-            $urlBeginning = 'http'.($isSecure ? 's' : '').'://'.$this->context['host'];
-            if (($isSecure && $port != 443) || (!$isSecure && $port != 80)) {
-                $urlBeginning .= ':'.$port;
+        if (isset($this->context['host'])) {
+            $scheme = isset($this->context['scheme']) ? $this->context['scheme'] : 'http';
+            if (isset($requirements['_scheme']) && ($req = strtolower($requirements['_scheme'])) && $scheme != $req) {
+                $absolute = true;
+                $scheme = $req;
+            }
+
+            if ($absolute) {
+                $port = '';
+                if ('http' === $scheme && 80 != ($httpPort = isset($this->context['http_port']) ? $this->context['http_port'] : 80)) {
+                    $port = ':'.$httpPort;
+                } elseif ('https' === $scheme && 443 != ($httpsPort = isset($this->context['https_port']) ? $this->context['https_port'] : 443)) {
+                    $port = ':'.$httpsPort;
+                }
+
+                $url = $scheme.'://'.$this->context['host'].$port.$url;
             }
-            $url = $urlBeginning.$url;
         }
 
         return $url;

+ 18 - 5
src/Symfony/Component/Routing/Matcher/Dumper/PhpMatcherDumper.php

@@ -41,17 +41,17 @@ class PhpMatcherDumper extends MatcherDumper
 
         // trailing slash support is only enabled if we know how to redirect the user
         $interfaces = class_implements($options['base_class']);
-        $supportsTrailingSlash = isset($interfaces['Symfony\Component\Routing\Matcher\RedirectableUrlMatcherInterface']);
+        $supportsRedirections = isset($interfaces['Symfony\Component\Routing\Matcher\RedirectableUrlMatcherInterface']);
 
         return
             $this->startClass($options['class'], $options['base_class']).
             $this->addConstructor().
-            $this->addMatcher($supportsTrailingSlash).
+            $this->addMatcher($supportsRedirections).
             $this->endClass()
         ;
     }
 
-    private function addMatcher($supportsTrailingSlash)
+    private function addMatcher($supportsRedirections)
     {
         $code = array();
 
@@ -61,7 +61,7 @@ class PhpMatcherDumper extends MatcherDumper
             $hasTrailingSlash = false;
             $matches = false;
             if (!count($compiledRoute->getVariables()) && false !== preg_match('#^(.)\^(?P<url>.*?)\$\1#', $compiledRoute->getRegex(), $m)) {
-                if ($supportsTrailingSlash && substr($m['url'], -1) === '/') {
+                if ($supportsRedirections && substr($m['url'], -1) === '/') {
                     $conditions[] = sprintf("rtrim(\$pathinfo, '/') === '%s'", rtrim(str_replace('\\', '', $m['url']), '/'));
                     $hasTrailingSlash = true;
                 } else {
@@ -73,7 +73,7 @@ class PhpMatcherDumper extends MatcherDumper
                 }
 
                 $regex = $compiledRoute->getRegex();
-                if ($supportsTrailingSlash && $pos = strpos($regex, '/$')) {
+                if ($supportsRedirections && $pos = strpos($regex, '/$')) {
                     $regex = substr($regex, 0, $pos).'/?$'.substr($regex, $pos + 2);
                     $hasTrailingSlash = true;
                 }
@@ -110,6 +110,19 @@ EOF
                 , $name);
             }
 
+            if ($scheme = $route->getRequirement('_scheme')) {
+                if (!$supportsRedirections) {
+                    throw new \LogicException('The "_scheme" requirement is only supported for route dumper that implements RedirectableUrlMatcherInterface.');
+                }
+
+                $code[] = sprintf(<<<EOF
+            if (isset(\$this->context['scheme']) && \$this->context['scheme'] !== '$scheme') {
+                return \$this->redirect(\$pathinfo, '%s', '$scheme');
+            }
+EOF
+                , $name);
+            }
+
             // optimize parameters array
             if (true === $matches && $compiledRoute->getDefaults()) {
                 $code[] = sprintf("            return array_merge(\$this->mergeDefaults(\$matches, %s), array('_route' => '%s'));"

+ 4 - 10
src/Symfony/Component/Routing/Matcher/RedirectableUrlMatcherInterface.php

@@ -21,17 +21,11 @@ interface RedirectableUrlMatcherInterface
     /**
      * Redirects the user to another URL.
      *
-     * As the Routing component does not know know to redirect the user,
-     * the default implementation throw an exception.
-     *
-     * Override this method to implement your own logic.
-     *
-     * If you are using a Dumper, don't forget to change the default base.
-     *
-     * @param string $pathinfo The path info to redirect to.
-     * @param string $route    The route that matched
+     * @param string  $path   The path info to redirect to.
+     * @param string  $route  The route that matched
+     * @param string  $scheme The URL scheme (null to keep the current one)
      *
      * @return array An array of parameters
      */
-    function redirect($pathinfo, $route);
+    function redirect($path, $route, $scheme = null);
 }

+ 3 - 2
tests/Symfony/Tests/Component/Routing/Fixtures/RedirectableUrlMatcher.php

@@ -19,11 +19,12 @@ use Symfony\Component\Routing\Matcher\RedirectableUrlMatcherInterface;
  */
 class RedirectableUrlMatcher extends UrlMatcher implements RedirectableUrlMatcherInterface
 {
-    public function redirect($pathinfo, $route)
+    public function redirect($path, $route, $scheme = null)
     {
         return array(
             '_controller' => 'Some controller reference...',
-            'url'         => $this->context['base_url'].$pathinfo,
+            'path'        => $path,
+            'scheme'      => $scheme,
         );
     }
 }

+ 16 - 0
tests/Symfony/Tests/Component/Routing/Fixtures/dumper/url_matcher2.php

@@ -100,6 +100,22 @@ class ProjectUrlMatcher extends Symfony\Tests\Component\Routing\Fixtures\Redirec
             return array (  'def' => 'test',  '_route' => 'foofoo',);
         }
 
+        // secure
+        if ($pathinfo === '/secure') {
+            if (isset($this->context['scheme']) && $this->context['scheme'] !== 'https') {
+                return $this->redirect($pathinfo, 'secure', 'https');
+            }
+            return array('_route' => 'secure');
+        }
+
+        // nonsecure
+        if ($pathinfo === '/nonsecure') {
+            if (isset($this->context['scheme']) && $this->context['scheme'] !== 'http') {
+                return $this->redirect($pathinfo, 'nonsecure', 'http');
+            }
+            return array('_route' => 'nonsecure');
+        }
+
         throw 0 < count($allow) ? new MethodNotAllowedException(array_unique($allow)) : new NotFoundException();
     }
 }

+ 2 - 4
tests/Symfony/Tests/Component/Routing/Generator/Dumper/PhpGeneratorDumperTest.php

@@ -61,8 +61,7 @@ class PhpGeneratorDumperTest extends \PHPUnit_Framework_TestCase
             'base_url' => '/app.php',
             'method' => 'GET',
             'host' => 'localhost',
-            'port' => 80,
-            'is_secure' => false
+            'scheme' => 'http',
         ));
 
         $absoluteUrlWithParameter    = $projectUrlGenerator->generate('Test', array('foo' => 'bar'), true);
@@ -88,8 +87,7 @@ class PhpGeneratorDumperTest extends \PHPUnit_Framework_TestCase
             'base_url' => '/app.php',
             'method' => 'GET',
             'host' => 'localhost',
-            'port' => 80,
-            'is_secure' => false
+            'scheme' => 'http',
         ));
 
         $projectUrlGenerator->generate('Test', array());

+ 24 - 5
tests/Symfony/Tests/Component/Routing/Generator/UrlGeneratorTest.php

@@ -28,7 +28,7 @@ class UrlGeneratorTest extends \PHPUnit_Framework_TestCase
     public function testAbsoluteSecureUrlWithPort443()
     {
         $routes = $this->getRoutes('test', new Route('/testing'));
-        $url = $this->getGenerator($routes, array('port' => 443, 'is_secure' => true))->generate('test', array(), true);
+        $url = $this->getGenerator($routes, array('scheme' => 'https'))->generate('test', array(), true);
 
         $this->assertEquals('https://localhost/app.php/testing', $url);
     }
@@ -36,7 +36,7 @@ class UrlGeneratorTest extends \PHPUnit_Framework_TestCase
     public function testAbsoluteUrlWithNonStandardPort()
     {
         $routes = $this->getRoutes('test', new Route('/testing'));
-        $url = $this->getGenerator($routes, array('port' => 8080))->generate('test', array(), true);
+        $url = $this->getGenerator($routes, array('http_port' => 8080))->generate('test', array(), true);
 
         $this->assertEquals('http://localhost:8080/app.php/testing', $url);
     }
@@ -44,7 +44,7 @@ class UrlGeneratorTest extends \PHPUnit_Framework_TestCase
     public function testAbsoluteSecureUrlWithNonStandardPort()
     {
         $routes = $this->getRoutes('test', new Route('/testing'));
-        $url = $this->getGenerator($routes, array('port' => 8080, 'is_secure' => true))->generate('test', array(), true);
+        $url = $this->getGenerator($routes, array('https_port' => 8080, 'scheme' => 'https'))->generate('test', array(), true);
 
         $this->assertEquals('https://localhost:8080/app.php/testing', $url);
     }
@@ -125,6 +125,24 @@ class UrlGeneratorTest extends \PHPUnit_Framework_TestCase
         $this->getGenerator($routes)->generate('test', array('foo' => 'bar'), true);
     }
 
+    public function testSchemeRequirementDoesNothingIfSameCurrentScheme()
+    {
+        $routes = $this->getRoutes('test', new Route('/', array(), array('_scheme' => 'http')));
+        $this->assertEquals('/app.php/', $this->getGenerator($routes)->generate('test'));
+
+        $routes = $this->getRoutes('test', new Route('/', array(), array('_scheme' => 'https')));
+        $this->assertEquals('/app.php/', $this->getGenerator($routes, array('scheme' => 'https'))->generate('test'));
+    }
+
+    public function testSchemeRequirementForcesAbsoluteUrl()
+    {
+        $routes = $this->getRoutes('test', new Route('/', array(), array('_scheme' => 'https')));
+        $this->assertEquals('https://localhost/app.php/', $this->getGenerator($routes)->generate('test'));
+
+        $routes = $this->getRoutes('test', new Route('/', array(), array('_scheme' => 'http')));
+        $this->assertEquals('http://localhost/app.php/', $this->getGenerator($routes, array('scheme' => 'https'))->generate('test'));
+    }
+
     protected function getGenerator(RouteCollection $routes, array $context = array())
     {
         $generator = new UrlGenerator($routes);
@@ -132,8 +150,9 @@ class UrlGeneratorTest extends \PHPUnit_Framework_TestCase
             'base_url' => '/app.php',
             'method' => 'GET',
             'host' => 'localhost',
-            'port' => 80,
-            'is_secure' => false,
+            'http_port' => 80,
+            'https_port' => 443,
+            'scheme' => 'http',
         ), $context));
 
         return $generator;

+ 32 - 9
tests/Symfony/Tests/Component/Routing/Matcher/Dumper/PhpMatcherDumperTest.php

@@ -17,13 +17,6 @@ use Symfony\Component\Routing\RouteCollection;
 
 class PhpMatcherDumperTest extends \PHPUnit_Framework_TestCase
 {
-    static protected $fixturesPath;
-
-    static public function setUpBeforeClass()
-    {
-        self::$fixturesPath = realpath(__DIR__.'/../../Fixtures/');
-    }
-
     public function testDump()
     {
         $collection = new RouteCollection();
@@ -75,7 +68,37 @@ class PhpMatcherDumperTest extends \PHPUnit_Framework_TestCase
         ));
 
         $dumper = new PhpMatcherDumper($collection);
-        $this->assertStringEqualsFile(self::$fixturesPath.'/dumper/url_matcher1.php', $dumper->dump(), '->dump() dumps basic routes to the correct PHP file.');
-        $this->assertStringEqualsFile(self::$fixturesPath.'/dumper/url_matcher2.php', $dumper->dump(array('base_class' => 'Symfony\Tests\Component\Routing\Fixtures\RedirectableUrlMatcher')), '->dump() dumps basic routes to the correct PHP file.');
+        $this->assertStringEqualsFile(__DIR__.'/../../Fixtures/dumper/url_matcher1.php', $dumper->dump(), '->dump() dumps basic routes to the correct PHP file.');
+
+        // force HTTPS redirection
+        $collection->add('secure', new Route(
+            '/secure',
+            array(),
+            array('_scheme' => 'https')
+        ));
+
+        // force HTTP redirection
+        $collection->add('nonsecure', new Route(
+            '/nonsecure',
+            array(),
+            array('_scheme' => 'http')
+        ));
+
+        $this->assertStringEqualsFile(__DIR__.'/../../Fixtures/dumper/url_matcher2.php', $dumper->dump(array('base_class' => 'Symfony\Tests\Component\Routing\Fixtures\RedirectableUrlMatcher')), '->dump() dumps basic routes to the correct PHP file.');
+    }
+
+    /**
+     * @expectedException \LogicException
+     */
+    public function testDumpWhenSchemeIsUsedWithoutAProperDumper()
+    {
+        $collection = new RouteCollection();
+        $collection->add('secure', new Route(
+            '/secure',
+            array(),
+            array('_scheme' => 'https')
+        ));
+        $dumper = new PhpMatcherDumper($collection);
+        $dumper->dump();
     }
 }