Skip to content

Commit 75f59eb

Browse files
committed
[Routing] add support for path-relative and scheme-relative URL generation
1 parent 18c520a commit 75f59eb

File tree

13 files changed

+306
-68
lines changed

13 files changed

+306
-68
lines changed

src/Symfony/Bridge/Twig/Extension/RoutingExtension.php

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -40,14 +40,14 @@ public function getFunctions()
4040
);
4141
}
4242

43-
public function getPath($name, $parameters = array())
43+
public function getPath($name, $parameters = array(), $relative = false)
4444
{
45-
return $this->generator->generate($name, $parameters, false);
45+
return $this->generator->generate($name, $parameters, $relative ? UrlGeneratorInterface::RELATIVE_PATH : UrlGeneratorInterface::ABSOLUTE_PATH);
4646
}
4747

48-
public function getUrl($name, $parameters = array())
48+
public function getUrl($name, $parameters = array(), $schemeRelative = false)
4949
{
50-
return $this->generator->generate($name, $parameters, true);
50+
return $this->generator->generate($name, $parameters, $schemeRelative ? UrlGeneratorInterface::NETWORK_PATH : UrlGeneratorInterface::ABSOLUTE_URL);
5151
}
5252

5353
/**

src/Symfony/Bundle/FrameworkBundle/Controller/Controller.php

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111

1212
namespace Symfony\Bundle\FrameworkBundle\Controller;
1313

14+
use Symfony\Component\HttpFoundation\Request;
1415
use Symfony\Component\HttpFoundation\Response;
1516
use Symfony\Component\HttpFoundation\RedirectResponse;
1617
use Symfony\Component\HttpFoundation\StreamedResponse;
@@ -19,8 +20,8 @@
1920
use Symfony\Component\Form\FormTypeInterface;
2021
use Symfony\Component\Form\Form;
2122
use Symfony\Component\Form\FormBuilder;
23+
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
2224
use Doctrine\Bundle\DoctrineBundle\Registry;
23-
use Symfony\Component\HttpFoundation\Request;
2425

2526
/**
2627
* Controller is a simple implementation of a Controller.
@@ -34,15 +35,15 @@ class Controller extends ContainerAware
3435
/**
3536
* Generates a URL from the given parameters.
3637
*
37-
* @param string $route The name of the route
38-
* @param mixed $parameters An array of parameters
39-
* @param Boolean $absolute Whether to generate an absolute URL
38+
* @param string $route The name of the route
39+
* @param mixed $parameters An array of parameters
40+
* @param string $referenceType The type of reference (see UrlGeneratorInterface)
4041
*
4142
* @return string The generated URL
4243
*/
43-
public function generateUrl($route, $parameters = array(), $absolute = false)
44+
public function generateUrl($route, $parameters = array(), $referenceType = UrlGeneratorInterface::ABSOLUTE_PATH)
4445
{
45-
return $this->container->get('router')->generate($route, $parameters, $absolute);
46+
return $this->container->get('router')->generate($route, $parameters, $referenceType);
4647
}
4748

4849
/**

src/Symfony/Bundle/FrameworkBundle/Controller/RedirectController.php

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,9 @@
1212
namespace Symfony\Bundle\FrameworkBundle\Controller;
1313

1414
use Symfony\Component\DependencyInjection\ContainerAware;
15-
use Symfony\Component\HttpFoundation\Response;
1615
use Symfony\Component\HttpFoundation\RedirectResponse;
16+
use Symfony\Component\HttpFoundation\Response;
17+
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
1718

1819
/**
1920
* Redirects a request to another URL.
@@ -45,7 +46,7 @@ public function redirectAction($route, $permanent = false)
4546
$attributes = $this->container->get('request')->attributes->get('_route_params');
4647
unset($attributes['route'], $attributes['permanent']);
4748

48-
return new RedirectResponse($this->container->get('router')->generate($route, $attributes, true), $permanent ? 301 : 302);
49+
return new RedirectResponse($this->container->get('router')->generate($route, $attributes, UrlGeneratorInterface::ABSOLUTE_URL), $permanent ? 301 : 302);
4950
}
5051

5152
/**

src/Symfony/Bundle/FrameworkBundle/Templating/Helper/RouterHelper.php

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -36,15 +36,15 @@ public function __construct(UrlGeneratorInterface $router)
3636
/**
3737
* Generates a URL from the given parameters.
3838
*
39-
* @param string $name The name of the route
40-
* @param mixed $parameters An array of parameters
41-
* @param Boolean $absolute Whether to generate an absolute URL
39+
* @param string $name The name of the route
40+
* @param mixed $parameters An array of parameters
41+
* @param string $referenceType The type of reference (see UrlGeneratorInterface)
4242
*
4343
* @return string The generated URL
4444
*/
45-
public function generate($name, $parameters = array(), $absolute = false)
45+
public function generate($name, $parameters = array(), $referenceType = UrlGeneratorInterface::ABSOLUTE_PATH)
4646
{
47-
return $this->generator->generate($name, $parameters, $absolute);
47+
return $this->generator->generate($name, $parameters, $referenceType);
4848
}
4949

5050
/**

src/Symfony/Bundle/SecurityBundle/Templating/Helper/LogoutUrlHelper.php

Lines changed: 16 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -55,36 +55,40 @@ public function registerListener($key, $logoutPath, $intention, $csrfParameter,
5555
}
5656

5757
/**
58-
* Generate the relative logout URL for the firewall.
58+
* Generates the absolute logout path for the firewall.
5959
*
6060
* @param string $key The firewall key
61-
* @return string The relative logout URL
61+
*
62+
* @return string The logout path
6263
*/
6364
public function getLogoutPath($key)
6465
{
65-
return $this->generateLogoutUrl($key, false);
66+
return $this->generateLogoutUrl($key, UrlGeneratorInterface::ABSOLUTE_PATH);
6667
}
6768

6869
/**
69-
* Generate the absolute logout URL for the firewall.
70+
* Generates the absolute logout URL for the firewall.
7071
*
7172
* @param string $key The firewall key
72-
* @return string The absolute logout URL
73+
*
74+
* @return string The logout URL
7375
*/
7476
public function getLogoutUrl($key)
7577
{
76-
return $this->generateLogoutUrl($key, true);
78+
return $this->generateLogoutUrl($key, UrlGeneratorInterface::ABSOLUTE_URL);
7779
}
7880

7981
/**
80-
* Generate the logout URL for the firewall.
82+
* Generates the logout URL for the firewall.
83+
*
84+
* @param string $key The firewall key
85+
* @param string $referenceType The type of reference (see UrlGeneratorInterface)
8186
*
82-
* @param string $key The firewall key
83-
* @param Boolean $absolute Whether to generate an absolute URL
8487
* @return string The logout URL
88+
*
8589
* @throws \InvalidArgumentException if no LogoutListener is registered for the key
8690
*/
87-
private function generateLogoutUrl($key, $absolute)
91+
private function generateLogoutUrl($key, $referenceType)
8892
{
8993
if (!array_key_exists($key, $this->listeners)) {
9094
throw new \InvalidArgumentException(sprintf('No LogoutListener found for firewall key "%s".', $key));
@@ -97,13 +101,13 @@ private function generateLogoutUrl($key, $absolute)
97101
if ('/' === $logoutPath[0]) {
98102
$request = $this->container->get('request');
99103

100-
$url = ($absolute ? $request->getUriForPath($logoutPath) : $request->getBasePath() . $logoutPath);
104+
$url = UrlGeneratorInterface::ABSOLUTE_URL === $referenceType ? $request->getUriForPath($logoutPath) : $request->getBasePath() . $logoutPath;
101105

102106
if (!empty($parameters)) {
103107
$url .= '?' . http_build_query($parameters);
104108
}
105109
} else {
106-
$url = $this->router->generate($logoutPath, $parameters, $absolute);
110+
$url = $this->router->generate($logoutPath, $parameters, $referenceType);
107111
}
108112

109113
return $url;

src/Symfony/Bundle/SecurityBundle/Tests/Functional/Bundle/FormLoginBundle/Security/LocalizedFormFailureHandler.php

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,9 +12,10 @@
1212
namespace Symfony\Bundle\SecurityBundle\Tests\Functional\Bundle\FormLoginBundle\Security;
1313

1414
use Symfony\Component\HttpFoundation\RedirectResponse;
15+
use Symfony\Component\HttpFoundation\Request;
16+
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
1517
use Symfony\Component\Routing\RouterInterface;
1618
use Symfony\Component\Security\Core\Exception\AuthenticationException;
17-
use Symfony\Component\HttpFoundation\Request;
1819
use Symfony\Component\Security\Http\Authentication\AuthenticationFailureHandlerInterface;
1920

2021
class LocalizedFormFailureHandler implements AuthenticationFailureHandlerInterface
@@ -28,6 +29,6 @@ public function __construct(RouterInterface $router)
2829

2930
public function onAuthenticationFailure(Request $request, AuthenticationException $exception)
3031
{
31-
return new RedirectResponse($this->router->generate('localized_login_path', array(), true));
32+
return new RedirectResponse($this->router->generate('localized_login_path', array(), UrlGeneratorInterface::ABSOLUTE_URL));
3233
}
3334
}

src/Symfony/Component/Routing/Generator/Dumper/PhpGeneratorDumper.php

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -108,15 +108,15 @@ private function generateDeclaredRoutes()
108108
private function generateGenerateMethod()
109109
{
110110
return <<<EOF
111-
public function generate(\$name, \$parameters = array(), \$absolute = false)
111+
public function generate(\$name, \$parameters = array(), \$referenceType = self::ABSOLUTE_PATH)
112112
{
113113
if (!isset(self::\$declaredRoutes[\$name])) {
114114
throw new RouteNotFoundException(sprintf('Route "%s" does not exist.', \$name));
115115
}
116116
117117
list(\$variables, \$defaults, \$requirements, \$tokens, \$hostnameTokens) = self::\$declaredRoutes[\$name];
118118
119-
return \$this->doGenerate(\$variables, \$defaults, \$requirements, \$tokens, \$parameters, \$name, \$absolute, \$hostnameTokens);
119+
return \$this->doGenerate(\$variables, \$defaults, \$requirements, \$tokens, \$parameters, \$name, \$referenceType, \$hostnameTokens);
120120
}
121121
EOF;
122122
}

src/Symfony/Component/Routing/Generator/UrlGenerator.php

Lines changed: 104 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -127,7 +127,7 @@ public function isStrictRequirements()
127127
/**
128128
* {@inheritDoc}
129129
*/
130-
public function generate($name, $parameters = array(), $absolute = false)
130+
public function generate($name, $parameters = array(), $referenceType = self::ABSOLUTE_PATH)
131131
{
132132
if (null === $route = $this->routes->get($name)) {
133133
throw new RouteNotFoundException(sprintf('Route "%s" does not exist.', $name));
@@ -136,15 +136,40 @@ public function generate($name, $parameters = array(), $absolute = false)
136136
// the Route has a cache of its own and is not recompiled as long as it does not get modified
137137
$compiledRoute = $route->compile();
138138

139-
return $this->doGenerate($compiledRoute->getVariables(), $route->getDefaults(), $route->getRequirements(), $compiledRoute->getTokens(), $parameters, $name, $absolute, $compiledRoute->getHostnameTokens());
139+
return $this->doGenerate($compiledRoute->getVariables(), $route->getDefaults(), $route->getRequirements(), $compiledRoute->getTokens(), $parameters, $name, $referenceType, $compiledRoute->getHostnameTokens());
140+
}
141+
142+
/**
143+
* This method converts the reference type to the new value introduced in Symfony 2.2. It can be used by
144+
* other UrlGenerator implementations to be BC with Symfony 2.1. Reference type was a Boolean called
145+
* $absolute in Symfony 2.1 and only supported two reference types.
146+
*
147+
* @param Boolean $absolute Whether to generate an absolute URL
148+
*
149+
* @return string The new reference type
150+
*
151+
* @deprecated Deprecated since version 2.2, to be removed in 2.3.
152+
*/
153+
public static function convertReferenceType($absolute)
154+
{
155+
if (false === $absolute) {
156+
return self::ABSOLUTE_PATH;
157+
}
158+
if (true === $absolute) {
159+
return self::ABSOLUTE_URL;
160+
}
161+
162+
return $absolute;
140163
}
141164

142165
/**
143166
* @throws MissingMandatoryParametersException When route has some missing mandatory parameters
144167
* @throws InvalidParameterException When a parameter value is not correct
145168
*/
146-
protected function doGenerate($variables, $defaults, $requirements, $tokens, $parameters, $name, $absolute, $hostnameTokens)
169+
protected function doGenerate($variables, $defaults, $requirements, $tokens, $parameters, $name, $referenceType, $hostnameTokens)
147170
{
171+
$referenceType = self::convertReferenceType($referenceType);
172+
148173
$variables = array_flip($variables);
149174
$mergedParams = array_replace($defaults, $this->context->getParameters(), $parameters);
150175

@@ -186,8 +211,8 @@ protected function doGenerate($variables, $defaults, $requirements, $tokens, $pa
186211
$url = '/';
187212
}
188213

189-
// do not encode the contexts base url as it is already encoded (see Symfony\Component\HttpFoundation\Request)
190-
$url = $this->context->getBaseUrl().strtr(rawurlencode($url), $this->decodedChars);
214+
// the contexts base url is already encoded (see Symfony\Component\HttpFoundation\Request)
215+
$url = strtr(rawurlencode($url), $this->decodedChars);
191216

192217
// the path segments "." and ".." are interpreted as relative reference when resolving a URI; see http://tools.ietf.org/html/rfc3986#section-3.3
193218
// so we need to encode them as they are not used for this purpose here
@@ -199,16 +224,11 @@ protected function doGenerate($variables, $defaults, $requirements, $tokens, $pa
199224
$url = substr($url, 0, -1) . '%2E';
200225
}
201226

202-
// add a query string if needed
203-
$extra = array_diff_key($parameters, $variables);
204-
if ($extra && $query = http_build_query($extra, '', '&')) {
205-
$url .= '?'.$query;
206-
}
207-
227+
$schemeAuthority = '';
208228
if ($host = $this->context->getHost()) {
209229
$scheme = $this->context->getScheme();
210-
if (isset($requirements['_scheme']) && ($req = strtolower($requirements['_scheme'])) && $scheme != $req) {
211-
$absolute = true;
230+
if (isset($requirements['_scheme']) && ($req = strtolower($requirements['_scheme'])) && $scheme !== $req) {
231+
$referenceType = self::ABSOLUTE_URL;
212232
$scheme = $req;
213233
}
214234

@@ -231,29 +251,95 @@ protected function doGenerate($variables, $defaults, $requirements, $tokens, $pa
231251
}
232252

233253
$routeHost = $token[1].$mergedParams[$token[3]].$routeHost;
234-
} elseif ('text' === $token[0]) {
254+
} else {
235255
$routeHost = $token[1].$routeHost;
236256
}
237257
}
238258

239-
if ($routeHost != $host) {
259+
if ($routeHost !== $host) {
240260
$host = $routeHost;
241-
$absolute = true;
261+
if (self::ABSOLUTE_URL !== $referenceType) {
262+
$referenceType = self::NETWORK_PATH;
263+
}
242264
}
243265
}
244266

245-
if ($absolute) {
267+
if (self::ABSOLUTE_URL === $referenceType || self::NETWORK_PATH === $referenceType) {
246268
$port = '';
247269
if ('http' === $scheme && 80 != $this->context->getHttpPort()) {
248270
$port = ':'.$this->context->getHttpPort();
249271
} elseif ('https' === $scheme && 443 != $this->context->getHttpsPort()) {
250272
$port = ':'.$this->context->getHttpsPort();
251273
}
252274

253-
$url = $scheme.'://'.$host.$port.$url;
275+
$schemeAuthority = self::NETWORK_PATH === $referenceType ? '//' : "$scheme://";
276+
$schemeAuthority .= $host.$port;
254277
}
255278
}
256279

280+
if (self::RELATIVE_PATH === $referenceType) {
281+
$url = self::getRelativePath($this->context->getPathInfo(), $url);
282+
} else {
283+
$url = $schemeAuthority.$this->context->getBaseUrl().$url;
284+
}
285+
286+
// add a query string if needed
287+
$extra = array_diff_key($parameters, $variables);
288+
if ($extra && $query = http_build_query($extra, '', '&')) {
289+
$url .= '?'.$query;
290+
}
291+
257292
return $url;
258293
}
294+
295+
/**
296+
* Returns the target path as relative reference from the base path.
297+
*
298+
* Only the URIs path component (no schema, hostname etc.) is relevant and must be given, starting with a slash.
299+
* Both paths must be absolute and not contain relative parts.
300+
* Relative URLs from one resource to another are useful when generating self-contained downloadable document archives.
301+
* Furthermore, they can be used to reduce the link size in documents.
302+
*
303+
* Example target paths, given a base path of "/a/b/c/d":
304+
* - "/a/b/c/d" -> ""
305+
* - "/a/b/c/" -> "./"
306+
* - "/a/b/" -> "../"
307+
* - "/a/b/c/other" -> "other"
308+
* - "/a/x/y" -> "../../x/y"
309+
*
310+
* @param string $basePath The base path
311+
* @param string $targetPath The target path
312+
*
313+
* @return string The relative target path
314+
*/
315+
public static function getRelativePath($basePath, $targetPath)
316+
{
317+
if ($basePath === $targetPath) {
318+
return '';
319+
}
320+
321+
$sourceDirs = explode('/', isset($basePath[0]) && '/' === $basePath[0] ? substr($basePath, 1) : $basePath);
322+
$targetDirs = explode('/', isset($targetPath[0]) && '/' === $targetPath[0] ? substr($targetPath, 1) : $targetPath);
323+
array_pop($sourceDirs);
324+
$targetFile = array_pop($targetDirs);
325+
326+
foreach ($sourceDirs as $i => $dir) {
327+
if (isset($targetDirs[$i]) && $dir === $targetDirs[$i]) {
328+
unset($sourceDirs[$i], $targetDirs[$i]);
329+
} else {
330+
break;
331+
}
332+
}
333+
334+
$targetDirs[] = $targetFile;
335+
$path = str_repeat('../', count($sourceDirs)) . implode('/', $targetDirs);
336+
337+
// A reference to the same base directory or an empty subdirectory must be prefixed with "./".
338+
// This also applies to a segment with a colon character (e.g., "file:colon") that cannot be used
339+
// as the first segment of a relative-path reference, as it would be mistaken for a scheme name
340+
// (see http://tools.ietf.org/html/rfc3986#section-4.2).
341+
return '' === $path || '/' === $path[0]
342+
|| false !== ($colonPos = strpos($path, ':')) && ($colonPos < ($slashPos = strpos($path, '/')) || false === $slashPos)
343+
? "./$path" : $path;
344+
}
259345
}

0 commit comments

Comments
 (0)