diff --git a/core/lib/Drupal/Core/Entity/Element/EntityAutocomplete.php b/core/lib/Drupal/Core/Entity/Element/EntityAutocomplete.php index 8a95237..2101150 100644 --- a/core/lib/Drupal/Core/Entity/Element/EntityAutocomplete.php +++ b/core/lib/Drupal/Core/Entity/Element/EntityAutocomplete.php @@ -107,13 +107,7 @@ public static function validateEntityAutocomplete(&$element, FormStateInterface foreach (Tags::explode($element['#value']) as $input) { // Take "label (entity id)', match the ID from parenthesis when it's a // number. - if (preg_match("/.+\((\d+)\)/", $input, $matches)) { - $match = $matches[1]; - } - // Match the ID when it's a string (e.g. for config entity types). - elseif (preg_match("/.+\(([\w.]+)\)/", $input, $matches)) { - $match = $matches[1]; - } + if (($match = static::extractEntityIdFormAutocompletionResult($input)) && $match !== NULL); else { // Try to get a match from the input string when the user didn't use // the autocomplete but filled in a value manually. @@ -146,6 +140,27 @@ public static function validateEntityAutocomplete(&$element, FormStateInterface } /** + * Extracts the entity ID from the autocompletion result. + * + * @param string $input + * The input coming from the autocompletion result. + * + * @return mixed|null + * NULL if no entity could be extracted, otherwise the entity ID. + */ + public static function extractEntityIdFormAutocompletionResult($input) { + $match = NULL; + if (preg_match("/.+\((\d+)\)/", $input, $matches)) { + $match = $matches[1]; + } + // Match the ID when it's a string (e.g. for config entity types). + elseif (preg_match("/.+\(([\w.]+)\)/", $input, $matches)) { + $match = $matches[1]; + } + return $match; + } + + /** * Creates a new entity from a label entered in the autocomplete input. * * @param string $entity_type_id diff --git a/core/lib/Drupal/Core/Path/PathValidator.php b/core/lib/Drupal/Core/Path/PathValidator.php index 51cdc2f..fcf0968 100644 --- a/core/lib/Drupal/Core/Path/PathValidator.php +++ b/core/lib/Drupal/Core/Path/PathValidator.php @@ -109,6 +109,9 @@ protected function getUrl($path, $access_check) { if ($parsed_url['path'] == '') { return new Url('', [], $options); } + elseif ($parsed_url['path'] == '') { + return new Url('', [], $options); + } elseif (UrlHelper::isExternal($path) && UrlHelper::isValid($path)) { if (empty($parsed_url['path'])) { return FALSE; diff --git a/core/modules/link/src/Plugin/Field/FieldWidget/LinkWidget.php b/core/modules/link/src/Plugin/Field/FieldWidget/LinkWidget.php index 0606f42..f6acbbb 100644 --- a/core/modules/link/src/Plugin/Field/FieldWidget/LinkWidget.php +++ b/core/modules/link/src/Plugin/Field/FieldWidget/LinkWidget.php @@ -9,6 +9,7 @@ use Drupal\Component\Utility\Unicode; use Drupal\Component\Utility\UrlHelper; +use Drupal\Core\Entity\Element\EntityAutocomplete; use Drupal\Core\Field\FieldItemListInterface; use Drupal\Core\Field\WidgetBase; use Drupal\Core\Form\FormStateInterface; @@ -44,15 +45,29 @@ public static function defaultSettings() { /** * Gets the URI without the 'user-path:' scheme, for display while editing. * + * This method is the inverse of ::getUserEnteredStringAsUri(). + * * @param string $uri * The URI to get the displayable string for. * * @return string + * + * @see getUserEnteredStringAsUri */ protected static function getUriAsDisplayableString($uri) { $scheme = parse_url($uri, PHP_URL_SCHEME); + $uri = str_replace('', '', $uri); + $uri = str_replace('', '/', $uri); + if ($scheme === 'user-path') { - $uri_reference = explode(':', $uri, 2)[1]; + $uri_reference = ltrim(explode(':', $uri, 2)[1], '/'); + if (!in_array($uri_reference[0], ['?', '#', '/'])) { + $uri_reference = '/' . $uri_reference; + } + } + elseif ($scheme === 'entity') { + list($entity_type, $entity_id) = explode('/', substr($uri, 7), 2); + $uri_reference = \Drupal::entityManager()->getStorage($entity_type)->load($entity_id)->label() . ' (' . $entity_id . ')'; } else { $uri_reference = $uri; @@ -65,18 +80,33 @@ protected static function getUriAsDisplayableString($uri) { * * Schemeless URIs are treated as 'user-path:' URIs. * + * This method is the inverse of ::getUriAsDisplayableString(). + * * @param string $string * The user-entered string. * * @return string - * The URI, if a non-empty $string was passed. + * The URI, if a non-empty $uri was passed. + * + * @see getUriAsDisplayableString */ protected static function getUserEnteredStringAsUri($string) { + $entity_id = EntityAutocomplete::extractEntityIdFormAutocompletionResult($string); + if ($entity_id !== NULL) { + $string = 'entity:node/' . $entity_id; + } + if (!empty($string)) { // Users can enter relative URLs, but we need a valid URI, so add an // explicit scheme when necessary. if (parse_url($string, PHP_URL_SCHEME) === NULL) { - return 'user-path:' . $string; + if (parse_url($string, PHP_URL_PATH) === NULL) { + $string = '' . $string; + } + if (parse_url($string, PHP_URL_PATH) === '/') { + $string = '' . ltrim($string, '/'); + } + $string = 'user-path:' . $string; } } return $string; @@ -87,6 +117,15 @@ protected static function getUserEnteredStringAsUri($string) { */ public static function validateUriElement($element, FormStateInterface $form_state, $form) { $uri = static::getUserEnteredStringAsUri($element['#value']); + $form_state->setValueForElement($element, $uri); + + // Figure out the user entered value after user-path. This might contain a + // fragment so we cannot use parse_url($string, PHP_URL_PATH), so we strip + // out the 10 chars of user-path:. + if (parse_url($uri, PHP_URL_SCHEME) === 'user-path' && !in_array(substr($uri, 10)[0], ['#', '/', '?', '<'], TRUE)) { + // + $form_state->setError($element, t('Manually entered paths should start with /, # or ?')); + } // If the URI is empty or not well-formed, the link field type's validation // constraint will detect it. @@ -131,18 +170,20 @@ public function formElement(FieldItemListInterface $items, $delta, array $elemen // If the field is configured to support internal links, it cannot use the // 'url' form element and we have to do the validation ourselves. if ($this->supportsInternalLinks()) { - $element['uri']['#type'] = 'textfield'; + $element['uri']['#type'] = 'entity_autocomplete'; + // @todo This should be a setting? + $element['uri']['#target_type'] = 'node'; } // If the field is configured to allow only internal links, add a useful // element prefix. if (!$this->supportsExternalLinks()) { - $element['uri']['#field_prefix'] = \Drupal::url('', array(), array('absolute' => TRUE)); + $element['uri']['#field_prefix'] = rtrim(\Drupal::url('', array(), array('absolute' => TRUE)), '/'); } // If the field is configured to allow both internal and external links, // show a useful description. elseif ($this->supportsExternalLinks() && $this->supportsInternalLinks()) { - $element['uri']['#description'] = $this->t('This can be an internal Drupal path such as %add-node or an external URL such as %drupal. Enter %front to link to the front page.', array('%front' => '', '%add-node' => 'node/add', '%drupal' => 'http://drupal.org')); + $element['uri']['#description'] = $this->t('This can be an internal Drupal path such as %add-node or an external URL such as %drupal. Enter %front to link to the front page.', array('%front' => '', '%add-node' => '/node/add', '%drupal' => 'http://drupal.org')); } $element['title'] = array( diff --git a/core/modules/link/src/Tests/LinkFieldTest.php b/core/modules/link/src/Tests/LinkFieldTest.php index 05a427e..7fed262 100644 --- a/core/modules/link/src/Tests/LinkFieldTest.php +++ b/core/modules/link/src/Tests/LinkFieldTest.php @@ -98,9 +98,11 @@ function testURLValidation() { 'http://www.example.com/', ); $valid_internal_entries = array( - 'entity_test/add', - 'a/path/alias', + '/entity_test/add', + '/a/path/alias', 'entity:user/1', + '#example', + '?example=llama', ); // Define some invalid URLs. @@ -113,7 +115,10 @@ function testURLValidation() { 'http://', ); $invalid_internal_entries = array( - 'non/existing/path', + // No existing. + '/non/existing/path', + // Missing start with / + 'no-slash-start', ); // Test external and internal URLs for 'link_type' = LinkItemInterface::LINK_GENERIC. diff --git a/core/modules/system/system.routing.yml b/core/modules/system/system.routing.yml index 7c87bb9..fecfd249 100644 --- a/core/modules/system/system.routing.yml +++ b/core/modules/system/system.routing.yml @@ -382,7 +382,8 @@ system.theme_settings_theme: path: '' options: _only_fragment: TRUE - + requirements: + _access: 'TRUE' '': path: ''