Commit 8d87f7fa authored by alexpott's avatar alexpott

Issue #2426181 by effulgentsia, mpdonadio, xjm: Add a Url::fromUserInput()...

Issue #2426181 by effulgentsia, mpdonadio, xjm: Add a Url::fromUserInput() wrapper method for generating URLs from user-entered paths
parent 4274d663
......@@ -34,7 +34,8 @@ 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'));
$url = Url::fromUri('user-path:/' . $options['path'], $options);
// @todo Revisit this in https://www.drupal.org/node/2418219.
$url = Url::fromUserInput('/' . $options['path'], $options);
}
// Check for a route-based cancel link.
else {
......
......@@ -166,7 +166,8 @@ public function __construct($route_name, $route_parameters = array(), $options =
* @return \Drupal\Core\Url
* A new Url object for a routed (internal to Drupal) URL.
*
* @see static::fromUri()
* @see \Drupal\Core\Url::fromUserInput()
* @see \Drupal\Core\Url::fromUri()
*/
public static function fromRoute($route_name, $route_parameters = array(), $options = array()) {
return new static($route_name, $route_parameters, $options);
......@@ -189,6 +190,56 @@ public static function fromRouteMatch(RouteMatchInterface $route_match) {
}
}
/**
* Creates a Url object for a relative URI reference submitted by user input.
*
* Use this method to create a URL for user-entered paths that may or may not
* correspond to a valid Drupal route.
*
* @param string $user_input
* User input for a link or path. The first character must be one of the
* following characters:
* - '/': A path within the current site. This path might be to a Drupal
* route (e.g., '/admin'), to a file (e.g., '/README.txt'), or to
* something processed by a non-Drupal script (e.g.,
* '/not/a/drupal/page'). If the path matches a Drupal route, then the
* URL generation will include Drupal's path processors (e.g.,
* language-prefixing and aliasing). Otherwise, the URL generation will
* just append the passed-in path to Drupal's base path.
* - '?': A query string for the current page or resource.
* - '#': A fragment (jump-link) on the current page or resource.
* This helps reduce ambiguity for user-entered links and paths, and
* supports user interfaces where users may normally use auto-completion
* to search for existing resources, but also may type one of these
* characters to link to (e.g.) a specific path on the site.
* (With regard to the URI specification, the user input is treated as a
* @link https://tools.ietf.org/html/rfc3986#section-4.2 relative URI reference @endlink
* where the relative part is of type
* @link https://tools.ietf.org/html/rfc3986#section-3.3 path-abempty @endlink.)
* @param array $options
* (optional) An array of options. See Url::fromUri() for details.
*
* @return static
* A new Url object based on user input.
*
* @throws \InvalidArgumentException
* Thrown when the user input does not begin with one of the following
* characters: '/', '?', or '#'.
*/
public static function fromUserInput($user_input, $options = []) {
// Ensuring one of these initial characters also enforces that what is
// passed is a relative URI reference rather than an absolute URI,
// because these are URI reserved characters that a scheme name may not
// start with.
if ((strpos($user_input, '/') !== 0) && (strpos($user_input, '#') !== 0) && (strpos($user_input, '?') !== 0)) {
throw new \InvalidArgumentException(String::format("The user-entered string @user_input must begin with a '/', '?', or '#'.", ['@user_input' => $user_input]));
}
// fromUri() requires an absolute URI, so prepend the appropriate scheme
// name.
return static::fromUri('user-path:' . $user_input, $options);
}
/**
* Creates a new Url object from a URI.
*
......@@ -239,7 +290,8 @@ public static function fromRouteMatch(RouteMatchInterface $route_match) {
* @throws \InvalidArgumentException
* Thrown when the passed in path has no scheme.
*
* @see static::fromRoute()
* @see \Drupal\Core\Url::fromRoute()
* @see \Drupal\Core\Url::fromUserInput()
*/
public static function fromUri($uri, $options = []) {
$uri_parts = parse_url($uri);
......
......@@ -61,7 +61,8 @@ public static function getNextDestination(array $destinations) {
$options['query']['destinations'] = $destinations;
}
// Redirect to any given path within the same domain.
$next_destination = Url::fromUri('user-path:/' . $options['path']);
// @todo Revisit this in https://www.drupal.org/node/2418219.
$next_destination = Url::fromUserInput('/' . $options['path']);
}
return $next_destination;
}
......
......@@ -86,10 +86,12 @@ public function adminOverview(Request $request) {
$destination = drupal_get_destination();
foreach ($this->aliasStorage->getAliasesForAdminListing($header, $keys) as $data) {
$row = array();
$row['data']['alias'] = $this->l(Unicode::truncate($data->alias, 50, FALSE, TRUE), Url::fromUri('user-path:/' . $data->source, array(
// @todo Should Path module store leading slashes? See
// https://www.drupal.org/node/2430593.
$row['data']['alias'] = $this->l(Unicode::truncate($data->alias, 50, FALSE, TRUE), Url::fromUserInput('/' . $data->source, array(
'attributes' => array('title' => $data->alias),
)));
$row['data']['source'] = $this->l(Unicode::truncate($data->source, 50, FALSE, TRUE), Url::fromUri('user-path:/' . $data->source, array(
$row['data']['source'] = $this->l(Unicode::truncate($data->source, 50, FALSE, TRUE), Url::fromUserInput('/' . $data->source, array(
'alias' => TRUE,
'attributes' => array('title' => $data->source),
)));
......
......@@ -31,7 +31,7 @@ function testLinkXSS() {
// Test \Drupal::l().
$text = $this->randomMachineName();
$path = "<SCRIPT>alert('XSS')</SCRIPT>";
$link = \Drupal::l($text, Url::fromUri('user-path:/' . $path));
$link = \Drupal::l($text, Url::fromUserInput('/' . $path));
$sanitized_path = check_url(Url::fromUri('base:' . $path)->toString());
$this->assertTrue(strpos($link, $sanitized_path) !== FALSE, format_string('XSS attack @path was filtered by _l().', array('@path' => $path)));
......
......@@ -55,7 +55,8 @@ public function submitForm(array &$form, FormStateInterface $form_state) {
if (!$form_state->isValueEmpty('redirection')) {
if (!$form_state->isValueEmpty('destination')) {
// The destination is a random URL, so we can't use routed URLs.
$form_state->setRedirectUrl(Url::fromUri('user-path:/' . $form_state->getValue('destination')));
// @todo Revist this in https://www.drupal.org/node/2418219.
$form_state->setRedirectUrl(Url::fromUserInput('/' . $form_state->getValue('destination')));
}
}
else {
......
......@@ -2060,7 +2060,9 @@ public function renderMoreLink() {
if ($this->getOption('link_display') == 'custom_url' && $override_path = $this->getOption('link_url')) {
$tokens = $this->getArgumentsTokens();
$path = $this->viewsTokenReplace($override_path, $tokens);
$url = Url::fromUri('user-path:/' . $path);
// @todo Views should expect and store a leading /. See:
// https://www.drupal.org/node/2423913
$url = Url::fromUserInput('/' . $path);
}
// Otherwise, use the URL for the display.
else {
......
......@@ -1248,7 +1248,9 @@ public function renderText($alter) {
$more_link_path = Unicode::substr($more_link_path, Unicode::strlen($base_path));
}
$more_link = \Drupal::l($more_link_text, CoreUrl::fromUri('user-path:/' . $more_link_path), array('attributes' => array('class' => array('views-more-link'))));
// @todo Views should expect and store a leading /. See:
// https://www.drupal.org/node/2423913
$more_link = \Drupal::l($more_link_text, CoreUrl::fromUserInput('/' . $more_link_path), array('attributes' => array('class' => array('views-more-link'))));
$suffix .= " " . $more_link;
}
......@@ -1313,7 +1315,9 @@ protected function renderAsLink($alter, $text, $tokens) {
$path = $alter['path'];
if (empty($alter['url'])) {
if (!parse_url($path, PHP_URL_SCHEME)) {
$alter['url'] = CoreUrl::fromUri('user-path:/' . ltrim($path, '/'));
// @todo Views should expect and store a leading /. See:
// https://www.drupal.org/node/2423913
$alter['url'] = CoreUrl::fromUserInput('/' . ltrim($path, '/'));
}
else {
$alter['url'] = CoreUrl::fromUri($path);
......
......@@ -49,7 +49,9 @@ public function buildOptionsForm(&$form, FormStateInterface $form_state) {
public function render(ResultRow $values) {
$value = $this->getValue($values);
if (!empty($this->options['display_as_link'])) {
return \Drupal::l($this->sanitizeValue($value), CoreUrl::fromUri('user-path:/' . $value), array('html' => TRUE));
// @todo Views should expect and store a leading /. See:
// https://www.drupal.org/node/2423913
return \Drupal::l($this->sanitizeValue($value), CoreUrl::fromUserInput('/' . $value), array('html' => TRUE));
}
else {
return $this->sanitizeValue($value, 'url');
......
......@@ -144,7 +144,9 @@ public function render($row) {
// Create the RSS item object.
$item = new \stdClass();
$item->title = $this->getField($row_index, $this->options['title_field']);
$item->link = Url::fromUri('user-path:/' . $this->getField($row_index, $this->options['link_field']))->setAbsolute()->toString();
// @todo Views should expect and store a leading /. See:
// https://www.drupal.org/node/2423913
$item->link = Url::fromUserInput('/' . $this->getField($row_index, $this->options['link_field']))->setAbsolute()->toString();
$item->description = $this->getField($row_index, $this->options['description_field']);
$item->elements = array(
array('key' => 'pubDate', 'value' => $this->getField($row_index, $this->options['date_field'])),
......@@ -158,7 +160,9 @@ public function render($row) {
$item_guid = $this->getField($row_index, $this->options['guid_field_options']['guid_field']);
if ($this->options['guid_field_options']['guid_field_is_permalink']) {
$guid_is_permalink_string = 'true';
$item_guid = Url::fromUri('user-path:/' . $item_guid)->setAbsolute()->toString();
// @todo Enforce GUIDs as system-generated rather than user input? See
// https://www.drupal.org/node/2430589.
$item_guid = Url::fromUserInput('/' . $item_guid)->setAbsolute()->toString();
}
$item->elements[] = array(
'key' => 'guid',
......
......@@ -328,7 +328,9 @@ function template_preprocess_views_view_summary(&$variables) {
$base_path = $argument->options['summary_options']['base_path'];
$tokens = $this->getArgumentsTokens();
$base_path = $this->viewsTokenReplace($base_path, $tokens);
$url = Url::fromUri('user-path:/' . $base_path);
// @todo Views should expect and store a leading /. See:
// https://www.drupal.org/node/2423913
$url = Url::fromUserInput('/' . $base_path);
}
else {
$url = $view->getUrl($args)->setOptions($url_options);
......@@ -395,7 +397,9 @@ function template_preprocess_views_view_summary_unformatted(&$variables) {
$base_path = $argument->options['summary_options']['base_path'];
$tokens = $this->getArgumentsTokens();
$base_path = $this->viewsTokenReplace($base_path, $tokens);
$url = Url::fromUri('user-path:/' . $base_path);
// @todo Views should expect and store a leading /. See:
// https://www.drupal.org/node/2423913
$url = Url::fromUserInput('/' . $base_path);
}
else {
$url = $view->getUrl($args)->setOptions($url_options);
......@@ -876,7 +880,9 @@ function template_preprocess_views_view_rss(&$variables) {
// Compare the link to the default home page; if it's the default home page,
// just use $base_url.
$url_string = $url->setOptions($url_options)->toString();
if ($url_string === Url::fromUri('user-path:/' . $config->get('page.front'))->toString()) {
// @todo Should page.front be stored with a leading slash? See
// https://www.drupal.org/node/2430595.
if ($url_string === Url::fromUserInput('/' . $config->get('page.front'))->toString()) {
$url_string = Url::fromRoute('<front>')->setAbsolute()->toString();
}
......
......@@ -264,7 +264,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('user-path:/' . $path));
// @todo Views should expect and store a leading /. See:
// https://www.drupal.org/node/2423913
$all_paths[] = \Drupal::l('/' . $path, Url::fromUserInput('/' . $path));
}
else {
$all_paths[] = String::checkPlain('/' . $path);
......
......@@ -723,7 +723,9 @@ public function renderPreview($display_id, $args = array()) {
Xss::filterAdmin($this->executable->getTitle()),
);
if (isset($path)) {
$path = \Drupal::l($path, Url::fromUri('user-path:/' . $path));
// @todo Views should expect and store a leading /. See:
// https://www.drupal.org/node/2423913
$path = \Drupal::l($path, Url::fromUserInput('/' . $path));
}
else {
$path = t('This display has no path.');
......
......@@ -7,6 +7,7 @@
namespace Drupal\Tests\Core;
use Drupal\Component\Utility\UrlHelper;
use Drupal\Core\Access\AccessManagerInterface;
use Drupal\Core\DependencyInjection\ContainerBuilder;
use Drupal\Core\Routing\RouteMatch;
......@@ -170,6 +171,49 @@ public function testFromRouteFront() {
$this->assertSame('<front>', $url->getRouteName());
}
/**
* Tests the fromUserInput method with valid paths.
*
* @covers ::fromUserInput
* @dataProvider providerFromValidUserPathUri
*/
public function testFromUserInput($path) {
$url = Url::fromUserInput($path);
$uri = $url->getUri();
$this->assertInstanceOf('Drupal\Core\Url', $url);
$this->assertFalse($url->isRouted());
$this->assertEquals(0, strpos($uri, 'base:'));
$parts = UrlHelper::parse($path);
$options = $url->getOptions();
if (!empty($parts['fragment'])) {
$this->assertSame($parts['fragment'], $options['fragment']);
}
else {
$this->assertArrayNotHasKey('fragment', $options);
}
if (!empty($parts['query'])) {
$this->assertEquals($parts['query'], $options['query']);
}
else {
$this->assertArrayNotHasKey('query', $options);
}
}
/**
* Tests the fromUserInput method with invalid paths.
*
* @covers ::fromUserInput
* @expectedException \InvalidArgumentException
* @dataProvider providerFromInvalidUserPathUri
*/
public function testFromInvalidUserInput($path) {
$url = Url::fromUserInput($path);
}
/**
* Tests fromUri() method with a user-entered path not matching any route.
*
......@@ -663,6 +707,7 @@ public function providerFromValidUserPathUri() {
['/kittens?page=1000'],
['/?page=1000'],
['?page=1000'],
['?breed=bengal&page=1000'],
// Paths with various token formats but no leading slash.
['/[duckies]'],
['/%bunnies'],
......
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