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

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.         // the following chars are general delimiters in the URI specification but have only special meaning in the authority component
  61.         // so they can safely be used in the path in unencoded form
  62.         '%40' => '@',
  63.         '%3A' => ':',
  64.         // these chars are only sub-delimiters that have no predefined meaning and can therefore be used literally
  65.         // so URI producing applications can use these chars to delimit subcomponents in a path segment without being encoded for better readability
  66.         '%3B' => ';',
  67.         '%2C' => ',',
  68.         '%3D' => '=',
  69.         '%2B' => '+',
  70.         '%21' => '!',
  71.         '%2A' => '*',
  72.         '%7C' => '|',
  73.     ];
  74.     public function __construct(RouteCollection $routesRequestContext $contextLoggerInterface $logger nullstring $defaultLocale null)
  75.     {
  76.         $this->routes $routes;
  77.         $this->context $context;
  78.         $this->logger $logger;
  79.         $this->defaultLocale $defaultLocale;
  80.     }
  81.     /**
  82.      * {@inheritdoc}
  83.      */
  84.     public function setContext(RequestContext $context)
  85.     {
  86.         $this->context $context;
  87.     }
  88.     /**
  89.      * {@inheritdoc}
  90.      */
  91.     public function getContext()
  92.     {
  93.         return $this->context;
  94.     }
  95.     /**
  96.      * {@inheritdoc}
  97.      */
  98.     public function setStrictRequirements($enabled)
  99.     {
  100.         $this->strictRequirements null === $enabled null : (bool) $enabled;
  101.     }
  102.     /**
  103.      * {@inheritdoc}
  104.      */
  105.     public function isStrictRequirements()
  106.     {
  107.         return $this->strictRequirements;
  108.     }
  109.     /**
  110.      * {@inheritdoc}
  111.      */
  112.     public function generate($name$parameters = [], $referenceType self::ABSOLUTE_PATH)
  113.     {
  114.         $route null;
  115.         $locale $parameters['_locale']
  116.             ?? $this->context->getParameter('_locale')
  117.             ?: $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 $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.      * @return string
  147.      */
  148.     protected function doGenerate($variables$defaults$requirements$tokens$parameters$name$referenceType$hostTokens, array $requiredSchemes = [])
  149.     {
  150.         $variables array_flip($variables);
  151.         $mergedParams array_replace($defaults$this->context->getParameters(), $parameters);
  152.         // all params must be given
  153.         if ($diff array_diff_key($variables$mergedParams)) {
  154.             throw new MissingMandatoryParametersException(sprintf('Some mandatory parameters are missing ("%s") to generate a URL for route "%s".'implode('", "'array_keys($diff)), $name));
  155.         }
  156.         $url '';
  157.         $optional true;
  158.         $message 'Parameter "{parameter}" for route "{route}" must match "{expected}" ("{given}" given) to generate a corresponding URL.';
  159.         foreach ($tokens as $token) {
  160.             if ('variable' === $token[0]) {
  161.                 $varName $token[3];
  162.                 // variable is not important by default
  163.                 $important $token[5] ?? false;
  164.                 if (!$optional || $important || !\array_key_exists($varName$defaults) || (null !== $mergedParams[$varName] && (string) $mergedParams[$varName] !== (string) $defaults[$varName])) {
  165.                     // check requirement (while ignoring look-around patterns)
  166.                     if (null !== $this->strictRequirements && !preg_match('#^'.preg_replace('/\(\?(?:=|<=|!|<!)((?:[^()\\\\]+|\\\\.|\((?1)\))*)\)/'''$token[2]).'$#i'.(empty($token[4]) ? '' 'u'), $mergedParams[$token[3]])) {
  167.                         if ($this->strictRequirements) {
  168.                             throw new InvalidParameterException(strtr($message, ['{parameter}' => $varName'{route}' => $name'{expected}' => $token[2], '{given}' => $mergedParams[$varName]]));
  169.                         }
  170.                         if ($this->logger) {
  171.                             $this->logger->error($message, ['parameter' => $varName'route' => $name'expected' => $token[2], 'given' => $mergedParams[$varName]]);
  172.                         }
  173.                         return '';
  174.                     }
  175.                     $url $token[1].$mergedParams[$varName].$url;
  176.                     $optional false;
  177.                 }
  178.             } else {
  179.                 // static text
  180.                 $url $token[1].$url;
  181.                 $optional false;
  182.             }
  183.         }
  184.         if ('' === $url) {
  185.             $url '/';
  186.         }
  187.         // the contexts base URL is already encoded (see Symfony\Component\HttpFoundation\Request)
  188.         $url strtr(rawurlencode($url), $this->decodedChars);
  189.         // the path segments "." and ".." are interpreted as relative reference when resolving a URI; see http://tools.ietf.org/html/rfc3986#section-3.3
  190.         // so we need to encode them as they are not used for this purpose here
  191.         // otherwise we would generate a URI that, when followed by a user agent (e.g. browser), does not match this route
  192.         $url strtr($url, ['/../' => '/%2E%2E/''/./' => '/%2E/']);
  193.         if ('/..' === substr($url, -3)) {
  194.             $url substr($url0, -2).'%2E%2E';
  195.         } elseif ('/.' === substr($url, -2)) {
  196.             $url substr($url0, -1).'%2E';
  197.         }
  198.         $schemeAuthority '';
  199.         $host $this->context->getHost();
  200.         $scheme $this->context->getScheme();
  201.         if ($requiredSchemes) {
  202.             if (!\in_array($scheme$requiredSchemestrue)) {
  203.                 $referenceType self::ABSOLUTE_URL;
  204.                 $scheme current($requiredSchemes);
  205.             }
  206.         }
  207.         if ($hostTokens) {
  208.             $routeHost '';
  209.             foreach ($hostTokens as $token) {
  210.                 if ('variable' === $token[0]) {
  211.                     // check requirement (while ignoring look-around patterns)
  212.                     if (null !== $this->strictRequirements && !preg_match('#^'.preg_replace('/\(\?(?:=|<=|!|<!)((?:[^()\\\\]+|\\\\.|\((?1)\))*)\)/'''$token[2]).'$#i'.(empty($token[4]) ? '' 'u'), $mergedParams[$token[3]])) {
  213.                         if ($this->strictRequirements) {
  214.                             throw new InvalidParameterException(strtr($message, ['{parameter}' => $token[3], '{route}' => $name'{expected}' => $token[2], '{given}' => $mergedParams[$token[3]]]));
  215.                         }
  216.                         if ($this->logger) {
  217.                             $this->logger->error($message, ['parameter' => $token[3], 'route' => $name'expected' => $token[2], 'given' => $mergedParams[$token[3]]]);
  218.                         }
  219.                         return '';
  220.                     }
  221.                     $routeHost $token[1].$mergedParams[$token[3]].$routeHost;
  222.                 } else {
  223.                     $routeHost $token[1].$routeHost;
  224.                 }
  225.             }
  226.             if ($routeHost !== $host) {
  227.                 $host $routeHost;
  228.                 if (self::ABSOLUTE_URL !== $referenceType) {
  229.                     $referenceType self::NETWORK_PATH;
  230.                 }
  231.             }
  232.         }
  233.         if (self::ABSOLUTE_URL === $referenceType || self::NETWORK_PATH === $referenceType) {
  234.             if ('' !== $host || ('' !== $scheme && 'http' !== $scheme && 'https' !== $scheme)) {
  235.                 $port '';
  236.                 if ('http' === $scheme && 80 !== $this->context->getHttpPort()) {
  237.                     $port ':'.$this->context->getHttpPort();
  238.                 } elseif ('https' === $scheme && 443 !== $this->context->getHttpsPort()) {
  239.                     $port ':'.$this->context->getHttpsPort();
  240.                 }
  241.                 $schemeAuthority self::NETWORK_PATH === $referenceType || '' === $scheme '//' "$scheme://";
  242.                 $schemeAuthority .= $host.$port;
  243.             }
  244.         }
  245.         if (self::RELATIVE_PATH === $referenceType) {
  246.             $url self::getRelativePath($this->context->getPathInfo(), $url);
  247.         } else {
  248.             $url $schemeAuthority.$this->context->getBaseUrl().$url;
  249.         }
  250.         // add a query string if needed
  251.         $extra array_udiff_assoc(array_diff_key($parameters$variables), $defaults, function ($a$b) {
  252.             return $a == $b 1;
  253.         });
  254.         // extract fragment
  255.         $fragment $defaults['_fragment'] ?? '';
  256.         if (isset($extra['_fragment'])) {
  257.             $fragment $extra['_fragment'];
  258.             unset($extra['_fragment']);
  259.         }
  260.         if ($extra && $query http_build_query($extra'''&', \PHP_QUERY_RFC3986)) {
  261.             $url .= '?'.strtr($queryself::QUERY_FRAGMENT_DECODED);
  262.         }
  263.         if ('' !== $fragment) {
  264.             $url .= '#'.strtr(rawurlencode($fragment), self::QUERY_FRAGMENT_DECODED);
  265.         }
  266.         return $url;
  267.     }
  268.     /**
  269.      * Returns the target path as relative reference from the base path.
  270.      *
  271.      * Only the URIs path component (no schema, host etc.) is relevant and must be given, starting with a slash.
  272.      * Both paths must be absolute and not contain relative parts.
  273.      * Relative URLs from one resource to another are useful when generating self-contained downloadable document archives.
  274.      * Furthermore, they can be used to reduce the link size in documents.
  275.      *
  276.      * Example target paths, given a base path of "/a/b/c/d":
  277.      * - "/a/b/c/d"     -> ""
  278.      * - "/a/b/c/"      -> "./"
  279.      * - "/a/b/"        -> "../"
  280.      * - "/a/b/c/other" -> "other"
  281.      * - "/a/x/y"       -> "../../x/y"
  282.      *
  283.      * @param string $basePath   The base path
  284.      * @param string $targetPath The target path
  285.      *
  286.      * @return string The relative target path
  287.      */
  288.     public static function getRelativePath($basePath$targetPath)
  289.     {
  290.         if ($basePath === $targetPath) {
  291.             return '';
  292.         }
  293.         $sourceDirs explode('/', isset($basePath[0]) && '/' === $basePath[0] ? substr($basePath1) : $basePath);
  294.         $targetDirs explode('/', isset($targetPath[0]) && '/' === $targetPath[0] ? substr($targetPath1) : $targetPath);
  295.         array_pop($sourceDirs);
  296.         $targetFile array_pop($targetDirs);
  297.         foreach ($sourceDirs as $i => $dir) {
  298.             if (isset($targetDirs[$i]) && $dir === $targetDirs[$i]) {
  299.                 unset($sourceDirs[$i], $targetDirs[$i]);
  300.             } else {
  301.                 break;
  302.             }
  303.         }
  304.         $targetDirs[] = $targetFile;
  305.         $path str_repeat('../', \count($sourceDirs)).implode('/'$targetDirs);
  306.         // A reference to the same base directory or an empty subdirectory must be prefixed with "./".
  307.         // This also applies to a segment with a colon character (e.g., "file:colon") that cannot be used
  308.         // as the first segment of a relative-path reference, as it would be mistaken for a scheme name
  309.         // (see http://tools.ietf.org/html/rfc3986#section-4.2).
  310.         return '' === $path || '/' === $path[0]
  311.             || false !== ($colonPos strpos($path':')) && ($colonPos < ($slashPos strpos($path'/')) || false === $slashPos)
  312.             ? "./$path$path;
  313.     }
  314. }