diff --git a/js/linkit.autocomplete.js b/js/linkit.autocomplete.js index 558278d..302a5d3 100644 --- a/js/linkit.autocomplete.js +++ b/js/linkit.autocomplete.js @@ -3,7 +3,7 @@ * Linkit Autocomplete based on jQuery UI. */ -(function ($, Drupal, _) { +(function ($, Drupal, drupalSettings) { 'use strict'; @@ -37,6 +37,11 @@ // Get the desired term and construct the autocomplete URL for it. var term = request.term; + var baseUrl = drupalSettings.base_url; + var term = term.replace(baseUrl,''); + + // Make sure that we aren't looking for absolute url to self. + request.term = term; // Check if the term is already cached. if (autocomplete.cache[elementId].hasOwnProperty(term)) { @@ -97,6 +102,43 @@ * jQuery collection of the ul element. */ function renderItem(ul, item) { + var baseUrl = drupalSettings.base_url; + var dialogForm = $('#editor-link-dialog-form form'); + var inputEl = $(dialogForm).find('input.form-linkit-autocomplete'); + var userInputVal = $(inputEl).val(); + if (typeof userInputVal != 'undefined') { + var newInput = userInputVal.replace(baseUrl,''); + if (userInputVal.length > newInput.length) { + var msg = Drupal.t('URL provided was absolute internal, rather than use absolute links for content within this website you should instead select from one of the items in the dropdown list.'); + $(dialogForm).find('div > label.form-item__label').text(msg).css('color', 'red'); + $(dialogForm).closest('div.editor-link-dialog').find('button.form-submit').attr("disabled","disabled"); + if (newInput.indexOf('auHash=') > 1 || newInput != userInputVal) { + $(inputEl).data('notabsolute', newInput); + $(inputEl).one("lostfocus", function() { + var notAbsolute = $(this).data('notabsolute'); + $(this).val(notAbsolute); + var dialogForm = $('#editor-link-dialog-form form'); + $(dialogForm).closest('div.editor-link-dialog').find('button.form-submit').attr("disabled","disabled"); + }); + $(inputEl).val(newInput); + } + $(dialogForm).closest('div.editor-link-dialog').find('ul.linkit-ui-autocomplete').each(function( index ) { + $(this).on('click.linkitresults' + index, function(e) { + // On click of the results, re-enable the button. + $('.editor-link-dialog button').each(function( index, el ) { + $(el).removeAttr("disabled"); + $('#editor-link-dialog-form form label').first().text(Drupal.t('URL')).css('color', 'black'); + }); + }); + }); + } + else { + // Restore original message. + $(dialogForm).closest('div.editor-link-dialog').find('button.form-submit').removeAttr("disabled","disabled"); + $('#editor-link-dialog-form form label').first().text(Drupal.t('URL')).css('color', 'black'); + } + } + var $line = $('
  • ').addClass('linkit-result-line'); var $wrapper = $('
    ').addClass('linkit-result-line-wrapper'); $wrapper.append($('').html(item.label).addClass('linkit-result-line--title')); @@ -212,4 +254,4 @@ } }; -})(jQuery, Drupal, _); +})(jQuery, Drupal, drupalSettings); diff --git a/src/Controller/AutocompleteController.php b/src/Controller/AutocompleteController.php index 0bcdeaf..722d5d1 100644 --- a/src/Controller/AutocompleteController.php +++ b/src/Controller/AutocompleteController.php @@ -76,7 +76,19 @@ class AutocompleteController implements ContainerInjectionInterface { $this->linkitProfile = $this->linkitProfileStorage->load($linkit_profile_id); $string = $request->query->get('q'); - $suggestionCollection = $this->suggestionManager->getSuggestions($this->linkitProfile, mb_strtolower($string)); + // Get params can be case sensitive, we don't want to modify those. + // So only modify path for lower case not get params. + $posParams = strpos($string, '?'); + if ($posParams > 0) { + $path = substr($string, 0, strpos($string, '?')); + $lowerPath = mb_strtolower($path); + $string = $lowerPath . substr($string, $posParams); + } + else { + // No get params. + $string = mb_strtolower($string); + } + $suggestionCollection = $this->suggestionManager->getSuggestions($this->linkitProfile, $string); /* * If there are no suggestions from the matcher plugins, we have to add a diff --git a/src/Element/Linkit.php b/src/Element/Linkit.php index 2bb7963..5da2718 100644 --- a/src/Element/Linkit.php +++ b/src/Element/Linkit.php @@ -69,6 +69,8 @@ class Linkit extends FormElement { $metadata = BubbleableMetadata::createFromRenderArray($element); if ($access->isAllowed()) { $element['#attributes']['class'][] = 'form-linkit-autocomplete'; + global $base_url; // Fully support multisite and any style base path. + $metadata->addAttachments(['drupalSettings' => ['base_url' => $base_url]]); $metadata->addAttachments(['library' => ['linkit/linkit.autocomplete']]); // Provide a data attribute for the JavaScript behavior to bind to. $element['#attributes']['data-autocomplete-path'] = $url->getGeneratedUrl(); diff --git a/src/Plugin/Linkit/Matcher/EntityMatcher.php b/src/Plugin/Linkit/Matcher/EntityMatcher.php index e5a9413..c1d31d8 100644 --- a/src/Plugin/Linkit/Matcher/EntityMatcher.php +++ b/src/Plugin/Linkit/Matcher/EntityMatcher.php @@ -3,6 +3,8 @@ namespace Drupal\linkit\Plugin\Linkit\Matcher; use Drupal\Component\Utility\Html; +use Drupal\Component\Utility\UrlHelper; +use Drupal\Core\Config\ConfigFactoryInterface; use Drupal\Core\Config\Entity\ConfigEntityTypeInterface; use Drupal\Core\Database\Connection; use Drupal\Core\Entity\EntityInterface; @@ -12,8 +14,10 @@ use Drupal\Core\Entity\EntityTypeManagerInterface; use Drupal\Core\Entity\Query\QueryInterface; use Drupal\Core\Extension\ModuleHandlerInterface; use Drupal\Core\Form\FormStateInterface; +use Drupal\path_alias\AliasManagerInterface; use Drupal\Core\Session\AccountInterface; use Drupal\Core\Url; +use Drupal\language\Plugin\LanguageNegotiation\LanguageNegotiationUrl; use Drupal\linkit\ConfigurableMatcherBase; use Drupal\linkit\MatcherTokensTrait; use Drupal\linkit\SubstitutionManagerInterface; @@ -97,10 +101,24 @@ class EntityMatcher extends ConfigurableMatcherBase { */ protected $substitutionManager; + /** + * The alias manager. + * + * @var \Drupal\path_alias\AliasManagerInterface + */ + protected $aliasManager; + + /** + * The config factory. + * + * @var \Drupal\Core\Config\ConfigFactoryInterface + */ + protected $configFactory; + /** * {@inheritdoc} */ - public function __construct(array $configuration, $plugin_id, $plugin_definition, Connection $database, EntityTypeManagerInterface $entity_type_manager, EntityTypeBundleInfoInterface $entity_type_bundle_info, EntityRepositoryInterface $entity_repository, ModuleHandlerInterface $module_handler, AccountInterface $current_user, SubstitutionManagerInterface $substitution_manager) { + public function __construct(array $configuration, $plugin_id, $plugin_definition, Connection $database, EntityTypeManagerInterface $entity_type_manager, EntityTypeBundleInfoInterface $entity_type_bundle_info, EntityRepositoryInterface $entity_repository, ModuleHandlerInterface $module_handler, AccountInterface $current_user, SubstitutionManagerInterface $substitution_manager, AliasManagerInterface $alias_manager, ConfigFactoryInterface $config_factory) { parent::__construct($configuration, $plugin_id, $plugin_definition); if (empty($plugin_definition['target_entity'])) { @@ -114,6 +132,8 @@ class EntityMatcher extends ConfigurableMatcherBase { $this->currentUser = $current_user; $this->targetType = $plugin_definition['target_entity']; $this->substitutionManager = $substitution_manager; + $this->aliasManager = $alias_manager; + $this->configFactory = $config_factory; } /** @@ -130,7 +150,9 @@ class EntityMatcher extends ConfigurableMatcherBase { $container->get('entity.repository'), $container->get('module_handler'), $container->get('current_user'), - $container->get('plugin.manager.linkit.substitution') + $container->get('plugin.manager.linkit.substitution'), + $container->get('path_alias.manager'), + $container->get('config.factory') ); } @@ -344,6 +366,12 @@ class EntityMatcher extends ConfigurableMatcherBase { $entity = $this->entityRepository->getTranslationFromContext($entity); $suggestion = $this->createSuggestion($entity); + if ($query = parse_url($string, PHP_URL_QUERY)) { + $suggestion->setPath($suggestion->getPath() . '?' . $query); + } + if ($fragment = parse_url($string, PHP_URL_FRAGMENT)) { + $suggestion->setPath($suggestion->getPath() . '#' . $fragment); + } $suggestions->addSuggestion($suggestion); } @@ -516,12 +544,46 @@ class EntityMatcher extends ConfigurableMatcherBase { * and a match is found, otherwise an empty array. */ protected function findEntityIdByUrl($user_input) { + $options = []; + if (UrlHelper::isExternal($user_input) && UrlHelper::isValid($user_input, TRUE)) { + if (UrlHelper::externalIsLocal($user_input, \Drupal::request()->getSchemeAndHttpHost())) { + // The link points to this domain. Make it relative so it can be + // matched in Url::fromUserInput(). + $host = parse_url($user_input, PHP_URL_HOST); + $host_end = strpos($user_input, $host) + strlen($host); + $user_input = substr($user_input, $host_end); + + if ($this->moduleHandler->moduleExists('language')) { + $config = $this->configFactory->get('language.negotiation')->get('url'); + if ($config['source'] === LanguageNegotiationUrl::CONFIG_PATH_PREFIX) { + $language_manager = \Drupal::service('language_manager'); + /** @var \Drupal\Core\Language\Language[] $languages */ + $languages = $language_manager->getLanguages(); + // Remove the leading slash for easier manipulation of the remaining + // args. + $path = urldecode(trim($user_input, '/')); + $path_args = explode('/', $path); + $prefix = array_shift($path_args); + + // Search prefix within added languages. + foreach ($languages as $language) { + if (isset($config['prefixes'][$language->getId()]) && $config['prefixes'][$language->getId()] == $prefix) { + $user_input = '/' . implode('/', $path_args); + $user_input = $this->aliasManager->getPathByAlias($user_input, $language->getId()); + break; + } + } + } + } + } + } + $result = []; try { - $params = Url::fromUserInput($user_input)->getRouteParameters(); - if (key($params) === $this->targetType) { - $result = [end($params)]; + $params = Url::fromUserInput($user_input, $options)->getRouteParameters(); + if (!empty($params[$this->targetType])) { + $result = [$params[$this->targetType]]; } } catch (Exception $e) { diff --git a/tests/src/Functional/LinkitBrowserTestBase.php b/tests/src/Functional/LinkitBrowserTestBase.php index bc61b6c..30b7372 100644 --- a/tests/src/Functional/LinkitBrowserTestBase.php +++ b/tests/src/Functional/LinkitBrowserTestBase.php @@ -14,7 +14,7 @@ abstract class LinkitBrowserTestBase extends BrowserTestBase { * * @var array */ - public static $modules = ['linkit', 'linkit_test', 'block']; + public static $modules = ['linkit', 'linkit_test', 'block', 'path_alias']; /** * {@inheritdoc} diff --git a/tests/src/FunctionalJavascript/LinkitDialogTest.php b/tests/src/FunctionalJavascript/LinkitDialogTest.php index 26eb74e..f9b4bc2 100644 --- a/tests/src/FunctionalJavascript/LinkitDialogTest.php +++ b/tests/src/FunctionalJavascript/LinkitDialogTest.php @@ -29,6 +29,7 @@ class LinkitDialogTest extends WebDriverTestBase { */ public static $modules = [ 'node', + 'path_alias', 'ckeditor', 'filter', 'linkit', diff --git a/tests/src/FunctionalJavascript/LinkitFormatAdminTest.php b/tests/src/FunctionalJavascript/LinkitFormatAdminTest.php index 7ab46ff..0d958cc 100644 --- a/tests/src/FunctionalJavascript/LinkitFormatAdminTest.php +++ b/tests/src/FunctionalJavascript/LinkitFormatAdminTest.php @@ -16,7 +16,7 @@ class LinkitFormatAdminTest extends WebDriverTestBase { * * @var array */ - public static $modules = ['editor', 'filter', 'linkit']; + public static $modules = ['editor', 'filter', 'linkit', 'path_alias']; /** * {@inheritdoc} diff --git a/tests/src/Kernel/EntityMatcherDeriverTest.php b/tests/src/Kernel/EntityMatcherDeriverTest.php index cf3832a..c7668dc 100644 --- a/tests/src/Kernel/EntityMatcherDeriverTest.php +++ b/tests/src/Kernel/EntityMatcherDeriverTest.php @@ -12,7 +12,7 @@ class EntityMatcherDeriverTest extends LinkitKernelTestBase { /** * {@inheritdoc} */ - public static $modules = ['block', 'block_content', 'node', 'field']; + public static $modules = ['block', 'block_content', 'node', 'path_alias', 'field']; /** * The matcher manager. diff --git a/tests/src/Kernel/LinkitKernelTestBase.php b/tests/src/Kernel/LinkitKernelTestBase.php index d04a2bc..6ba9e2a 100644 --- a/tests/src/Kernel/LinkitKernelTestBase.php +++ b/tests/src/Kernel/LinkitKernelTestBase.php @@ -21,6 +21,7 @@ abstract class LinkitKernelTestBase extends KernelTestBase { 'user', 'filter', 'text', + 'path_alias', 'linkit', 'linkit_test', ]; diff --git a/tests/src/Kernel/Matchers/NodeMatcherTest.php b/tests/src/Kernel/Matchers/NodeMatcherTest.php index f225eaf..a36d4e0 100644 --- a/tests/src/Kernel/Matchers/NodeMatcherTest.php +++ b/tests/src/Kernel/Matchers/NodeMatcherTest.php @@ -2,6 +2,7 @@ namespace Drupal\Tests\linkit\Kernel\Matchers; +use Drupal\language\Entity\ConfigurableLanguage; use Drupal\node\Entity\Node; use Drupal\node\Entity\NodeType; use Drupal\Tests\linkit\Kernel\LinkitKernelTestBase; @@ -18,7 +19,14 @@ class NodeMatcherTest extends LinkitKernelTestBase { * * @var array */ - public static $modules = ['field', 'node', 'content_moderation', 'workflows']; + public static $modules = [ + 'field', + 'node', + 'path_alias', + 'content_moderation', + 'workflows', + 'language', + ]; /** * The matcher manager. @@ -35,7 +43,7 @@ class NodeMatcherTest extends LinkitKernelTestBase { $this->installEntitySchema('node'); $this->installSchema('node', ['node_access']); - $this->installConfig(['field', 'node']); + $this->installConfig(['field', 'node', 'language', 'path_alias']); $this->manager = $this->container->get('plugin.manager.linkit.matcher'); @@ -180,4 +188,49 @@ class NodeMatcherTest extends LinkitKernelTestBase { } } + /** + * Test node matches generated from an absolute URL input. + */ + public function testNodeMatcherFromAbsoluteUrl() { + /** @var \Drupal\linkit\MatcherInterface $plugin */ + $plugin = $this->manager->createInstance('entity:node'); + + /** @var \Drupal\node\NodeInterface[] $nodes */ + $nodes = $this->container->get('entity_type.manager')->getStorage('node')->loadByProperties(['title' => 'Lorem Ipsum 1']); + $node = reset($nodes); + + $suggestions = $plugin->execute($node->toUrl()->setAbsolute()->toString()); + $this->assertEquals(1, count($suggestions->getSuggestions())); + } + + /** + * Test node matches generated from an absolute URL input. + */ + public function testNodeMatcherFromAbsoluteUrlWithLanguagePrefix() { + /** @var \Drupal\linkit\MatcherInterface $plugin */ + $plugin = $this->manager->createInstance('entity:node'); + + $langcode = 'nl'; + ConfigurableLanguage::createFromLangcode($langcode)->save(); + \Drupal::configFactory()->getEditable('language.negotiation') + ->set('url.prefixes.nl', $langcode) + ->save(); + + // In order to reflect the changes for a multilingual site in the container + // we have to rebuild it. + \Drupal::service('kernel')->rebuildContainer(); + + /** @var \Drupal\node\NodeInterface[] $nodes */ + $nodes = $this->container->get('entity_type.manager')->getStorage('node')->loadByProperties(['title' => 'Lorem Ipsum 1']); + $node = reset($nodes); + $translation = $node->addTranslation($langcode, $node->toArray()); + $translation->save(); + + $translated_url = $translation->toUrl()->setAbsolute()->toString(); + // Make sure the translated URL contains our prefix. + $this->assertStringContainsString('/' . $langcode . '/', (string) $translated_url); + $suggestions = $plugin->execute($translated_url); + $this->assertEquals(1, count($suggestions->getSuggestions())); + } + }