[Routing] add support for path-relative and scheme-relative URL generation

This commit is contained in:
Tobias Schultze 2012-04-16 10:25:33 +02:00
parent 18c520a5e8
commit 75f59ebe01
13 changed files with 306 additions and 68 deletions

View File

@ -40,14 +40,14 @@ class RoutingExtension extends \Twig_Extension
);
}
public function getPath($name, $parameters = array())
public function getPath($name, $parameters = array(), $relative = false)
{
return $this->generator->generate($name, $parameters, false);
return $this->generator->generate($name, $parameters, $relative ? UrlGeneratorInterface::RELATIVE_PATH : UrlGeneratorInterface::ABSOLUTE_PATH);
}
public function getUrl($name, $parameters = array())
public function getUrl($name, $parameters = array(), $schemeRelative = false)
{
return $this->generator->generate($name, $parameters, true);
return $this->generator->generate($name, $parameters, $schemeRelative ? UrlGeneratorInterface::NETWORK_PATH : UrlGeneratorInterface::ABSOLUTE_URL);
}
/**

View File

@ -11,6 +11,7 @@
namespace Symfony\Bundle\FrameworkBundle\Controller;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpFoundation\RedirectResponse;
use Symfony\Component\HttpFoundation\StreamedResponse;
@ -19,8 +20,8 @@ use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
use Symfony\Component\Form\FormTypeInterface;
use Symfony\Component\Form\Form;
use Symfony\Component\Form\FormBuilder;
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
use Doctrine\Bundle\DoctrineBundle\Registry;
use Symfony\Component\HttpFoundation\Request;
/**
* Controller is a simple implementation of a Controller.
@ -34,15 +35,15 @@ class Controller extends ContainerAware
/**
* Generates a URL from the given parameters.
*
* @param string $route The name of the route
* @param mixed $parameters An array of parameters
* @param Boolean $absolute Whether to generate an absolute URL
* @param string $route The name of the route
* @param mixed $parameters An array of parameters
* @param string $referenceType The type of reference (see UrlGeneratorInterface)
*
* @return string The generated URL
*/
public function generateUrl($route, $parameters = array(), $absolute = false)
public function generateUrl($route, $parameters = array(), $referenceType = UrlGeneratorInterface::ABSOLUTE_PATH)
{
return $this->container->get('router')->generate($route, $parameters, $absolute);
return $this->container->get('router')->generate($route, $parameters, $referenceType);
}
/**

View File

@ -12,8 +12,9 @@
namespace Symfony\Bundle\FrameworkBundle\Controller;
use Symfony\Component\DependencyInjection\ContainerAware;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpFoundation\RedirectResponse;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
/**
* Redirects a request to another URL.
@ -45,7 +46,7 @@ class RedirectController extends ContainerAware
$attributes = $this->container->get('request')->attributes->get('_route_params');
unset($attributes['route'], $attributes['permanent']);
return new RedirectResponse($this->container->get('router')->generate($route, $attributes, true), $permanent ? 301 : 302);
return new RedirectResponse($this->container->get('router')->generate($route, $attributes, UrlGeneratorInterface::ABSOLUTE_URL), $permanent ? 301 : 302);
}
/**

View File

@ -36,15 +36,15 @@ class RouterHelper extends Helper
/**
* Generates a URL from the given parameters.
*
* @param string $name The name of the route
* @param mixed $parameters An array of parameters
* @param Boolean $absolute Whether to generate an absolute URL
* @param string $name The name of the route
* @param mixed $parameters An array of parameters
* @param string $referenceType The type of reference (see UrlGeneratorInterface)
*
* @return string The generated URL
*/
public function generate($name, $parameters = array(), $absolute = false)
public function generate($name, $parameters = array(), $referenceType = UrlGeneratorInterface::ABSOLUTE_PATH)
{
return $this->generator->generate($name, $parameters, $absolute);
return $this->generator->generate($name, $parameters, $referenceType);
}
/**

View File

@ -55,36 +55,40 @@ class LogoutUrlHelper extends Helper
}
/**
* Generate the relative logout URL for the firewall.
* Generates the absolute logout path for the firewall.
*
* @param string $key The firewall key
* @return string The relative logout URL
*
* @return string The logout path
*/
public function getLogoutPath($key)
{
return $this->generateLogoutUrl($key, false);
return $this->generateLogoutUrl($key, UrlGeneratorInterface::ABSOLUTE_PATH);
}
/**
* Generate the absolute logout URL for the firewall.
* Generates the absolute logout URL for the firewall.
*
* @param string $key The firewall key
* @return string The absolute logout URL
*
* @return string The logout URL
*/
public function getLogoutUrl($key)
{
return $this->generateLogoutUrl($key, true);
return $this->generateLogoutUrl($key, UrlGeneratorInterface::ABSOLUTE_URL);
}
/**
* Generate the logout URL for the firewall.
* Generates the logout URL for the firewall.
*
* @param string $key The firewall key
* @param string $referenceType The type of reference (see UrlGeneratorInterface)
*
* @param string $key The firewall key
* @param Boolean $absolute Whether to generate an absolute URL
* @return string The logout URL
*
* @throws \InvalidArgumentException if no LogoutListener is registered for the key
*/
private function generateLogoutUrl($key, $absolute)
private function generateLogoutUrl($key, $referenceType)
{
if (!array_key_exists($key, $this->listeners)) {
throw new \InvalidArgumentException(sprintf('No LogoutListener found for firewall key "%s".', $key));
@ -97,13 +101,13 @@ class LogoutUrlHelper extends Helper
if ('/' === $logoutPath[0]) {
$request = $this->container->get('request');
$url = ($absolute ? $request->getUriForPath($logoutPath) : $request->getBasePath() . $logoutPath);
$url = UrlGeneratorInterface::ABSOLUTE_URL === $referenceType ? $request->getUriForPath($logoutPath) : $request->getBasePath() . $logoutPath;
if (!empty($parameters)) {
$url .= '?' . http_build_query($parameters);
}
} else {
$url = $this->router->generate($logoutPath, $parameters, $absolute);
$url = $this->router->generate($logoutPath, $parameters, $referenceType);
}
return $url;

View File

@ -12,9 +12,10 @@
namespace Symfony\Bundle\SecurityBundle\Tests\Functional\Bundle\FormLoginBundle\Security;
use Symfony\Component\HttpFoundation\RedirectResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
use Symfony\Component\Routing\RouterInterface;
use Symfony\Component\Security\Core\Exception\AuthenticationException;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Security\Http\Authentication\AuthenticationFailureHandlerInterface;
class LocalizedFormFailureHandler implements AuthenticationFailureHandlerInterface
@ -28,6 +29,6 @@ class LocalizedFormFailureHandler implements AuthenticationFailureHandlerInterfa
public function onAuthenticationFailure(Request $request, AuthenticationException $exception)
{
return new RedirectResponse($this->router->generate('localized_login_path', array(), true));
return new RedirectResponse($this->router->generate('localized_login_path', array(), UrlGeneratorInterface::ABSOLUTE_URL));
}
}

View File

@ -108,7 +108,7 @@ EOF;
private function generateGenerateMethod()
{
return <<<EOF
public function generate(\$name, \$parameters = array(), \$absolute = false)
public function generate(\$name, \$parameters = array(), \$referenceType = self::ABSOLUTE_PATH)
{
if (!isset(self::\$declaredRoutes[\$name])) {
throw new RouteNotFoundException(sprintf('Route "%s" does not exist.', \$name));
@ -116,7 +116,7 @@ EOF;
list(\$variables, \$defaults, \$requirements, \$tokens, \$hostnameTokens) = self::\$declaredRoutes[\$name];
return \$this->doGenerate(\$variables, \$defaults, \$requirements, \$tokens, \$parameters, \$name, \$absolute, \$hostnameTokens);
return \$this->doGenerate(\$variables, \$defaults, \$requirements, \$tokens, \$parameters, \$name, \$referenceType, \$hostnameTokens);
}
EOF;
}

View File

@ -127,7 +127,7 @@ class UrlGenerator implements UrlGeneratorInterface, ConfigurableRequirementsInt
/**
* {@inheritDoc}
*/
public function generate($name, $parameters = array(), $absolute = false)
public function generate($name, $parameters = array(), $referenceType = self::ABSOLUTE_PATH)
{
if (null === $route = $this->routes->get($name)) {
throw new RouteNotFoundException(sprintf('Route "%s" does not exist.', $name));
@ -136,15 +136,40 @@ class UrlGenerator implements UrlGeneratorInterface, ConfigurableRequirementsInt
// the Route has a cache of its own and is not recompiled as long as it does not get modified
$compiledRoute = $route->compile();
return $this->doGenerate($compiledRoute->getVariables(), $route->getDefaults(), $route->getRequirements(), $compiledRoute->getTokens(), $parameters, $name, $absolute, $compiledRoute->getHostnameTokens());
return $this->doGenerate($compiledRoute->getVariables(), $route->getDefaults(), $route->getRequirements(), $compiledRoute->getTokens(), $parameters, $name, $referenceType, $compiledRoute->getHostnameTokens());
}
/**
* This method converts the reference type to the new value introduced in Symfony 2.2. It can be used by
* other UrlGenerator implementations to be BC with Symfony 2.1. Reference type was a Boolean called
* $absolute in Symfony 2.1 and only supported two reference types.
*
* @param Boolean $absolute Whether to generate an absolute URL
*
* @return string The new reference type
*
* @deprecated Deprecated since version 2.2, to be removed in 2.3.
*/
public static function convertReferenceType($absolute)
{
if (false === $absolute) {
return self::ABSOLUTE_PATH;
}
if (true === $absolute) {
return self::ABSOLUTE_URL;
}
return $absolute;
}
/**
* @throws MissingMandatoryParametersException When route has some missing mandatory parameters
* @throws InvalidParameterException When a parameter value is not correct
*/
protected function doGenerate($variables, $defaults, $requirements, $tokens, $parameters, $name, $absolute, $hostnameTokens)
protected function doGenerate($variables, $defaults, $requirements, $tokens, $parameters, $name, $referenceType, $hostnameTokens)
{
$referenceType = self::convertReferenceType($referenceType);
$variables = array_flip($variables);
$mergedParams = array_replace($defaults, $this->context->getParameters(), $parameters);
@ -186,8 +211,8 @@ class UrlGenerator implements UrlGeneratorInterface, ConfigurableRequirementsInt
$url = '/';
}
// do not encode the contexts base url as it is already encoded (see Symfony\Component\HttpFoundation\Request)
$url = $this->context->getBaseUrl().strtr(rawurlencode($url), $this->decodedChars);
// the contexts base url is already encoded (see Symfony\Component\HttpFoundation\Request)
$url = strtr(rawurlencode($url), $this->decodedChars);
// the path segments "." and ".." are interpreted as relative reference when resolving a URI; see http://tools.ietf.org/html/rfc3986#section-3.3
// so we need to encode them as they are not used for this purpose here
@ -199,16 +224,11 @@ class UrlGenerator implements UrlGeneratorInterface, ConfigurableRequirementsInt
$url = substr($url, 0, -1) . '%2E';
}
// add a query string if needed
$extra = array_diff_key($parameters, $variables);
if ($extra && $query = http_build_query($extra, '', '&')) {
$url .= '?'.$query;
}
$schemeAuthority = '';
if ($host = $this->context->getHost()) {
$scheme = $this->context->getScheme();
if (isset($requirements['_scheme']) && ($req = strtolower($requirements['_scheme'])) && $scheme != $req) {
$absolute = true;
if (isset($requirements['_scheme']) && ($req = strtolower($requirements['_scheme'])) && $scheme !== $req) {
$referenceType = self::ABSOLUTE_URL;
$scheme = $req;
}
@ -231,18 +251,20 @@ class UrlGenerator implements UrlGeneratorInterface, ConfigurableRequirementsInt
}
$routeHost = $token[1].$mergedParams[$token[3]].$routeHost;
} elseif ('text' === $token[0]) {
} else {
$routeHost = $token[1].$routeHost;
}
}
if ($routeHost != $host) {
if ($routeHost !== $host) {
$host = $routeHost;
$absolute = true;
if (self::ABSOLUTE_URL !== $referenceType) {
$referenceType = self::NETWORK_PATH;
}
}
}
if ($absolute) {
if (self::ABSOLUTE_URL === $referenceType || self::NETWORK_PATH === $referenceType) {
$port = '';
if ('http' === $scheme && 80 != $this->context->getHttpPort()) {
$port = ':'.$this->context->getHttpPort();
@ -250,10 +272,74 @@ class UrlGenerator implements UrlGeneratorInterface, ConfigurableRequirementsInt
$port = ':'.$this->context->getHttpsPort();
}
$url = $scheme.'://'.$host.$port.$url;
$schemeAuthority = self::NETWORK_PATH === $referenceType ? '//' : "$scheme://";
$schemeAuthority .= $host.$port;
}
}
if (self::RELATIVE_PATH === $referenceType) {
$url = self::getRelativePath($this->context->getPathInfo(), $url);
} else {
$url = $schemeAuthority.$this->context->getBaseUrl().$url;
}
// add a query string if needed
$extra = array_diff_key($parameters, $variables);
if ($extra && $query = http_build_query($extra, '', '&')) {
$url .= '?'.$query;
}
return $url;
}
/**
* Returns the target path as relative reference from the base path.
*
* Only the URIs path component (no schema, hostname etc.) is relevant and must be given, starting with a slash.
* Both paths must be absolute and not contain relative parts.
* Relative URLs from one resource to another are useful when generating self-contained downloadable document archives.
* Furthermore, they can be used to reduce the link size in documents.
*
* Example target paths, given a base path of "/a/b/c/d":
* - "/a/b/c/d" -> ""
* - "/a/b/c/" -> "./"
* - "/a/b/" -> "../"
* - "/a/b/c/other" -> "other"
* - "/a/x/y" -> "../../x/y"
*
* @param string $basePath The base path
* @param string $targetPath The target path
*
* @return string The relative target path
*/
public static function getRelativePath($basePath, $targetPath)
{
if ($basePath === $targetPath) {
return '';
}
$sourceDirs = explode('/', isset($basePath[0]) && '/' === $basePath[0] ? substr($basePath, 1) : $basePath);
$targetDirs = explode('/', isset($targetPath[0]) && '/' === $targetPath[0] ? substr($targetPath, 1) : $targetPath);
array_pop($sourceDirs);
$targetFile = array_pop($targetDirs);
foreach ($sourceDirs as $i => $dir) {
if (isset($targetDirs[$i]) && $dir === $targetDirs[$i]) {
unset($sourceDirs[$i], $targetDirs[$i]);
} else {
break;
}
}
$targetDirs[] = $targetFile;
$path = str_repeat('../', count($sourceDirs)) . implode('/', $targetDirs);
// A reference to the same base directory or an empty subdirectory must be prefixed with "./".
// This also applies to a segment with a colon character (e.g., "file:colon") that cannot be used
// as the first segment of a relative-path reference, as it would be mistaken for a scheme name
// (see http://tools.ietf.org/html/rfc3986#section-4.2).
return '' === $path || '/' === $path[0]
|| false !== ($colonPos = strpos($path, ':')) && ($colonPos < ($slashPos = strpos($path, '/')) || false === $slashPos)
? "./$path" : $path;
}
}

View File

@ -18,20 +18,34 @@ use Symfony\Component\Routing\Exception\RouteNotFoundException;
* UrlGeneratorInterface is the interface that all URL generator classes must implement.
*
* @author Fabien Potencier <fabien@symfony.com>
* @author Tobias Schultze <http://tobion.de>
*
* @api
*/
interface UrlGeneratorInterface extends RequestContextAwareInterface
{
/**
* These constants define the different types of resource references that are declared
* in RFC 3986: http://tools.ietf.org/html/rfc3986
* We are using the term "URL" instead of "URI" as this is more common in web applications
* and we do not need to distinguish them as the difference is mostly semantical and
* less technical. Generating URIs, i.e. representation-independent resource identifiers,
* is still possible.
*/
const ABSOLUTE_URL = 'url';
const ABSOLUTE_PATH = 'path';
const RELATIVE_PATH = 'relative';
const NETWORK_PATH = 'network';
/**
* Generates a URL from the given parameters.
*
* If the generator is not able to generate the url, it must throw the RouteNotFoundException
* as documented below.
*
* @param string $name The name of the route
* @param mixed $parameters An array of parameters
* @param Boolean $absolute Whether to generate an absolute URL
* @param string $name The name of the route
* @param mixed $parameters An array of parameters
* @param string $referenceType The type of reference to be generated (see defined constants)
*
* @return string The generated URL
*
@ -39,5 +53,5 @@ interface UrlGeneratorInterface extends RequestContextAwareInterface
*
* @api
*/
public function generate($name, $parameters = array(), $absolute = false);
public function generate($name, $parameters = array(), $referenceType = self::ABSOLUTE_PATH);
}

View File

@ -23,6 +23,7 @@ use Symfony\Component\HttpFoundation\Request;
class RequestContext
{
private $baseUrl;
private $pathInfo;
private $method;
private $host;
private $scheme;
@ -46,7 +47,7 @@ class RequestContext
*
* @api
*/
public function __construct($baseUrl = '', $method = 'GET', $host = 'localhost', $scheme = 'http', $httpPort = 80, $httpsPort = 443)
public function __construct($baseUrl = '', $method = 'GET', $host = 'localhost', $scheme = 'http', $httpPort = 80, $httpsPort = 443, $path = '/')
{
$this->baseUrl = $baseUrl;
$this->method = strtoupper($method);
@ -54,11 +55,13 @@ class RequestContext
$this->scheme = strtolower($scheme);
$this->httpPort = $httpPort;
$this->httpsPort = $httpsPort;
$this->pathInfo = $path;
}
public function fromRequest(Request $request)
{
$this->setBaseUrl($request->getBaseUrl());
$this->setPathInfo($request->getPathInfo());
$this->setMethod($request->getMethod());
$this->setHost($request->getHost());
$this->setScheme($request->getScheme());
@ -88,6 +91,26 @@ class RequestContext
$this->baseUrl = $baseUrl;
}
/**
* Gets the path info.
*
* @return string The path info
*/
public function getPathInfo()
{
return $this->pathInfo;
}
/**
* Sets the path info.
*
* @param string $pathInfo The path info
*/
public function setPathInfo($pathInfo)
{
$this->pathInfo = $pathInfo;
}
/**
* Gets the HTTP method.
*

View File

@ -202,9 +202,9 @@ class Router implements RouterInterface
/**
* {@inheritdoc}
*/
public function generate($name, $parameters = array(), $absolute = false)
public function generate($name, $parameters = array(), $referenceType = self::ABSOLUTE_PATH)
{
return $this->getGenerator()->generate($name, $parameters, $absolute);
return $this->getGenerator()->generate($name, $parameters, $referenceType);
}
/**

View File

@ -379,7 +379,6 @@ class UrlGeneratorTest extends \PHPUnit_Framework_TestCase
*/
public function testDefaultRequirementOfVariableDisallowsNextSeparator()
{
$routes = $this->getRoutes('test', new Route('/{page}.{_format}'));
$this->getGenerator($routes)->generate('test', array('page' => 'do.t', '_format' => 'html'));
}
@ -388,7 +387,7 @@ class UrlGeneratorTest extends \PHPUnit_Framework_TestCase
{
$routes = $this->getRoutes('test', new Route('/{name}', array(), array(), array(), '{locale}.example.com'));
$this->assertEquals('http://fr.example.com/app.php/Fabien', $this->getGenerator($routes)->generate('test', array('name' =>'Fabien', 'locale' => 'fr')));
$this->assertEquals('//fr.example.com/app.php/Fabien', $this->getGenerator($routes)->generate('test', array('name' =>'Fabien', 'locale' => 'fr')));
}
public function testWithHostnameSameAsContext()
@ -440,6 +439,120 @@ class UrlGeneratorTest extends \PHPUnit_Framework_TestCase
$this->assertNull($generator->generate('test', array('foo' => 'baz'), false));
}
/**
* @dataProvider provideRelativePaths
*/
public function testGetRelativePath($sourcePath, $targetPath, $expectedPath)
{
$this->assertSame($expectedPath, UrlGenerator::getRelativePath($sourcePath, $targetPath));
}
public function provideRelativePaths()
{
return array(
array(
'/same/dir/',
'/same/dir/',
''
),
array(
'/same/file',
'/same/file',
''
),
array(
'/',
'/file',
'file'
),
array(
'/',
'/dir/file',
'dir/file'
),
array(
'/dir/file.html',
'/dir/different-file.html',
'different-file.html'
),
array(
'/same/dir/extra-file',
'/same/dir/',
'./'
),
array(
'/parent/dir/',
'/parent/',
'../'
),
array(
'/parent/dir/extra-file',
'/parent/',
'../'
),
array(
'/a/b/',
'/x/y/z/',
'../../x/y/z/'
),
array(
'/a/b/c/d/e',
'/a/c/d',
'../../../c/d'
),
array(
'/a/b/c//',
'/a/b/c/',
'../'
),
array(
'/a/b/c/',
'/a/b/c//',
'.//'
),
array(
'/root/a/b/c/',
'/root/x/b/c/',
'../../../x/b/c/'
),
array(
'/a/b/c/d/',
'/a',
'../../../../a'
),
array(
'/special-chars/sp%20ce/1€/mäh/e=mc²',
'/special-chars/sp%20ce/1€/<µ>/e=mc²',
'../<µ>/e=mc²'
),
array(
'not-rooted',
'dir/file',
'dir/file'
),
array(
'//dir/',
'',
'../../'
),
array(
'/dir/',
'/dir/file:with-colon',
'./file:with-colon'
),
array(
'/dir/',
'/dir/subdir/file:with-colon',
'subdir/file:with-colon'
),
array(
'/dir/',
'/dir/:subdir/',
'./:subdir/'
),
);
}
protected function getGenerator(RouteCollection $routes, array $parameters = array(), $logger = null)
{
$context = new RequestContext('/app.php');

View File

@ -67,8 +67,8 @@ class HttpUtils
public function createRequest(Request $request, $path)
{
$newRequest = Request::create($this->generateUri($request, $path), 'get', array(), $request->cookies->all(), array(), $request->server->all());
if ($session = $request->getSession()) {
$newRequest->setSession($session);
if ($request->hasSession()) {
$newRequest->setSession($request->getSession());
}
if ($request->attributes->has(SecurityContextInterface::AUTHENTICATION_ERROR)) {
@ -127,15 +127,10 @@ class HttpUtils
return $request->getUriForPath($path);
}
return $this->generateUrl($path, true);
}
private function generateUrl($route, $absolute = false)
{
if (null === $this->urlGenerator) {
throw new \LogicException('You must provide a UrlGeneratorInterface instance to be able to use routes.');
}
return $this->urlGenerator->generate($route, array(), $absolute);
return $this->urlGenerator->generate($path, array(), UrlGeneratorInterface::ABSOLUTE_URL);
}
}