diff --git a/core/modules/ckeditor5/ckeditor5.ckeditor5.yml b/core/modules/ckeditor5/ckeditor5.ckeditor5.yml index 3467d34ec8be664f7ae6e227e3d095ea77464333..d57aa63fb625c333bcadf920b50b3e475618cf77 100644 --- a/core/modules/ckeditor5/ckeditor5.ckeditor5.yml +++ b/core/modules/ckeditor5/ckeditor5.ckeditor5.yml @@ -62,7 +62,7 @@ ckeditor5_heading: - <h5> - <h6> -ckeditor5_htmlSupport: +ckeditor5_arbitraryHtmlSupport: ckeditor5: plugins: [htmlSupport.GeneralHtmlSupport] config: @@ -82,6 +82,17 @@ ckeditor5_htmlSupport: # @see \Drupal\ckeditor5\Plugin\CKEditor5PluginManagerInterface::getEnabledDefinitions() conditions: [] +ckeditor5_wildcardHtmlSupport: + ckeditor5: + plugins: [htmlSupport.GeneralHtmlSupport] + drupal: + label: Wildcard HTML support + # @see \Drupal\ckeditor5\Plugin\CKEditor5PluginManager::getCKEditor5PluginConfig() + elements: false + library: core/ckeditor5.htmlSupport + # @see \Drupal\ckeditor5\Plugin\CKEditor5PluginManagerInterface::getEnabledDefinitions() + conditions: [] + ckeditor5_specialCharacters: ckeditor5: plugins: diff --git a/core/modules/ckeditor5/src/HTMLRestrictions.php b/core/modules/ckeditor5/src/HTMLRestrictions.php index 08ea69113180ca5a98f2b2f5e4c9f2d826bd223a..c79b9a65a0d01e510497be721d470a5fac2c2b8e 100644 --- a/core/modules/ckeditor5/src/HTMLRestrictions.php +++ b/core/modules/ckeditor5/src/HTMLRestrictions.php @@ -770,8 +770,8 @@ public function merge(HTMLRestrictions $other): HTMLRestrictions { private static function applyOperation(HTMLRestrictions $a, HTMLRestrictions $b, string $operation_method_name): HTMLRestrictions { // 1. Operation applied to wildcard tags that exist in both operands. // For example: <$block id> in both operands. - $a_wildcard = self::getWildcardSubset($a); - $b_wildcard = self::getWildcardSubset($b); + $a_wildcard = $a->getWildcardSubset(); + $b_wildcard = $b->getWildcardSubset(); $wildcard_op_result = $a_wildcard->$operation_method_name($b_wildcard); // Early return if both operands contain only wildcard tags. @@ -828,14 +828,23 @@ private static function getRegExForWildCardAttributeName(string $wildcard_attrib /** * Gets the subset of allowed elements whose tags are wildcards. * - * @param \Drupal\ckeditor5\HTMLRestrictions $r - * A set of HTML restrictions. + * @return \Drupal\ckeditor5\HTMLRestrictions + * The subset of the given set of HTML restrictions. + */ + public function getWildcardSubset(): HTMLRestrictions { + return new self(array_filter($this->elements, [__CLASS__, 'isWildcardTag'], ARRAY_FILTER_USE_KEY)); + } + + /** + * Gets the subset of allowed elements whose tags are concrete. * * @return \Drupal\ckeditor5\HTMLRestrictions * The subset of the given set of HTML restrictions. */ - private static function getWildcardSubset(HTMLRestrictions $r): HTMLRestrictions { - return new self(array_filter($r->elements, [__CLASS__, 'isWildcardTag'], ARRAY_FILTER_USE_KEY)); + public function getConcreteSubset(): HTMLRestrictions { + return new self(array_filter($this->elements, function (string $tag_name) { + return !self::isWildcardTag($tag_name); + }, ARRAY_FILTER_USE_KEY)); } /** @@ -899,7 +908,7 @@ private static function resolveWildcards(HTMLRestrictions $r): HTMLRestrictions // - then $naive will be `<p class="foo">` // - merging them yields `<p class> <$block class="foo">` again // - diffing the wildcard subsets yields just `<p class>` - return $r->merge($naive_resolution)->doDiff(self::getWildcardSubset($r)); + return $r->merge($naive_resolution)->doDiff($r->getWildcardSubset()); } /** @@ -964,7 +973,10 @@ public function toCKEditor5ElementsArray(): array { * @see \Drupal\filter\Plugin\Filter\FilterHtml */ public function toFilterHtmlAllowedTagsString(): string { - return implode(' ', $this->toCKEditor5ElementsArray()); + // Resolve wildcard tags, because Drupal's filter_html filter plugin does + // not support those. + $concrete = self::resolveWildcards($this); + return implode(' ', $concrete->toCKEditor5ElementsArray()); } /** @@ -979,7 +991,16 @@ public function toFilterHtmlAllowedTagsString(): string { */ public function toGeneralHtmlSupportConfig(): array { $allowed = []; - foreach ($this->elements as $tag => $attributes) { + // Resolve any remaining wildcards based on Drupal's assumptions on + // wildcards to ensure all HTML tags that Drupal thinks are supported are + // truly supported by CKEditor 5. For example: the <$block> wildcard does + // NOT correspond to block-level HTML tags, but to CKEditor 5 elements that + // behave like blocks. Knowing the list of concrete HTML tags this maps to + // is impossible without executing JavaScript, which PHP cannot do. By + // generating this GHS configuration, we can guarantee that Drupal's only + // possible interpretation also actually works. + $elements = self::resolveWildcards($this)->getAllowedElements(); + foreach ($elements as $tag => $attributes) { $to_allow = ['name' => $tag]; assert($attributes === FALSE || is_array($attributes)); if (is_array($attributes)) { diff --git a/core/modules/ckeditor5/src/Plugin/CKEditor5Plugin/SourceEditing.php b/core/modules/ckeditor5/src/Plugin/CKEditor5Plugin/SourceEditing.php index 49fe161fe83b12e68b3218829ef8f79e20b47b02..1da134970807b5bf4647ea09be85ca8048c3159e 100644 --- a/core/modules/ckeditor5/src/Plugin/CKEditor5Plugin/SourceEditing.php +++ b/core/modules/ckeditor5/src/Plugin/CKEditor5Plugin/SourceEditing.php @@ -76,9 +76,13 @@ public function getElementsSubset(): array { */ public function getDynamicPluginConfig(array $static_plugin_config, EditorInterface $editor): array { $restrictions = HTMLRestrictions::fromString(implode(' ', $this->configuration['allowed_tags'])); + // Only handle concrete HTML elements to allow the Wildcard HTML support + // plugin to handle wildcards. + // @see \Drupal\ckeditor5\Plugin\CKEditor5PluginManager::getCKEditor5PluginConfig() + $concrete_restrictions = $restrictions->getConcreteSubset(); return [ 'htmlSupport' => [ - 'allow' => $restrictions->toGeneralHtmlSupportConfig(), + 'allow' => $concrete_restrictions->toGeneralHtmlSupportConfig(), ], ]; } diff --git a/core/modules/ckeditor5/src/Plugin/CKEditor5PluginManager.php b/core/modules/ckeditor5/src/Plugin/CKEditor5PluginManager.php index 295699ee420b9474ab65b6459f002b5819043d34..a3df9c9cb44af8ac409a073e541bc41528c3f339 100644 --- a/core/modules/ckeditor5/src/Plugin/CKEditor5PluginManager.php +++ b/core/modules/ckeditor5/src/Plugin/CKEditor5PluginManager.php @@ -160,12 +160,12 @@ public function getEnabledDefinitions(EditorInterface $editor): array { } } - // Only enable the General HTML Support plugin on text formats with no HTML - // restrictions. + // Only enable the arbitrary HTML Support plugin on text formats with no + // HTML restrictions. // @see https://ckeditor.com/docs/ckeditor5/latest/api/html-support.html // @see https://github.com/ckeditor/ckeditor5/issues/9856 if ($editor->getFilterFormat()->getHtmlRestrictions() !== FALSE) { - unset($definitions['ckeditor5_htmlSupport']); + unset($definitions['ckeditor5_arbitraryHtmlSupport']); } // Evaluate `plugins` condition. @@ -175,6 +175,22 @@ public function getEnabledDefinitions(EditorInterface $editor): array { } } + if (!isset($definitions['ckeditor5_arbitraryHtmlSupport'])) { + $restrictions = new HTMLRestrictions($this->getProvidedElements(array_keys($definitions), $editor, FALSE)); + if ($restrictions->getWildcardSubset()->isEmpty()) { + // This is only reached if arbitrary HTML is not enabled. If wildcard + // tags (such as $block) are present, they need to be resolved via the + // wildcardHtmlSupport plugin. + // @see \Drupal\ckeditor5\Plugin\CKEditor5PluginManager::getCKEditor5PluginConfig() + unset($definitions['ckeditor5_wildcardHtmlSupport']); + } + } + // When arbitrary HTML is already supported, there is no need to support + // wildcard tags. + else { + unset($definitions['ckeditor5_wildcardHtmlSupport']); + } + return $definitions; } @@ -254,6 +270,25 @@ public function getCKEditor5PluginConfig(EditorInterface $editor): array { $config[$plugin_id] = $plugin->getDynamicPluginConfig($definition->getCKEditor5Config(), $editor); } + // CKEditor 5 interprets wildcards from a "CKEditor 5 model element" + // perspective, Drupal interprets wildcards from a "HTML element" + // perspective. GHS is used to reconcile those two perspectives, to ensure + // all expected HTML elements truly are supported. + // The `ckeditor5_wildcardHtmlSupport` is automatically enabled when + // necessary, and only when necessary. + // @see \Drupal\ckeditor5\Plugin\CKEditor5PluginManager::getEnabledDefinitions() + if (isset($definitions['ckeditor5_wildcardHtmlSupport'])) { + $allowed_elements = new HTMLRestrictions($this->getProvidedElements(array_keys($definitions), $editor, FALSE)); + // Compute the net new elements that the wildcard tags resolve into. + $concrete_allowed_elements = $allowed_elements->getConcreteSubset(); + $net_new_elements = $allowed_elements->diff($concrete_allowed_elements); + $config['ckeditor5_wildcardHtmlSupport'] = [ + 'htmlSupport' => [ + 'allow' => $net_new_elements->toGeneralHtmlSupportConfig(), + ], + ]; + } + return [ 'plugins' => $this->mergeDefinitionValues('getCKEditor5Plugins', $definitions), 'config' => NestedArray::mergeDeepArray($config), @@ -263,7 +298,7 @@ public function getCKEditor5PluginConfig(EditorInterface $editor): array { /** * {@inheritdoc} */ - public function getProvidedElements(array $plugin_ids = [], EditorInterface $editor = NULL): array { + public function getProvidedElements(array $plugin_ids = [], EditorInterface $editor = NULL, bool $resolve_wildcards = TRUE): array { $plugins = $this->getDefinitions(); if (!empty($plugin_ids)) { $plugins = array_intersect_key($plugins, array_flip($plugin_ids)); @@ -312,7 +347,7 @@ public function getProvidedElements(array $plugin_ids = [], EditorInterface $edi } } - return $elements->getAllowedElements(); + return $elements->getAllowedElements($resolve_wildcards); } /** diff --git a/core/modules/ckeditor5/src/Plugin/CKEditor5PluginManagerInterface.php b/core/modules/ckeditor5/src/Plugin/CKEditor5PluginManagerInterface.php index adf8967f914ef23788e35ee27361574eb8111f1d..4c46eac057e70304f5fcf2be3622ab925536eb1d 100644 --- a/core/modules/ckeditor5/src/Plugin/CKEditor5PluginManagerInterface.php +++ b/core/modules/ckeditor5/src/Plugin/CKEditor5PluginManagerInterface.php @@ -98,22 +98,29 @@ public function findPluginSupportingElement(string $tag): ?string; public function getCKEditor5PluginConfig(EditorInterface $editor): array; /** - * Create a list of elements with attributes declared for the CKEditor5 build. + * Gets all supported elements for the given plugins and text editor. * * @param string[] $plugin_ids - * An array of plugin IDs. - * @param \Drupal\editor\EditorInterface $editor - * A configured text editor object. + * (optional) An array of CKEditor 5 plugin IDs. When not set, gets elements + * for all plugins. + * @param \Drupal\editor\EditorInterface|null $editor + * (optional) A configured text editor object using CKEditor 5. When not + * set, plugins depending on the text editor cannot provide elements. + * @param bool $resolve_wildcards + * (optional) Whether to resolve wildcards. Defaults to TRUE. When set to + * FALSE, the raw allowed elements will be returned (with no processing + * applied hence no resolved wildcards). * * @return array * A nested array with a structure as described in * \Drupal\filter\Plugin\FilterInterface::getHTMLRestrictions(). * * @throws \LogicException - * Thrown when an invalid CKEditor5PluginElementsSubsetInterface implementation is encountered. + * Thrown when an invalid CKEditor5PluginElementsSubsetInterface + * implementation is encountered. * * @see \Drupal\filter\Plugin\FilterInterface::getHTMLRestrictions() */ - public function getProvidedElements(array $plugin_ids = [], EditorInterface $editor = NULL): array; + public function getProvidedElements(array $plugin_ids = [], EditorInterface $editor = NULL, bool $resolve_wildcards = TRUE): array; } diff --git a/core/modules/ckeditor5/src/Plugin/Validation/Constraint/CKEditor5ElementConstraintValidator.php b/core/modules/ckeditor5/src/Plugin/Validation/Constraint/CKEditor5ElementConstraintValidator.php index fe62d144218f1446531fa95da91510ac3482afc1..ca0854c693ca4e2ad4c6df5c3d227613fd277a9f 100644 --- a/core/modules/ckeditor5/src/Plugin/Validation/Constraint/CKEditor5ElementConstraintValidator.php +++ b/core/modules/ckeditor5/src/Plugin/Validation/Constraint/CKEditor5ElementConstraintValidator.php @@ -4,7 +4,7 @@ namespace Drupal\ckeditor5\Plugin\Validation\Constraint; -use Drupal\Component\Utility\Html; +use Drupal\ckeditor5\HTMLRestrictions; use Symfony\Component\Validator\ConstraintValidator; use Symfony\Component\Validator\Exception\UnexpectedTypeException; @@ -25,9 +25,9 @@ public function validate($element, $constraint) { if (!$constraint instanceof CKEditor5ElementConstraint) { throw new UnexpectedTypeException($constraint, __NAMESPACE__ . '\CKEditor5Element'); } - $body_child_nodes = Html::load(str_replace('>', ' />', trim($element)))->getElementsByTagName('body')->item(0)->childNodes; - if ($body_child_nodes->count() !== 1 || $body_child_nodes->item(0)->nodeType !== XML_ELEMENT_NODE) { + $parsed = HTMLRestrictions::fromString($element); + if ($parsed->isEmpty() || count($parsed->getAllowedElements()) > 1 || $element !== $parsed->toCKEditor5ElementsArray()[0]) { $this->context->buildViolation($constraint->message) ->setParameter('%provided_element', $element) ->addViolation(); diff --git a/core/modules/ckeditor5/src/Plugin/Validation/Constraint/SourceEditingRedundantTagsConstraintValidator.php b/core/modules/ckeditor5/src/Plugin/Validation/Constraint/SourceEditingRedundantTagsConstraintValidator.php index a07fad7d021bd8a900519642f642897a08a26557..44cb5252f6e1c5a1f0ca22825fe459dde05b6c2c 100644 --- a/core/modules/ckeditor5/src/Plugin/Validation/Constraint/SourceEditingRedundantTagsConstraintValidator.php +++ b/core/modules/ckeditor5/src/Plugin/Validation/Constraint/SourceEditingRedundantTagsConstraintValidator.php @@ -46,12 +46,15 @@ public function validate($value, Constraint $constraint) { // The single tag for which source editing is enabled, which we are checking // now. $source_enabled_tags = HTMLRestrictions::fromString($value); + // Test for empty allowed elements with resolved wildcards since, for the + // purposes of this validator, HTML restrictions containing only wildcards + // should be considered empty. // @todo Remove this early return in // https://www.drupal.org/project/drupal/issues/2820364. It is only // necessary because CKEditor5ElementConstraintValidator does not run // before this, which means that this validator cannot assume it receives // valid values. - if ($source_enabled_tags->isEmpty() || count($source_enabled_tags->getAllowedElements()) > 1) { + if (count($source_enabled_tags->getAllowedElements()) !== 1) { return; } // This validation constraint currently only validates tags, not attributes; diff --git a/core/modules/ckeditor5/tests/src/FunctionalJavascript/SourceEditingTest.php b/core/modules/ckeditor5/tests/src/FunctionalJavascript/SourceEditingTest.php index 692c9db22f2ccf6f71a14c5cb3117e7cbc9bee3f..4542392e7d88a0d5cf3a3ef9d74cbd090e09aabf 100644 --- a/core/modules/ckeditor5/tests/src/FunctionalJavascript/SourceEditingTest.php +++ b/core/modules/ckeditor5/tests/src/FunctionalJavascript/SourceEditingTest.php @@ -13,6 +13,7 @@ /** * @coversDefaultClass \Drupal\ckeditor5\Plugin\CKEditor5Plugin\SourceEditing + * @covers \Drupal\ckeditor5\Plugin\CKEditor5PluginManager::getCKEditor5PluginConfig() * @group ckeditor5 * @internal */ @@ -61,7 +62,7 @@ protected function setUp(): void { 'filter_html' => [ 'status' => TRUE, 'settings' => [ - 'allowed_html' => '<p> <br> <a href>', + 'allowed_html' => '<div class> <p> <br> <a href>', ], ], 'filter_align' => ['status' => TRUE], @@ -80,7 +81,7 @@ protected function setUp(): void { ], 'plugins' => [ 'ckeditor5_sourceEditing' => [ - 'allowed_tags' => [], + 'allowed_tags' => ['<div class>'], ], ], ], @@ -107,7 +108,7 @@ function (ConstraintViolation $v) { 'type' => 'page', 'title' => 'Animals with strange names', 'body' => [ - 'value' => '<p>The <a href="https://example.com/pirate" class="button" data-grammar="subject">pirate</a> is <a href="https://example.com/irate" class="use-ajax" data-grammar="adjective">irate</a>.</p>', + 'value' => '<div class="llama" data-llama="🦙"><p data-llama="🦙">The <a href="https://example.com/pirate" class="button" data-grammar="subject">pirate</a> is <a href="https://example.com/irate" class="use-ajax" data-grammar="adjective">irate</a>.</p></div>', 'format' => 'test_format', ], ]); @@ -168,69 +169,80 @@ function (ConstraintViolation $v) { public function providerAllowingExtraAttributes(): array { return [ 'no extra attributes allowed' => [ - '<p>The <a href="https://example.com/pirate">pirate</a> is <a href="https://example.com/irate">irate</a>.</p>', + '<div class="llama"><p>The <a href="https://example.com/pirate">pirate</a> is <a href="https://example.com/irate">irate</a>.</p></div>', ], // Common case: any attribute that is not `style` or `class`. '<a data-grammar="subject">' => [ - '<p>The <a href="https://example.com/pirate" data-grammar="subject">pirate</a> is <a href="https://example.com/irate">irate</a>.</p>', + '<div class="llama"><p>The <a href="https://example.com/pirate" data-grammar="subject">pirate</a> is <a href="https://example.com/irate">irate</a>.</p></div>', '<a data-grammar="subject">', ], '<a data-grammar="adjective">' => [ - '<p>The <a href="https://example.com/pirate">pirate</a> is <a href="https://example.com/irate" data-grammar="adjective">irate</a>.</p>', + '<div class="llama"><p>The <a href="https://example.com/pirate">pirate</a> is <a href="https://example.com/irate" data-grammar="adjective">irate</a>.</p></div>', '<a data-grammar="adjective">', ], '<a data-grammar>' => [ - '<p>The <a href="https://example.com/pirate" data-grammar="subject">pirate</a> is <a href="https://example.com/irate" data-grammar="adjective">irate</a>.</p>', + '<div class="llama"><p>The <a href="https://example.com/pirate" data-grammar="subject">pirate</a> is <a href="https://example.com/irate" data-grammar="adjective">irate</a>.</p></div>', '<a data-grammar>', ], // Edge case: `class`. '<a class="button">' => [ - '<p>The <a class="button" href="https://example.com/pirate">pirate</a> is <a href="https://example.com/irate">irate</a>.</p>', + '<div class="llama"><p>The <a class="button" href="https://example.com/pirate">pirate</a> is <a href="https://example.com/irate">irate</a>.</p></div>', '<a class="button">', ], '<a class="use-ajax">' => [ - '<p>The <a href="https://example.com/pirate">pirate</a> is <a class="use-ajax" href="https://example.com/irate">irate</a>.</p>', + '<div class="llama"><p>The <a href="https://example.com/pirate">pirate</a> is <a class="use-ajax" href="https://example.com/irate">irate</a>.</p></div>', '<a class="use-ajax">', ], '<a class>' => [ - '<p>The <a class="button" href="https://example.com/pirate">pirate</a> is <a class="use-ajax" href="https://example.com/irate">irate</a>.</p>', + '<div class="llama"><p>The <a class="button" href="https://example.com/pirate">pirate</a> is <a class="use-ajax" href="https://example.com/irate">irate</a>.</p></div>', '<a class>', ], + // Edge case: $block wildcard with additional attribute. + '<$block data-llama>' => [ + '<div class="llama" data-llama="🦙"><p data-llama="🦙">The <a href="https://example.com/pirate">pirate</a> is <a href="https://example.com/irate">irate</a>.</p></div>', + '<$block data-llama>', + ], + // Edge case: $block wildcard with stricter attribute constrain. + '<$block class="not-llama">' => [ + '<div class="llama"><p>The <a href="https://example.com/pirate">pirate</a> is <a href="https://example.com/irate">irate</a>.</p></div>', + '<$block class="not-llama">', + ], + // Edge case: wildcard attribute names: // - prefix, f.e. `data-*` // - infix, f.e. `*gramma*` // - suffix, f.e. `*-grammar` '<a data-*>' => [ - '<p>The <a href="https://example.com/pirate" data-grammar="subject">pirate</a> is <a href="https://example.com/irate" data-grammar="adjective">irate</a>.</p>', + '<div class="llama"><p>The <a href="https://example.com/pirate" data-grammar="subject">pirate</a> is <a href="https://example.com/irate" data-grammar="adjective">irate</a>.</p></div>', '<a data-*>', ], '<a *gramma*>' => [ - '<p>The <a href="https://example.com/pirate" data-grammar="subject">pirate</a> is <a href="https://example.com/irate" data-grammar="adjective">irate</a>.</p>', + '<div class="llama"><p>The <a href="https://example.com/pirate" data-grammar="subject">pirate</a> is <a href="https://example.com/irate" data-grammar="adjective">irate</a>.</p></div>', '<a *gramma*>', ], '<a *-grammar>' => [ - '<p>The <a href="https://example.com/pirate" data-grammar="subject">pirate</a> is <a href="https://example.com/irate" data-grammar="adjective">irate</a>.</p>', + '<div class="llama"><p>The <a href="https://example.com/pirate" data-grammar="subject">pirate</a> is <a href="https://example.com/irate" data-grammar="adjective">irate</a>.</p></div>', '<a *-grammar>', ], // Edge case: concrete attribute with wildcard class value. '<a class="use-*">' => [ - '<p>The <a href="https://example.com/pirate">pirate</a> is <a class="use-ajax" href="https://example.com/irate">irate</a>.</p>', + '<div class="llama"><p>The <a href="https://example.com/pirate">pirate</a> is <a class="use-ajax" href="https://example.com/irate">irate</a>.</p></div>', '<a class="use-*">', ], // Edge case: concrete attribute with wildcard attribute value. '<a data-grammar="sub*">' => [ - '<p>The <a href="https://example.com/pirate" data-grammar="subject">pirate</a> is <a href="https://example.com/irate">irate</a>.</p>', + '<div class="llama"><p>The <a href="https://example.com/pirate" data-grammar="subject">pirate</a> is <a href="https://example.com/irate">irate</a>.</p></div>', '<a data-grammar="sub*">', ], // Edge case: `data-*` with wildcard attribute value. '<a data-*="sub*">' => [ - '<p>The <a href="https://example.com/pirate" data-grammar="subject">pirate</a> is <a href="https://example.com/irate">irate</a>.</p>', + '<div class="llama"><p>The <a href="https://example.com/pirate" data-grammar="subject">pirate</a> is <a href="https://example.com/irate">irate</a>.</p></div>', '<a data-*="sub*">', ], diff --git a/core/modules/ckeditor5/tests/src/Kernel/CKEditor5PluginManagerTest.php b/core/modules/ckeditor5/tests/src/Kernel/CKEditor5PluginManagerTest.php index 03d642bc964abfc1b22d0a2d6190080259f5445e..98654422b0ce72884071996eb1e04fe4e884c5a7 100644 --- a/core/modules/ckeditor5/tests/src/Kernel/CKEditor5PluginManagerTest.php +++ b/core/modules/ckeditor5/tests/src/Kernel/CKEditor5PluginManagerTest.php @@ -1113,16 +1113,34 @@ public function testEnabledPlugins() { sort($expected_libraries); $this->assertSame($expected_libraries, $this->manager->getEnabledLibraries($editor)); - // Case 7: GHS is only enabled for Full HTML (or any other text format that - // has no TYPE_HTML_RESTRICTOR filters). + // Case 7: GHS is enabled for other text editors if they are using a + // CKEditor 5 plugin that uses wildcard tags. + $settings['toolbar']['items'][] = 'alignment:center'; + $editor->setSettings($settings); + $plugin_ids = array_keys($this->manager->getEnabledDefinitions($editor)); + $expected_plugins = array_merge($expected_plugins, [ + 'ckeditor5_alignment.center', + 'ckeditor5_wildcardHtmlSupport', + ]); + sort($expected_plugins); + $this->assertSame(array_values($expected_plugins), $plugin_ids); + $expected_libraries = array_merge($expected_libraries, [ + 'core/ckeditor5.alignment', + 'core/ckeditor5.htmlSupport', + ]); + sort($expected_libraries); + $this->assertSame($expected_libraries, $this->manager->getEnabledLibraries($editor)); + + // Case 8: GHS is enabled for Full HTML (or any other text format that has + // no TYPE_HTML_RESTRICTOR filters). $editor = Editor::load('full_html'); $definitions = array_keys($this->manager->getEnabledDefinitions($editor)); $default_plugins = [ + 'ckeditor5_arbitraryHtmlSupport', 'ckeditor5_bold', 'ckeditor5_emphasis', 'ckeditor5_essentials', 'ckeditor5_heading', - 'ckeditor5_htmlSupport', 'ckeditor5_paragraph', 'ckeditor5_pasteFromOffice', ]; @@ -1165,6 +1183,10 @@ public function testProvidedElements(array $plugins, array $text_editor_settings 'settings' => $text_editor_settings, 'image_upload' => [], ]); + FilterFormat::create([ + 'format' => 'dummy', + 'name' => 'dummy', + ])->save(); $this->assertConfigSchema( $this->typedConfig, $text_editor->getConfigDependencyName(), diff --git a/core/modules/ckeditor5/tests/src/Kernel/ValidatorsTest.php b/core/modules/ckeditor5/tests/src/Kernel/ValidatorsTest.php index b879e408e682f69074e4eac0485d071d8d586924..daf3b5162a067f460cf9155f0b337f360d4b7a1e 100644 --- a/core/modules/ckeditor5/tests/src/Kernel/ValidatorsTest.php +++ b/core/modules/ckeditor5/tests/src/Kernel/ValidatorsTest.php @@ -813,6 +813,38 @@ public function providerPair(): array { 'settings.toolbar.items.0' => 'The <em class="placeholder">Drupal media</em> toolbar item requires the <em class="placeholder">Embed media</em> filter to be enabled.', ], ]; + $data['VALID: HTML format: very minimal toolbar + wildcard in source editing HTML'] = [ + 'settings' => [ + 'toolbar' => [ + 'items' => [ + 'bold', + 'sourceEditing', + ], + ], + 'plugins' => [ + 'ckeditor5_sourceEditing' => [ + 'allowed_tags' => ['<$block data-llama>'], + ], + ], + ], + 'image_upload' => [ + 'status' => FALSE, + ], + 'filters' => [ + 'filter_html' => [ + 'id' => 'filter_html', + 'provider' => 'filter', + 'status' => TRUE, + 'weight' => 0, + 'settings' => [ + 'allowed_html' => '<p data-llama> <br> <strong>', + 'filter_html_help' => TRUE, + 'filter_html_nofollow' => TRUE, + ], + ], + ], + 'violations' => [], + ]; return $data; } diff --git a/core/modules/ckeditor5/tests/src/Kernel/WildcardHtmlSupportTest.php b/core/modules/ckeditor5/tests/src/Kernel/WildcardHtmlSupportTest.php new file mode 100644 index 0000000000000000000000000000000000000000..f936d37ab1ffceaa004396f9e69c4b48dcf35060 --- /dev/null +++ b/core/modules/ckeditor5/tests/src/Kernel/WildcardHtmlSupportTest.php @@ -0,0 +1,227 @@ +<?php + +namespace Drupal\Tests\ckeditor5\Kernel; + +use Drupal\ckeditor5\Plugin\Editor\CKEditor5; +use Drupal\editor\Entity\Editor; +use Drupal\filter\Entity\FilterFormat; +use Drupal\KernelTests\KernelTestBase; +use Symfony\Component\Validator\ConstraintViolation; + +/** + * @covers \Drupal\ckeditor5\Plugin\CKEditor5PluginManager::getCKEditor5PluginConfig() + * @group ckeditor5 + * @internal + */ +class WildcardHtmlSupportTest extends KernelTestBase { + + /** + * {@inheritdoc} + */ + protected static $modules = [ + 'ckeditor5', + 'filter', + 'editor', + ]; + + /** + * The manager for "CKEditor 5 plugin" plugins. + * + * @var \Drupal\ckeditor5\Plugin\CKEditor5PluginManagerInterface + */ + protected $manager; + + /** + * {@inheritdoc} + */ + protected function setUp(): void { + parent::setUp(); + $this->manager = $this->container->get('plugin.manager.ckeditor5.plugin'); + } + + /** + * @covers \Drupal\ckeditor5\Plugin\CKEditor5Plugin\SourceEditing::getDynamicPluginConfig() + * @covers \Drupal\ckeditor5\Plugin\CKEditor5PluginManager::getCKEditor5PluginConfig() + * @dataProvider providerGhsConfiguration + */ + public function testGhsConfiguration(string $filter_html_allowed, array $source_editing_tags, array $expected_ghs_configuration, ?array $additional_toolbar_items = []): void { + FilterFormat::create([ + 'format' => 'test_format', + 'name' => 'Test format', + 'filters' => [ + 'filter_html' => [ + 'status' => TRUE, + 'settings' => [ + 'allowed_html' => $filter_html_allowed, + ], + ], + ], + ])->save(); + $editor = Editor::create([ + 'editor' => 'ckeditor5', + 'format' => 'test_format', + 'settings' => [ + 'toolbar' => [ + 'items' => array_merge(['sourceEditing'], $additional_toolbar_items), + ], + 'plugins' => [ + 'ckeditor5_sourceEditing' => [ + 'allowed_tags' => $source_editing_tags, + ], + ], + ], + 'image_upload' => [ + 'status' => FALSE, + ], + ]); + $editor->save(); + $this->assertSame([], array_map( + function (ConstraintViolation $v) { + return (string) $v->getMessage(); + }, + iterator_to_array(CKEditor5::validatePair( + Editor::load('test_format'), + FilterFormat::load('test_format') + )) + )); + $config = $this->manager->getCKEditor5PluginConfig($editor); + $this->assertEquals($expected_ghs_configuration, $config['config']['htmlSupport']['allow']); + } + + public function providerGhsConfiguration(): array { + return [ + 'empty source editing' => [ + '<p> <br>', + [], + [], + ], + 'without wildcard' => [ + '<p> <br> <a href> <blockquote> <div data-llama>', + ['<div data-llama>'], + [ + [ + 'name' => 'div', + 'attributes' => [ + [ + 'key' => 'data-llama', + 'value' => TRUE, + ], + ], + ], + ], + ['link', 'blockQuote'], + ], + '<$block> minimal configuration' => [ + '<p data-llama> <br>', + ['<$block data-llama>'], + [ + [ + 'name' => 'p', + 'attributes' => [ + [ + 'key' => 'data-llama', + 'value' => TRUE, + ], + ], + ], + ], + ], + '<$block> from multiple plugins' => [ + '<p data-llama class="text-align-left text-align-center text-align-right text-align-justify"> <br>', + ['<$block data-llama>'], + [ + [ + 'name' => 'p', + 'attributes' => [ + [ + 'key' => 'data-llama', + 'value' => TRUE, + ], + ], + 'classes' => [ + 'regexp' => [ + 'pattern' => '/^(text-align-left|text-align-center|text-align-right|text-align-justify)$/', + ], + ], + ], + ], + ['alignment'], + ], + '<$block> with attribute from multiple plugins' => [ + '<p data-llama class"> <br>', + ['<$block data-llama>', '<p class>'], + [ + [ + 'name' => 'p', + 'classes' => TRUE, + ], + [ + 'name' => 'p', + 'attributes' => [ + [ + 'key' => 'data-llama', + 'value' => TRUE, + ], + ], + 'classes' => [ + 'regexp' => [ + 'pattern' => '/^(text-align-left|text-align-center|text-align-right|text-align-justify)$/', + ], + ], + ], + ], + ['alignment'], + ], + '<$block> realistic configuration' => [ + '<p data-llama> <br> <a href> <blockquote data-llama> <div data-llama> <mark> <abbr title>', + ['<$block data-llama>', '<div>', '<mark>', '<abbr title>'], + [ + [ + 'name' => 'div', + ], + [ + 'name' => 'mark', + ], + [ + 'name' => 'abbr', + 'attributes' => [ + [ + 'key' => 'title', + 'value' => TRUE, + ], + ], + ], + [ + 'name' => 'p', + 'attributes' => [ + [ + 'key' => 'data-llama', + 'value' => TRUE, + ], + ], + ], + [ + 'name' => 'div', + 'attributes' => [ + [ + 'key' => 'data-llama', + 'value' => TRUE, + ], + ], + ], + [ + 'name' => 'blockquote', + 'attributes' => [ + [ + 'key' => 'data-llama', + 'value' => TRUE, + ], + ], + ], + ], + ['link', 'blockQuote'], + ], + ]; + } + +} diff --git a/core/modules/ckeditor5/tests/src/Unit/HTMLRestrictionsTest.php b/core/modules/ckeditor5/tests/src/Unit/HTMLRestrictionsTest.php index e0b7669f0d9e74a4ef80fcc75f1efbf667080558..0a3628ad97722f42e9182cc0e2e7d6cc2e6b32a0 100644 --- a/core/modules/ckeditor5/tests/src/Unit/HTMLRestrictionsTest.php +++ b/core/modules/ckeditor5/tests/src/Unit/HTMLRestrictionsTest.php @@ -455,6 +455,39 @@ public function providerRepresentations(): \Generator { ], ]; + yield '$block wildcard' => [ + new HTMLRestrictions(['$block' => ['class' => TRUE, 'data-llama' => TRUE], 'div' => FALSE, 'span' => FALSE, 'blockquote' => ['cite' => TRUE]]), + ['<$block class data-llama>', '<div>', '<span>', '<blockquote cite>'], + '<div class data-llama> <span> <blockquote cite class data-llama>', + [ + [ + 'name' => 'div', + 'classes' => TRUE, + 'attributes' => [ + [ + 'key' => 'data-llama', + 'value' => TRUE, + ], + ], + ], + ['name' => 'span'], + [ + 'name' => 'blockquote', + 'attributes' => [ + [ + 'key' => 'cite', + 'value' => TRUE, + ], + [ + 'key' => 'data-llama', + 'value' => TRUE, + ], + ], + 'classes' => TRUE, + ], + ], + ]; + yield 'realistic' => [ new HTMLRestrictions(['a' => ['href' => TRUE, 'hreflang' => ['en' => TRUE, 'fr' => TRUE]], 'p' => ['data-*' => TRUE, 'class' => ['block' => TRUE]], 'br' => FALSE]), ['<a href hreflang="en fr">', '<p data-* class="block">', '<br>'], @@ -498,12 +531,12 @@ public function providerRepresentations(): \Generator { // Wildcard tag, attribute and attribute value. yield '$block' => [ - new HTMLRestrictions(['$block' => ['data-*' => TRUE]]), - ['<$block data-*>'], - '<$block data-*>', + new HTMLRestrictions(['p' => FALSE, '$block' => ['data-*' => TRUE]]), + ['<p>', '<$block data-*>'], + '<p data-*>', [ [ - 'name' => '$block', + 'name' => 'p', 'attributes' => [ [ 'key' => [ @@ -1080,4 +1113,40 @@ public function providerOperands(): \Generator { } } + /** + * @covers ::getWildcardSubset + * @covers ::getConcreteSubset + * @dataProvider providerSubsets + */ + public function testSubsets(HTMLRestrictions $input, HTMLRestrictions $expected_wildcard_subset, HTMLRestrictions $expected_concrete_subset): void { + $this->assertEquals($expected_wildcard_subset, $input->getWildcardSubset()); + $this->assertEquals($expected_concrete_subset, $input->getConcreteSubset()); + } + + public function providerSubsets(): \Generator { + yield 'empty set' => [ + new HTMLRestrictions([]), + new HTMLRestrictions([]), + new HTMLRestrictions([]), + ]; + + yield 'without wildcards' => [ + new HTMLRestrictions(['div' => FALSE]), + new HTMLRestrictions([]), + new HTMLRestrictions(['div' => FALSE]), + ]; + + yield 'with wildcards' => [ + new HTMLRestrictions(['div' => FALSE, '$block' => ['data-llama' => TRUE]]), + new HTMLRestrictions(['$block' => ['data-llama' => TRUE]]), + new HTMLRestrictions(['div' => FALSE]), + ]; + + yield 'only wildcards' => [ + new HTMLRestrictions(['$block' => ['data-llama' => TRUE]]), + new HTMLRestrictions(['$block' => ['data-llama' => TRUE]]), + new HTMLRestrictions([]), + ]; + } + }