From 31022924e123319b2db3aecded0ec60afe761a97 Mon Sep 17 00:00:00 2001
From: Alex Pott <alex.a.pott@googlemail.com>
Date: Tue, 30 Sep 2014 08:50:14 +0200
Subject: [PATCH] Issue #2343759 by pwolanin, larowlan, dawehner, tim.plunkett,
 effulgentsia, xjm, Wim Leers: Provide an API function to replace url()/l()
 for external urls.

---
 core/includes/batch.inc                       |  15 +-
 core/includes/install.core.inc                |   5 +-
 core/lib/Drupal/Core/Menu/MenuLinkBase.php    |   4 +-
 core/lib/Drupal/Core/Path/PathValidator.php   |   2 +-
 core/lib/Drupal/Core/Render/Element/Link.php  |   1 +
 core/lib/Drupal/Core/Url.php                  | 263 +++++++++++++-----
 .../Core/Utility/UnroutedUrlAssembler.php     |   2 +-
 .../Utility/UnroutedUrlAssemblerInterface.php |  20 +-
 core/modules/aggregator/aggregator.module     |   2 +
 core/modules/aggregator/src/Entity/Item.php   |   2 +-
 core/modules/field_ui/src/FieldUI.php         |   6 +-
 core/modules/link/link.module                 |   2 +-
 .../Field/FieldFormatter/LinkFormatter.php    |   8 +-
 .../src/Entity/MenuLinkContent.php            |   2 +-
 .../src/Form/MenuLinkContentForm.php          |   2 +-
 .../rdf/src/Tests/Field/LinkFieldRdfaTest.php |   6 +-
 core/modules/shortcut/src/Entity/Shortcut.php |  11 +-
 core/modules/system/system.module             |   2 +-
 .../src/Form/FormTestRedirectForm.php         |   2 +-
 core/modules/toolbar/toolbar.module           |   2 +-
 core/modules/update/update.report.inc         |   3 +-
 core/modules/user/user.module                 |  14 +-
 core/modules/views_ui/src/ViewEditForm.php    |   7 +-
 .../Drupal/Tests/Core/ExternalUrlTest.php     | 190 -------------
 .../Drupal/Tests/Core/UnroutedUrlTest.php     | 225 +++++++++++++++
 core/tests/Drupal/Tests/Core/UrlTest.php      |  94 +++----
 .../Tests/Core/Utility/LinkGeneratorTest.php  |  20 +-
 .../Core/Utility/UnroutedUrlAssemblerTest.php |   2 +-
 28 files changed, 553 insertions(+), 361 deletions(-)
 delete mode 100644 core/tests/Drupal/Tests/Core/ExternalUrlTest.php
 create mode 100644 core/tests/Drupal/Tests/Core/UnroutedUrlTest.php

diff --git a/core/includes/batch.inc b/core/includes/batch.inc
index 7e6e12f65cb3..bbb43f40e737 100644
--- a/core/includes/batch.inc
+++ b/core/includes/batch.inc
@@ -452,11 +452,18 @@ function _batch_finished() {
     if ($_batch['form_state']->getRedirect() === NULL) {
       $redirect = $_batch['batch_redirect'] ?: $_batch['source_url'];
       $options = UrlHelper::parse($redirect);
-      if (!UrlHelper::isExternal($options['path'])) {
-        $options['path'] = $GLOBALS['base_url'] . '/' . $options['path'];
+      // Any path with a scheme does not correspond to a route.
+      if (parse_url($options['path'], PHP_URL_SCHEME)) {
+        $redirect = Url::fromUri($options['path'], $options);
+      }
+      else {
+        $redirect = \Drupal::pathValidator()->getUrlIfValid($options['path']);
+        if (!$redirect) {
+          // Stay on the same page if the redirect was invalid.
+          $redirect = Url::fromRoute('<current>');
+        }
+        $redirect->setOptions($options);
       }
-      $redirect = Url::createFromPath($options['path']);
-      $redirect->setOptions($options);
       $_batch['form_state']->setRedirectUrl($redirect);
     }
 
diff --git a/core/includes/install.core.inc b/core/includes/install.core.inc
index 5a6859d4df0f..7c48116d9d0a 100644
--- a/core/includes/install.core.inc
+++ b/core/includes/install.core.inc
@@ -543,7 +543,10 @@ function install_run_task($task, &$install_state) {
       }
       // Process the batch. For progressive batches, this will redirect.
       // Otherwise, the batch will complete.
-      $response = batch_process(install_redirect_url($install_state), install_full_redirect_url($install_state));
+      // install_redirect_url() returns core/install.php, so let's ensure to
+      // drop it from it and use base:// as batch_process() is using the
+      // unrouted URL assembler, which requires base://.
+      $response = batch_process(preg_replace('@^core/@', 'base://', install_redirect_url($install_state)), install_full_redirect_url($install_state));
       if ($response instanceof Response) {
         // Save $_SESSION data from batch.
         \Drupal::service('session_manager')->save();
diff --git a/core/lib/Drupal/Core/Menu/MenuLinkBase.php b/core/lib/Drupal/Core/Menu/MenuLinkBase.php
index b09bfa92ecae..c82bef3dced6 100644
--- a/core/lib/Drupal/Core/Menu/MenuLinkBase.php
+++ b/core/lib/Drupal/Core/Menu/MenuLinkBase.php
@@ -141,9 +141,7 @@ public function getUrlObject($title_attribute = TRUE) {
       return new Url($this->pluginDefinition['route_name'], $this->pluginDefinition['route_parameters'], $options);
     }
     else {
-      $url = Url::createFromPath($this->pluginDefinition['url']);
-      $url->setOptions($options);
-      return $url;
+      return Url::fromUri($this->pluginDefinition['url'], $options);
     }
   }
 
diff --git a/core/lib/Drupal/Core/Path/PathValidator.php b/core/lib/Drupal/Core/Path/PathValidator.php
index 1b519c733609..e6ebf5d4ef5e 100644
--- a/core/lib/Drupal/Core/Path/PathValidator.php
+++ b/core/lib/Drupal/Core/Path/PathValidator.php
@@ -99,7 +99,7 @@ public function getUrlIfValid($path) {
       if (empty($parsed_url['path'])) {
         return FALSE;
       }
-      return Url::createFromPath($path);
+      return Url::fromUri($path);
     }
 
     $path = ltrim($path, '/');
diff --git a/core/lib/Drupal/Core/Render/Element/Link.php b/core/lib/Drupal/Core/Render/Element/Link.php
index 77ee795eb9cb..7e10976dc93c 100644
--- a/core/lib/Drupal/Core/Render/Element/Link.php
+++ b/core/lib/Drupal/Core/Render/Element/Link.php
@@ -88,6 +88,7 @@ public static function preRenderLink($element) {
       $element['#markup'] = \Drupal::l($element['#title'], new UrlObject($element['#route_name'], $element['#route_parameters'], $element['#options']));
     }
     else {
+      // @todo Convert to \Drupal::l(): https://www.drupal.org/node/2347045.
       $element['#markup'] = l($element['#title'], $element['#href'], $element['#options']);
     }
     return $element;
diff --git a/core/lib/Drupal/Core/Url.php b/core/lib/Drupal/Core/Url.php
index d97f017db9f0..a6f0220710fa 100644
--- a/core/lib/Drupal/Core/Url.php
+++ b/core/lib/Drupal/Core/Url.php
@@ -7,10 +7,10 @@
 
 namespace Drupal\Core;
 
-use Drupal\Component\Utility\UrlHelper;
 use Drupal\Core\DependencyInjection\DependencySerializationTrait;
 use Drupal\Core\Routing\UrlGeneratorInterface;
 use Drupal\Core\Session\AccountInterface;
+use Drupal\Core\Utility\UnroutedUrlAssemblerInterface;
 use Symfony\Cmf\Component\Routing\RouteObjectInterface;
 use Symfony\Component\HttpFoundation\Request;
 
@@ -27,6 +27,13 @@ class Url {
    */
   protected $urlGenerator;
 
+  /**
+   * The unrouted URL assembler.
+   *
+   * @var \Drupal\Core\Utility\UnroutedUrlAssemblerInterface
+   */
+  protected $urlAssembler;
+
   /**
    * The access manager
    *
@@ -56,24 +63,35 @@ class Url {
   protected $options = array();
 
   /**
-   * Indicates whether this URL is external.
+   * Indicates whether this object contains an external URL.
    *
    * @var bool
    */
   protected $external = FALSE;
 
   /**
-   * The external path.
+   * Indicates whether this URL is for a URI without a Drupal route.
+   *
+   * @var bool
+   */
+  protected $unrouted = FALSE;
+
+  /**
+   * The non-route URI.
    *
-   * Only used if self::$external is TRUE.
+   * Only used if self::$unrouted is TRUE.
    *
    * @var string
    */
-  protected $path;
+  protected $uri;
 
   /**
    * Constructs a new Url object.
    *
+   * In most cases, use Url::fromRoute() or Url::fromUri() rather than
+   * constructing Url objects directly in order to avoid ambiguity and make your
+   * code more self-documenting.
+   *
    * @param string $route_name
    *   The name of the route
    * @param array $route_parameters
@@ -95,6 +113,12 @@ class Url {
    *     defined, the current scheme is used, so the user stays on HTTP or HTTPS
    *     respectively. if mixed mode sessions are permitted, TRUE enforces HTTPS
    *     and FALSE enforces HTTP.
+   *
+   * @see static::fromRoute()
+   * @see static::fromUri()
+   *
+   * @todo Update this documentation for non-routed URIs in
+   *   https://www.drupal.org/node/2346787
    */
   public function __construct($route_name, $route_parameters = array(), $options = array()) {
     $this->routeName = $route_name;
@@ -103,44 +127,101 @@ public function __construct($route_name, $route_parameters = array(), $options =
   }
 
   /**
-   * Returns the Url object matching a path. READ THE FOLLOWING SECURITY NOTE.
+   * Creates a new Url object for a URL that has a Drupal route.
    *
-   * SECURITY NOTE: The path is not checked to be valid and accessible by the
-   * current user to allow storing and reusing Url objects by different users.
-   * The 'path.validator' service getUrlIfValid() method should be used instead
-   * of this one if validation and access check is desired. Otherwise,
-   * 'access_manager' service checkNamedRoute() method should be used on the
-   * router name and parameters stored in the Url object returned by this
-   * method.
+   * This method is for URLs that have Drupal routes (that is, most pages
+   * generated by Drupal). For non-routed local URIs relative to the base
+   * path (like robots.txt) use Url::fromUri() with the base:// scheme.
    *
-   * @param string $path
-   *   A path (e.g. 'node/1', 'http://drupal.org').
+   * @param string $route_name
+   *   The name of the route
+   * @param array $route_parameters
+   *   (optional) An associative array of route parameter names and values.
+   * @param array $options
+   *   (optional) An associative array of additional URL options, with the
+   *   following elements:
+   *   - 'query': An array of query key/value-pairs (without any URL-encoding)
+   *     to append to the URL. Merged with the parameters array.
+   *   - 'fragment': A fragment identifier (named anchor) to append to the URL.
+   *     Do not include the leading '#' character.
+   *   - 'absolute': Defaults to FALSE. Whether to force the output to be an
+   *     absolute link (beginning with http:). Useful for links that will be
+   *     displayed outside the site, such as in an RSS feed.
+   *   - 'language': An optional language object used to look up the alias
+   *     for the URL. If $options['language'] is omitted, it defaults to the
+   *     current language for the language type LanguageInterface::TYPE_URL.
+   *   - 'https': Whether this URL should point to a secure location. If not
+   *     defined, the current scheme is used, so the user stays on HTTP or HTTPS
+   *     respectively. if mixed mode sessions are permitted, TRUE enforces HTTPS
+   *     and FALSE enforces HTTP.
    *
-   * @return static
-   *   An Url object. Warning: the object is created even if the current user
-   *   can not access the path.
+   * @return \Drupal\Core\Url
+   *   A new Url object for a routed (internal to Drupal) URL.
    *
-   * @throws \Drupal\Core\Routing\MatchingRouteNotFoundException
-   *   Thrown when the path cannot be matched.
+   * @see static::fromUri()
    */
-  public static function createFromPath($path) {
-    if (UrlHelper::isExternal($path)) {
-      $url = new static($path);
-      $url->setExternal();
-      return $url;
-    }
+  public static function fromRoute($route_name, $route_parameters = array(), $options = array()) {
+    return new static($route_name, $route_parameters, $options);
+  }
 
-    // Special case the front page route.
-    if ($path == '<front>') {
-      return new static($path);
-    }
-    else {
-      return static::createFromRequest(Request::create("/$path"));
+  /**
+   * Creates a new Url object for a URI that does not have a Drupal route.
+   *
+   * This method is for generating URLs for URIs that do not have Drupal
+   * routes, both external URLs and unrouted local URIs like
+   * base://robots.txt. For URLs that have Drupal routes (that is, most pages
+   * generated by Drupal), use Url::fromRoute().
+   *
+   * @param string $uri
+   *   The URI of the external resource including the scheme. For Drupal paths
+   *   that are not handled by the routing system, use base:// for the scheme.
+   * @param array $options
+   *   (optional) An associative array of additional URL options, with the
+   *   following elements:
+   *   - 'query': An array of query key/value-pairs (without any URL-encoding)
+   *     to append to the URL. Merged with the parameters array.
+   *   - 'fragment': A fragment identifier (named anchor) to append to the URL.
+   *     Do not include the leading '#' character.
+   *   - 'absolute': Defaults to FALSE. Whether to force the output to be an
+   *     absolute link (beginning with http:). Useful for links that will be
+   *     displayed outside the site, such as in an RSS feed.
+   *   - 'language': An optional language object used to look up the alias
+   *     for the URL. If $options['language'] is omitted, it defaults to the
+   *     current language for the language type LanguageInterface::TYPE_URL.
+   *   - 'https': Whether this URL should point to a secure location. If not
+   *     defined, the current scheme is used, so the user stays on HTTP or HTTPS
+   *     respectively. if mixed mode sessions are permitted, TRUE enforces HTTPS
+   *     and FALSE enforces HTTP.
+   *
+   * @return \Drupal\Core\Url
+   *   A new Url object for an unrouted (non-Drupal) URL.
+   *
+   * @throws \InvalidArgumentException
+   *   Thrown when the passed in path has no scheme.
+   *
+   * @see static::fromRoute()
+   */
+  public static function fromUri($uri, $options = array()) {
+    if (!parse_url($uri, PHP_URL_SCHEME)) {
+      throw new \InvalidArgumentException('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.');
     }
+
+    $url = new static($uri, array(), $options);
+    $url->setUnrouted();
+
+    return $url;
   }
 
   /**
-   * Returns the Url object matching a request. READ THE SECURITY NOTE ON createFromPath().
+   * Returns the Url object matching a request.
+   *
+   * SECURITY NOTE: The request path is not checked to be valid and accessible
+   * by the current user to allow storing and reusing Url objects by different
+   * users. The 'path.validator' service getUrlIfValid() method should be used
+   * instead of this one if validation and access check is desired. Otherwise,
+   * 'access_manager' service checkNamedRoute() method should be used on the
+   * router name and parameters stored in the Url object returned by this
+   * method.
    *
    * @param \Symfony\Component\HttpFoundation\Request $request
    *   A request object.
@@ -163,23 +244,23 @@ public static function createFromRequest(Request $request) {
   }
 
   /**
-   * Sets this Url to be external.
+   * Sets this Url to encapsulate an unrouted URI.
    *
    * @return $this
    */
-  protected function setExternal() {
-    $this->external = TRUE;
-
-    // What was passed in as the route name is actually the path.
-    $this->path = $this->routeName;
-
+  protected function setUnrouted() {
+    $this->unrouted = TRUE;
+    // What was passed in as the route name is actually the URI.
+    // @todo Consider fixing this in https://www.drupal.org/node/2346787.
+    $this->uri = $this->routeName;
     // Set empty route name and parameters.
     $this->routeName = NULL;
     $this->routeParameters = array();
-    // Flag the path as external so the UrlGenerator does not need to check.
-    $this->options['external'] = TRUE;
-
-    return $this;
+    // @todo Add a method for the check below in
+    // https://www.drupal.org/node/2346859.
+    if ($this->external = strpos($this->uri, 'base://') !== 0) {
+      $this->options['external'] = TRUE;
+    }
   }
 
   /**
@@ -191,16 +272,25 @@ public function isExternal() {
     return $this->external;
   }
 
+  /**
+   * Indicates if this Url has a Drupal route.
+   *
+   * @return bool
+   */
+  public function isRouted() {
+    return !$this->unrouted;
+  }
+
   /**
    * Returns the route name.
    *
    * @return string
    *
    * @throws \UnexpectedValueException.
-   *   If this is an external URL with no corresponding route.
+   *   If this is a URI with no corresponding route.
    */
   public function getRouteName() {
-    if ($this->isExternal()) {
+    if ($this->unrouted) {
       throw new \UnexpectedValueException('External URLs do not have an internal route name.');
     }
 
@@ -213,10 +303,10 @@ public function getRouteName() {
    * @return array
    *
    * @throws \UnexpectedValueException.
-   *   If this is an external URL with no corresponding route.
+   *   If this is a URI with no corresponding route.
    */
   public function getRouteParameters() {
-    if ($this->isExternal()) {
+    if ($this->unrouted) {
       throw new \UnexpectedValueException('External URLs do not have internal route parameters.');
     }
 
@@ -230,10 +320,13 @@ public function getRouteParameters() {
    *   The array of parameters.
    *
    * @return $this
+   *
+   * @throws \UnexpectedValueException.
+   *   If this is a URI with no corresponding route.
    */
   public function setRouteParameters($parameters) {
-    if ($this->isExternal()) {
-      throw new \Exception('External URLs do not have route parameters.');
+    if ($this->unrouted) {
+      throw new \UnexpectedValueException('External URLs do not have route parameters.');
     }
     $this->routeParameters = $parameters;
     return $this;
@@ -248,10 +341,13 @@ public function setRouteParameters($parameters) {
    *   The route parameter.
    *
    * @return $this
+   *
+   * @throws \UnexpectedValueException.
+   *   If this is a URI with no corresponding route.
    */
   public function setRouteParameter($key, $value) {
-    if ($this->isExternal()) {
-      throw new \Exception('External URLs do not have route parameters.');
+    if ($this->unrouted) {
+      throw new \UnexpectedValueException('External URLs do not have route parameters.');
     }
     $this->routeParameters[$key] = $value;
     return $this;
@@ -312,22 +408,22 @@ public function setOption($name, $value) {
   }
 
   /**
-   * Returns the external path of the URL.
+   * Returns the URI of the URL.
    *
-   * Only to be used if self::$external is TRUE.
+   * Only to be used if self::$unrouted is TRUE.
    *
    * @return string
-   *   The external path.
+   *   A URI not connected to a route. May be an external URL.
    *
    * @throws \UnexpectedValueException
-   *   Thrown when the path was requested for an internal URL.
+   *   Thrown when the URI was requested for a routed URL.
    */
-  public function getPath() {
-    if (!$this->isExternal()) {
-      throw new \UnexpectedValueException('Internal URLs do not have external paths.');
+  public function getUri() {
+    if (!$this->unrouted) {
+      throw new \UnexpectedValueException('This URL has a Drupal route, so the canonical form is not a URI.');
     }
 
-    return $this->path;
+    return $this->uri;
   }
 
   /**
@@ -344,11 +440,11 @@ public function setAbsolute($absolute = TRUE) {
   }
 
   /**
-   * Generates the path for this Url object.
+   * Generates the URI for this Url object.
    */
   public function toString() {
-    if ($this->isExternal()) {
-      return $this->urlGenerator()->generateFromPath($this->getPath(), $this->getOptions());
+    if ($this->unrouted) {
+      return $this->unroutedUrlAssembler()->assemble($this->getUri(), $this->getOptions());
     }
 
     return $this->urlGenerator()->generateFromRoute($this->getRouteName(), $this->getRouteParameters(), $this->getOptions());
@@ -361,9 +457,10 @@ public function toString() {
    *   An associative array containing all the properties of the route.
    */
   public function toArray() {
-    if ($this->isExternal()) {
+    if ($this->unrouted) {
       return array(
-        'path' => $this->getPath(),
+        // @todo Change 'path' to 'href': https://www.drupal.org/node/2347025.
+        'path' => $this->getUri(),
         'options' => $this->getOptions(),
       );
     }
@@ -383,9 +480,9 @@ public function toArray() {
    *   An associative array suitable for a render array.
    */
   public function toRenderArray() {
-    if ($this->isExternal()) {
+    if ($this->unrouted) {
       return array(
-        '#href' => $this->getPath(),
+        '#href' => $this->getUri(),
         '#options' => $this->getOptions(),
       );
     }
@@ -408,14 +505,14 @@ public function toRenderArray() {
    *   The internal path for this route.
    *
    * @throws \UnexpectedValueException.
-   *   If this is an external URL with no corresponding system path.
+   *   If this is a URI with no corresponding system path.
    *
    * @deprecated in Drupal 8.x-dev, will be removed before Drupal 8.0.
    *   System paths should not be used - use route names and parameters.
    */
   public function getInternalPath() {
-    if ($this->isExternal()) {
-      throw new \UnexpectedValueException('External URLs do not have internal representations.');
+    if ($this->unrouted) {
+      throw new \UnexpectedValueException('Unrouted URIs do not have internal representations.');
     }
     return $this->urlGenerator()->getPathFromRoute($this->getRouteName(), $this->getRouteParameters());
   }
@@ -472,6 +569,19 @@ protected function urlGenerator() {
     return $this->urlGenerator;
   }
 
+  /**
+   * Gets the unrouted URL assembler for non-Drupal URLs.
+   *
+   * @return \Drupal\Core\Utility\UnroutedUrlAssemblerInterface
+   *   The unrouted URL assembler.
+   */
+  protected function unroutedUrlAssembler() {
+    if (!$this->urlAssembler) {
+      $this->urlAssembler = \Drupal::service('unrouted_url_assembler');
+    }
+    return $this->urlAssembler;
+  }
+
   /**
    * Sets the URL generator.
    *
@@ -485,4 +595,17 @@ public function setUrlGenerator(UrlGeneratorInterface $url_generator) {
     return $this;
   }
 
+  /**
+   * Sets the unrouted URL assembler.
+   *
+   * @param \Drupal\Core\Utility\UnroutedUrlAssemblerInterface
+   *   The unrouted URL assembler.
+   *
+   * @return $this
+   */
+  public function setUnroutedUrlAssembler(UnroutedUrlAssemblerInterface $url_assembler) {
+    $this->urlAssembler = $url_assembler;
+    return $this;
+  }
+
 }
diff --git a/core/lib/Drupal/Core/Utility/UnroutedUrlAssembler.php b/core/lib/Drupal/Core/Utility/UnroutedUrlAssembler.php
index 05fb19f22439..52d1d1397b50 100644
--- a/core/lib/Drupal/Core/Utility/UnroutedUrlAssembler.php
+++ b/core/lib/Drupal/Core/Utility/UnroutedUrlAssembler.php
@@ -56,7 +56,7 @@ public function assemble($uri, array $options = []) {
       // UrlHelper::isExternal() only returns true for safe protocols.
       return $this->buildExternalUrl($uri, $options);
     }
-    throw new \InvalidArgumentException('You must use a valid URI scheme.  Use base:// for a path e.g. to a Drupal file that needs the base path.');
+    throw new \InvalidArgumentException('You must use a valid URI scheme. Use base:// for a path e.g. to a Drupal file that needs the base path.');
   }
 
   /**
diff --git a/core/lib/Drupal/Core/Utility/UnroutedUrlAssemblerInterface.php b/core/lib/Drupal/Core/Utility/UnroutedUrlAssemblerInterface.php
index cba87febdbe9..c1caa44ef860 100644
--- a/core/lib/Drupal/Core/Utility/UnroutedUrlAssemblerInterface.php
+++ b/core/lib/Drupal/Core/Utility/UnroutedUrlAssemblerInterface.php
@@ -12,23 +12,24 @@
 interface UnroutedUrlAssemblerInterface {
 
   /**
-   * Builds a domain-local or external URL from a path or URL.
+   * Builds a domain-local or external URL from a URI.
    *
    * For actual implementations the logic probably has to be split up between
-   * domain-local and external URLs.
+   * domain-local URIs and external URLs.
    *
    * @param string $uri
-   *   A path on the same domain or external URL being linked to, such as "foo"
+   *   A local URI or an external URL being linked to, such as "base://foo"
    *    or "http://example.com/foo".
    *   - If you provide a full URL, it will be considered an external URL as
    *     long as it has an allowed protocol.
-   *   - If you provide only a path (e.g. "foo"), it will be
-   *     considered a URL local to the same domain. Additional query
-   *     arguments for local paths must be supplied in $options['query'], not
-   *     included in $path.
+   *   - If you provide only a local URI (e.g. "base://foo"), it will be
+   *     considered a path local to Drupal, but not handled by the routing
+   *     system.  The base path (the subdirectory where the front controller
+   *     is found) will be added to the path. Additional query arguments for
+   *     local paths must be supplied in $options['query'], not part of $uri.
    *   - If your external URL contains a query (e.g. http://example.com/foo?a=b),
    *     then you can either URL encode the query keys and values yourself and
-   *     include them in $path, or use $options['query'] to let this method
+   *     include them in $uri, or use $options['query'] to let this method
    *     URL encode them.
    *
    * @param array $options
@@ -48,6 +49,9 @@ interface UnroutedUrlAssemblerInterface {
    *
    * @return
    *   A string containing a relative or absolute URL.
+   *
+   * @throws \InvalidArgumentException
+   *   Thrown when the passed in path has no scheme.
    */
   public function assemble($uri, array $options = array());
 
diff --git a/core/modules/aggregator/aggregator.module b/core/modules/aggregator/aggregator.module
index aecd844d5037..5e8507831c04 100644
--- a/core/modules/aggregator/aggregator.module
+++ b/core/modules/aggregator/aggregator.module
@@ -26,6 +26,7 @@ function aggregator_help($route_name, RouteMatchInterface $route_match) {
       $output .= '<p>' . t('The Aggregator module is an on-site syndicator and news reader that gathers and displays fresh content from RSS-, RDF-, and Atom-based feeds made available across the web. Thousands of sites (particularly news sites and blogs) publish their latest headlines in feeds, using a number of standardized XML-based formats. For more information, see the <a href="!aggregator-module">online documentation for the Aggregator module</a>.', array('!aggregator-module' => 'https://drupal.org/documentation/modules/aggregator')) . '</p>';
       $output .= '<h3>' . t('Uses') . '</h3>';
       $output .= '<dl>';
+      // Check if the aggregator sources View is enabled.
       if ($url = $path_validator->getUrlIfValid('aggregator/sources')) {
         $output .= '<dt>' . t('Viewing feeds') . '</dt>';
         $output .= '<dd>' . t('Users view feed content in the <a href="!aggregator">main aggregator display</a>, or by <a href="!aggregator-sources">their source</a> (usually via an RSS feed reader). The most recent content in a feed can be displayed as a block through the <a href="!admin-block">Blocks administration page</a>.', array('!aggregator' => \Drupal::url('aggregator.page_last'), '!aggregator-sources' => $url->toString(), '!admin-block' => \Drupal::url('block.admin_display'))) . '</dd>';
@@ -33,6 +34,7 @@ function aggregator_help($route_name, RouteMatchInterface $route_match) {
       $output .= '<dt>' . t('Adding, editing, and deleting feeds') . '</dt>';
       $output .= '<dd>' . t('Administrators can add, edit, and delete feeds, and choose how often to check each feed for newly updated items on the <a href="!feededit">Feed aggregator administration page</a>.', array('!feededit' => \Drupal::url('aggregator.admin_overview'))) . '</dd>';
       $output .= '<dt>' . t('<abbr title="Outline Processor Markup Language">OPML</abbr> integration') . '</dt>';
+      // Check if the aggregator opml View is enabled.
       if ($url = $path_validator->getUrlIfValid('aggregator/opml')) {
         $output .= '<dd>' . t('A <a href="!aggregator-opml">machine-readable OPML file</a> of all feeds is available. OPML is an XML-based file format used to share outline-structured information such as a list of RSS feeds. Feeds can also be <a href="!import-opml">imported via an OPML file</a>.', array('!aggregator-opml' => $url->toString(), '!import-opml' => \Drupal::url('aggregator.opml_add'))) . '</dd>';
       }
diff --git a/core/modules/aggregator/src/Entity/Item.php b/core/modules/aggregator/src/Entity/Item.php
index 493e4cb61bc5..0cebc3b32e76 100644
--- a/core/modules/aggregator/src/Entity/Item.php
+++ b/core/modules/aggregator/src/Entity/Item.php
@@ -244,7 +244,7 @@ public function getListCacheTags() {
    * Entity URI callback.
    */
   public static function buildUri(ItemInterface $item) {
-    return Url::createFromPath($item->getLink());
+    return Url::fromUri($item->getLink());
   }
 
 }
diff --git a/core/modules/field_ui/src/FieldUI.php b/core/modules/field_ui/src/FieldUI.php
index 77ec6513e2b6..7edb1c6d8311 100644
--- a/core/modules/field_ui/src/FieldUI.php
+++ b/core/modules/field_ui/src/FieldUI.php
@@ -51,15 +51,15 @@ public static function getNextDestination(array $destinations) {
       $next_destination += array(
         'route_parameters' => array(),
       );
-      $next_destination = new Url($next_destination['route_name'], $next_destination['route_parameters'], $next_destination['options']);
+      $next_destination = Url::fromRoute($next_destination['route_name'], $next_destination['route_parameters'], $next_destination['options']);
     }
     else {
       $options = UrlHelper::parse($next_destination);
       if ($destinations) {
         $options['query']['destinations'] = $destinations;
       }
-      $next_destination = Url::createFromPath($options['path']);
-      $next_destination->setOptions($options);
+      // Redirect to any given path within the same domain.
+      $next_destination = Url::fromUri('base://' . $options['path']);
     }
     return $next_destination;
   }
diff --git a/core/modules/link/link.module b/core/modules/link/link.module
index 58d305924950..0fd42d507310 100644
--- a/core/modules/link/link.module
+++ b/core/modules/link/link.module
@@ -69,6 +69,6 @@ function template_preprocess_link_formatter_link_separate(&$variables) {
     $variables['link'] = \Drupal::l($variables['url_title'], $variables['url']);
   }
   else {
-    $variables['link'] = l($variables['url_title'], $variables['url']->getPath(), $variables['url']->getOptions());
+    $variables['link'] = l($variables['url_title'], $variables['url']->getUri(), $variables['url']->getOptions());
   }
 }
diff --git a/core/modules/link/src/Plugin/Field/FieldFormatter/LinkFormatter.php b/core/modules/link/src/Plugin/Field/FieldFormatter/LinkFormatter.php
index 05caf9745423..5713eae267aa 100644
--- a/core/modules/link/src/Plugin/Field/FieldFormatter/LinkFormatter.php
+++ b/core/modules/link/src/Plugin/Field/FieldFormatter/LinkFormatter.php
@@ -13,6 +13,7 @@
 use Drupal\Core\Form\FormStateInterface;
 use Drupal\Core\Url;
 use Drupal\link\LinkItemInterface;
+use Symfony\Component\HttpFoundation\Request;
 
 /**
  * Plugin implementation of the 'link' formatter.
@@ -163,7 +164,7 @@ public function viewElements(FieldItemListInterface $items) {
           '#options' => $url->getOptions(),
         );
         if ($url->isExternal()) {
-          $element[$delta]['#href'] = $url->getPath();
+          $element[$delta]['#href'] = $url->getUri();
         }
         else {
           $element[$delta]['#route_name'] = $url->getRouteName();
@@ -206,11 +207,10 @@ protected function buildUrl(LinkItemInterface $item) {
     }
 
     if ($item->isExternal()) {
-      $url = Url::createFromPath($item->url);
-      $url->setOptions($options);
+      $url = Url::fromUri($item->url, $options);
     }
     else {
-      $url = new Url($item->route_name, (array) $item->route_parameters, (array) $options);
+      $url = Url::fromRoute($item->route_name, (array) $item->route_parameters, (array) $options);
     }
 
     return $url;
diff --git a/core/modules/menu_link_content/src/Entity/MenuLinkContent.php b/core/modules/menu_link_content/src/Entity/MenuLinkContent.php
index 55f348930798..49bc7f252707 100644
--- a/core/modules/menu_link_content/src/Entity/MenuLinkContent.php
+++ b/core/modules/menu_link_content/src/Entity/MenuLinkContent.php
@@ -107,7 +107,7 @@ public function getUrlObject() {
     else {
       $path = $this->getUrl();
       if (isset($path)) {
-        $url = Url::createFromPath($path);
+        $url = Url::fromUri($path);
       }
       else {
         $url = new Url('<front>');
diff --git a/core/modules/menu_link_content/src/Form/MenuLinkContentForm.php b/core/modules/menu_link_content/src/Form/MenuLinkContentForm.php
index 2d37566dbc89..4fb93b224dfb 100644
--- a/core/modules/menu_link_content/src/Form/MenuLinkContentForm.php
+++ b/core/modules/menu_link_content/src/Form/MenuLinkContentForm.php
@@ -198,7 +198,7 @@ public function extractFormValues(array &$form, FormStateInterface $form_state)
 
     if ($extracted) {
       if ($extracted->isExternal()) {
-        $new_definition['url'] = $extracted->getPath();
+        $new_definition['url'] = $extracted->getUri();
       }
       else {
         $new_definition['route_name'] = $extracted->getRouteName();
diff --git a/core/modules/rdf/src/Tests/Field/LinkFieldRdfaTest.php b/core/modules/rdf/src/Tests/Field/LinkFieldRdfaTest.php
index 8c23c0705d97..8478baec00e7 100644
--- a/core/modules/rdf/src/Tests/Field/LinkFieldRdfaTest.php
+++ b/core/modules/rdf/src/Tests/Field/LinkFieldRdfaTest.php
@@ -75,7 +75,8 @@ public function testAllFormattersInternal() {
     // Set up test values.
     $this->testValue = 'admin';
     $this->entity = entity_create('entity_test', array());
-    $this->entity->{$this->fieldName}->url = $this->testValue;
+    $this->entity->{$this->fieldName}->route_name = 'system.admin';
+    $this->entity->{$this->fieldName}->url = 'admin';
 
     // Set up the expected result.
     // AssertFormatterRdfa looks for a full path.
@@ -94,7 +95,8 @@ public function testAllFormattersFront() {
     // Set up test values.
     $this->testValue = '<front>';
     $this->entity = entity_create('entity_test', array());
-    $this->entity->{$this->fieldName}->url = $this->testValue;
+    $this->entity->{$this->fieldName}->route_name = $this->testValue;
+    $this->entity->{$this->fieldName}->url = '<front>';
 
     // Set up the expected result.
     $expected_rdf = array(
diff --git a/core/modules/shortcut/src/Entity/Shortcut.php b/core/modules/shortcut/src/Entity/Shortcut.php
index df96d6fdc243..4bd041efa8c3 100644
--- a/core/modules/shortcut/src/Entity/Shortcut.php
+++ b/core/modules/shortcut/src/Entity/Shortcut.php
@@ -14,6 +14,7 @@
 use Drupal\Core\Field\BaseFieldDefinition;
 use Drupal\Core\Url;
 use Drupal\shortcut\ShortcutInterface;
+use Symfony\Component\HttpFoundation\Request;
 
 /**
  * Defines the shortcut entity class.
@@ -134,7 +135,15 @@ public static function preCreate(EntityStorageInterface $storage, array &$values
   public function preSave(EntityStorageInterface $storage) {
     parent::preSave($storage);
 
-    $url = Url::createFromPath($this->path->value);
+    // @todo fix PathValidatorInterface::getUrlIfValid() so we can use it
+    //   here. The problem is that we need an exception, not a FALSE
+    //   return value. https://www.drupal.org/node/2346695
+    if ($this->path->value == '<front>') {
+      $url = new Url($this->path->value);
+    }
+    else {
+      $url = Url::createFromRequest(Request::create("/{$this->path->value}"));
+    }
     $this->setRouteName($url->getRouteName());
     $this->setRouteParams($url->getRouteParameters());
   }
diff --git a/core/modules/system/system.module b/core/modules/system/system.module
index 2a022bd12cd3..17fe8c8d0eaf 100644
--- a/core/modules/system/system.module
+++ b/core/modules/system/system.module
@@ -457,7 +457,7 @@ function system_authorized_get_url(array $options = array()) {
   // Force HTTPS if available, regardless of what the caller specifies.
   $options['https'] = TRUE;
   // Prefix with $base_url so url() treats it as an external link.
-  $url = Url::createFromPath($base_url . '/core/authorize.php');
+  $url = Url::fromUri('base://core/authorize.php');
   $url_options = $url->getOptions();
   $url->setOptions($options + $url_options);
   return $url;
diff --git a/core/modules/system/tests/modules/form_test/src/Form/FormTestRedirectForm.php b/core/modules/system/tests/modules/form_test/src/Form/FormTestRedirectForm.php
index 7fc352e8aae2..f7c0d4a67186 100644
--- a/core/modules/system/tests/modules/form_test/src/Form/FormTestRedirectForm.php
+++ b/core/modules/system/tests/modules/form_test/src/Form/FormTestRedirectForm.php
@@ -54,7 +54,7 @@ public function buildForm(array $form, FormStateInterface $form_state) {
   public function submitForm(array &$form, FormStateInterface $form_state) {
     if (!$form_state->isValueEmpty('redirection')) {
       if (!$form_state->isValueEmpty('destination')) {
-        $form_state->setRedirectUrl(Url::createFromPath($GLOBALS['base_url'] . '/' . $form_state->getValue('destination')));
+        $form_state->setRedirectUrl(Url::fromUri('base://' . $form_state->getValue('destination')));
       }
     }
     else {
diff --git a/core/modules/toolbar/toolbar.module b/core/modules/toolbar/toolbar.module
index b9174bccea8d..815d1cf11928 100644
--- a/core/modules/toolbar/toolbar.module
+++ b/core/modules/toolbar/toolbar.module
@@ -298,7 +298,7 @@ function toolbar_menu_navigation_links(array $tree) {
     $url = $link->getUrlObject();
     if ($url->isExternal()) {
       // This is an unusual case, so just get a distinct, safe string.
-      $id = substr(Crypt::hashBase64($url->getPath()), 0, 16);
+      $id = substr(Crypt::hashBase64($url->getUri()), 0, 16);
     }
     else {
       $id = str_replace(array('.', '<', '>'), array('-', '', ''), $url->getRouteName());
diff --git a/core/modules/update/update.report.inc b/core/modules/update/update.report.inc
index 9ee4dbfa2b04..38c724c45f42 100644
--- a/core/modules/update/update.report.inc
+++ b/core/modules/update/update.report.inc
@@ -6,6 +6,7 @@
  */
 
 use Drupal\Core\Template\Attribute;
+use Drupal\Core\Url;
 
 /**
  * Prepares variables for project status report templates.
@@ -135,7 +136,7 @@ function template_preprocess_update_project_status(&$variables) {
 
   // Set the project title and URL.
   $variables['title'] = (isset($project['title'])) ? $project['title'] : $project['name'];
-  $variables['url'] = (isset($project['link'])) ? url($project['link']) : NULL;
+  $variables['url'] = (isset($project['link'])) ? Url::fromUri($project['link'])->toString() : NULL;
 
   $variables['install_type'] = $project['install_type'];
   if ($project['install_type'] == 'dev' && !empty($project['datestamp'])) {
diff --git a/core/modules/user/user.module b/core/modules/user/user.module
index e33cd2874d21..82aca31730ad 100644
--- a/core/modules/user/user.module
+++ b/core/modules/user/user.module
@@ -576,6 +576,7 @@ function template_preprocess_username(&$variables) {
   $variables['name'] = String::checkPlain($name);
   $variables['profile_access'] = \Drupal::currentUser()->hasPermission('access user profiles');
 
+  $external = FALSE;
   // Populate link path and attributes if appropriate.
   if ($variables['uid'] && $variables['profile_access']) {
     // We are linking to a local user.
@@ -589,10 +590,19 @@ function template_preprocess_username(&$variables) {
     $variables['attributes']['rel'] = 'nofollow';
     $variables['link_path'] = $account->homepage;
     $variables['homepage'] = $account->homepage;
+    $external = TRUE;
   }
-  // We have a link path, so we should generate a link using url().
+  // We have a link path, so we should generate a URL.
   if (isset($variables['link_path'])) {
-    $variables['attributes']['href'] = url($variables['link_path'], $variables['link_options']);
+    if ($external) {
+      $variables['attributes']['href'] = Url::fromUri($variables['link_path'], $variables['link_options'])
+        ->toString();
+    }
+    else {
+      $variables['attributes']['href'] = Url::fromRoute('entity.user.canonical', array(
+        'user' => $variables['uid'],
+      ))->toString();
+    }
   }
 }
 
diff --git a/core/modules/views_ui/src/ViewEditForm.php b/core/modules/views_ui/src/ViewEditForm.php
index e8490705add7..e6215133c235 100644
--- a/core/modules/views_ui/src/ViewEditForm.php
+++ b/core/modules/views_ui/src/ViewEditForm.php
@@ -20,6 +20,7 @@
 use Drupal\Core\Url;
 use Drupal\user\TempStoreFactory;
 use Drupal\views\Views;
+use Symfony\Component\HttpFoundation\Request;
 use Symfony\Component\HttpFoundation\RequestStack;
 use Symfony\Component\DependencyInjection\ContainerInterface;
 
@@ -322,13 +323,11 @@ public function save(array $form, FormStateInterface $form_state) {
           $query->remove('destination');
         }
       }
-      if (!UrlHelper::isExternal($destination)) {
-        $destination = $GLOBALS['base_url'] . '/' . $destination;
-      }
-      $form_state->setRedirectUrl(Url::createFromPath($destination));
+      $form_state->setRedirectUrl(Url::fromUri("base://$destination"));
     }
 
     $view->save();
+
     drupal_set_message($this->t('The view %name has been saved.', array('%name' => $view->label())));
 
     // Remove this view from cache so we can edit it properly.
diff --git a/core/tests/Drupal/Tests/Core/ExternalUrlTest.php b/core/tests/Drupal/Tests/Core/ExternalUrlTest.php
deleted file mode 100644
index 57a51d4fa6b6..000000000000
--- a/core/tests/Drupal/Tests/Core/ExternalUrlTest.php
+++ /dev/null
@@ -1,190 +0,0 @@
-<?php
-
-/**
- * @file
- * Contains \Drupal\Tests\Core\ExternalUrlTest.
- */
-
-namespace Drupal\Tests\Core;
-
-use Drupal\Core\DependencyInjection\ContainerBuilder;
-use Drupal\Core\Url;
-use Drupal\Tests\UnitTestCase;
-use Symfony\Component\HttpFoundation\Request;
-use Symfony\Component\Routing\Exception\ResourceNotFoundException;
-
-/**
- * @coversDefaultClass \Drupal\Core\Url
- * @group ExternalUrlTest
- */
-class ExternalUrlTest extends UnitTestCase {
-
-  /**
-   * The URL generator
-   *
-   * @var \Drupal\Core\Routing\UrlGeneratorInterface|\PHPUnit_Framework_MockObject_MockObject
-   */
-  protected $urlGenerator;
-
-  /**
-   * The router.
-   *
-   * @var \Drupal\Tests\Core\Routing\TestRouterInterface|\PHPUnit_Framework_MockObject_MockObject
-   */
-  protected $router;
-
-  /**
-   * An external URL to test.
-   *
-   * @var string
-   */
-  protected $path = 'http://drupal.org';
-
-  /**
-   * {@inheritdoc}
-   */
-  protected function setUp() {
-    parent::setUp();
-
-    $this->urlGenerator = $this->getMock('Drupal\Core\Routing\UrlGeneratorInterface');
-    $this->urlGenerator->expects($this->any())
-      ->method('generateFromPath')
-      ->will($this->returnArgument(0));
-
-    $this->router = $this->getMock('Drupal\Tests\Core\Routing\TestRouterInterface');
-    $container = new ContainerBuilder();
-    $container->set('router.no_access_checks', $this->router);
-    $container->set('url_generator', $this->urlGenerator);
-    \Drupal::setContainer($container);
-  }
-
-  /**
-   * Tests the createFromPath method.
-   *
-   * @covers ::createFromPath()
-   * @covers ::setExternal()
-   */
-  public function testCreateFromPath() {
-    $url = Url::createFromPath($this->path);
-    $this->assertInstanceOf('Drupal\Core\Url', $url);
-    $this->assertTrue($url->isExternal());
-    return $url;
-  }
-
-  /**
-   * Tests the createFromRequest method.
-   *
-   * @covers ::createFromRequest()
-   *
-   * @expectedException \Symfony\Component\Routing\Exception\ResourceNotFoundException
-   */
-  public function testCreateFromRequest() {
-    $request = Request::create('/test-path');
-
-    $this->router->expects($this->once())
-      ->method('matchRequest')
-      ->with($request)
-      ->will($this->throwException(new ResourceNotFoundException()));
-
-    $this->assertNull(Url::createFromRequest($request));
-  }
-
-  /**
-   * Tests the isExternal() method.
-   *
-   * @depends testCreateFromPath
-   *
-   * @covers ::isExternal()
-   */
-  public function testIsExternal(Url $url) {
-    $this->assertTrue($url->isExternal());
-  }
-
-  /**
-   * Tests the toString() method.
-   *
-   * @depends testCreateFromPath
-   *
-   * @covers ::toString()
-   */
-  public function testToString(Url $url) {
-    $this->assertSame($this->path, $url->toString());
-  }
-
-  /**
-   * Tests the toArray() method.
-   *
-   * @depends testCreateFromPath
-   *
-   * @covers ::toArray()
-   */
-  public function testToArray(Url $url) {
-    $expected = array(
-      'path' => $this->path,
-      'options' => array('external' => TRUE),
-    );
-    $this->assertSame($expected, $url->toArray());
-  }
-
-  /**
-   * Tests the getRouteName() method.
-   *
-   * @depends testCreateFromPath
-   *
-   * @expectedException \UnexpectedValueException
-   *
-   * @covers ::getRouteName()
-   */
-  public function testGetRouteName(Url $url) {
-    $url->getRouteName();
-  }
-
-  /**
-   * Tests the getRouteParameters() method.
-   *
-   * @depends testCreateFromPath
-   *
-   * @expectedException \UnexpectedValueException
-   *
-   * @covers ::getRouteParameters()
-   */
-  public function testGetRouteParameters(Url $url) {
-    $url->getRouteParameters();
-  }
-
-  /**
-   * Tests the getInternalPath() method.
-   *
-   * @depends testCreateFromPath
-   *
-   * @covers ::getInternalPath()
-   *
-   * @expectedException \Exception
-   */
-  public function testGetInternalPath(Url $url) {
-    $this->assertNull($url->getInternalPath());
-  }
-
-  /**
-   * Tests the getPath() method.
-   *
-   * @depends testCreateFromPath
-   *
-   * @covers ::getPath()
-   */
-  public function testGetPath(Url $url) {
-    $this->assertNotNull($url->getPath());
-  }
-
-  /**
-   * Tests the getOptions() method.
-   *
-   * @depends testCreateFromPath
-   *
-   * @covers ::getOptions()
-   */
-  public function testGetOptions(Url $url) {
-    $this->assertInternalType('array', $url->getOptions());
-  }
-
-}
diff --git a/core/tests/Drupal/Tests/Core/UnroutedUrlTest.php b/core/tests/Drupal/Tests/Core/UnroutedUrlTest.php
new file mode 100644
index 000000000000..9d6ea001331f
--- /dev/null
+++ b/core/tests/Drupal/Tests/Core/UnroutedUrlTest.php
@@ -0,0 +1,225 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\Tests\Core\UnroutedUrlTest.
+ */
+
+namespace Drupal\Tests\Core;
+
+use Drupal\Core\DependencyInjection\ContainerBuilder;
+use Drupal\Core\Url;
+use Drupal\Tests\UnitTestCase;
+use Symfony\Component\HttpFoundation\Request;
+use Symfony\Component\Routing\Exception\ResourceNotFoundException;
+
+/**
+ * @coversDefaultClass \Drupal\Core\Url
+ * @group UrlTest
+ */
+class UnroutedUrlTest extends UnitTestCase {
+
+  /**
+   * The URL assembler
+   *
+   * @var \Drupal\Core\Utility\UnroutedUrlAssemblerInterface|\PHPUnit_Framework_MockObject_MockObject
+   */
+  protected $urlAssembler;
+
+  /**
+   * The router.
+   *
+   * @var \Drupal\Tests\Core\Routing\TestRouterInterface|\PHPUnit_Framework_MockObject_MockObject
+   */
+  protected $router;
+
+  /**
+   * An unrouted, external URL to test.
+   *
+   * @var string
+   */
+  protected $unroutedExternal = 'http://drupal.org';
+
+  /**
+   * An unrouted, internal URL to test.
+   *
+   * @var string
+   */
+  protected $unroutedInternal = 'base://robots.txt';
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function setUp() {
+    parent::setUp();
+
+    $this->urlAssembler = $this->getMock('Drupal\Core\Utility\UnroutedUrlAssemblerInterface');
+    $this->urlAssembler->expects($this->any())
+      ->method('assemble')
+      ->will($this->returnArgument(0));
+
+    $this->router = $this->getMock('Drupal\Tests\Core\Routing\TestRouterInterface');
+    $container = new ContainerBuilder();
+    $container->set('router.no_access_checks', $this->router);
+    $container->set('unrouted_url_assembler', $this->urlAssembler);
+    \Drupal::setContainer($container);
+  }
+
+  /**
+   * Tests the fromUri() method.
+   *
+   * @covers ::fromUri
+   */
+  public function testFromUri() {
+    $urls = [
+      Url::fromUri($this->unroutedExternal),
+      Url::fromUri($this->unroutedInternal)
+    ];
+
+    $this->assertInstanceOf('Drupal\Core\Url', $urls[0]);
+    $this->assertTrue($urls[0]->isExternal());
+
+    $this->assertInstanceOf('Drupal\Core\Url', $urls[1]);
+    $this->assertFalse($urls[1]->isExternal());
+
+    return $urls;
+  }
+
+  /**
+   * Tests the createFromRequest method.
+   *
+   * @covers ::createFromRequest
+   *
+   * @expectedException \Symfony\Component\Routing\Exception\ResourceNotFoundException
+   */
+  public function testCreateFromRequest() {
+    $request = Request::create('/test-path');
+
+    $this->router->expects($this->once())
+      ->method('matchRequest')
+      ->with($request)
+      ->will($this->throwException(new ResourceNotFoundException()));
+
+    $this->assertNull(Url::createFromRequest($request));
+  }
+
+  /**
+   * Tests the fromUri() method with an non-scheme path.
+   *
+   * @covers ::fromUri
+   *
+   * @expectedException \InvalidArgumentException
+   */
+  public function testFromUriWithNonScheme() {
+    Url::fromUri('test');
+  }
+
+  /**
+   * Tests the isExternal() method.
+   *
+   * @depends testFromUri
+   *
+   * @covers ::isExternal
+   */
+  public function testIsExternal(array $urls) {
+    $this->assertTrue($urls[0]->isExternal());
+    $this->assertFalse($urls[1]->isExternal());
+  }
+
+  /**
+   * Tests the toString() method.
+   *
+   * @depends testFromUri
+   *
+   * @covers ::toString
+   */
+  public function testToString(array $urls) {
+    $this->assertSame($this->unroutedExternal, $urls[0]->toString());
+    $this->assertSame($this->unroutedInternal, $urls[1]->toString());
+  }
+
+  /**
+   * Tests the toArray() method.
+   *
+   * @depends testFromUri
+   *
+   * @covers ::toArray
+   */
+  public function testToArray(array $urls) {
+    $expected = array(
+      'path' => $this->unroutedExternal,
+      'options' => array('external' => TRUE),
+    );
+    $this->assertSame($expected, $urls[0]->toArray());
+
+    $expected = array(
+      'path' => $this->unroutedInternal,
+      'options' => array(),
+    );
+    $this->assertSame($expected, $urls[1]->toArray());
+  }
+
+  /**
+   * Tests the getRouteName() method.
+   *
+   * @depends testFromUri
+   *
+   * @expectedException \UnexpectedValueException
+   *
+   * @covers ::getRouteName
+   */
+  public function testGetRouteName(array $urls) {
+    $urls[0]->getRouteName();
+  }
+
+  /**
+   * Tests the getRouteParameters() method.
+   *
+   * @depends testFromUri
+   *
+   * @expectedException \UnexpectedValueException
+   *
+   * @covers ::getRouteParameters
+   */
+  public function testGetRouteParameters(array $urls) {
+    $urls[0]->getRouteParameters();
+  }
+
+  /**
+   * Tests the getInternalPath() method.
+   *
+   * @depends testFromUri
+   *
+   * @covers ::getInternalPath
+   *
+   * @expectedException \Exception
+   */
+  public function testGetInternalPath(array $urls) {
+    $this->assertNull($urls[0]->getInternalPath());
+  }
+
+  /**
+   * Tests the getPath() method.
+   *
+   * @depends testFromUri
+   *
+   * @covers ::getUri
+   */
+  public function testGetUri(array $urls) {
+    $this->assertNotNull($urls[0]->getUri());
+    $this->assertNotNull($urls[1]->getUri());
+  }
+
+  /**
+   * Tests the getOptions() method.
+   *
+   * @depends testFromUri
+   *
+   * @covers ::getOptions
+   */
+  public function testGetOptions(array $urls) {
+    $this->assertInternalType('array', $urls[0]->getOptions());
+    $this->assertInternalType('array', $urls[1]->getOptions());
+  }
+
+}
diff --git a/core/tests/Drupal/Tests/Core/UrlTest.php b/core/tests/Drupal/Tests/Core/UrlTest.php
index 133a73cb0eaa..cf0496f694f3 100644
--- a/core/tests/Drupal/Tests/Core/UrlTest.php
+++ b/core/tests/Drupal/Tests/Core/UrlTest.php
@@ -73,11 +73,9 @@ protected function setUp() {
   }
 
   /**
-   * Tests the createFromPath method.
-   *
-   * @covers ::createFromPath()
+   * Tests creating a Url from a request.
    */
-  public function testCreateFromPath() {
+  public function testUrlFromRequest() {
     $this->router->expects($this->at(0))
       ->method('matchRequest')
       ->with($this->getRequestConstraint('/node'))
@@ -102,8 +100,8 @@ public function testCreateFromPath() {
 
     $urls = array();
     foreach ($this->map as $index => $values) {
-      $path = trim(array_pop($values), '/');
-      $url = Url::createFromPath($path);
+      $path = array_pop($values);
+      $url = Url::createFromRequest(Request::create("$path"));
       $this->assertSame($values, array_values($url->toArray()));
       $urls[$index] = $url;
     }
@@ -126,35 +124,19 @@ protected function getRequestConstraint($path) {
   }
 
   /**
-   * Tests the createFromPath method with the special <front> path.
+   * Tests the fromRoute() method with the special <front> path.
    *
-   * @covers ::createFromPath()
+   * @covers ::fromRoute
    */
-  public function testCreateFromPathFront() {
-    $url = Url::createFromPath('<front>');
+  public function testFromRouteFront() {
+    $url = Url::fromRoute('<front>');
     $this->assertSame('<front>', $url->getRouteName());
   }
 
-  /**
-   * Tests that an invalid path will thrown an exception.
-   *
-   * @covers ::createFromPath()
-   *
-   * @expectedException \Symfony\Component\Routing\Exception\ResourceNotFoundException
-   */
-  public function testCreateFromPathInvalid() {
-    $this->router->expects($this->once())
-      ->method('matchRequest')
-      ->with($this->getRequestConstraint('/non-existent'))
-      ->will($this->throwException(new ResourceNotFoundException()));
-
-    $this->assertNull(Url::createFromPath('non-existent'));
-  }
-
   /**
    * Tests the createFromRequest method.
    *
-   * @covers ::createFromRequest()
+   * @covers ::createFromRequest
    */
   public function testCreateFromRequest() {
     $attributes = array(
@@ -178,11 +160,11 @@ public function testCreateFromRequest() {
   /**
    * Tests that an invalid request will thrown an exception.
    *
-   * @covers ::createFromRequest()
+   * @covers ::createFromRequest
    *
    * @expectedException \Symfony\Component\Routing\Exception\ResourceNotFoundException
    */
-  public function testCreateFromRequestInvalid() {
+  public function testUrlFromRequestInvalid() {
     $request = Request::create('/test-path');
 
     $this->router->expects($this->once())
@@ -196,9 +178,9 @@ public function testCreateFromRequestInvalid() {
   /**
    * Tests the isExternal() method.
    *
-   * @depends testCreateFromPath
+   * @depends testUrlFromRequest
    *
-   * @covers ::isExternal()
+   * @covers ::isExternal
    */
   public function testIsExternal($urls) {
     foreach ($urls as $url) {
@@ -207,28 +189,31 @@ public function testIsExternal($urls) {
   }
 
   /**
-   * Tests the getPath() method for internal URLs.
+   * Tests the getUri() method for internal URLs.
    *
-   * @depends testCreateFromPath
+   * @param \Drupal\Core\Url[] $urls
+   *   Array of URL objects.
+   *
+   * @depends testUrlFromRequest
    *
    * @expectedException \UnexpectedValueException
    *
-   * @covers ::getPath()
+   * @covers ::getUri
    */
-  public function testGetPathForInternalUrl($urls) {
+  public function testGetUriForInternalUrl($urls) {
     foreach ($urls as $url) {
-      $url->getPath();
+      $url->getUri();
     }
   }
 
   /**
-   * Tests the getPath() method for external URLs.
+   * Tests the getUri() method for external URLs.
    *
-   * @covers ::getPath
+   * @covers ::getUri
    */
-  public function testGetPathForExternalUrl() {
-    $url = Url::createFromPath('http://example.com/test');
-    $this->assertEquals('http://example.com/test', $url->getPath());
+  public function testGetUriForExternalUrl() {
+    $url = Url::fromUri('http://example.com/test');
+    $this->assertEquals('http://example.com/test', $url->getUri());
   }
 
   /**
@@ -237,9 +222,9 @@ public function testGetPathForExternalUrl() {
    * @param \Drupal\Core\Url[] $urls
    *   An array of Url objects.
    *
-   * @depends testCreateFromPath
+   * @depends testUrlFromRequest
    *
-   * @covers ::toString()
+   * @covers ::toString
    */
   public function testToString($urls) {
     foreach ($urls as $index => $url) {
@@ -254,9 +239,9 @@ public function testToString($urls) {
    * @param \Drupal\Core\Url[] $urls
    *   An array of Url objects.
    *
-   * @depends testCreateFromPath
+   * @depends testUrlFromRequest
    *
-   * @covers ::toArray()
+   * @covers ::toArray
    */
   public function testToArray($urls) {
     foreach ($urls as $index => $url) {
@@ -275,9 +260,9 @@ public function testToArray($urls) {
    * @param \Drupal\Core\Url[] $urls
    *   An array of Url objects.
    *
-   * @depends testCreateFromPath
+   * @depends testUrlFromRequest
    *
-   * @covers ::getRouteName()
+   * @covers ::getRouteName
    */
   public function testGetRouteName($urls) {
     foreach ($urls as $index => $url) {
@@ -292,7 +277,7 @@ public function testGetRouteName($urls) {
    * @expectedException \UnexpectedValueException
    */
   public function testGetRouteNameWithExternalUrl() {
-    $url = Url::createFromPath('http://example.com');
+    $url = Url::fromUri('http://example.com');
     $url->getRouteName();
   }
 
@@ -302,9 +287,9 @@ public function testGetRouteNameWithExternalUrl() {
    * @param \Drupal\Core\Url[] $urls
    *   An array of Url objects.
    *
-   * @depends testCreateFromPath
+   * @depends testUrlFromRequest
    *
-   * @covers ::getRouteParameters()
+   * @covers ::getRouteParameters
    */
   public function testGetRouteParameters($urls) {
     foreach ($urls as $index => $url) {
@@ -319,7 +304,7 @@ public function testGetRouteParameters($urls) {
    * @expectedException \UnexpectedValueException
    */
   public function testGetRouteParametersWithExternalUrl() {
-    $url = Url::createFromPath('http://example.com');
+    $url = Url::fromUri('http://example.com');
     $url->getRouteParameters();
   }
 
@@ -329,9 +314,9 @@ public function testGetRouteParametersWithExternalUrl() {
    * @param \Drupal\Core\Url[] $urls
    *   An array of Url objects.
    *
-   * @depends testCreateFromPath
+   * @depends testUrlFromRequest
    *
-   * @covers ::getOptions()
+   * @covers ::getOptions
    */
   public function testGetOptions($urls) {
     foreach ($urls as $index => $url) {
@@ -345,8 +330,7 @@ public function testGetOptions($urls) {
    * @param bool $access
    *
    * @covers ::access
-   * @covers ::getAccessManager
-   * @covers ::setAccessManager
+   * @covers ::accessManager
    * @dataProvider accessProvider
    */
   public function testAccess($access) {
diff --git a/core/tests/Drupal/Tests/Core/Utility/LinkGeneratorTest.php b/core/tests/Drupal/Tests/Core/Utility/LinkGeneratorTest.php
index 7093fbf9df8a..5721010c4b23 100644
--- a/core/tests/Drupal/Tests/Core/Utility/LinkGeneratorTest.php
+++ b/core/tests/Drupal/Tests/Core/Utility/LinkGeneratorTest.php
@@ -39,6 +39,13 @@ class LinkGeneratorTest extends UnitTestCase {
    */
   protected $moduleHandler;
 
+  /**
+   * The mocked URL Assembler service.
+   *
+   * @var \PHPUnit_Framework_MockObject_MockObject|\Drupal\Core\Utility\UnroutedUrlAssemblerInterface
+   */
+  protected $urlAssembler;
+
   /**
    * Contains the LinkGenerator default options.
    */
@@ -60,6 +67,7 @@ protected function setUp() {
     $this->moduleHandler = $this->getMock('Drupal\Core\Extension\ModuleHandlerInterface');
 
     $this->linkGenerator = new LinkGenerator($this->urlGenerator, $this->moduleHandler);
+    $this->urlAssembler = $this->getMock('\Drupal\Core\Utility\UnroutedUrlAssemblerInterface');
   }
 
   /**
@@ -144,8 +152,8 @@ public function testGenerate() {
    * @covers ::generate()
    */
   public function testGenerateExternal() {
-    $this->urlGenerator->expects($this->once())
-      ->method('generateFromPath')
+    $this->urlAssembler->expects($this->once())
+      ->method('assemble')
       ->with('http://drupal.org', array('set_active_class' => TRUE, 'external' => TRUE) + $this->defaultOptions)
       ->will($this->returnArgument(0));
 
@@ -153,8 +161,14 @@ public function testGenerateExternal() {
       ->method('alter')
       ->with('link', $this->isType('array'));
 
-    $url = Url::createFromPath('http://drupal.org');
+    $this->urlAssembler->expects($this->once())
+      ->method('assemble')
+      ->with('http://drupal.org', array('set_active_class' => TRUE, 'external' => TRUE) + $this->defaultOptions)
+      ->willReturnArgument(0);
+
+    $url = Url::fromUri('http://drupal.org');
     $url->setUrlGenerator($this->urlGenerator);
+    $url->setUnroutedUrlAssembler($this->urlAssembler);
     $url->setOption('set_active_class', TRUE);
 
     $result = $this->linkGenerator->generate('Drupal', $url);
diff --git a/core/tests/Drupal/Tests/Core/Utility/UnroutedUrlAssemblerTest.php b/core/tests/Drupal/Tests/Core/Utility/UnroutedUrlAssemblerTest.php
index 517625f8e8c1..03b33754260d 100644
--- a/core/tests/Drupal/Tests/Core/Utility/UnroutedUrlAssemblerTest.php
+++ b/core/tests/Drupal/Tests/Core/Utility/UnroutedUrlAssemblerTest.php
@@ -33,7 +33,7 @@ class UnroutedUrlAssemblerTest extends UnitTestCase {
   protected $configFactory;
 
   /**
-   * The tested unrouted url assembler.
+   * The tested unrouted URL assembler.
    *
    * @var \Drupal\Core\Utility\UnroutedUrlAssembler
    */
-- 
GitLab