diff --git a/src/Symfony/Bridge/Twig/CHANGELOG.md b/src/Symfony/Bridge/Twig/CHANGELOG.md index 644a130e2aef5..420a99b61ff8b 100644 --- a/src/Symfony/Bridge/Twig/CHANGELOG.md +++ b/src/Symfony/Bridge/Twig/CHANGELOG.md @@ -7,6 +7,8 @@ CHANGELOG * [BC BREAK] restricted the `render` tag to only accept URIs as reference (the signature changed) * added a render function to render a request * The `app` global variable is now injected even when using the twig service directly. + * Added an optional parameter to the `path` and `url` function which allows to generate + relative paths (e.g. "../parent-file") and scheme-relative URLs (e.g. "//example.com/dir/file"). 2.1.0 ----- diff --git a/src/Symfony/Bridge/Twig/Extension/RoutingExtension.php b/src/Symfony/Bridge/Twig/Extension/RoutingExtension.php index 65d7dd22e898b..54befc2cf9eff 100644 --- a/src/Symfony/Bridge/Twig/Extension/RoutingExtension.php +++ b/src/Symfony/Bridge/Twig/Extension/RoutingExtension.php @@ -40,14 +40,14 @@ public function getFunctions() ); } - 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); } /** diff --git a/src/Symfony/Bundle/FrameworkBundle/Controller/Controller.php b/src/Symfony/Bundle/FrameworkBundle/Controller/Controller.php index 87900a1242380..a19030e11b4de 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Controller/Controller.php +++ b/src/Symfony/Bundle/FrameworkBundle/Controller/Controller.php @@ -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\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,17 @@ 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 Boolean|string $referenceType The type of reference (one of the constants in UrlGeneratorInterface) * * @return string The generated URL + * + * @see UrlGeneratorInterface */ - 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); } /** diff --git a/src/Symfony/Bundle/FrameworkBundle/Controller/RedirectController.php b/src/Symfony/Bundle/FrameworkBundle/Controller/RedirectController.php index 513d4de3f59c9..5d8b71906de40 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Controller/RedirectController.php +++ b/src/Symfony/Bundle/FrameworkBundle/Controller/RedirectController.php @@ -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 @@ public function redirectAction($route, $permanent = false) $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); } /** diff --git a/src/Symfony/Bundle/FrameworkBundle/Templating/Helper/RouterHelper.php b/src/Symfony/Bundle/FrameworkBundle/Templating/Helper/RouterHelper.php index 6ce78960dfcc8..845b75d59a78c 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Templating/Helper/RouterHelper.php +++ b/src/Symfony/Bundle/FrameworkBundle/Templating/Helper/RouterHelper.php @@ -36,15 +36,17 @@ public function __construct(UrlGeneratorInterface $router) /** * 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 Boolean|string $referenceType The type of reference (one of the constants in UrlGeneratorInterface) * * @return string The generated URL + * + * @see UrlGeneratorInterface */ - 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); } /** diff --git a/src/Symfony/Bundle/SecurityBundle/Templating/Helper/LogoutUrlHelper.php b/src/Symfony/Bundle/SecurityBundle/Templating/Helper/LogoutUrlHelper.php index 4b79957c3b527..59f7182bc3e84 100644 --- a/src/Symfony/Bundle/SecurityBundle/Templating/Helper/LogoutUrlHelper.php +++ b/src/Symfony/Bundle/SecurityBundle/Templating/Helper/LogoutUrlHelper.php @@ -55,36 +55,40 @@ public function registerListener($key, $logoutPath, $intention, $csrfParameter, } /** - * 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 Boolean|string $referenceType The type of reference (one of the constants in 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 @@ private function generateLogoutUrl($key, $absolute) 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; diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/Functional/Bundle/FormLoginBundle/Security/LocalizedFormFailureHandler.php b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/Bundle/FormLoginBundle/Security/LocalizedFormFailureHandler.php index fda394d8cc9a2..7b971990657eb 100644 --- a/src/Symfony/Bundle/SecurityBundle/Tests/Functional/Bundle/FormLoginBundle/Security/LocalizedFormFailureHandler.php +++ b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/Bundle/FormLoginBundle/Security/LocalizedFormFailureHandler.php @@ -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 @@ public function __construct(RouterInterface $router) 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)); } } diff --git a/src/Symfony/Component/Routing/CHANGELOG.md b/src/Symfony/Component/Routing/CHANGELOG.md index 14b9065fefbfc..503838b550252 100644 --- a/src/Symfony/Component/Routing/CHANGELOG.md +++ b/src/Symfony/Component/Routing/CHANGELOG.md @@ -81,6 +81,12 @@ CHANGELOG are now also allowed. * [BC BREAK] `RouteCompilerInterface::compile(Route $route)` was made static (only relevant if you implemented your own RouteCompiler). + * Added possibility to generate relative paths and network paths in the UrlGenerator, e.g. + "../parent-file" and "//example.com/dir/file". The third parameter in + `UrlGeneratorInterface::generate($name, $parameters = array(), $referenceType = self::ABSOLUTE_PATH)` + now accepts more values and you should use the constants defined in `UrlGeneratorInterface` for + claritiy. The old method calls with a Boolean parameter will continue to work because they + equal the signature using the constants. 2.1.0 ----- diff --git a/src/Symfony/Component/Routing/Generator/Dumper/PhpGeneratorDumper.php b/src/Symfony/Component/Routing/Generator/Dumper/PhpGeneratorDumper.php index c85f0201e450a..abdf309d1ba09 100644 --- a/src/Symfony/Component/Routing/Generator/Dumper/PhpGeneratorDumper.php +++ b/src/Symfony/Component/Routing/Generator/Dumper/PhpGeneratorDumper.php @@ -108,7 +108,7 @@ private function generateDeclaredRoutes() private function generateGenerateMethod() { return <<doGenerate(\$variables, \$defaults, \$requirements, \$tokens, \$parameters, \$name, \$absolute, \$hostnameTokens); + return \$this->doGenerate(\$variables, \$defaults, \$requirements, \$tokens, \$parameters, \$name, \$referenceType, \$hostnameTokens); } EOF; } diff --git a/src/Symfony/Component/Routing/Generator/UrlGenerator.php b/src/Symfony/Component/Routing/Generator/UrlGenerator.php index aa0a3ee8f28e2..337af75992294 100644 --- a/src/Symfony/Component/Routing/Generator/UrlGenerator.php +++ b/src/Symfony/Component/Routing/Generator/UrlGenerator.php @@ -19,7 +19,8 @@ use Symfony\Component\HttpKernel\Log\LoggerInterface; /** - * UrlGenerator generates a URL based on a set of routes. + * UrlGenerator can generate a URL or a path for any route in the RouteCollection + * based on the passed parameters. * * @author Fabien Potencier * @author Tobias Schultze @@ -127,7 +128,7 @@ public function isStrictRequirements() /** * {@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,21 +137,22 @@ public function generate($name, $parameters = array(), $absolute = false) // 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()); } /** - * @throws MissingMandatoryParametersException When route has some missing mandatory parameters - * @throws InvalidParameterException When a parameter value is not correct + * @throws MissingMandatoryParametersException When some parameters are missing that mandatory for the route + * @throws InvalidParameterException When a parameter value for a placeholder is not correct because + * it does not match the requirement */ - protected function doGenerate($variables, $defaults, $requirements, $tokens, $parameters, $name, $absolute, $hostnameTokens) + protected function doGenerate($variables, $defaults, $requirements, $tokens, $parameters, $name, $referenceType, $hostnameTokens) { $variables = array_flip($variables); $mergedParams = array_replace($defaults, $this->context->getParameters(), $parameters); // all params must be given if ($diff = array_diff_key($variables, $mergedParams)) { - throw new MissingMandatoryParametersException(sprintf('The "%s" route has some missing mandatory parameters ("%s").', $name, implode('", "', array_keys($diff)))); + throw new MissingMandatoryParametersException(sprintf('Some mandatory parameters are missing ("%s") to generate a URL for route "%s".', implode('", "', array_keys($diff)), $name)); } $url = ''; @@ -160,7 +162,7 @@ protected function doGenerate($variables, $defaults, $requirements, $tokens, $pa if (!$optional || !array_key_exists($token[3], $defaults) || (string) $mergedParams[$token[3]] !== (string) $defaults[$token[3]]) { // check requirement if (null !== $this->strictRequirements && !preg_match('#^'.$token[2].'$#', $mergedParams[$token[3]])) { - $message = sprintf('Parameter "%s" for route "%s" must match "%s" ("%s" given).', $token[3], $name, $token[2], $mergedParams[$token[3]]); + $message = sprintf('Parameter "%s" for route "%s" must match "%s" ("%s" given) to generate a corresponding URL.', $token[3], $name, $token[2], $mergedParams[$token[3]]); if ($this->strictRequirements) { throw new InvalidParameterException($message); } @@ -186,8 +188,8 @@ protected function doGenerate($variables, $defaults, $requirements, $tokens, $pa $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 +201,11 @@ protected function doGenerate($variables, $defaults, $requirements, $tokens, $pa $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; } @@ -217,7 +214,7 @@ protected function doGenerate($variables, $defaults, $requirements, $tokens, $pa foreach ($hostnameTokens as $token) { if ('variable' === $token[0]) { if (null !== $this->strictRequirements && !preg_match('#^'.$token[2].'$#', $mergedParams[$token[3]])) { - $message = sprintf('Parameter "%s" for route "%s" must match "%s" ("%s" given).', $token[3], $name, $token[2], $mergedParams[$token[3]]); + $message = sprintf('Parameter "%s" for route "%s" must match "%s" ("%s" given) to generate a corresponding URL.', $token[3], $name, $token[2], $mergedParams[$token[3]]); if ($this->strictRequirements) { throw new InvalidParameterException($message); @@ -231,18 +228,20 @@ protected function doGenerate($variables, $defaults, $requirements, $tokens, $pa } $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 +249,74 @@ protected function doGenerate($variables, $defaults, $requirements, $tokens, $pa $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; + } } diff --git a/src/Symfony/Component/Routing/Generator/UrlGeneratorInterface.php b/src/Symfony/Component/Routing/Generator/UrlGeneratorInterface.php index e28c79d2d6ec3..c38f31022771c 100644 --- a/src/Symfony/Component/Routing/Generator/UrlGeneratorInterface.php +++ b/src/Symfony/Component/Routing/Generator/UrlGeneratorInterface.php @@ -11,33 +11,77 @@ namespace Symfony\Component\Routing\Generator; -use Symfony\Component\Routing\RequestContextAwareInterface; +use Symfony\Component\Routing\Exception\InvalidParameterException; +use Symfony\Component\Routing\Exception\MissingMandatoryParametersException; use Symfony\Component\Routing\Exception\RouteNotFoundException; +use Symfony\Component\Routing\RequestContextAwareInterface; /** * UrlGeneratorInterface is the interface that all URL generator classes must implement. * + * The constants in this interface 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 also possible. + * * @author Fabien Potencier + * @author Tobias Schultze * * @api */ interface UrlGeneratorInterface extends RequestContextAwareInterface { /** - * Generates a URL from the given parameters. + * Generates an absolute URL, e.g. "http://example.com/dir/file". + */ + const ABSOLUTE_URL = true; + + /** + * Generates an absolute path, e.g. "/dir/file". + */ + const ABSOLUTE_PATH = false; + + /** + * Generates a relative path based on the current request path, e.g. "../parent-file". + * @see UrlGenerator::getRelativePath() + */ + const RELATIVE_PATH = 'relative'; + + /** + * Generates a network path, e.g. "//example.com/dir/file". + * Such reference reuses the current scheme but specifies the hostname. + */ + const NETWORK_PATH = 'network'; + + /** + * Generates a URL or path for a specific route based on the given parameters. + * + * Parameters that reference placeholders in the route pattern will substitute them in the + * path or hostname. Extra params are added as query string to the URL. + * + * When the passed reference type cannot be generated for the route because it requires a different + * hostname or scheme than the current one, the method will return a more comprehensive reference + * that includes the required params. For example, when you call this method with $referenceType = ABSOLUTE_PATH + * but the route requires the https scheme whereas the current scheme is http, it will instead return an + * ABSOLUTE_URL with the https scheme and the current hostname. This makes sure the generated URL matches + * the route in any case. * - * If the generator is not able to generate the url, it must throw the RouteNotFoundException - * as documented below. + * If there is no route with the given name, the generator must throw the RouteNotFoundException. * - * @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 Boolean|string $referenceType The type of reference to be generated (one of the constants) * * @return string The generated URL * - * @throws RouteNotFoundException if route doesn't exist + * @throws RouteNotFoundException If the named route doesn't exist + * @throws MissingMandatoryParametersException When some parameters are missing that are mandatory for the route + * @throws InvalidParameterException When a parameter value for a placeholder is not correct because + * it does not match the requirement * * @api */ - public function generate($name, $parameters = array(), $absolute = false); + public function generate($name, $parameters = array(), $referenceType = self::ABSOLUTE_PATH); } diff --git a/src/Symfony/Component/Routing/RequestContext.php b/src/Symfony/Component/Routing/RequestContext.php index 1f9cf3c02c920..20a700ac5f200 100644 --- a/src/Symfony/Component/Routing/RequestContext.php +++ b/src/Symfony/Component/Routing/RequestContext.php @@ -23,6 +23,7 @@ class RequestContext { private $baseUrl; + private $pathInfo; private $method; private $host; private $scheme; @@ -43,10 +44,11 @@ class RequestContext * @param string $scheme The HTTP scheme * @param integer $httpPort The HTTP port * @param integer $httpsPort The HTTPS port + * @param string $pathInfo The path info * * @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, $pathInfo = '/') { $this->baseUrl = $baseUrl; $this->method = strtoupper($method); @@ -54,11 +56,13 @@ public function __construct($baseUrl = '', $method = 'GET', $host = 'localhost', $this->scheme = strtolower($scheme); $this->httpPort = $httpPort; $this->httpsPort = $httpsPort; + $this->pathInfo = $pathInfo; } 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 +92,26 @@ public function setBaseUrl($baseUrl) $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. * diff --git a/src/Symfony/Component/Routing/Router.php b/src/Symfony/Component/Routing/Router.php index 68d73ab24db45..a85054a4508da 100644 --- a/src/Symfony/Component/Routing/Router.php +++ b/src/Symfony/Component/Routing/Router.php @@ -202,9 +202,9 @@ public function getContext() /** * {@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); } /** diff --git a/src/Symfony/Component/Routing/Tests/Generator/UrlGeneratorTest.php b/src/Symfony/Component/Routing/Tests/Generator/UrlGeneratorTest.php index 7b6495f3c9eb7..713ab7a704eef 100644 --- a/src/Symfony/Component/Routing/Tests/Generator/UrlGeneratorTest.php +++ b/src/Symfony/Component/Routing/Tests/Generator/UrlGeneratorTest.php @@ -14,6 +14,7 @@ use Symfony\Component\Routing\RouteCollection; use Symfony\Component\Routing\Route; use Symfony\Component\Routing\Generator\UrlGenerator; +use Symfony\Component\Routing\Generator\UrlGeneratorInterface; use Symfony\Component\Routing\RequestContext; class UrlGeneratorTest extends \PHPUnit_Framework_TestCase @@ -379,7 +380,6 @@ public function testDefaultRequirementOfVariableDisallowsSlash() */ public function testDefaultRequirementOfVariableDisallowsNextSeparator() { - $routes = $this->getRoutes('test', new Route('/{page}.{_format}')); $this->getGenerator($routes)->generate('test', array('page' => 'do.t', '_format' => 'html')); } @@ -388,7 +388,7 @@ public function testWithHostnameDifferentFromContext() { $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 +440,172 @@ public function testUrlWithInvalidParameterInHostnameInNonStrictMode() $this->assertNull($generator->generate('test', array('foo' => 'baz'), false)); } + public function testGenerateNetworkPath() + { + $routes = $this->getRoutes('test', new Route('/{name}', array(), array('_scheme' => 'http'), array(), '{locale}.example.com')); + + $this->assertSame('//fr.example.com/app.php/Fabien', $this->getGenerator($routes)->generate('test', + array('name' =>'Fabien', 'locale' => 'fr'), UrlGeneratorInterface::NETWORK_PATH), 'network path with different host' + ); + $this->assertSame('//fr.example.com/app.php/Fabien?query=string', $this->getGenerator($routes, array('host' => 'fr.example.com'))->generate('test', + array('name' =>'Fabien', 'locale' => 'fr', 'query' => 'string'), UrlGeneratorInterface::NETWORK_PATH), 'network path although host same as context' + ); + $this->assertSame('http://fr.example.com/app.php/Fabien', $this->getGenerator($routes, array('scheme' => 'https'))->generate('test', + array('name' =>'Fabien', 'locale' => 'fr'), UrlGeneratorInterface::NETWORK_PATH), 'absolute URL because scheme requirement does not match context' + ); + $this->assertSame('http://fr.example.com/app.php/Fabien', $this->getGenerator($routes)->generate('test', + array('name' =>'Fabien', 'locale' => 'fr'), UrlGeneratorInterface::ABSOLUTE_URL), 'absolute URL with same scheme because it is requested' + ); + } + + public function testGenerateRelativePath() + { + $routes = new RouteCollection(); + $routes->add('article', new Route('/{author}/{article}/')); + $routes->add('comments', new Route('/{author}/{article}/comments')); + $routes->add('hostname', new Route('/{article}', array(), array(), array(), '{author}.example.com')); + $routes->add('scheme', new Route('/{author}', array(), array('_scheme' => 'https'))); + $routes->add('unrelated', new Route('/about')); + + $generator = $this->getGenerator($routes, array('host' => 'example.com', 'pathInfo' => '/fabien/symfony-is-great/')); + + $this->assertSame('comments', $generator->generate('comments', + array('author' =>'fabien', 'article' => 'symfony-is-great'), UrlGeneratorInterface::RELATIVE_PATH) + ); + $this->assertSame('comments?page=2', $generator->generate('comments', + array('author' =>'fabien', 'article' => 'symfony-is-great', 'page' => 2), UrlGeneratorInterface::RELATIVE_PATH) + ); + $this->assertSame('../twig-is-great/', $generator->generate('article', + array('author' =>'fabien', 'article' => 'twig-is-great'), UrlGeneratorInterface::RELATIVE_PATH) + ); + $this->assertSame('../../bernhard/forms-are-great/', $generator->generate('article', + array('author' =>'bernhard', 'article' => 'forms-are-great'), UrlGeneratorInterface::RELATIVE_PATH) + ); + $this->assertSame('//bernhard.example.com/app.php/forms-are-great', $generator->generate('hostname', + array('author' =>'bernhard', 'article' => 'forms-are-great'), UrlGeneratorInterface::RELATIVE_PATH) + ); + $this->assertSame('https://example.com/app.php/bernhard', $generator->generate('scheme', + array('author' =>'bernhard'), UrlGeneratorInterface::RELATIVE_PATH) + ); + $this->assertSame('../../about', $generator->generate('unrelated', + array(), UrlGeneratorInterface::RELATIVE_PATH) + ); + } + + /** + * @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'); diff --git a/src/Symfony/Component/Security/Http/HttpUtils.php b/src/Symfony/Component/Security/Http/HttpUtils.php index 81893ffc3490c..a3c6f61bf8251 100644 --- a/src/Symfony/Component/Security/Http/HttpUtils.php +++ b/src/Symfony/Component/Security/Http/HttpUtils.php @@ -71,8 +71,8 @@ public function createRedirectResponse(Request $request, $path, $status = 302) 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)) { @@ -136,15 +136,10 @@ public function generateUri($request, $path) 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); } }