diff --git a/src/Plugin/Linkit/Matcher/EntityMatcher.php b/src/Plugin/Linkit/Matcher/EntityMatcher.php index 33296be..ffb59ad 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\Core\Path\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\Core\Path\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)) { + 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/Kernel/Matchers/NodeMatcherTest.php b/tests/src/Kernel/Matchers/NodeMatcherTest.php index 0ab5550..9b0ed6e 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,13 @@ class NodeMatcherTest extends LinkitKernelTestBase { * * @var array */ - public static $modules = ['field', 'node', 'content_moderation', 'workflows']; + public static $modules = [ + 'field', + 'node', + 'content_moderation', + 'workflows', + 'language', + ]; /** * The matcher manager. @@ -35,7 +42,7 @@ class NodeMatcherTest extends LinkitKernelTestBase { $this->installEntitySchema('node'); $this->installSchema('node', ['node_access']); - $this->installConfig(['field', 'node']); + $this->installConfig(['field', 'node', 'language']); $this->manager = $this->container->get('plugin.manager.linkit.matcher'); @@ -180,4 +187,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->assertContains('/' . $langcode . '/', $translated_url); + $suggestions = $plugin->execute($translated_url); + $this->assertEquals(1, count($suggestions->getSuggestions())); + } + }