.../src/Plugin/Field/FieldWidget/LinkWidget.php | 40 ++++++++---- core/modules/link/src/Tests/LinkFieldTest.php | 72 ++++++++++------------ 2 files changed, 61 insertions(+), 51 deletions(-) diff --git a/core/modules/link/src/Plugin/Field/FieldWidget/LinkWidget.php b/core/modules/link/src/Plugin/Field/FieldWidget/LinkWidget.php index 15e13e1..574a514 100644 --- a/core/modules/link/src/Plugin/Field/FieldWidget/LinkWidget.php +++ b/core/modules/link/src/Plugin/Field/FieldWidget/LinkWidget.php @@ -15,6 +15,7 @@ use Drupal\Core\Form\FormStateInterface; use Drupal\Core\Url; use Drupal\link\LinkItemInterface; +use Symfony\Component\Routing\Exception\RouteNotFoundException; use Symfony\Component\Validator\ConstraintViolation; use Symfony\Component\Validator\ConstraintViolationInterface; use Symfony\Component\Validator\ConstraintViolationListInterface; @@ -59,6 +60,11 @@ protected static function getUriAsDisplayableString($uri) { $uri = str_replace('', '', $uri); $uri = str_replace('', '/', $uri); + // By default, the displayable string is the URI. + $displayable_string = $uri; + + // A different displayable string may be chosen in case of the 'user-path:' + // or 'entity:' built-in schemes. if ($scheme === 'user-path') { $uri_reference = explode(':', $uri, 2)[1]; // Add / in case we don't start with ?, # and / already, which are the @@ -66,23 +72,24 @@ protected static function getUriAsDisplayableString($uri) { if (!($uri_reference && in_array($uri_reference[0], ['?', '#', '/']))) { $uri_reference = '/' . $uri_reference; } + $displayable_string = $uri_reference; } elseif ($scheme === 'entity') { list($entity_type, $entity_id) = explode('/', substr($uri, 7), 2); - $entity = \Drupal::entityManager()->getStorage($entity_type)->load($entity_id); - // Only expose the entity label if the current user is allowed to view it, - // otherwise just show the entity URI. - if ($entity->access('view')) { - $uri_reference = $entity->label() . ' (' . $entity_id . ')'; - } - else { - $uri_reference = $uri; + // Show the 'entity:' URI as the entity autocomplete would, but only if: + // - the entity could be loaded, and; + // - the current user is allowed to view the entity (otherwise we have a + // information disclosure security problem). + $entity_manager = \Drupal::entityManager(); + if ($entity_manager->getDefinition($entity_type, FALSE)) { + $entity = \Drupal::entityManager()->getStorage($entity_type)->load($entity_id); + if ($entity && $entity->access('view')) { + $displayable_string = $entity->label() . ' (' . $entity_id . ')'; + } } } - else { - $uri_reference = $uri; - } - return $uri_reference; + + return $displayable_string;; } /** @@ -150,6 +157,15 @@ public static function validateUriElement($element, FormStateInterface $form_sta $disallowed = $disallowed || (!\Drupal::currentUser()->hasPermission('link to any page') && !$url->access()); // Disallow external URLs using untrusted protocols. $disallowed = $disallowed || ($url->isExternal() && !in_array(parse_url($uri, PHP_URL_SCHEME), UrlHelper::getAllowedProtocols())); + // Disallow routed URLs that don't exist. + if (!$disallowed && $url->isRouted()) { + try { + $url->toString(); + } + catch (RouteNotFoundException $e) { + $disallowed = TRUE; + } + } if ($disallowed) { $form_state->setError($element, t("The path '@link_path' is either invalid or you do not have access to it.", ['@link_path' => static::getUriAsDisplayableString($uri)])); diff --git a/core/modules/link/src/Tests/LinkFieldTest.php b/core/modules/link/src/Tests/LinkFieldTest.php index e9abd29..8aab5c2 100644 --- a/core/modules/link/src/Tests/LinkFieldTest.php +++ b/core/modules/link/src/Tests/LinkFieldTest.php @@ -25,7 +25,7 @@ class LinkFieldTest extends WebTestBase { * * @var array */ - public static $modules = ['entity_test', 'link']; + public static $modules = ['entity_test', 'link', 'node']; /** * A field to use in this test class. @@ -93,32 +93,45 @@ function testURLValidation() { // Create a path alias. \Drupal::service('path.alias_storage')->save('admin', 'a/path/alias'); - // Define some valid URLs. + + // Create a node to test the link widget. + $node = $this->drupalCreateNode(); + + // Define some valid URLs (keys are the entered values, values are the + // strings displayed to the user). $valid_external_entries = array( - 'http://www.example.com/', + 'http://www.example.com/' => 'http://www.example.com/', ); $valid_internal_entries = array( - '/entity_test/add', - '/a/path/alias', - 'entity:user/1', - '#example', - '?example=llama', + '/entity_test/add' => '/entity_test/add', + '/a/path/alias' => '/a/path/alias', + '#example' => '#example', + '?example=llama' => '?example=llama', + // Entity reference autocomplete value. + $node->label() . ' (1)' => $node->label() . ' (1)', + # URI for an entity that exists. + 'entity:user/1' => 'entity:user/1', + # URI for an entity that doesn't exist, but with a valid ID. + 'entity:user/999999' => 'entity:user/999999', + # URI for an entity that doesn't exist, with an invalid ID. + 'entity:user/invalid-parameter' => 'entity:user/invalid-parameter', ); // Define some invalid URLs. + $validation_error_1 = "The path '@link_path' is either invalid or you do not have access to it."; + $validation_error_2 = 'Manually entered paths should start with /, # or ?'; $invalid_external_entries = array( // Missing protcol - 'not-an-url', + 'not-an-url' => $validation_error_2, // Invalid protocol - 'invalid://not-a-valid-protocol', + 'invalid://not-a-valid-protocol' => $validation_error_1, // Missing host name - 'http://', + 'http://' => $validation_error_1, ); $invalid_internal_entries = array( - // No existing. - '/non/existing/path', - // Missing start with / - 'no-slash-start', + '/non/existing/path' => $validation_error_1, + 'no-leading-slash' => $validation_error_2, + 'entity:non_existing_entity_type/yar' => $validation_error_1, ); // Test external and internal URLs for 'link_type' = LinkItemInterface::LINK_GENERIC. @@ -147,21 +160,15 @@ function testURLValidation() { * An array of valid URL entries. */ protected function assertValidEntries($field_name, array $valid_entries) { - foreach ($valid_entries as $value) { + foreach ($valid_entries as $uri => $string) { $edit = array( - "{$field_name}[0][uri]" => $value, + "{$field_name}[0][uri]" => $uri, ); $this->drupalPostForm('entity_test/add', $edit, t('Save')); preg_match('|entity_test/manage/(\d+)|', $this->url, $match); $id = $match[1]; $this->assertText(t('entity_test @id has been created.', array('@id' => $id))); - // Special case entity URLs. - if (parse_url($value, PHP_URL_SCHEME) === 'entity') { - list($entity_type, $entity_id) = explode('/', parse_url($value, PHP_URL_PATH)); - $entity = \Drupal::entityManager()->getStorage($entity_type)->load($entity_id); - $value = $entity->label() . ' (' . $entity_id . ')'; - } - $this->assertRaw($value); + $this->assertRaw($string); } } @@ -174,25 +181,12 @@ protected function assertValidEntries($field_name, array $valid_entries) { * An array of invalid URL entries. */ protected function assertInvalidEntries($field_name, array $invalid_entries) { - foreach ($invalid_entries as $invalid_value) { + foreach ($invalid_entries as $invalid_value => $error_message) { $edit = array( "{$field_name}[0][uri]" => $invalid_value, ); $this->drupalPostForm('entity_test/add', $edit, t('Save')); - - // There are two types of invalid entries: Either the path does not exist/ - // the user does not have access to it, OR the slash at the beginning is - // missing. - // If a schema is specified there is nothing to see, its kinda valid. - if (parse_url($invalid_value, PHP_URL_SCHEME)) { - $this->assertText(t("The path '@link_path' is either invalid or you do not have access to it.", array('@link_path' => $invalid_value))); - } - elseif (($path = parse_url($invalid_value, PHP_URL_PATH)) && !in_array($path[0], ['?', '#', '/'])) { - $this->assertText(t('Manually entered paths should start with /, # or ?')); - } - else { - $this->assertText(t("The path '@link_path' is either invalid or you do not have access to it.", array('@link_path' => $invalid_value))); - } + $this->assertText(t($error_message, array('@link_path' => $invalid_value))); } }