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 = 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;
......
This diff is collapsed.
......@@ -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);
}
return $url;
......
......@@ -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>');
......
......@@ -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();
......
......@@ -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(
......
......@@ -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());
}
......
......@@ -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;
......
......@@ -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 {
......
......@@ -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());
......
......@@ -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'])) {
......
......@@ -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();
}
}
}
......
......@@ -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.
......
......@@ -2,7 +2,7 @@
/**
* @file
* Contains \Drupal\Tests\Core\ExternalUrlTest.
* Contains \Drupal\Tests\Core\UnroutedUrlTest.
*/
namespace Drupal\Tests\Core;
......@@ -15,16 +15,16 @@
/**
* @coversDefaultClass \Drupal\Core\Url
* @group ExternalUrlTest
* @group UrlTest
*/
class ExternalUrlTest extends UnitTestCase {
class UnroutedUrlTest extends UnitTestCase {
/**
* The URL generator
* The URL assembler
*
* @var \Drupal\Core\Routing\UrlGeneratorInterface|\PHPUnit_Framework_MockObject_MockObject
* @var \Drupal\Core\Utility\UnroutedUrlAssemblerInterface|\PHPUnit_Framework_MockObject_MockObject
*/
protected $urlGenerator;
protected $urlAssembler;
/**
* The router.
......@@ -34,11 +34,18 @@ class ExternalUrlTest extends UnitTestCase {
protected $router;
/**
* An external URL to test.
* An unrouted, external URL to test.
*
* @var string
*/
protected $path = 'http://drupal.org';
protected $unroutedExternal = 'http://drupal.org';
/**
* An unrouted, internal URL to test.
*
* @var string
*/
protected $unroutedInternal = 'base://robots.txt';
/**
* {@inheritdoc}
......@@ -46,35 +53,42 @@ class ExternalUrlTest extends UnitTestCase {
protected function setUp() {
parent::setUp();
$this->urlGenerator = $this->getMock('Drupal\Core\Routing\UrlGeneratorInterface');
$this->urlGenerator->expects($this->any())
->method('generateFromPath')
$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('url_generator', $this->urlGenerator);
$container->set('unrouted_url_assembler', $this->urlAssembler);
\Drupal::setContainer($container);
}
/**
* Tests the createFromPath method.
* Tests the fromUri() method.
*
* @covers ::createFromPath()
* @covers ::setExternal()
* @covers ::fromUri
*/
public function testCreateFromPath() {
$url = Url::createFromPath($this->path);
$this->assertInstanceOf('Drupal\Core\Url', $url);
$this->assertTrue($url->isExternal());
return $url;
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()
* @covers ::createFromRequest
*
* @expectedException \Symfony\Component\Routing\Exception\ResourceNotFoundException
*/
......@@ -89,102 +103,123 @@ public function testCreateFromRequest() {
$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 testCreateFromPath
* @depends testFromUri
*
* @covers ::isExternal()
* @covers ::isExternal
*/
public function testIsExternal(Url $url) {
$this->assertTrue($url->isExternal());
public function testIsExternal(array $urls) {
$this->assertTrue($urls[0]->isExternal());
$this->assertFalse($urls[1]->isExternal());
}
/**
* Tests the toString() method.
*
* @depends testCreateFromPath
* @depends testFromUri
*
* @covers ::toString()
* @covers ::toString
*/
public function testToString(Url $url) {
$this->assertSame($this->path, $url->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 testCreateFromPath
* @depends testFromUri
*
* @covers ::toArray()
* @covers ::toArray
*/
public function testToArray(Url $url) {
public function testToArray(array $urls) {
$expected = array(
'path' => $this->path,
'path' => $this->unroutedExternal,
'options' => array('external' => TRUE),
);
$this->assertSame($expected, $url->toArray());
$this->assertSame($expected, $urls[0]->toArray());
$expected = array(
'path' => $this->unroutedInternal,
'options' => array(),
);
$this->assertSame($expected, $urls[1]->toArray());
}
/**
* Tests the getRouteName() method.
*
* @depends testCreateFromPath
* @depends testFromUri
*
* @expectedException \UnexpectedValueException
*
* @covers ::getRouteName()
* @covers ::getRouteName
*/
public function testGetRouteName(Url $url) {
$url->getRouteName();
public function testGetRouteName(array $urls) {
$urls[0]->getRouteName();
}
/**
* Tests the getRouteParameters() method.
*
* @depends testCreateFromPath
* @depends testFromUri
*
* @expectedException \UnexpectedValueException
*
* @covers ::getRouteParameters()
* @covers ::getRouteParameters
*/
public function testGetRouteParameters(Url $url) {
$url->getRouteParameters();
public function testGetRouteParameters(array $urls) {
$urls[0]->getRouteParameters();
}
/**
* Tests the getInternalPath() method.
*
* @depends testCreateFromPath
* @depends testFromUri
*
* @covers ::getInternalPath()
* @covers ::getInternalPath
*
* @expectedException \Exception
*/
public function testGetInternalPath(Url $url) {
$this->assertNull($url->getInternalPath());
public function testGetInternalPath(array $urls) {
$this->assertNull($urls[0]->getInternalPath());
}
/**
* Tests the getPath() method.
*
* @depends testCreateFromPath
* @depends testFromUri
*
* @covers ::getPath()
* @covers ::getUri
*/
public function testGetPath(Url $url) {
$this->assertNotNull($url->getPath());
public function testGetUri(array $urls) {
$this->assertNotNull($urls[0]->getUri());
$this->assertNotNull($urls[1]->getUri());
}
/**
* Tests the getOptions() method.
*
* @depends testCreateFromPath
* @depends testFromUri
*
* @covers ::getOptions()
* @covers ::getOptions
*/
public function testGetOptions(Url $url) {