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

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