UrlGenerator.php

Namespace

Symfony\Component\Routing\Generator

File

drupal/core/vendor/symfony/routing/Symfony/Component/Routing/Generator/UrlGenerator.php
View source
<?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>
 *
 * @api
 */
class UrlGenerator implements UrlGeneratorInterface, ConfigurableRequirementsInterface {

  /**
   * @var RouteCollection
   */
  protected $routes;

  /**
   * @var RequestContext
   */
  protected $context;

  /**
   * @var Boolean|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
   *
   * @api
   */
  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());
  }

  /**
   * @throws MissingMandatoryParametersException When some parameters are missing that 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) {
    $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 null;
          }
          $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 (isset($requirements['_scheme']) && ($req = strtolower($requirements['_scheme'])) && $scheme !== $req) {
        $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] . '$#', $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 null;
            }
            $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
    $extra = array_diff_key($parameters, $variables, $defaults);
    if ($extra && ($query = http_build_query($extra, '', '&'))) {
      $url .= '?' . $query;
    }
    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;
  }

}

Classes

Namesort descending Description
UrlGenerator UrlGenerator can generate a URL or a path for any route in the RouteCollection based on the passed parameters.