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

  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.     public function setContext(RequestContext $context)
  84.     {
  85.         $this->context $context;
  86.     }
  87.     public function getContext(): RequestContext
  88.     {
  89.         return $this->context;
  90.     }
  91.     public function setStrictRequirements(?bool $enabled)
  92.     {
  93.         $this->strictRequirements $enabled;
  94.     }
  95.     public function isStrictRequirements(): ?bool
  96.     {
  97.         return $this->strictRequirements;
  98.     }
  99.     public function generate(string $name, array $parameters = [], int $referenceType self::ABSOLUTE_PATH): string
  100.     {
  101.         $route null;
  102.         $locale $parameters['_locale'] ?? $this->context->getParameter('_locale') ?: $this->defaultLocale;
  103.         if (null !== $locale) {
  104.             do {
  105.                 if (null !== ($route $this->routes->get($name.'.'.$locale)) && $route->getDefault('_canonical_route') === $name) {
  106.                     break;
  107.                 }
  108.             } while (false !== $locale strstr($locale'_'true));
  109.         }
  110.         if (null === $route ??= $this->routes->get($name)) {
  111.             throw new RouteNotFoundException(sprintf('Unable to generate a URL for the named route "%s" as such route does not exist.'$name));
  112.         }
  113.         // the Route has a cache of its own and is not recompiled as long as it does not get modified
  114.         $compiledRoute $route->compile();
  115.         $defaults $route->getDefaults();
  116.         $variables $compiledRoute->getVariables();
  117.         if (isset($defaults['_canonical_route']) && isset($defaults['_locale'])) {
  118.             if (!\in_array('_locale'$variablestrue)) {
  119.                 unset($parameters['_locale']);
  120.             } elseif (!isset($parameters['_locale'])) {
  121.                 $parameters['_locale'] = $defaults['_locale'];
  122.             }
  123.         }
  124.         return $this->doGenerate($variables$defaults$route->getRequirements(), $compiledRoute->getTokens(), $parameters$name$referenceType$compiledRoute->getHostTokens(), $route->getSchemes());
  125.     }
  126.     /**
  127.      * @throws MissingMandatoryParametersException When some parameters are missing that are mandatory for the route
  128.      * @throws InvalidParameterException           When a parameter value for a placeholder is not correct because
  129.      *                                             it does not match the requirement
  130.      */
  131.     protected function doGenerate(array $variables, array $defaults, array $requirements, array $tokens, array $parametersstring $nameint $referenceType, array $hostTokens, array $requiredSchemes = []): string
  132.     {
  133.         $variables array_flip($variables);
  134.         $mergedParams array_replace($defaults$this->context->getParameters(), $parameters);
  135.         // all params must be given
  136.         if ($diff array_diff_key($variables$mergedParams)) {
  137.             throw new MissingMandatoryParametersException($namearray_keys($diff));
  138.         }
  139.         $url '';
  140.         $optional true;
  141.         $message 'Parameter "{parameter}" for route "{route}" must match "{expected}" ("{given}" given) to generate a corresponding URL.';
  142.         foreach ($tokens as $token) {
  143.             if ('variable' === $token[0]) {
  144.                 $varName $token[3];
  145.                 // variable is not important by default
  146.                 $important $token[5] ?? false;
  147.                 if (!$optional || $important || !\array_key_exists($varName$defaults) || (null !== $mergedParams[$varName] && (string) $mergedParams[$varName] !== (string) $defaults[$varName])) {
  148.                     // check requirement (while ignoring look-around patterns)
  149.                     if (null !== $this->strictRequirements && !preg_match('#^'.preg_replace('/\(\?(?:=|<=|!|<!)((?:[^()\\\\]+|\\\\.|\((?1)\))*)\)/'''$token[2]).'$#i'.(empty($token[4]) ? '' 'u'), $mergedParams[$token[3]] ?? '')) {
  150.                         if ($this->strictRequirements) {
  151.                             throw new InvalidParameterException(strtr($message, ['{parameter}' => $varName'{route}' => $name'{expected}' => $token[2], '{given}' => $mergedParams[$varName]]));
  152.                         }
  153.                         $this->logger?->error($message, ['parameter' => $varName'route' => $name'expected' => $token[2], 'given' => $mergedParams[$varName]]);
  154.                         return '';
  155.                     }
  156.                     $url $token[1].$mergedParams[$varName].$url;
  157.                     $optional false;
  158.                 }
  159.             } else {
  160.                 // static text
  161.                 $url $token[1].$url;
  162.                 $optional false;
  163.             }
  164.         }
  165.         if ('' === $url) {
  166.             $url '/';
  167.         }
  168.         // the contexts base URL is already encoded (see Symfony\Component\HttpFoundation\Request)
  169.         $url strtr(rawurlencode($url), $this->decodedChars);
  170.         // the path segments "." and ".." are interpreted as relative reference when resolving a URI; see http://tools.ietf.org/html/rfc3986#section-3.3
  171.         // so we need to encode them as they are not used for this purpose here
  172.         // otherwise we would generate a URI that, when followed by a user agent (e.g. browser), does not match this route
  173.         $url strtr($url, ['/../' => '/%2E%2E/''/./' => '/%2E/']);
  174.         if (str_ends_with($url'/..')) {
  175.             $url substr($url0, -2).'%2E%2E';
  176.         } elseif (str_ends_with($url'/.')) {
  177.             $url substr($url0, -1).'%2E';
  178.         }
  179.         $schemeAuthority '';
  180.         $host $this->context->getHost();
  181.         $scheme $this->context->getScheme();
  182.         if ($requiredSchemes) {
  183.             if (!\in_array($scheme$requiredSchemestrue)) {
  184.                 $referenceType self::ABSOLUTE_URL;
  185.                 $scheme current($requiredSchemes);
  186.             }
  187.         }
  188.         if ($hostTokens) {
  189.             $routeHost '';
  190.             foreach ($hostTokens as $token) {
  191.                 if ('variable' === $token[0]) {
  192.                     // check requirement (while ignoring look-around patterns)
  193.                     if (null !== $this->strictRequirements && !preg_match('#^'.preg_replace('/\(\?(?:=|<=|!|<!)((?:[^()\\\\]+|\\\\.|\((?1)\))*)\)/'''$token[2]).'$#i'.(empty($token[4]) ? '' 'u'), $mergedParams[$token[3]])) {
  194.                         if ($this->strictRequirements) {
  195.                             throw new InvalidParameterException(strtr($message, ['{parameter}' => $token[3], '{route}' => $name'{expected}' => $token[2], '{given}' => $mergedParams[$token[3]]]));
  196.                         }
  197.                         $this->logger?->error($message, ['parameter' => $token[3], 'route' => $name'expected' => $token[2], 'given' => $mergedParams[$token[3]]]);
  198.                         return '';
  199.                     }
  200.                     $routeHost $token[1].$mergedParams[$token[3]].$routeHost;
  201.                 } else {
  202.                     $routeHost $token[1].$routeHost;
  203.                 }
  204.             }
  205.             if ($routeHost !== $host) {
  206.                 $host $routeHost;
  207.                 if (self::ABSOLUTE_URL !== $referenceType) {
  208.                     $referenceType self::NETWORK_PATH;
  209.                 }
  210.             }
  211.         }
  212.         if (self::ABSOLUTE_URL === $referenceType || self::NETWORK_PATH === $referenceType) {
  213.             if ('' !== $host || ('' !== $scheme && 'http' !== $scheme && 'https' !== $scheme)) {
  214.                 $port '';
  215.                 if ('http' === $scheme && 80 !== $this->context->getHttpPort()) {
  216.                     $port ':'.$this->context->getHttpPort();
  217.                 } elseif ('https' === $scheme && 443 !== $this->context->getHttpsPort()) {
  218.                     $port ':'.$this->context->getHttpsPort();
  219.                 }
  220.                 $schemeAuthority self::NETWORK_PATH === $referenceType || '' === $scheme '//' "$scheme://";
  221.                 $schemeAuthority .= $host.$port;
  222.             }
  223.         }
  224.         if (self::RELATIVE_PATH === $referenceType) {
  225.             $url self::getRelativePath($this->context->getPathInfo(), $url);
  226.         } else {
  227.             $url $schemeAuthority.$this->context->getBaseUrl().$url;
  228.         }
  229.         // add a query string if needed
  230.         $extra array_udiff_assoc(array_diff_key($parameters$variables), $defaults, function ($a$b) {
  231.             return $a == $b 1;
  232.         });
  233.         array_walk_recursive($extra$caster = static function (&$v) use (&$caster) {
  234.             if (\is_object($v)) {
  235.                 if ($vars get_object_vars($v)) {
  236.                     array_walk_recursive($vars$caster);
  237.                     $v $vars;
  238.                 } elseif (method_exists($v'__toString')) {
  239.                     $v = (string) $v;
  240.                 }
  241.             }
  242.         });
  243.         // extract fragment
  244.         $fragment $defaults['_fragment'] ?? '';
  245.         if (isset($extra['_fragment'])) {
  246.             $fragment $extra['_fragment'];
  247.             unset($extra['_fragment']);
  248.         }
  249.         if ($extra && $query http_build_query($extra'''&'\PHP_QUERY_RFC3986)) {
  250.             $url .= '?'.strtr($queryself::QUERY_FRAGMENT_DECODED);
  251.         }
  252.         if ('' !== $fragment) {
  253.             $url .= '#'.strtr(rawurlencode($fragment), self::QUERY_FRAGMENT_DECODED);
  254.         }
  255.         return $url;
  256.     }
  257.     /**
  258.      * Returns the target path as relative reference from the base path.
  259.      *
  260.      * Only the URIs path component (no schema, host etc.) is relevant and must be given, starting with a slash.
  261.      * Both paths must be absolute and not contain relative parts.
  262.      * Relative URLs from one resource to another are useful when generating self-contained downloadable document archives.
  263.      * Furthermore, they can be used to reduce the link size in documents.
  264.      *
  265.      * Example target paths, given a base path of "/a/b/c/d":
  266.      * - "/a/b/c/d"     -> ""
  267.      * - "/a/b/c/"      -> "./"
  268.      * - "/a/b/"        -> "../"
  269.      * - "/a/b/c/other" -> "other"
  270.      * - "/a/x/y"       -> "../../x/y"
  271.      *
  272.      * @param string $basePath   The base path
  273.      * @param string $targetPath The target path
  274.      */
  275.     public static function getRelativePath(string $basePathstring $targetPath): string
  276.     {
  277.         if ($basePath === $targetPath) {
  278.             return '';
  279.         }
  280.         $sourceDirs explode('/', isset($basePath[0]) && '/' === $basePath[0] ? substr($basePath1) : $basePath);
  281.         $targetDirs explode('/', isset($targetPath[0]) && '/' === $targetPath[0] ? substr($targetPath1) : $targetPath);
  282.         array_pop($sourceDirs);
  283.         $targetFile array_pop($targetDirs);
  284.         foreach ($sourceDirs as $i => $dir) {
  285.             if (isset($targetDirs[$i]) && $dir === $targetDirs[$i]) {
  286.                 unset($sourceDirs[$i], $targetDirs[$i]);
  287.             } else {
  288.                 break;
  289.             }
  290.         }
  291.         $targetDirs[] = $targetFile;
  292.         $path str_repeat('../'\count($sourceDirs)).implode('/'$targetDirs);
  293.         // A reference to the same base directory or an empty subdirectory must be prefixed with "./".
  294.         // This also applies to a segment with a colon character (e.g., "file:colon") that cannot be used
  295.         // as the first segment of a relative-path reference, as it would be mistaken for a scheme name
  296.         // (see http://tools.ietf.org/html/rfc3986#section-4.2).
  297.         return '' === $path || '/' === $path[0]
  298.             || false !== ($colonPos strpos($path':')) && ($colonPos < ($slashPos strpos($path'/')) || false === $slashPos)
  299.             ? "./$path$path;
  300.     }
  301. }