2015-08-18 00:00:26 +00:00
< ? 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\Generator ;
use Symfony\Component\Routing\RouteCollection ;
use Symfony\Component\Routing\RequestContext ;
use Symfony\Component\Routing\Exception\InvalidParameterException ;
use Symfony\Component\Routing\Exception\RouteNotFoundException ;
use Symfony\Component\Routing\Exception\MissingMandatoryParametersException ;
use Psr\Log\LoggerInterface ;
/**
* UrlGenerator can generate a URL or a path for any route in the RouteCollection
* based on the passed parameters .
*
* @ author Fabien Potencier < fabien @ symfony . com >
* @ author Tobias Schultze < http :// tobion . de >
*/
class UrlGenerator implements UrlGeneratorInterface , ConfigurableRequirementsInterface
{
/**
* @ var RouteCollection
*/
protected $routes ;
/**
* @ var RequestContext
*/
protected $context ;
/**
* @ var bool | null
*/
protected $strictRequirements = true ;
/**
* @ var LoggerInterface | null
*/
protected $logger ;
/**
* This array defines the characters ( besides alphanumeric ones ) that will not be percent - encoded in the path segment of the generated URL .
*
* PHP ' s rawurlencode () encodes all chars except " a-zA-Z0-9-._~ " according to RFC 3986. But we want to allow some chars
* to be used in their literal form ( reasons below ) . Other chars inside the path must of course be encoded , e . g .
* " ? " and " # " ( would be interpreted wrongly as query and fragment identifier ),
* " ' " and " " " (are used as delimiters in HTML).
*/
protected $decodedChars = array (
// the slash can be used to designate a hierarchical structure and we want allow using it with this meaning
// some webservers don't allow the slash in encoded form in the path for security reasons anyway
// see http://stackoverflow.com/questions/4069002/http-400-if-2f-part-of-get-url-in-jboss
'%2F' => '/' ,
// the following chars are general delimiters in the URI specification but have only special meaning in the authority component
// so they can safely be used in the path in unencoded form
'%40' => '@' ,
'%3A' => ':' ,
// these chars are only sub-delimiters that have no predefined meaning and can therefore be used literally
// so URI producing applications can use these chars to delimit subcomponents in a path segment without being encoded for better readability
'%3B' => ';' ,
'%2C' => ',' ,
'%3D' => '=' ,
'%2B' => '+' ,
'%21' => '!' ,
'%2A' => '*' ,
'%7C' => '|' ,
);
/**
* Constructor .
*
* @ param RouteCollection $routes A RouteCollection instance
* @ param RequestContext $context The context
* @ param LoggerInterface | null $logger A logger instance
*/
public function __construct ( RouteCollection $routes , RequestContext $context , LoggerInterface $logger = null )
{
$this -> routes = $routes ;
$this -> context = $context ;
$this -> logger = $logger ;
}
/**
* { @ inheritdoc }
*/
public function setContext ( RequestContext $context )
{
$this -> context = $context ;
}
/**
* { @ inheritdoc }
*/
public function getContext ()
{
return $this -> context ;
}
/**
* { @ inheritdoc }
*/
public function setStrictRequirements ( $enabled )
{
$this -> strictRequirements = null === $enabled ? null : ( bool ) $enabled ;
}
/**
* { @ inheritdoc }
*/
public function isStrictRequirements ()
{
return $this -> strictRequirements ;
}
/**
* { @ inheritdoc }
*/
public function generate ( $name , $parameters = array (), $referenceType = self :: ABSOLUTE_PATH )
{
if ( null === $route = $this -> routes -> get ( $name )) {
throw new RouteNotFoundException ( sprintf ( 'Unable to generate a URL for the named route "%s" as such route does not exist.' , $name ));
}
// 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 , $referenceType , $compiledRoute -> getHostTokens (), $route -> getSchemes ());
}
/**
* @ 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
*/
protected function doGenerate ( $variables , $defaults , $requirements , $tokens , $parameters , $name , $referenceType , $hostTokens , array $requiredSchemes = array ())
{
2016-04-20 16:56:34 +00:00
if ( is_bool ( $referenceType ) || is_string ( $referenceType )) {
@ trigger_error ( 'The hardcoded value you are using for the $referenceType argument of the ' . __CLASS__ . '::generate method is deprecated since version 2.8 and will not be supported anymore in 3.0. Use the constants defined in the UrlGeneratorInterface instead.' , E_USER_DEPRECATED );
if ( true === $referenceType ) {
$referenceType = self :: ABSOLUTE_URL ;
} elseif ( false === $referenceType ) {
$referenceType = self :: ABSOLUTE_PATH ;
} elseif ( 'relative' === $referenceType ) {
$referenceType = self :: RELATIVE_PATH ;
} elseif ( 'network' === $referenceType ) {
$referenceType = self :: NETWORK_PATH ;
}
}
2015-08-18 00:00:26 +00:00
$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 ( 'Some mandatory parameters are missing ("%s") to generate a URL for route "%s".' , implode ( '", "' , array_keys ( $diff )), $name ));
}
$url = '' ;
$optional = true ;
foreach ( $tokens as $token ) {
if ( 'variable' === $token [ 0 ]) {
if ( ! $optional || ! array_key_exists ( $token [ 3 ], $defaults ) || null !== $mergedParams [ $token [ 3 ]] && ( 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) to generate a corresponding URL.' , $token [ 3 ], $name , $token [ 2 ], $mergedParams [ $token [ 3 ]]);
if ( $this -> strictRequirements ) {
throw new InvalidParameterException ( $message );
}
if ( $this -> logger ) {
$this -> logger -> error ( $message );
}
return ;
}
$url = $token [ 1 ] . $mergedParams [ $token [ 3 ]] . $url ;
$optional = false ;
}
} else {
// static text
$url = $token [ 1 ] . $url ;
$optional = false ;
}
}
if ( '' === $url ) {
$url = '/' ;
}
// 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
// otherwise we would generate a URI that, when followed by a user agent (e.g. browser), does not match this route
$url = strtr ( $url , array ( '/../' => '/%2E%2E/' , '/./' => '/%2E/' ));
if ( '/..' === substr ( $url , - 3 )) {
$url = substr ( $url , 0 , - 2 ) . '%2E%2E' ;
} elseif ( '/.' === substr ( $url , - 2 )) {
$url = substr ( $url , 0 , - 1 ) . '%2E' ;
}
$schemeAuthority = '' ;
if ( $host = $this -> context -> getHost ()) {
$scheme = $this -> context -> getScheme ();
if ( $requiredSchemes ) {
2016-04-20 16:56:34 +00:00
if ( ! in_array ( $scheme , $requiredSchemes , true )) {
2015-08-18 00:00:26 +00:00
$referenceType = self :: ABSOLUTE_URL ;
$scheme = current ( $requiredSchemes );
}
} elseif ( isset ( $requirements [ '_scheme' ]) && ( $req = strtolower ( $requirements [ '_scheme' ])) && $scheme !== $req ) {
// We do this for BC; to be removed if _scheme is not supported anymore
$referenceType = self :: ABSOLUTE_URL ;
$scheme = $req ;
}
if ( $hostTokens ) {
$routeHost = '' ;
foreach ( $hostTokens as $token ) {
if ( 'variable' === $token [ 0 ]) {
if ( null !== $this -> strictRequirements && ! preg_match ( '#^' . $token [ 2 ] . '$#i' , $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 );
}
if ( $this -> logger ) {
$this -> logger -> error ( $message );
}
return ;
}
$routeHost = $token [ 1 ] . $mergedParams [ $token [ 3 ]] . $routeHost ;
} else {
$routeHost = $token [ 1 ] . $routeHost ;
}
}
if ( $routeHost !== $host ) {
$host = $routeHost ;
if ( self :: ABSOLUTE_URL !== $referenceType ) {
$referenceType = self :: NETWORK_PATH ;
}
}
}
if ( self :: ABSOLUTE_URL === $referenceType || self :: NETWORK_PATH === $referenceType ) {
$port = '' ;
if ( 'http' === $scheme && 80 != $this -> context -> getHttpPort ()) {
$port = ':' . $this -> context -> getHttpPort ();
} elseif ( 'https' === $scheme && 443 != $this -> context -> getHttpsPort ()) {
$port = ':' . $this -> context -> getHttpsPort ();
}
$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
2017-02-03 00:28:38 +00:00
$extra = array_udiff_assoc ( array_diff_key ( $parameters , $variables ), $defaults , function ( $a , $b ) {
return $a == $b ? 0 : 1 ;
});
2015-08-18 00:00:26 +00:00
if ( $extra && $query = http_build_query ( $extra , '' , '&' )) {
// "/" and "?" can be left decoded for better user experience, see
// http://tools.ietf.org/html/rfc3986#section-3.4
$url .= '?' . strtr ( $query , array ( '%2F' => '/' ));
}
return $url ;
}
/**
* Returns the target path as relative reference from the base path .
*
* Only the URIs path component ( no schema , host 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 ;
}
}