Same name and namespace in other branches
  1. 8.9.x core/lib/Drupal/Core/Utility/UnroutedUrlAssembler.php \Drupal\Core\Utility\UnroutedUrlAssembler
  2. 9 core/lib/Drupal/Core/Utility/UnroutedUrlAssembler.php \Drupal\Core\Utility\UnroutedUrlAssembler

Provides a way to build external or non Drupal local domain URLs.

It takes into account configured safe HTTP protocols.

Hierarchy

  • class \Drupal\Core\Utility\UnroutedUrlAssembler implements \Drupal\Core\Utility\UnroutedUrlAssemblerInterface

Expanded class hierarchy of UnroutedUrlAssembler

1 file declares its use of UnroutedUrlAssembler
FieldPluginBaseTest.php in core/modules/views/tests/src/Unit/Plugin/field/FieldPluginBaseTest.php
1 string reference to 'UnroutedUrlAssembler'
core.services.yml in core/core.services.yml
core/core.services.yml
1 service uses UnroutedUrlAssembler
unrouted_url_assembler in core/core.services.yml
Drupal\Core\Utility\UnroutedUrlAssembler

File

core/lib/Drupal/Core/Utility/UnroutedUrlAssembler.php, line 16

Namespace

Drupal\Core\Utility
View source
class UnroutedUrlAssembler implements UnroutedUrlAssemblerInterface {

  /**
   * A request stack object.
   *
   * @var \Symfony\Component\HttpFoundation\RequestStack
   */
  protected $requestStack;

  /**
   * The outbound path processor.
   *
   * @var \Drupal\Core\PathProcessor\OutboundPathProcessorInterface
   */
  protected $pathProcessor;

  /**
   * Constructs a new unroutedUrlAssembler object.
   *
   * @param \Symfony\Component\HttpFoundation\RequestStack $request_stack
   *   A request stack object.
   * @param \Drupal\Core\PathProcessor\OutboundPathProcessorInterface $path_processor
   *   The output path processor.
   * @param string[] $filter_protocols
   *   (optional) An array of protocols allowed for URL generation.
   */
  public function __construct(RequestStack $request_stack, OutboundPathProcessorInterface $path_processor, array $filter_protocols = [
    'http',
    'https',
  ]) {
    UrlHelper::setAllowedProtocols($filter_protocols);
    $this->requestStack = $request_stack;
    $this->pathProcessor = $path_processor;
  }

  /**
   * {@inheritdoc}
   *
   * This is a helper function that calls buildExternalUrl() or buildLocalUrl()
   * based on a check of whether the path is a valid external URL.
   */
  public function assemble($uri, array $options = [], $collect_bubbleable_metadata = FALSE) {

    // Note that UrlHelper::isExternal will return FALSE if the $uri has a
    // disallowed protocol.  This is later made safe since we always add at
    // least a leading slash.
    if (parse_url($uri, PHP_URL_SCHEME) === 'base') {
      return $this
        ->buildLocalUrl($uri, $options, $collect_bubbleable_metadata);
    }
    elseif (UrlHelper::isExternal($uri)) {

      // UrlHelper::isExternal() only returns true for safe protocols.
      return $this
        ->buildExternalUrl($uri, $options, $collect_bubbleable_metadata);
    }
    throw new \InvalidArgumentException("The URI '{$uri}' is invalid. You must use a valid URI scheme. Use base: for a path, e.g., to a Drupal file that needs the base path. Do not use this for internal paths controlled by Drupal.");
  }

  /**
   * {@inheritdoc}
   */
  protected function buildExternalUrl($uri, array $options = [], $collect_bubbleable_metadata = FALSE) {
    $this
      ->addOptionDefaults($options);

    // Split off the query & fragment.
    $parsed = UrlHelper::parse($uri);
    $uri = $parsed['path'];
    $parsed += [
      'query' => [],
    ];
    $options += [
      'query' => [],
    ];
    $options['query'] = NestedArray::mergeDeepArray([
      $parsed['query'],
      $options['query'],
    ], TRUE);
    if ($parsed['fragment'] && !$options['fragment']) {
      $options['fragment'] = '#' . $parsed['fragment'];
    }
    if (isset($options['https'])) {
      if ($options['https'] === TRUE) {
        $uri = str_replace('http://', 'https://', $uri);
      }
      elseif ($options['https'] === FALSE) {
        $uri = str_replace('https://', 'http://', $uri);
      }
    }

    // Append the query.
    if ($options['query']) {
      $uri .= '?' . UrlHelper::buildQuery($options['query']);
    }

    // Reassemble.
    $url = $uri . $options['fragment'];
    return $collect_bubbleable_metadata ? (new GeneratedUrl())
      ->setGeneratedUrl($url) : $url;
  }

  /**
   * {@inheritdoc}
   */
  protected function buildLocalUrl($uri, array $options = [], $collect_bubbleable_metadata = FALSE) {
    $generated_url = $collect_bubbleable_metadata ? new GeneratedUrl() : NULL;
    $this
      ->addOptionDefaults($options);
    $request = $this->requestStack
      ->getCurrentRequest();

    // Remove the base: scheme.
    // @todo Consider using a class constant for this in
    //   https://www.drupal.org/node/2417459
    $uri = substr($uri, 5);

    // Allow (outbound) path processing, if needed. A valid use case is the path
    // alias overview form:
    // @see \Drupal\path\Controller\PathController::adminOverview().
    if (!empty($options['path_processing'])) {

      // Do not pass the request, since this is a special case and we do not
      // want to include e.g. the request language in the processing.
      $uri = $this->pathProcessor
        ->processOutbound($uri, $options, NULL, $generated_url);
    }

    // Strip leading slashes from internal paths to prevent them becoming
    // external URLs without protocol. /example.com should not be turned into
    // //example.com.
    $uri = ltrim($uri, '/');

    // Add any subdirectory where Drupal is installed.
    $current_base_path = $request
      ->getBasePath() . '/';
    if ($options['absolute']) {
      $current_base_url = $request
        ->getSchemeAndHttpHost() . $current_base_path;
      if (isset($options['https'])) {
        if (!empty($options['https'])) {
          $base = str_replace('http://', 'https://', $current_base_url);
          $options['absolute'] = TRUE;
        }
        else {
          $base = str_replace('https://', 'http://', $current_base_url);
          $options['absolute'] = TRUE;
        }
      }
      else {
        $base = $current_base_url;
      }
      if ($collect_bubbleable_metadata) {
        $generated_url
          ->addCacheContexts([
          'url.site',
        ]);
      }
    }
    else {
      $base = $current_base_path;
    }
    $prefix = empty($uri) ? rtrim($options['prefix'], '/') : $options['prefix'];
    $uri = str_replace('%2F', '/', rawurlencode($prefix . $uri));
    $query = $options['query'] ? '?' . UrlHelper::buildQuery($options['query']) : '';
    $url = $base . $options['script'] . $uri . $query . $options['fragment'];
    return $collect_bubbleable_metadata ? $generated_url
      ->setGeneratedUrl($url) : $url;
  }

  /**
   * Merges in default defaults.
   *
   * @param array $options
   *   The options to merge in the defaults.
   */
  protected function addOptionDefaults(array &$options) {
    $request = $this->requestStack
      ->getCurrentRequest();
    $current_base_path = $request
      ->getBasePath() . '/';
    $current_script_path = '';
    $base_path_with_script = $request
      ->getBaseUrl();

    // If the current request was made with the script name (eg, index.php) in
    // it, then extract it, making sure the leading / is gone, and a trailing /
    // is added, to allow simple string concatenation with other parts.
    if (!empty($base_path_with_script)) {
      $script_name = $request
        ->getScriptName();
      if (str_contains($base_path_with_script, $script_name)) {
        $current_script_path = ltrim(substr($script_name, strlen($current_base_path)), '/') . '/';
      }
    }

    // Merge in defaults.
    $options += [
      'fragment' => '',
      'query' => [],
      'absolute' => FALSE,
      'prefix' => '',
      'script' => $current_script_path,
    ];
    if (isset($options['fragment']) && $options['fragment'] !== '') {
      $options['fragment'] = '#' . $options['fragment'];
    }
  }

}

Members

Namesort descending Modifiers Type Description Overrides
UnroutedUrlAssembler::$pathProcessor protected property The outbound path processor.
UnroutedUrlAssembler::$requestStack protected property A request stack object.
UnroutedUrlAssembler::addOptionDefaults protected function Merges in default defaults.
UnroutedUrlAssembler::assemble public function This is a helper function that calls buildExternalUrl() or buildLocalUrl() based on a check of whether the path is a valid external URL.
UnroutedUrlAssembler::buildExternalUrl protected function
UnroutedUrlAssembler::buildLocalUrl protected function
UnroutedUrlAssembler::__construct public function Constructs a new unroutedUrlAssembler object.