Commit 5f5819c2 authored by webchick's avatar webchick
Browse files

Issue #2418139 by pwolanin, kgoel, dawehner, almaudoh, Wim Leers: Add a...

Issue #2418139 by pwolanin, kgoel, dawehner, almaudoh, Wim Leers: Add a toUriString method to Url class and add a route: scheme
parent 00797235
......@@ -8,6 +8,7 @@
namespace Drupal\Core;
use Drupal\Component\Utility\String;
use Drupal\Component\Utility\UrlHelper;
use Drupal\Core\DependencyInjection\DependencySerializationTrait;
use Drupal\Core\Routing\RouteMatchInterface;
use Drupal\Core\Routing\UrlGeneratorInterface;
......@@ -213,7 +214,7 @@ public static function fromRouteMatch(RouteMatchInterface $route_match) {
* (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.
* to append to the URL.
* - '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
......@@ -240,21 +241,37 @@ public static function fromRouteMatch(RouteMatchInterface $route_match) {
*
* @see static::fromRoute()
*/
public static function fromUri($uri, $options = array()) {
public static function fromUri($uri, $options = []) {
$uri_parts = parse_url($uri);
if ($uri_parts === FALSE) {
throw new \InvalidArgumentException(String::format('The URI "@uri" is malformed.', ['@uri' => $uri]));
}
if (empty($uri_parts['scheme'])) {
throw new \InvalidArgumentException(String::format('The URI "@uri" is invalid. You must use a valid URI scheme. Use base: for items like a static file that needs the base path. Use user-path: for user input without a scheme. Use entity: for referencing the canonical route of a content entity.', ['@uri' => $uri]));
throw new \InvalidArgumentException(String::format('The URI "@uri" is invalid. You must use a valid URI scheme. Use base: for items like a static file that needs the base path. Use user-path: for user input without a scheme. Use entity: for referencing the canonical route of a content entity. Use route: for directly representing a route name and parameters.', ['@uri' => $uri]));
}
$uri_parts += ['path' => ''];
// Extract query parameters and fragment and merge them into $uri_options,
// but preserve the original $options for the fallback case.
$uri_options = $options;
if (!empty($uri_parts['fragment'])) {
$uri_options += ['fragment' => $uri_parts['fragment']];
}
unset($uri_parts['fragment']);
if (!empty($uri_parts['query'])) {
$uri_query = [];
parse_str($uri_parts['query'], $uri_query);
$uri_options['query'] = isset($uri_options['query']) ? $uri_options['query'] + $uri_query : $uri_query;
unset($uri_parts['query']);
}
if ($uri_parts['scheme'] === 'entity') {
$url = static::fromEntityUri($uri);
$url = static::fromEntityUri($uri_parts, $uri_options, $uri);
}
elseif ($uri_parts['scheme'] === 'user-path') {
$url = static::fromUserPathUri($uri, $options);
$url = static::fromUserPathUri($uri_parts, $uri_options);
}
elseif ($uri_parts['scheme'] === 'route') {
$url = static::fromRouteUri($uri_parts, $uri_options, $uri);
}
else {
$url = new static($uri, array(), $options);
......@@ -271,8 +288,13 @@ public static function fromUri($uri, $options = array()) {
/**
* Create a new Url object for entity URIs.
*
* @param array $uri_parts
* Parts from an URI of the form entity:{entity_type}/{entity_id} as from
* parse_url().
* @param array $options
* An array of options, see static::fromUri() for details.
* @param string $uri
* An URI of the form entity:{entity_type}/{entity_id}.
* The original entered URI.
*
* @return \Drupal\Core\Url
* A new Url object for an entity's canonical route.
......@@ -280,37 +302,67 @@ public static function fromUri($uri, $options = array()) {
* @throws \InvalidArgumentException
* Thrown if the entity URI is invalid.
*/
protected static function fromEntityUri($uri) {
$uri_parts = parse_url($uri);
protected static function fromEntityUri(array $uri_parts, $options, $uri) {
list($entity_type_id, $entity_id) = explode('/', $uri_parts['path'], 2);
if ($uri_parts['scheme'] != 'entity' || $entity_id === '') {
throw new \InvalidArgumentException(String::format('The entity URI "@uri" is invalid. You must specify the entity id in the URL. e.g., entity:node/1 for loading the canonical path to node entity with id 1.', ['@uri' => $uri]));
}
return new static("entity.$entity_type_id.canonical", [$entity_type_id => $entity_id]);
return new static("entity.$entity_type_id.canonical", [$entity_type_id => $entity_id], $options);
}
/**
* Creates a new Url object for 'user-path:' URIs.
*
* @param string $uri
* An URI of the form user-path:{path}.
* @param array $uri_parts
* Parts from an URI of the form user-path:{path} as from parse_url().
* @param array $options
* An array of options, see static::fromUri() for details.
*
* @return \Drupal\Core\Url
* A new Url object for a 'user-path:' URI.
*/
protected static function fromUserPathUri($uri, array $options) {
$uri_reference = explode(':', $uri, 2)[1];
protected static function fromUserPathUri(array $uri_parts, array $options) {
$url = \Drupal::pathValidator()
->getUrlIfValidWithoutAccessCheck($uri_reference) ?: static::fromUri('base:' . $uri_reference);
// Allow to specify additional or override options from the path.
->getUrlIfValidWithoutAccessCheck($uri_parts['path']) ?: static::fromUri('base:' . $uri_parts['path'], $options);
// Allow specifying additional options.
$url->setOptions($options + $url->getOptions());
return $url;
}
/**
* Creates a new Url object for 'route:' URIs.
*
* @param array $uri_parts
* Parts from an URI of the form route:{route_name};{route_parameters} as
* from parse_url(), where the path is the route name optionally followed by
* a ";" followed by route parameters in key=value format with & separators.
* @param array $options
* An array of options, see static::fromUri() for details.
* @param string $uri
* The original passed in URI.
*
* @return \Drupal\Core\Url
* A new Url object for a 'route:' URI.
*
* @throws \InvalidArgumentException
* Thrown when the route URI does not have a route name.
*/
protected static function fromRouteUri(array $uri_parts, array $options, $uri) {
$route_parts = explode(';', $uri_parts['path'], 2);
$route_name = $route_parts[0];
if ($route_name === '') {
throw new \InvalidArgumentException(String::format('The route URI "@uri" is invalid. You must have a route name in the URI. e.g., route:system.admin', ['@uri' => $uri]));
}
$route_parameters = [];
if (!empty($route_parts[1])) {
parse_str($route_parts[1], $route_parameters);
}
return new static($route_name, $route_parameters, $options);
}
/**
* Returns the Url object matching a request.
*
......@@ -357,6 +409,32 @@ protected function setUnrouted() {
$this->routeParameters = array();
}
/**
* Return a URI string that represents tha data in the Url object.
*
* The URI will typically have the scheme of route: even if the object was
* constructed using an entity: or user-path: scheme. A user-path: URI
* that does not match a Drupal route with be returned here with the base:
* scheme, and external URLs will be returned in their original form.
*
* @return string
* A URI representation of the Url object data.
*/
public function toUriString() {
if ($this->isRouted()) {
$uri = 'route:' . $this->routeName;
if ($this->routeParameters) {
$uri .= ';' . UrlHelper::buildQuery($this->routeParameters);
}
}
else {
$uri = $this->uri;
}
$query = !empty($this->options['query']) ? ('?' . UrlHelper::buildQuery($this->options['query'])) : '';
$fragment = !empty($this->options['fragment']) ? '#' . $this->options['fragment'] : '';
return $uri . $query . $fragment;
}
/**
* Indicates if this Url is external.
*
......
......@@ -265,7 +265,7 @@ function testExternalUrls() {
// Verify external URL can contain a query string.
$url = $test_url . '?drupal=awesome';
$result = Url::fromUri($url)->toString();
$this->assertEqual($url, $result, 'External URL with query string works without a query string in $options.');
$this->assertEqual($url, $result);
// Verify external URL can be extended with a query string.
$url = $test_url;
......@@ -277,6 +277,6 @@ function testExternalUrls() {
$url = $test_url . '?drupal=awesome';
$query = array($this->randomMachineName(5) => $this->randomMachineName(5));
$result = Url::fromUri($url, array('query' => $query))->toString();
$this->assertEqual($url . '&' . http_build_query($query, '', '&'), $result, 'External URL query string can be extended with a custom query string in $options.');
$this->assertEqual($url . '&' . http_build_query($query, '', '&'), $result);
}
}
......@@ -9,7 +9,6 @@
use Drupal\Core\Access\AccessManagerInterface;
use Drupal\Core\DependencyInjection\ContainerBuilder;
use Drupal\Core\Entity\EntityType;
use Drupal\Core\Routing\RouteMatch;
use Drupal\Core\Url;
use Drupal\Tests\UnitTestCase;
......@@ -18,7 +17,6 @@
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Routing\Exception\InvalidParameterException;
use Symfony\Component\Routing\Exception\ResourceNotFoundException;
use Symfony\Component\Routing\Exception\RouteNotFoundException;
use Symfony\Component\Routing\Route;
/**
......@@ -480,15 +478,51 @@ public function testFromRouteMatch() {
$this->assertEquals(['foo' => '1'] , $url->getRouteParameters());
}
/**
* Data provider for testing entity URIs
*/
public function providerTestEntityUris() {
return [
[
'entity:test_entity/1',
[],
'entity.test_entity.canonical',
['test_entity' => '1'],
NULL,
NULL,
],
[
'entity:test_entity/2?page=1&foo=bar#bottom',
[], 'entity.test_entity.canonical',
['test_entity' => '2'],
['page' => '1', 'foo' => 'bar'],
'bottom',
],
[
'entity:test_entity/2?page=1&foo=bar#bottom',
['fragment' => 'top', 'query' => ['foo' => 'yes', 'focus' => 'no']],
'entity.test_entity.canonical',
['test_entity' => '2'],
['page' => '1', 'foo' => 'yes', 'focus' => 'no'],
'top',
],
];
}
/**
* Tests the fromUri() method with an entity: URI.
*
* @covers ::fromUri
*
* @dataProvider providerTestEntityUris
*/
public function testEntityUris() {
$url = Url::fromUri('entity:test_entity/1');
$this->assertSame('entity.test_entity.canonical', $url->getRouteName());
$this->assertEquals(['test_entity' => '1'], $url->getRouteParameters());
public function testEntityUris($uri, $options, $route_name, $route_parameters, $query, $fragment) {
$url = Url::fromUri($uri, $options);
$this->assertSame($route_name, $url->getRouteName());
$this->assertEquals($route_parameters, $url->getRouteParameters());
$this->assertEquals($url->getOption('query'), $query);
$this->assertSame($url->getOption('fragment'), $fragment);
}
/**
......@@ -507,6 +541,89 @@ public function testInvalidEntityUriParameter() {
Url::fromUri('entity:test_entity/1/blah')->toString();
}
/**
* Tests the toUriString() method with entity: URIs.
*
* @covers ::toUriString
*
* @dataProvider providerTestToUriStringForEntity
*/
public function testToUriStringForEntity($uri, $options, $uri_string) {
$url = Url::fromUri($uri, $options);
$this->assertSame($url->toUriString(), $uri_string);
}
/**
* Data provider for testing string entity URIs
*/
public function providerTestToUriStringForEntity() {
return [
['entity:test_entity/1', [], 'route:entity.test_entity.canonical;test_entity=1'],
['entity:test_entity/1', ['fragment' => 'top', 'query' => ['page' => '2']], 'route:entity.test_entity.canonical;test_entity=1?page=2#top'],
['entity:test_entity/1?page=2#top', [], 'route:entity.test_entity.canonical;test_entity=1?page=2#top'],
];
}
/**
* Tests the toUriString() method with user-path: URIs.
*
* @covers ::toUriString
*
* @dataProvider providerTestToUriStringForUserPath
*/
public function testToUriStringForUserPath($uri, $options, $uri_string) {
$url = Url::fromRoute('entity.test_entity.canonical', ['test_entity' => '1']);
$this->pathValidator->expects($this->any())
->method('getUrlIfValidWithoutAccessCheck')
->with('test-entity/1')
->willReturn($url);
$url = Url::fromUri($uri, $options);
$this->assertSame($url->toUriString(), $uri_string);
}
/**
* Data provider for testing user-path URIs
*/
public function providerTestToUriStringForUserPath() {
return [
['user-path:test-entity/1', [], 'route:entity.test_entity.canonical;test_entity=1'],
['user-path:test-entity/1', ['fragment' => 'top'], 'route:entity.test_entity.canonical;test_entity=1#top'],
['user-path:test-entity/1', ['fragment' => 'top', 'query' => ['page' => '2']], 'route:entity.test_entity.canonical;test_entity=1?page=2#top'],
['user-path:test-entity/1?page=2#top', [], 'route:entity.test_entity.canonical;test_entity=1?page=2#top'],
];
}
/**
* Tests the toUriString() method with route: URIs.
*
* @covers ::toUriString
*
* @dataProvider providerTestToUriStringForRoute
*/
public function testToUriStringForRoute($uri, $options, $uri_string) {
$url = Url::fromUri($uri, $options);
$this->assertSame($url->toUriString(), $uri_string);
}
/**
* Data provider for testing user-path URIs
*/
public function providerTestToUriStringForRoute() {
return [
['route:entity.test_entity.canonical;test_entity=1', [], 'route:entity.test_entity.canonical;test_entity=1'],
['route:entity.test_entity.canonical;test_entity=1', ['fragment' => 'top', 'query' => ['page' => '2']], 'route:entity.test_entity.canonical;test_entity=1?page=2#top'],
['route:entity.test_entity.canonical;test_entity=1?page=2#top', [], 'route:entity.test_entity.canonical;test_entity=1?page=2#top'],
];
}
/**
* @expectedException \InvalidArgumentException
* @expectedExceptionMessage The route URI "route:" is invalid.
*/
public function testFromRouteUriWithMissingRouteName() {
Url::fromUri('route:');
}
/**
* Creates a mock access manager for the access tests.
*
......
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment