Commit c689cd00 authored by Sam152's avatar Sam152 Committed by anon

Issue #2786049 by Sam152, anon: Make entity URL substitutions pluggable to...

Issue #2786049 by Sam152, anon: Make entity URL substitutions pluggable to support a wider variety of use cases
parent 01b39f63
......@@ -15,4 +15,5 @@ matchers:
result_description: 'by [node:author] | [node:created:medium]'
bundles: { }
group_by_bundle: false
include_unpublished: false
\ No newline at end of file
include_unpublished: false
substitution_type: canonical
......@@ -43,6 +43,8 @@ linkit.matcher.entity:
nullable: true
group_by_bundle:
type: boolean
substitution_type:
type: string
linkit.matcher.entity:*:
type: linkit.matcher.entity
......
......@@ -122,3 +122,34 @@ function linkit_update_8500() {
$config->save(TRUE);
}
}
/**
* Prepare anchor attributes for substitution plugins.
*/
function linkit_update_8501() {
$config_factory = \Drupal::configFactory();
// Update all filter formats that allow the data-entity-uuid attribute to also
// allow the data-entity-substitution attribute.
foreach ($config_factory->listAll('filter.format.') as $id) {
$filter = $config_factory->getEditable($id);
if ($allowed_html = $filter->get('filters.filter_html.settings.allowed_html')) {
$allowed_html = str_replace('data-entity-uuid', 'data-entity-uuid data-entity-substitution', $allowed_html);
$filter->set('filters.filter_html.settings.allowed_html', $allowed_html);
$filter->save(TRUE);
}
}
// Update all "file" matchers to the "file" substitution plugin, to maintain
// existing behavior out of the box.
$config_factory = \Drupal::configFactory();
foreach ($config_factory->listAll('linkit.linkit_profile.') as $id) {
$profile = $config_factory->getEditable($id);
foreach ($profile->get('matchers') as $key => $matcher) {
$settings = $profile->get('matchers.' . $key . '.settings');
$settings['substitution_type'] = $matcher['id'] === 'entity:file' ? 'file' : 'canonical';
$profile->set('matchers.' . $key . '.settings', $settings);
}
$profile->save(TRUE);
}
}
......@@ -169,12 +169,13 @@ function linkit_form_editor_link_dialog_validate(array &$form, FormStateInterfac
if (!empty($link_element)) {
$form_state->setValue(['attributes', 'data-entity-type'], '');
$form_state->setValue(['attributes', 'data-entity-uuid'], '');
$form_state->setValue(['attributes', 'data-entity-substitution'], '');
}
return;
}
// Parse the entity: URI into an entity type ID and entity ID.
list($entity_type_id, $entity_id) = explode('/', $uri_parts['path'], 2);
list($substitution_type, $entity_type_id, $entity_id) = explode('/', $uri_parts['path'], 3);
// Check if the given entity type exists, to prevent the entity load method
// to throw exceptions.
......@@ -192,6 +193,7 @@ function linkit_form_editor_link_dialog_validate(array &$form, FormStateInterfac
if (!empty($entity)) {
$form_state->setValue(['attributes', 'data-entity-type'], $entity->getEntityTypeId());
$form_state->setValue(['attributes', 'data-entity-uuid'], $entity->uuid());
$form_state->setValue(['attributes', 'data-entity-substitution'], $substitution_type);
}
else {
$form_state->setError($form['attributes']['href'], t('Invalid URI'));
......
......@@ -2,6 +2,8 @@ services:
plugin.manager.linkit.matcher:
class: Drupal\linkit\MatcherManager
parent: default_plugin_manager
plugin.manager.linkit.substitution:
class: Drupal\linkit\SubstitutionManager
parent: default_plugin_manager
linkit.suggestion_manager:
class: Drupal\linkit\SuggestionManager
<?php
namespace Drupal\linkit\Annotation;
use Drupal\Component\Annotation\Plugin;
/**
* Defines a substitution annotation object.
*
* @Annotation
*/
class Substitution extends Plugin {
/**
* The plugin ID.
*
* @var string
*/
public $id;
/**
* The human-readable name of the substitution.
*
* @var \Drupal\Core\Annotation\Translation
*/
public $label;
/**
* An array of applicable entity types.
*
* @var array
*/
public $entity_types = [];
}
......@@ -8,6 +8,7 @@ use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
use Drupal\filter\FilterProcessResult;
use Drupal\filter\Plugin\FilterBase;
use Drupal\linkit\SubstitutionManagerInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
......@@ -37,6 +38,13 @@ class LinkitFilter extends FilterBase implements ContainerFactoryPluginInterface
*/
protected $entityRepository;
/**
* The substitution manager.
*
* @var \Drupal\linkit\SubstitutionManagerInterface
*/
protected $substitutionManager;
/**
* Constructs a LinkitFilter object.
*
......@@ -49,10 +57,11 @@ class LinkitFilter extends FilterBase implements ContainerFactoryPluginInterface
* @param \Drupal\Core\Entity\EntityRepositoryInterface $entity_repository
* The entity repository service.
*/
public function __construct(array $configuration, $plugin_id, $plugin_definition, EntityRepositoryInterface $entity_repository) {
public function __construct(array $configuration, $plugin_id, $plugin_definition, EntityRepositoryInterface $entity_repository, SubstitutionManagerInterface $substitution_manager) {
parent::__construct($configuration, $plugin_id, $plugin_definition);
$this->entityRepository = $entity_repository;
$this->substitutionManager = $substitution_manager;
}
/**
......@@ -63,7 +72,8 @@ class LinkitFilter extends FilterBase implements ContainerFactoryPluginInterface
$configuration,
$plugin_id,
$plugin_definition,
$container->get('entity.repository')
$container->get('entity.repository'),
$container->get('plugin.manager.linkit.substitution')
);
}
......@@ -83,24 +93,25 @@ class LinkitFilter extends FilterBase implements ContainerFactoryPluginInterface
// Load the appropriate translation of the linked entity.
$entity_type = $element->getAttribute('data-entity-type');
$uuid = $element->getAttribute('data-entity-uuid');
// Make the substitution optional, for backwards compatibility,
// maintaining the previous hard-coded direct file link assumptions,
// for content created before the substitution feature.
if (!$substitution_type = $element->getAttribute('data-entity-substitution')) {
$substitution_type = $entity_type === 'file' ? 'file' : SubstitutionManagerInterface::DEFAULT_SUBSTITUTION;
}
$entity = $this->entityRepository->loadEntityByUuid($entity_type, $uuid);
if ($entity) {
$entity = $this->entityRepository->getTranslationFromContext($entity, $langcode);
// Set the appropriate href attribute.
// The file entity has not declared any "links" in its entity
// definition. We therefor have to use the file entity specific
// getFileUri() instead.
if ($entity_type === 'file') {
/** @var \Drupal\file\Entity\File $entity */
$url = file_create_url($entity->getFileUri());
$element->setAttribute('href', $url);
}
else {
$url = $entity->toUrl('canonical')->toString(TRUE);
$element->setAttribute('href', $url->getGeneratedUrl());
}
/** @var \Drupal\Core\GeneratedUrl $url */
$url = $this->substitutionManager
->createInstance($substitution_type)
->getUrl($entity);
$element->setAttribute('href', $url->getGeneratedUrl());
$access = $entity->access('view', NULL, TRUE);
// Set the appropriate title attribute.
......
......@@ -13,6 +13,8 @@ use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Session\AccountInterface;
use Drupal\linkit\ConfigurableMatcherBase;
use Drupal\linkit\MatcherTokensTrait;
use Drupal\linkit\SubstitutionManager;
use Drupal\linkit\SubstitutionManagerInterface;
use Drupal\linkit\Suggestion\DescriptionSuggestion;
use Drupal\linkit\Suggestion\SuggestionCollection;
use Drupal\linkit\Utility\LinkitXss;
......@@ -80,10 +82,17 @@ class EntityMatcher extends ConfigurableMatcherBase {
*/
protected $targetType;
/**
* The substitution manager.
*
* @var \Drupal\linkit\SubstitutionManagerInterface
*/
protected $substitutionManager;
/**
* {@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) {
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) {
parent::__construct($configuration, $plugin_id, $plugin_definition);
if (empty($plugin_definition['target_entity'])) {
......@@ -96,6 +105,7 @@ class EntityMatcher extends ConfigurableMatcherBase {
$this->moduleHandler = $module_handler;
$this->currentUser = $current_user;
$this->targetType = $plugin_definition['target_entity'];
$this->substitutionManager = $substitution_manager;
}
/**
......@@ -111,7 +121,8 @@ class EntityMatcher extends ConfigurableMatcherBase {
$container->get('entity_type.bundle.info'),
$container->get('entity.repository'),
$container->get('module_handler'),
$container->get('current_user')
$container->get('current_user'),
$container->get('plugin.manager.linkit.substitution')
);
}
......@@ -160,6 +171,7 @@ class EntityMatcher extends ConfigurableMatcherBase {
'result_description' => '',
'bundles' => [],
'group_by_bundle' => FALSE,
'substitution_type' => SubstitutionManagerInterface::DEFAULT_SUBSTITUTION,
];
}
......@@ -205,6 +217,17 @@ class EntityMatcher extends ConfigurableMatcherBase {
];
}
$substitution_options = $this->substitutionManager->getApplicablePluginsOptionList($this->targetType);
$form['substitution_type'] = [
'#title' => $this->t('Substitution Type'),
'#type' => 'select',
'#default_value' => $this->configuration['substitution_type'],
'#options' => $substitution_options,
'#description' => $this->t('Configure how the selected entity should be transformed into a URL for insertion.'),
'#weight' => -49,
'#access' => count($substitution_options) !== 1,
];
return $form;
}
......@@ -221,6 +244,7 @@ class EntityMatcher extends ConfigurableMatcherBase {
$this->configuration['result_description'] = $form_state->getValue('result_description');
$this->configuration['bundles'] = $form_state->getValue('bundles');
$this->configuration['group_by_bundle'] = $form_state->getValue('group_by_bundle');
$this->configuration['substitution_type'] = $form_state->getValue('substitution_type');
}
/**
......@@ -344,7 +368,7 @@ class EntityMatcher extends ConfigurableMatcherBase {
* @see \Drupal\Core\Url::fromEntityUri()
*/
protected function buildPath(EntityInterface $entity) {
return 'entity:' . $entity->getEntityTypeId() . '/' . $entity->id();
return 'entity:' . $this->configuration['substitution_type'] . '/' . $entity->getEntityTypeId() . '/' . $entity->id();
}
/**
......
<?php
namespace Drupal\linkit\Plugin\Linkit\Substitution;
use Drupal\Core\Entity\EntityInterface;
use Drupal\linkit\SubstitutionInterface;
use Drupal\views\Plugin\views\PluginBase;
/**
* A substitution plugin for the canonical URL of an entity.
*
* @Substitution(
* id = "canonical",
* label = @Translation("Canonical URL"),
* )
*/
class Canonical extends PluginBase implements SubstitutionInterface {
/**
* {@inheritdoc}
*/
public function getUrl(EntityInterface $entity) {
return $entity->toUrl('canonical')->toString(TRUE);
}
}
<?php
namespace Drupal\linkit\Plugin\Linkit\Substitution;
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\GeneratedUrl;
use Drupal\linkit\SubstitutionInterface;
use Drupal\views\Plugin\views\PluginBase;
/**
* A substitution plugin for the URL to a file.
*
* @Substitution(
* id = "file",
* label = @Translation("Direct File URL"),
* entity_types = {"file"},
* )
*/
class File extends PluginBase implements SubstitutionInterface {
/**
* {@inheritdoc}
*/
public function getUrl(EntityInterface $entity) {
$url = new GeneratedUrl();
/** @var \Drupal\file\FileInterface $entity */
$url->setGeneratedUrl(file_create_url($entity->getFileUri()));
$url->addCacheableDependency($entity);
return $url;
}
}
<?php
namespace Drupal\linkit;
use Drupal\Component\Plugin\PluginInspectionInterface;
use Drupal\Core\Entity\EntityInterface;
/**
* Interface for substitution plugins.
*/
interface SubstitutionInterface extends PluginInspectionInterface {
/**
* Get the URL associated with a given entity.
*
* @param \Drupal\Core\Entity\EntityInterface $entity
* The entity to get a URL for.
*
* @return \Drupal\Core\GeneratedUrl
* A url to replace.
*/
public function getUrl(EntityInterface $entity);
}
<?php
namespace Drupal\linkit;
use Drupal\Core\Cache\CacheBackendInterface;
use Drupal\Core\Extension\ModuleHandlerInterface;
use Drupal\Core\Plugin\DefaultPluginManager;
/**
* A plugin manager for the substitution plugins.
*/
class SubstitutionManager extends DefaultPluginManager implements SubstitutionManagerInterface {
/**
* {@inheritdoc}
*/
public function __construct(\Traversable $namespaces, CacheBackendInterface $cache_backend, ModuleHandlerInterface $module_handler) {
parent::__construct('Plugin/Linkit/Substitution', $namespaces, $module_handler, 'Drupal\linkit\SubstitutionInterface', 'Drupal\linkit\Annotation\Substitution');
$this->alterInfo('linkit_substitution');
$this->setCacheBackend($cache_backend, 'linkit_substitution');
}
/**
* {@inheritdoc}
*/
public function filterPluginDefinitions($definitions, $entity_type_id) {
return array_filter($definitions, function($definition) use ($entity_type_id) {
return empty($definition['entity_types']) || in_array($entity_type_id, $definition['entity_types']);
});
}
/**
* {@inheritdoc}
*/
public function getApplicablePluginsOptionList($entity_type_id) {
$options = [];
foreach ($this->filterPluginDefinitions($this->getDefinitions(), $entity_type_id) as $id => $definition) {
$options[$id] = $definition['label'];
}
return $options;
}
}
<?php
namespace Drupal\linkit;
use Drupal\Component\Plugin\PluginManagerInterface;
/**
* An interface for the substitution manager.
*/
interface SubstitutionManagerInterface extends PluginManagerInterface {
/**
* Get the default substitution.
*/
const DEFAULT_SUBSTITUTION = 'canonical';
/**
* Filter a list of plugin definitions by entity ID.
*
* @param array $definitions
* An array of plugin definitions.
* @param string $entity_type_id
* The entity type ID to get applicable plugins for.
*
* @return array
* The definitions appropriate for the given entity ID.
*/
public function filterPluginDefinitions($definitions, $entity_type_id);
/**
* Get a form API options list for the entity ID.
*
* @param string $entity_type_id
* The entity type ID.
*
* @return array
* An options list.
*/
public function getApplicablePluginsOptionList($entity_type_id);
}
<?php
namespace Drupal\linkit\Tests\Update;
use Drupal\filter\Entity\FilterFormat;
use Drupal\system\Tests\Update\UpdatePathTestBase;
/**
* Tests Linkit upgrade path for update 8501.
*
* @group Update
*/
class LinkitUpdate8501 extends UpdatePathTestBase {
/**
* The config factory service.
*
* @var \Drupal\Core\Config\ConfigFactoryInterface
*/
protected $configFactory;
/**
* {@inheritdoc}
*/
protected function setUp() {
parent::setUp();
$this->configFactory = $this->container->get('config.factory');
}
/**
* Set database dump files to be used.
*/
protected function setDatabaseDumpFiles() {
$this->databaseDumpFiles = [
__DIR__ . '/../../../tests/fixtures/update/linkit-4-to-5/drupal-8.linkit-enabled.standard.php.gz',
__DIR__ . '/../../../tests/fixtures/update/8501/linkit-update-8501.php',
];
}
/**
* Tests linkit_update_8501().
*
* @see linkit_update_8501()
*/
public function testLinkitUpdate8501() {
$this->runUpdates();
$test_profile = $this->configFactory->get('linkit.linkit_profile.test_profile');
$this->assertEqual('canonical', $test_profile->get('matchers.fc48c807-2a9c-44eb-b86b-7e134c1aa252.settings.substitution_type'), 'Content matcher has a substitution type of canonical.');
$this->assertEqual('file', $test_profile->get('matchers.b8d6d672-6377-493f-b492-3cc69511cf17.settings.substitution_type'), 'File matcher has a substitution type of file.');
$htmlRestrictions = FilterFormat::load('format_1')->getHtmlRestrictions();
$this->assertTrue(array_key_exists("data-entity-type", $htmlRestrictions['allowed']['a']));
$this->assertTrue(array_key_exists("data-entity-uuid", $htmlRestrictions['allowed']['a']));
$this->assertTrue(array_key_exists("data-entity-substitution", $htmlRestrictions['allowed']['a']));
}
}
uuid: 5600b759-8f91-49fa-88f0-a69f4c8ed440
langcode: en
status: true
dependencies:
config:
- filter.format.format_1
module:
- ckeditor
format: format_1
editor: ckeditor
settings:
toolbar:
rows:
-
-
name: Links
items:
- Linkit
plugins:
stylescombo:
styles: ''
language:
language_list: un
linkit:
linkit_profile: test_profile
uuid: ff6b71a0-5051-4857-a0a8-28a670b7317d
langcode: en
status: true
dependencies: { }
name: 'Format 1'
format: format_1
weight: 0
filters:
filter_html:
id: filter_html
provider: filter
status: true
weight: -10
settings:
allowed_html: '<a href hreflang data-entity-type data-entity-uuid> <em> <strong> <cite> <blockquote cite> <code> <ul type> <ol start type=''1 A I''> <li> <dl> <dt> <dd> <h2 id=''jump-*''> <h3 id> <h4 id> <h5 id> <h6 id>'
filter_html_help: true
filter_html_nofollow: false
<?php
/**
* @file
* Database fixture for testing the upgrade path for Linkit 4 to 5.
*
* Contains database additions to drupal-8.bare.standard.php.gz for testing the
* upgrade path for Linkit 4 to 5.
*/
use Drupal\Component\Serialization\Yaml;
use Drupal\Core\Database\Database;
$connection = Database::getConnection();
// Configuration for linkit profiles.
$config = Yaml::decode(file_get_contents(__DIR__ . '/linkit.linkit_profile.test_profile.yml'));
$connection->insert('config')
->fields([
'collection',
'name',
'data',
])
->values([
'collection' => '',
'name' => 'linkit.linkit_profile.' . $config['id'],
'data' => serialize($config),
])
->execute();
// Configuration for text formats.
$configs = [];
$configs[] = Yaml::decode(file_get_contents(__DIR__ . '/filter.format.format_1.yml'));
foreach ($configs as $config) {
$connection->insert('config')
->fields([
'collection',
'name',
'data',
])
->values([
'collection' => '',
'name' => 'filter.format.' . $config['format'],
'data' => serialize($config),
])
->execute();
}
// Configuration for editors.
$configs = [];
$configs[] = Yaml::decode(file_get_contents(__DIR__ . '/editor.editor.format_1.yml'));
foreach ($configs as $config) {
$connection->insert('config')
->fields([
'collection',
'name',
'data',
])
->values([
'collection' => '',
'name' => 'editor.editor.' . $config['format'],
'data' => serialize($config),
])
->execute();
}
uuid: e91c2255-2146-46fb-af58-1b391113c352
langcode: en
status: true
dependencies:
module:
- file
- node
id: test_profile
label: 'Test profile'
description: 'This is a test profile'
attributes:
target:
id: target
weight: 0
settings:
widget_type: simple_checkbox
title:
id: title
weight: 0
settings:
automatic_title: false
accesskey:
id: accesskey
weight: 0
settings: { }
matchers:
fc48c807-2a9c-44eb-b86b-7e134c1aa252:
uuid: fc48c807-2a9c-44eb-b86b-7e134c1aa252
id: 'entity:node'
weight: 0
settings:
result_description: 'by [node:author] | [node:created:medium]'
bundles: { }
group_by_bundle: false
include_unpublished: false
b8d6d672-6377-493f-b492-3cc69511cf17:
uuid: b8d6d672-6377-493f-b492-3cc69511cf17
id: 'entity:file'
weight: 0
settings:
result_description: '[file:path] [file:url]'
bundles: null
group_by_bundle: null
images:
show_dimensions: false
show_thumbnail: false
thumbnail_image_style: null
......@@ -170,7 +170,7 @@ class LinkitDialogTest extends JavascriptTestBase {
$page->find('xpath', '(//li[contains(@class, "linkit-result") and contains(@class, "ui-menu-item")])[1]')->click();
// Make sure the href field is populated with the node uri.
$this->assertEquals('entity:' . $this->demoEntity->getEntityTypeId() . '/' . $this->demoEntity->id(), $input_field->getValue(), 'The href field is populated with the node uri');
$this->assertEquals('entity:canonical/' . $this->demoEntity->getEntityTypeId() . '/' . $this->demoEntity->id(), $input_field->getValue(), 'The href field is populated with the node uri');
// Make sure the link information is populated.
$javascript = "(function (){ return jQuery('.linkit-link-information > span').text(); })()";
......
......@@ -125,7 +125,7 @@ class LinkitEditorLinkDialogTest extends LinkitKernelTestBase {
$this->assertEquals('', $form['attributes']['href']['#default_value'], 'The href attribute is empty.');
$this->assertEquals('', $form['attributes']['link-information']['#context']['link_target'], 'Link information is empty.');
$form_state->setValue(['attributes', 'href'], 'entity:missing_entity/1');
$form_state->setValue(['attributes', 'href'], 'entity:canonical/missing_entity/1');
$form_builder->submitForm($form_object, $form_state);
$this->assertNotEmpty($form_state->getErrors(), 'Got validation errors for none existing entity type.');
......@@ -135,7 +135,7 @@ class LinkitEditorLinkDialogTest extends LinkitKernelTestBase {
$this->assertEquals('', $form_state->getValue(['attributes', 'data-entity-type']));
$this->assertEquals('', $form_state->getValue(['attributes', 'data-entity-uuid']));
$form_state->setValue(['attributes', 'href'], 'entity:entity_test/1');
$form_state->setValue(['attributes', 'href'], 'entity:canonical/entity_test/1');
$form_builder->submitForm($form_object, $form_state);
$this->assertEmpty($form_state->getErrors(), 'Got no validation errors for correct URI.');
$this->assertEquals($entity->getEntityTypeId(), $form_state->getValue(['attributes', 'data-entity-type']), 'Attribute "data-entity-type" exists and has the correct value.');
......
......@@ -19,7 +19,7 @@ trait AssertResultUriTrait {
*/
public function assertResultUri($entity_type, SuggestionCollection $suggestions) {
foreach ($suggestions->getSuggestions() as $suggestion) {
$this->assertTrue(preg_match("/^entity:" . $entity_type . "\\/\\w+$/i", $suggestion->getPath()), 'Result URI correct formatted.');
$this->assertTrue(preg_match("/^entity:canonical\/" . $entity_type . "\\/\\w+$/i", $suggestion->getPath()), 'Result URI correct formatted.');
}
}
......