diff --git a/core/lib/Drupal/Core/Form/ConfirmFormHelper.php b/core/lib/Drupal/Core/Form/ConfirmFormHelper.php index b58078d..73ba98e 100644 --- a/core/lib/Drupal/Core/Form/ConfirmFormHelper.php +++ b/core/lib/Drupal/Core/Form/ConfirmFormHelper.php @@ -34,9 +34,7 @@ public static function buildCancelLink(ConfirmFormInterface $form, Request $requ // If a destination is specified, that serves as the cancel link. if ($query->has('destination')) { $options = UrlHelper::parse($query->get('destination')); - // @todo Use Url::fromPath() once https://www.drupal.org/node/2351379 is - // resolved. - $url = Url::fromUri('base://' . $options['path'], $options); + $url = Url::fromUri('user-path://' . $options['path'], $options); } // Check for a route-based cancel link. else { diff --git a/core/lib/Drupal/Core/Url.php b/core/lib/Drupal/Core/Url.php index 999f447..62d6eb8 100644 --- a/core/lib/Drupal/Core/Url.php +++ b/core/lib/Drupal/Core/Url.php @@ -197,8 +197,11 @@ public static function fromRouteMatch(RouteMatchInterface $route_match) { * 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. + * The URI of the external resource including the scheme. For user input + * that may correspond to a Drupal route, use user-path:// for the scheme. + * For paths that are known not to be handled by the Drupal routing system + * (such as static files), use base:// for the scheme to get a link + * relative to the Drupal base path (like the HTML element). * @param array $options * (optional) An associative array of additional URL options, with the * following elements: @@ -216,8 +219,14 @@ public static function fromRouteMatch(RouteMatchInterface $route_match) { * defined, the current scheme is used, so the user stays on HTTP or HTTPS * respectively. TRUE enforces HTTPS and FALSE enforces HTTP. * + * Note: the user-path:// scheme should be avoided except when processing actual + * user input that may or may not correspond to a Drupal route. Normally use + * Url::fromRoute() for code linking to any any Drupal page. + * + * You can call access() on the returned object to do access checking. + * * @return \Drupal\Core\Url - * A new Url object for an unrouted (non-Drupal) URL. + * A new Url object. * * @throws \InvalidArgumentException * Thrown when the passed in path has no scheme. @@ -225,12 +234,21 @@ public static function fromRouteMatch(RouteMatchInterface $route_match) { * @see static::fromRoute() */ public static function fromUri($uri, $options = array()) { - if (!parse_url($uri, PHP_URL_SCHEME)) { - throw new \InvalidArgumentException(String::format('The URI "@uri" is invalid. You must use a valid URI scheme. Use base:// for a path, e.g., to a Drupal file that needs the base path. Do not use this for internal paths controlled by Drupal.', ['@uri' => $uri])); + $scheme = parse_url($uri, PHP_URL_SCHEME); + if (!$scheme) { + throw new \InvalidArgumentException(String::format('The URI "@uri" is invalid. You must use a valid URI scheme. Use base:// for a path, e.g., to a Drupal file that needs the base path. Do not use this for internal paths controlled by Drupal. Use user-path:// for user input without a scheme.', ['@uri' => $uri])); + } + if ($scheme == 'user-path'){ + $path = substr($uri, 12); + $url = \Drupal::pathValidator() + ->getUrlIfValidWithoutAccessCheck($path) ?: Url::fromUri('base://' . $path); + // Allow to specify additional or override options from the path. + $url->setOptions($options + $url->getOptions()); + } + else { + $url = new static($uri, array(), $options); + $url->setUnrouted(); } - - $url = new static($uri, array(), $options); - $url->setUnrouted(); return $url; } @@ -552,7 +570,11 @@ public function getInternalPath() { * Returns TRUE if the user has access to the url, otherwise FALSE. */ public function access(AccountInterface $account = NULL) { - return $this->accessManager()->checkNamedRoute($this->getRouteName(), $this->getRouteParameters(), $account); + if ($this->isRouted()) { + return $this->accessManager() + ->checkNamedRoute($this->getRouteName(), $this->getRouteParameters(), $account); + } + return TRUE; } /** diff --git a/core/modules/dblog/src/Controller/DbLogController.php b/core/modules/dblog/src/Controller/DbLogController.php index e8b1ba5..732816c 100644 --- a/core/modules/dblog/src/Controller/DbLogController.php +++ b/core/modules/dblog/src/Controller/DbLogController.php @@ -255,11 +255,11 @@ public function eventDetails($event_id) { ), array( array('data' => $this->t('Location'), 'header' => TRUE), - $this->l($dblog->location, $dblog->location ? Url::fromUri('base://' . $dblog->location) : Url::fromRoute('')), + $this->l($dblog->location, $dblog->location ? Url::fromUri('user-path://' . $dblog->location) : Url::fromRoute('')), ), array( array('data' => $this->t('Referrer'), 'header' => TRUE), - $this->l($dblog->referer, $dblog->referer ? Url::fromUri('base://' . $dblog->referer) : Url::fromRoute('')), + $this->l($dblog->referer, $dblog->referer ? Url::fromUri('user-path://' . $dblog->location) : Url::fromRoute('')), ), array( array('data' => $this->t('Message'), 'header' => TRUE), diff --git a/core/modules/field_ui/src/FieldUI.php b/core/modules/field_ui/src/FieldUI.php index 4bbd93a..6ea2aa6 100644 --- a/core/modules/field_ui/src/FieldUI.php +++ b/core/modules/field_ui/src/FieldUI.php @@ -61,9 +61,7 @@ public static function getNextDestination(array $destinations) { $options['query']['destinations'] = $destinations; } // Redirect to any given path within the same domain. - // @todo Use Url::fromPath() once https://www.drupal.org/node/2351379 is - // resolved. - $next_destination = Url::fromUri('base://' . $options['path']); + $next_destination = Url::fromUri('user-path://' . $options['path']); } return $next_destination; } diff --git a/core/modules/menu_ui/src/Tests/MenuTest.php b/core/modules/menu_ui/src/Tests/MenuTest.php index e33e53a..1b09140 100644 --- a/core/modules/menu_ui/src/Tests/MenuTest.php +++ b/core/modules/menu_ui/src/Tests/MenuTest.php @@ -115,8 +115,8 @@ function testMenu() { // Verify delete link exists and reset link does not exist. $this->drupalGet('admin/structure/menu/manage/' . $this->menu->id()); - $this->assertLinkByHref('admin/structure/menu/item/' . $this->items[0]->id() . '/delete'); - $this->assertNoLinkByHref(Url::fromUri('base://admin/structure/menu/link/' . $this->items[0]->getPluginId() . '/reset')->toString()); + $this->assertLinkByHref(Url::fromRoute('entity.menu_link_content.delete_form', ['menu_link_content' => $this->items[0]->id()])->toString()); + $this->assertNoLinkByHref(Url::fromRoute('menu_ui.link_reset', ['menu_link_plugin' => $this->items[0]->getPluginId()])->toString()); // Check delete and reset access. $this->drupalGet('admin/structure/menu/item/' . $this->items[0]->id() . '/delete'); $this->assertResponse(200); diff --git a/core/modules/system/system.routing.yml b/core/modules/system/system.routing.yml index 2bb9e62..f5e0e99 100644 --- a/core/modules/system/system.routing.yml +++ b/core/modules/system/system.routing.yml @@ -383,7 +383,6 @@ system.theme_settings_theme: options: _only_fragment: TRUE - '': path: '' 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 925c84e..f287a19 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,8 +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')) { - // @todo Use Url::fromPath() once https://www.drupal.org/node/2351379 is - // resolved. + // The destination is a random URL, so we can't use routed URLs. $form_state->setRedirectUrl(Url::fromUri('base://' . $form_state->getValue('destination'))); } } diff --git a/core/modules/views_ui/src/ViewListBuilder.php b/core/modules/views_ui/src/ViewListBuilder.php index f9ac0e1..b318a3c 100644 --- a/core/modules/views_ui/src/ViewListBuilder.php +++ b/core/modules/views_ui/src/ViewListBuilder.php @@ -263,7 +263,9 @@ protected function getDisplayPaths(EntityInterface $view) { if ($display->hasPath()) { $path = $display->getPath(); if ($view->status() && strpos($path, '%') === FALSE) { - $all_paths[] = \Drupal::l('/' . $path, Url::fromUri('base://' . $path)); + // @todo Use the method on the executable when + // https://www.drupal.org/node/2364157 is available. + $all_paths[] = \Drupal::l('/' . $path, Url::fromUri('user-path://' . $path)); } else { $all_paths[] = String::checkPlain('/' . $path); diff --git a/core/modules/views_ui/src/ViewUI.php b/core/modules/views_ui/src/ViewUI.php index e8464fa..a42fb24 100644 --- a/core/modules/views_ui/src/ViewUI.php +++ b/core/modules/views_ui/src/ViewUI.php @@ -702,7 +702,7 @@ public function renderPreview($display_id, $args = array()) { Xss::filterAdmin($this->executable->getTitle()), ); if (isset($path)) { - $path = \Drupal::l($path, Url::fromUri('base://' . $path)); + $path = \Drupal::l($path, Url::fromUri('user-path://' . $path)); } else { $path = t('This display has no path.'); diff --git a/core/tests/Drupal/Tests/Core/Form/ConfirmFormHelperTest.php b/core/tests/Drupal/Tests/Core/Form/ConfirmFormHelperTest.php index b80567f..a96ac58 100644 --- a/core/tests/Drupal/Tests/Core/Form/ConfirmFormHelperTest.php +++ b/core/tests/Drupal/Tests/Core/Form/ConfirmFormHelperTest.php @@ -10,6 +10,7 @@ use Drupal\Core\Form\ConfirmFormHelper; use Drupal\Core\Url; use Drupal\Tests\UnitTestCase; +use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\HttpFoundation\Request; /** @@ -95,8 +96,20 @@ public function testCancelLinkRouteWithUrl() { public function testCancelLinkDestination() { $query = array('destination' => 'baz'); $form = $this->getMock('Drupal\Core\Form\ConfirmFormInterface'); + + $path_validator = $this->getMock('Drupal\Core\Path\PathValidatorInterface'); + $container_builder = new ContainerBuilder(); + $container_builder->set('path.validator', $path_validator); + \Drupal::setContainer($container_builder); + + $url = Url::fromRoute('test_route'); + $path_validator->expects($this->atLeastOnce()) + ->method('getUrlIfValidWithoutAccessCheck') + ->with('baz') + ->willReturn($url); + $link = ConfirmFormHelper::buildCancelLink($form, new Request($query)); - $this->assertSame('base://' . $query['destination'], $link['#url']->getUri()); + $this->assertSame($url, $link['#url']); } } diff --git a/core/tests/Drupal/Tests/Core/UrlTest.php b/core/tests/Drupal/Tests/Core/UrlTest.php index cea8770..a08fe59 100644 --- a/core/tests/Drupal/Tests/Core/UrlTest.php +++ b/core/tests/Drupal/Tests/Core/UrlTest.php @@ -58,6 +58,13 @@ class UrlTest extends UnitTestCase { protected $map; /** + * The mocked path validator. + * + * @var \Drupal\Core\Path\PathValidatorInterface|\PHPUnit_Framework_MockObject_MockObject + */ + protected $pathValidator; + + /** * {@inheritdoc} */ protected function setUp() { @@ -90,10 +97,13 @@ protected function setUp() { ->will($this->returnValueMap($alias_map)); $this->router = $this->getMock('Drupal\Tests\Core\Routing\TestRouterInterface'); + $this->pathValidator = $this->getMock('Drupal\Core\Path\PathValidatorInterface'); + $this->container = new ContainerBuilder(); $this->container->set('router.no_access_checks', $this->router); $this->container->set('url_generator', $this->urlGenerator); $this->container->set('path.alias_manager', $this->pathAliasManager); + $this->container->set('path.validator', $this->pathValidator); \Drupal::setContainer($this->container); } @@ -160,6 +170,36 @@ public function testFromRouteFront() { } /** + * Tests fromUri() method with a user-entered path not matching any route. + * + * @covers ::fromUri + */ + public function testFromRoutedPathWithInvalidRoute() { + $this->pathValidator->expects($this->once()) + ->method('getUrlIfValidWithoutAccessCheck') + ->with('invalid-path') + ->willReturn(FALSE); + $url = Url::fromUri('user-path://invalid-path'); + $this->assertSame(FALSE, $url->isRouted()); + $this->assertSame('base://invalid-path', $url->getUri()); + } + + /** + * Tests fromUri() method with user-entered path matching a valid route. + * + * @covers ::fromUri + */ + public function testFromRoutedPathWithValidRoute() { + $url = Url::fromRoute('test_route'); + $this->pathValidator->expects($this->once()) + ->method('getUrlIfValidWithoutAccessCheck') + ->with('valid-path') + ->willReturn($url); + $result_url = Url::fromUri('user-path://valid-path'); + $this->assertSame($url, $result_url); + } + + /** * Tests the createFromRequest method. * * @covers ::createFromRequest