vendor/symfony/routing/Generator/UrlGenerator.php line 171

Open in your IDE?
  1. <?php
  2. /*
  3.  * This file is part of the Symfony package.
  4.  *
  5.  * (c) Fabien Potencier <fabien@symfony.com>
  6.  *
  7.  * For the full copyright and license information, please view the LICENSE
  8.  * file that was distributed with this source code.
  9.  */
  10. namespace Symfony\Component\Routing\Generator;
  11. use Psr\Log\LoggerInterface;
  12. use Symfony\Component\Routing\Exception\InvalidParameterException;
  13. use Symfony\Component\Routing\Exception\MissingMandatoryParametersException;
  14. use Symfony\Component\Routing\Exception\RouteNotFoundException;
  15. use Symfony\Component\Routing\RequestContext;
  16. use Symfony\Component\Routing\RouteCollection;
  17. /**
  18.  * UrlGenerator can generate a URL or a path for any route in the RouteCollection
  19.  * based on the passed parameters.
  20.  *
  21.  * @author Fabien Potencier <fabien@symfony.com>
  22.  * @author Tobias Schultze <http://tobion.de>
  23.  */
  24. class UrlGenerator implements UrlGeneratorInterfaceConfigurableRequirementsInterface
  25. {
  26.     private const QUERY_FRAGMENT_DECODED = [
  27.         // RFC 3986 explicitly allows those in the query/fragment to reference other URIs unencoded
  28.         '%2F' => '/',
  29.         '%3F' => '?',
  30.         // reserved chars that have no special meaning for HTTP URIs in a query or fragment
  31.         // this excludes esp. "&", "=" and also "+" because PHP would treat it as a space (form-encoded)
  32.         '%40' => '@',
  33.         '%3A' => ':',
  34.         '%21' => '!',
  35.         '%3B' => ';',
  36.         '%2C' => ',',
  37.         '%2A' => '*',
  38.     ];
  39.     protected $routes;
  40.     protected $context;
  41.     /**
  42.      * @var bool|null
  43.      */
  44.     protected $strictRequirements true;
  45.     protected $logger;
  46.     private $defaultLocale;
  47.     /**
  48.      * This array defines the characters (besides alphanumeric ones) that will not be percent-encoded in the path segment of the generated URL.
  49.      *
  50.      * PHP's rawurlencode() encodes all chars except "a-zA-Z0-9-._~" according to RFC 3986. But we want to allow some chars
  51.      * to be used in their literal form (reasons below). Other chars inside the path must of course be encoded, e.g.
  52.      * "?" and "#" (would be interpreted wrongly as query and fragment identifier),
  53.      * "'" and """ (are used as delimiters in HTML).
  54.      */
  55.     protected $decodedChars = [
  56.         // the slash can be used to designate a hierarchical structure and we want allow using it with this meaning
  57.         // some webservers don't allow the slash in encoded form in the path for security reasons anyway
  58.         // see http://stackoverflow.com/questions/4069002/http-400-if-2f-part-of-get-url-in-jboss
  59.         '%2F' => '/',
  60.         '%252F' => '%2F',
  61.         // the following chars are general delimiters in the URI specification but have only special meaning in the authority component
  62.         // so they can safely be used in the path in unencoded form
  63.         '%40' => '@',
  64.         '%3A' => ':',
  65.         // these chars are only sub-delimiters that have no predefined meaning and can therefore be used literally
  66.         // so URI producing applications can use these chars to delimit subcomponents in a path segment without being encoded for better readability
  67.         '%3B' => ';',
  68.         '%2C' => ',',
  69.         '%3D' => '=',
  70.         '%2B' => '+',
  71.         '%21' => '!',
  72.         '%2A' => '*',
  73.         '%7C' => '|',
  74.     ];
  75.     public function __construct(RouteCollection $routesRequestContext $contextLoggerInterface $logger nullstring $defaultLocale null)
  76.     {
  77.         $this->routes $routes;
  78.         $this->context $context;
  79.         $this->logger $logger;
  80.         $this->defaultLocale $defaultLocale;
  81.     }
  82.     /**
  83.      * {@inheritdoc}
  84.      */
  85.     public function setContext(RequestContext $context)
  86.     {
  87.         $this->context $context;
  88.     }
  89.     /**
  90.      * {@inheritdoc}
  91.      */
  92.     public function getContext()
  93.     {
  94.         return $this->context;
  95.     }
  96.     /**
  97.      * {@inheritdoc}
  98.      */
  99.     public function setStrictRequirements(?bool $enabled)
  100.     {
  101.         $this->strictRequirements $enabled;
  102.     }
  103.     /**
  104.      * {@inheritdoc}
  105.      */
  106.     public function isStrictRequirements()
  107.     {
  108.         return $this->strictRequirements;
  109.     }
  110.     /**
  111.      * {@inheritdoc}
  112.      */
  113.     public function generate(string $name, array $parameters = [], int $referenceType self::ABSOLUTE_PATH)
  114.     {
  115.         $route null;
  116.         $locale $parameters['_locale']
  117.             ?? $this->context->getParameter('_locale')
  118.             ?: $this->defaultLocale;
  119.         if (null !== $locale) {
  120.             do {
  121.                 if (null !== ($route $this->routes->get($name.'.'.$locale)) && $route->getDefault('_canonical_route') === $name) {
  122.                     break;
  123.                 }
  124.             } while (false !== $locale strstr($locale'_'true));
  125.         }
  126.         if (null === $route $route ?? $this->routes->get($name)) {
  127.             throw new RouteNotFoundException(sprintf('Unable to generate a URL for the named route "%s" as such route does not exist.'$name));
  128.         }
  129.         // the Route has a cache of its own and is not recompiled as long as it does not get modified
  130.         $compiledRoute $route->compile();
  131.         $defaults $route->getDefaults();
  132.         $variables $compiledRoute->getVariables();
  133.         if (isset($defaults['_canonical_route']) && isset($defaults['_locale'])) {
  134.             if (!\in_array('_locale'$variablestrue)) {
  135.                 unset($parameters['_locale']);
  136.             } elseif (!isset($parameters['_locale'])) {
  137.                 $parameters['_locale'] = $defaults['_locale'];
  138.             }
  139.         }
  140.         return $this->doGenerate($variables$defaults$route->getRequirements(), $compiledRoute->getTokens(), $parameters$name$referenceType$compiledRoute->getHostTokens(), $route->getSchemes());
  141.     }
  142.     /**
  143.      * @return string
  144.      *
  145.      * @throws MissingMandatoryParametersException When some parameters are missing that are mandatory for the route
  146.      * @throws InvalidParameterException           When a parameter value for a placeholder is not correct because
  147.      *                                             it does not match the requirement
  148.      */
  149.     protected function doGenerate(array $variables, array $defaults, array $requirements, array $tokens, array $parametersstring $nameint $referenceType, array $hostTokens, array $requiredSchemes = [])
  150.     {
  151.         $variables array_flip($variables);
  152.         $mergedParams array_replace($defaults$this->context->getParameters(), $parameters);
  153.         // all params must be given
  154.         if ($diff array_diff_key($variables$mergedParams)) {
  155.             throw new MissingMandatoryParametersException(sprintf('Some mandatory parameters are missing ("%s") to generate a URL for route "%s".'implode('", "'array_keys($diff)), $name));
  156.         }
  157.         $url '';
  158.         $optional true;
  159.         $message 'Parameter "{parameter}" for route "{route}" must match "{expected}" ("{given}" given) to generate a corresponding URL.';
  160.         foreach ($tokens as $token) {
  161.             if ('variable' === $token[0]) {
  162.                 $varName $token[3];
  163.                 // variable is not important by default
  164.                 $important $token[5] ?? false;
  165.                 if (!$optional || $important || !\array_key_exists($varName$defaults) || (null !== $mergedParams[$varName] && (string) $mergedParams[$varName] !== (string) $defaults[$varName])) {
  166.                     // check requirement (while ignoring look-around patterns)
  167.                     if (null !== $this->strictRequirements && !preg_match('#^'.preg_replace('/\(\?(?:=|<=|!|<!)((?:[^()\\\\]+|\\\\.|\((?1)\))*)\)/'''$token[2]).'$#i'.(empty($token[4]) ? '' 'u'), $mergedParams[$token[3]] ?? '')) {
  168.                         if ($this->strictRequirements) {
  169.                             throw new InvalidParameterException(strtr($message, ['{parameter}' => $varName'{route}' => $name'{expected}' => $token[2], '{given}' => $mergedParams[$varName]]));
  170.                         }
  171.                         if ($this->logger) {
  172.                             $this->logger->error($message, ['parameter' => $varName'route' => $name'expected' => $token[2], 'given' => $mergedParams[$varName]]);
  173.                         }
  174.                         return '';
  175.                     }
  176.                     $url $token[1].$mergedParams[$varName].$url;
  177.                     $optional false;
  178.                 }
  179.             } else {
  180.                 // static text
  181.                 $url $token[1].$url;
  182.                 $optional false;
  183.             }
  184.         }
  185.         if ('' === $url) {
  186.             $url '/';
  187.         }
  188.         // the contexts base URL is already encoded (see Symfony\Component\HttpFoundation\Request)
  189.         $url strtr(rawurlencode($url), $this->decodedChars);
  190.         // the path segments "." and ".." are interpreted as relative reference when resolving a URI; see http://tools.ietf.org/html/rfc3986#section-3.3
  191.         // so we need to encode them as they are not used for this purpose here
  192.         // otherwise we would generate a URI that, when followed by a user agent (e.g. browser), does not match this route
  193.         $url strtr($url, ['/../' => '/%2E%2E/''/./' => '/%2E/']);
  194.         if (str_ends_with($url'/..')) {
  195.             $url substr($url0, -2).'%2E%2E';
  196.         } elseif (str_ends_with($url'/.')) {
  197.             $url substr($url0, -1).'%2E';
  198.         }
  199.         $schemeAuthority '';
  200.         $host $this->context->getHost();
  201.         $scheme $this->context->getScheme();
  202.         if ($requiredSchemes) {
  203.             if (!\in_array($scheme$requiredSchemestrue)) {
  204.                 $referenceType self::ABSOLUTE_URL;
  205.                 $scheme current($requiredSchemes);
  206.             }
  207.         }
  208.         if ($hostTokens) {
  209.             $routeHost '';
  210.             foreach ($hostTokens as $token) {
  211.                 if ('variable' === $token[0]) {
  212.                     // check requirement (while ignoring look-around patterns)
  213.                     if (null !== $this->strictRequirements && !preg_match('#^'.preg_replace('/\(\?(?:=|<=|!|<!)((?:[^()\\\\]+|\\\\.|\((?1)\))*)\)/'''$token[2]).'$#i'.(empty($token[4]) ? '' 'u'), $mergedParams[$token[3]])) {
  214.                         if ($this->strictRequirements) {
  215.                             throw new InvalidParameterException(strtr($message, ['{parameter}' => $token[3], '{route}' => $name'{expected}' => $token[2], '{given}' => $mergedParams[$token[3]]]));
  216.                         }
  217.                         if ($this->logger) {
  218.                             $this->logger->error($message, ['parameter' => $token[3], 'route' => $name'expected' => $token[2], 'given' => $mergedParams[$token[3]]]);
  219.                         }
  220.                         return '';
  221.                     }
  222.                     $routeHost $token[1].$mergedParams[$token[3]].$routeHost;
  223.                 } else {
  224.                     $routeHost $token[1].$routeHost;
  225.                 }
  226.             }
  227.             if ($routeHost !== $host) {
  228.                 $host $routeHost;
  229.                 if (self::ABSOLUTE_URL !== $referenceType) {
  230.                     $referenceType self::NETWORK_PATH;
  231.                 }
  232.             }
  233.         }
  234.         if (self::ABSOLUTE_URL === $referenceType || self::NETWORK_PATH === $referenceType) {
  235.             if ('' !== $host || ('' !== $scheme && 'http' !== $scheme && 'https' !== $scheme)) {
  236.                 $port '';
  237.                 if ('http' === $scheme && 80 !== $this->context->getHttpPort()) {
  238.                     $port ':'.$this->context->getHttpPort();
  239.                 } elseif ('https' === $scheme && 443 !== $this->context->getHttpsPort()) {
  240.                     $port ':'.$this->context->getHttpsPort();
  241.                 }
  242.                 $schemeAuthority self::NETWORK_PATH === $referenceType || '' === $scheme '//' "$scheme://";
  243.                 $schemeAuthority .= $host.$port;
  244.             }
  245.         }
  246.         if (self::RELATIVE_PATH === $referenceType) {
  247.             $url self::getRelativePath($this->context->getPathInfo(), $url);
  248.         } else {
  249.             $url $schemeAuthority.$this->context->getBaseUrl().$url;
  250.         }
  251.         // add a query string if needed
  252.         $extra array_udiff_assoc(array_diff_key($parameters$variables), $defaults, function ($a$b) {
  253.             return $a == $b 1;
  254.         });
  255.         array_walk_recursive($extra$caster = static function (&$v) use (&$caster) {
  256.             if (\is_object($v)) {
  257.                 if ($vars get_object_vars($v)) {
  258.                     array_walk_recursive($vars$caster);
  259.                     $v $vars;
  260.                 } elseif (method_exists($v'__toString')) {
  261.                     $v = (string) $v;
  262.                 }
  263.             }
  264.         });
  265.         // extract fragment
  266.         $fragment $defaults['_fragment'] ?? '';
  267.         if (isset($extra['_fragment'])) {
  268.             $fragment $extra['_fragment'];
  269.             unset($extra['_fragment']);
  270.         }
  271.         if ($extra && $query http_build_query($extra'''&', \PHP_QUERY_RFC3986)) {
  272.             $url .= '?'.strtr($queryself::QUERY_FRAGMENT_DECODED);
  273.         }
  274.         if ('' !== $fragment) {
  275.             $url .= '#'.strtr(rawurlencode($fragment), self::QUERY_FRAGMENT_DECODED);
  276.         }
  277.         return $url;
  278.     }
  279.     /**
  280.      * Returns the target path as relative reference from the base path.
  281.      *
  282.      * Only the URIs path component (no schema, host etc.) is relevant and must be given, starting with a slash.
  283.      * Both paths must be absolute and not contain relative parts.
  284.      * Relative URLs from one resource to another are useful when generating self-contained downloadable document archives.
  285.      * Furthermore, they can be used to reduce the link size in documents.
  286.      *
  287.      * Example target paths, given a base path of "/a/b/c/d":
  288.      * - "/a/b/c/d"     -> ""
  289.      * - "/a/b/c/"      -> "./"
  290.      * - "/a/b/"        -> "../"
  291.      * - "/a/b/c/other" -> "other"
  292.      * - "/a/x/y"       -> "../../x/y"
  293.      *
  294.      * @param string $basePath   The base path
  295.      * @param string $targetPath The target path
  296.      *
  297.      * @return string
  298.      */
  299.     public static function getRelativePath(string $basePathstring $targetPath)
  300.     {
  301.         if ($basePath === $targetPath) {
  302.             return '';
  303.         }
  304.         $sourceDirs explode('/', isset($basePath[0]) && '/' === $basePath[0] ? substr($basePath1) : $basePath);
  305.         $targetDirs explode('/', isset($targetPath[0]) && '/' === $targetPath[0] ? substr($targetPath1) : $targetPath);
  306.         array_pop($sourceDirs);
  307.         $targetFile array_pop($targetDirs);
  308.         foreach ($sourceDirs as $i => $dir) {
  309.             if (isset($targetDirs[$i]) && $dir === $targetDirs[$i]) {
  310.                 unset($sourceDirs[$i], $targetDirs[$i]);
  311.             } else {
  312.                 break;
  313.             }
  314.         }
  315.         $targetDirs[] = $targetFile;
  316.         $path str_repeat('../', \count($sourceDirs)).implode('/'$targetDirs);
  317.         // A reference to the same base directory or an empty subdirectory must be prefixed with "./".
  318.         // This also applies to a segment with a colon character (e.g., "file:colon") that cannot be used
  319.         // as the first segment of a relative-path reference, as it would be mistaken for a scheme name
  320.         // (see http://tools.ietf.org/html/rfc3986#section-4.2).
  321.         return '' === $path || '/' === $path[0]
  322.             || false !== ($colonPos strpos($path':')) && ($colonPos < ($slashPos strpos($path'/')) || false === $slashPos)
  323.             ? "./$path$path;
  324.     }
  325. }