Problem/Motivation

When two different wildcard tags resolve to the same concrete tag, HTMLRestrictions::resolveWildcards() keeps only the attributes from the last one. The naive resolution assigns each wildcard's attributes instead of merging them:

foreach ($wildcard_tags as $wildcard_tag) {
  if (isset($r->elements[$wildcard_tag])) {
    $naively_resolved_wildcard_elements[$wildcard_tag] = $tag_config;
  }
}

So the result depends on the order of the wildcards:

  • <p> <$text-container class="foo"> <$any-html5-element id> resolves to <p id>.
  • <p> <$any-html5-element id> <$text-container class="foo"> resolves to <p class="foo">.

The expected result in both cases is <p class="foo" id>.

This has a real impact. The Alignment plugin declares <$text-container class="text-align-left text-align-center text-align-right text-align-justify">. The contrib module CKEditor5 ID Attributes declares <$any-html5-element id>. With both enabled, the elements CKEditor 5 requires for "Limit allowed HTML tags and correct faulty HTML" no longer include the text-align-* classes. Saving the text format removes them from the allowed tags, and filter_html then strips alignment from existing content. This was reported against the contrib module in #3528997, but the bug is in core.

Steps to reproduce

use Drupal\ckeditor5\HTMLRestrictions;

$r = HTMLRestrictions::fromString('<p> <$text-container class="foo"> <$any-html5-element id>');
print $r->toFilterHtmlAllowedTagsString();
// Prints "<p id>". Expected "<p class="foo" id>".

Or, through the UI:

  1. Install the CKEditor5 ID Attributes module.
  2. Edit a text format that uses CKEditor 5 and "Limit allowed HTML tags and correct faulty HTML".
  3. Add the Text alignment and ID Attributes buttons to the toolbar, and save.
  4. The "Allowed HTML tags" no longer include class="text-align-…" on <p> or <h2>–<h6>.

Proposed resolution

In resolveWildcards(), merge the attributes from each wildcard instead of assigning them:

$naive_resolution = self::emptySet();
foreach ($r->elements as $tag_name => $tag_config) {
  if (self::isWildcardTag($tag_name)) {
    $wildcard_tags = self::getWildcardTags($tag_name);
    foreach ($wildcard_tags as $wildcard_tag) {
      if (isset($r->elements[$wildcard_tag])) {
        $naive_resolution = $naive_resolution->merge(new self([$wildcard_tag => $tag_config]));
      }
    }
  }
}

Tested outside core (on 10.6.18):

  • <p> <$text-container class="foo"> <$any-html5-element id> resolves to <p class="foo" id>, in either order.
  • <p class="bar"> <$text-container class="foo"> <$any-html5-element id> resolves to <p class="bar foo" id>.

Remaining tasks

  • Merge request with the fix.
  • Add test cases to HTMLRestrictionsTest covering two different wildcards on the same tag, in both orders.
  • Review.

User interface changes

None.

Introduced terminology

None.

API changes

None. resolveWildcards() is private.

Data model changes

None.

Release notes snippet

Not needed.

Comments

benjifisher created an issue.

benjifisher’s picture

The analysis and issue description were written by Claude.

I am assigning this issue to myself so that I can validate the analysis and the steps to reproduce before I ask anyone else to look at this issue.