8000 [Routing] added support for hostname requirement to routes by gunnarlium · Pull Request #3057 · symfony/symfony · GitHub
[go: up one dir, main page]

Skip to content

[Routing] added support for hostname requirement to routes #3057

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
<?php

/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

namespace Symfony\Component\Routing\Exception;

/**
* The resource was found but the hostname is not allowed.
*
* This exception should trigger an HTTP 404 response in your application code.
*
* @author Gunnar Lium <post@gunnarlium.com>
*
*/
class HostnameNotAllowedException extends \RuntimeException implements ExceptionInterface
{
protected $allowedHostnames;

public function __construct(array $allowedHostnames, $message = null, $code = 0, \Exception $previous = null)
{
$this->allowedHostnames = array_map('strtolower', $allowedHostnames);

parent::__construct($message, $code, $previous);
}

public function getAllowedHostnames()
{
return $this->allowedHostnames;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
<?php

/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

namespace Symfony\Component\Routing\Exception;

/**
* Exception thrown when hostname parameter doesn't match hostname from requirements
*
* @author Gunnar Lium <post@gunnarlium.com>
*
* @api
*/
class InvalidHostnameParameterException extends \InvalidArgumentException implements ExceptionInterface
{
}
27 changes: 26 additions & 1 deletion src/Symfony/Component/Routing/Generator/UrlGenerator.php
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
use Symfony\Component\Routing\RouteCollection;
use Symfony\Component\Routing\RequestContext;
use Symfony\Component\Routing\Exception\InvalidParameterException;
use Symfony\Component\Routing\Exception\InvalidHostnameParameterException;
use Symfony\Component\Routing\Exception\RouteNotFoundException;
use Symfony\Component\Routing\Exception\MissingMandatoryParametersException;

Expand Down Expand Up @@ -99,6 +100,11 @@ protected function doGenerate($variables, $defaults, $requirements, $tokens, $pa
{
$variables = array_flip($variables);

$preferredHost = null;
if (isset($parameters['_host'])) {
$preferredHost = $parameters['_host'];
unset($parameters['_host']);
}
$originParameters = $parameters;
$parameters = array_replace($this->context->getParameters(), $parameters);
$tparams = array_replace($defaults, $parameters);
Expand Down Expand Up @@ -151,6 +157,25 @@ protected function doGenerate($variables, $defaults, $requirements, $tokens, $pa
$scheme = $req;
}

$host = $this->context->getHost();
if (isset($requirements['_host']) && ($req = strtolower($requirements['_host'])) && $host != $req) {
$hosts = explode('|', $req);
$absolute = true;
if (1 === count($hosts)) {
$host = $req;
} else {
if ($preferredHost) {
if (in_array($preferredHost, $hosts)) {
$host = $preferredHost;
} else {
throw new InvalidHostnameParameterException(sprintf('Preferred hostname for route "%s" must match "%s" ("%s" given).', $name, $req, $preferredHost));
}
} elseif (!in_array($host, $hosts)) {
8000 $host = $hosts[0];
}
}
}

if ($absolute) {
$port = '';
if ('http' === $scheme && 80 != $this->context->getHttpPort()) {
Expand All @@ -159,7 +184,7 @@ protected function doGenerate($variables, $defaults, $requirements, $tokens, $pa
$port = ':'.$this->context->getHttpsPort();
}

$url = $scheme.'://'.$this->context->getHost().$port.$url;
$url = $scheme.'://'.$host.$port.$url;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is not enough. If the current host (the one in the context) does not match the requirement, the url should be generated as absolute even if the call to generate did not set the absolute parameter to true

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do you agree that it makes sense that routes with host requirement always are generated as absolute? If so, I believe adding $absolute = true at line 105 should do the trick.

8000 Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

no, they should be generated as absolute only if the host needs to be changed. if the current host already match the requirement, the control should be left to the developer if he doesn't want an absolute url

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

oups sorry, I missed the line 163 which already implements it

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Right, missed it myself too ;)

}
}

Expand Down
34 changes: 31 additions & 3 deletions src/Symfony/Component/Routing/Matcher/Dumper/PhpMatcherDumper.php
Original file line number Diff line number Diff line change
Expand Up @@ -62,11 +62,14 @@ private function addMatcher($supportsRedirections)
public function match(\$pathinfo)
{
\$allow = array();
\$hosts = array();
\$pathinfo = urldecode(\$pathinfo);

$code

throw 0 < count(\$allow) ? new MethodNotAllowedException(array_unique(\$allow)) : new ResourceNotFoundException();
throw 0 < count(\$allow) ? new MethodNotAllowedException(array_unique(\$allow)) :
0 < count(\$hosts) ? new HostnameNotAllowedException(array_unique(\$hosts)) :
new ResourceNotFoundException();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

missing ( ) around the second condition.

}

EOF;
Expand Down Expand Up @@ -148,14 +151,17 @@ private function compileRoute(Route $route, $name, $supportsRedirections, $paren
$hasTrailingSlash = false;
$matches = false;
$methods = array();

$hostnames = array();
if ($req = $route->getRequirement('_method')) {
$methods = explode('|', strtoupper($req));
// GET and HEAD are equivalent
if (in_array('GET', $methods) && !in_array('HEAD', $methods)) {
$methods[] = 'HEAD';
}
}
if ($req = $route->getRequirement('_host')) {
$hostnames = explode('|', strtolower($req));
}

$supportsTrailingSlash = $supportsRedirections && (!$methods || in_array('HEAD', $methods));

Expand Down Expand Up @@ -208,6 +214,27 @@ private function compileRoute(Route $route, $name, $supportsRedirections, $paren
goto $gotoname;
}

EOF;
}
}

if ($hostnames) {
$gotoname = 'not_'.preg_replace('/[^A-Za-z0-9_]/', '', $name);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this one is duplicated now. you could use this above the corresponding code generation

if ($methods || $hostnames) {
    $gotoname = 'not_'.preg_replace('/[^A-Za-z0-9_]/', '', $name);
}

if (1 === count($hostnames)) {
$code .= <<<EOF
if (\$this->context->getHost() != '$hostnames[0]') {
\$hosts[] = '$hostnames[0]';
goto $gotoname;
}
EOF;
} else {
$hostnames = implode('\', \'', $hostnames);
$code .= <<<EOF
if (!in_array(\$this->context->getHost(), array('$hostnames'))) {
\$hosts = array_merge(\$hosts, array('$hostnames'));
goto $gotoname;
}

EOF;
}
}
Expand Down Expand Up @@ -248,7 +275,7 @@ private function compileRoute(Route $route, $name, $supportsRedirections, $paren
}
$code .= " }\n";

if ($methods) {
if ($methods || $hostnames) {
$code .= " $gotoname:\n";
}

Expand All @@ -261,6 +288,7 @@ private function startClass($class, $baseClass)
<?php

use Symfony\Component\Routing\Exception\MethodNotAllowedException;
use Symfony\Component\Routing\Exception\HostnameNotAllowedException;
use Symfony\Component\Routing\Exception\ResourceNotFoundException;
use Symfony\Component\Routing\RequestContext;

Expand Down
22 changes: 19 additions & 3 deletions src/Symfony/Component/Routing/Matcher/UrlMatcher.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
namespace Symfony\Component\Routing\Matcher;

use Symfony\Component\Routing\Exception\MethodNotAllowedException;
use Symfony\Component\Routing\Exception\HostnameNotAllowedException;
use Symfony\Component\Routing\Exception\ResourceNotFoundException;
use Symfony\Component\Routing\RouteCollection;
use Symfony\Component\Routing\RequestContext;
Expand Down Expand Up @@ -79,14 +80,19 @@ public function getContext()
public function match($pathinfo)
{
$this->allow = array();
$this->hosts = array();

if ($ret = $this->matchCollection(urldecode($pathinfo), $this->routes)) {
return $ret;
}

throw 0 < count($this->allow)
? new MethodNotAllowedException(array_unique(array_map('strtoupper', $this->allow)))
: new ResourceNotFoundException();
if (0 < count($this->allow)) {
throw new MethodNotAllowedException(array_unique(array_map('strtoupper', $this->allow)));
}
if (0 < count($this->hosts)) {
throw new HostnameNotAllowedException(array_unique(array_map('strtolower', $this->hosts)));
}
throw new ResourceNotFoundException();
}

/**
Expand Down Expand Up @@ -150,6 +156,16 @@ protected function matchCollection($pathinfo, RouteCollection $routes)
continue;
}

// check hostname requirement
if ($req = $route->getRequirement('_host')) {
$host = $this->context->getHost();
if (!in_array($host, $req = explode('|', $req))) {
$this->hosts = array_merge($this->hosts, $req);

continue;
}
}

return array_merge($this->mergeDefaults($matches, $route->getDefaults()), array('_route' => $name));
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
<?php

use Symfony\Component\Routing\Exception\MethodNotAllowedException;
use Symfony\Component\Routing\Exception\HostnameNotAllowedException;
use Symfony\Component\Routing\Exception\ResourceNotFoundException;
use Symfony\Component\Routing\RequestContext;

Expand All @@ -23,6 +24,7 @@ public function __construct(RequestContext $context)
public function match($pathinfo)
{
$allow = array();
$hosts = array();
$pathinfo = urldecode($pathinfo);

// foo
Expand Down Expand Up @@ -52,6 +54,17 @@ public function match($pathinfo)
}
not_barhead:

// barhost
if (0 === strpos($pathinfo, '/barhost') && preg_match('#^/barhost/(?P<foo>[^/]+?)$#xs', $pathinfo, $matches)) {
if (!in_array($this->context->getHost(), array('symfony.com', 'symfony.org'))) {
$hosts = array_merge($hosts, array('symfony.com', 'symfony.org'));
goto not_barhost;
}
$matches['_route'] = 'barhost';
return $matches;
}
not_barhost:

// baz
if ($pathinfo === '/test/baz') {
return array('_route' => 'baz');
Expand Down Expand Up @@ -162,6 +175,8 @@ public function match($pathinfo)
return $matches;
}

throw 0 < count($allow) ? new MethodNotAllowedException(array_unique($allow)) : new ResourceNotFoundException();
throw 0 < count($allow) ? new MethodNotAllowedException(array_unique($allow)) :
0 < count($hosts) ? new HostnameNotAllowedException(array_unique($hosts)) :
new ResourceNotFoundException();
}
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
<?php

use Symfony\Component\Routing\Exception\MethodNotAllowedException;
use Symfony\Component\Routing\Exception\HostnameNotAllowedException;
use Symfony\Component\Routing\Exception\ResourceNotFoundException;
use Symfony\Component\Routing\RequestContext;

Expand All @@ -23,6 +24,7 @@ public function __construct(RequestContext $context)
public function match($pathinfo)
{
$allow = array();
$hosts = array();
$pathinfo = urldecode($pathinfo);

// foo
Expand Down Expand Up @@ -52,6 +54,17 @@ public function match($pathinfo)
}
not_barhead:

// barhost
if (0 === strpos($pathinfo, '/barhost') && preg_match('#^/barhost/(?P<foo>[^/]+?)$#xs', $pathinfo, $matches)) {
if (!in_array($this->context->getHost(), array('symfony.com', 'symfony.org'))) {
$hosts = array_merge($hosts, array('symfony.com', 'symfony.org'));
goto not_barhost;
}
$matches['_route'] = 'barhost';
return $matches;
}
not_barhost:

// baz
if ($pathinfo === '/test/baz') {
return array('_route' => 'baz');
Expand Down Expand Up @@ -184,6 +197,8 @@ public function match($pathinfo)
return array('_route' => 'nonsecure');
}

throw 0 < count($allow) ? new MethodNotAllowedException(array_unique($allow)) : new ResourceNotFoundException();
throw 0 < count($allow) ? new MethodNotAllowedException(array_unique($allow)) :
0 < count($hosts) ? new HostnameNotAllowedException(array_unique($hosts)) :
new ResourceNotFoundException();
}
}
32 changes: 32 additions & 0 deletions src/Symfony/Component/Routing/Tests/Generator/UrlGeneratorTest.php
F438
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,38 @@ public function testAbsoluteSecureUrlWithNonStandardPort()
$this->assertEquals('https://localhost:8080/app.php/testing', $url);
}

public function testAbsoluteUrlWithOneHostnameAddsHostname()
{
$routes = $this->getRoutes('test', new Route('/hostname', array(), array('_host' => 'symfony.com')));
$url = $this->getGenerator($routes)->generate('test', array(), true);

$this->assertEquals('http://symfony.com/app.php/hostname', $url);
}

public function testAbsoluteUrlWithMultipleHostnamesPicksFirstHostnameIfHostnameNotInContext()
{
$routes = $this->getRoutes('test', new Route('/hostname', array(), array('_host' => 'symfony.com|symfony.org')));
$url = $this->getGenerator($routes, array('host' => 'symfony.net'))->generate('test', array(), true);

$this->assertEquals('http://symfony.com/app.php/hostname', $url);
}

public function testAbsoluteUrlWithMultipleHostnamesPicksHostnameFromContextIfAvailable()
{
$routes = $this->getRoutes('test', new Route('/hostname', array(), array('_host' => 'symfony.com|symfony.org')));
$url = $this->getGenerator($routes, array('host' => 'symfony.org'))->generate('test', array(), true);

$this->assertEquals('http://symfony.org/app.php/hostname', $url);
}

public function testAbsoluteUrlWithMultipleHostnamesAndSpecifiedHostUsesSpecified()
{
$routes = $this->getRoutes('test', new Route('/hostname', array(), array('_host' => 'symfony.com|symfony.org')));
$url = $this->getGenerator($routes)->generate('test', array('_host' => 'symfony.org'), true);

$this->assertEquals('http://symfony.org/app.php/hostname', $url);
}

public function testRelativeUrlWithoutParameters()
{
$routes = $this->getRoutes('test', new Route('/testing'));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,12 @@ protected function getRouteCollection()
array(),
array('_method' => 'GET')
));
// hostname requirement
$collection->add('barhost', new Route(
'/barhost/{foo}',
array(),
array('_host' => 'symfony.com|symfony.org')
));
// simple
$collection->add('baz', new Route(
'/test/baz'
Expand Down
Loading
0