Commit 31022924 authored by alexpott's avatar alexpott

Issue #2343759 by pwolanin, larowlan, dawehner, tim.plunkett, effulgentsia,...

Issue #2343759 by pwolanin, larowlan, dawehner, tim.plunkett, effulgentsia, xjm, Wim Leers: Provide an API function to replace url()/l() for external urls.
parent 5fdcfc74
......@@ -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);
}
......
......@@ -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();
......
......@@ -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);
}
}
......
......@@ -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, '/');
......
......@@ -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;
......
......@@ -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;
}
}
......@@ -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.');
}
/**
......
......@@ -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());
......
......@@ -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>';
}
......
......@@ -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());
}
}
......@@ -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;
}
......
......@@ -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());
}
}
......@@ -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);
}