Commit 1b2dfeb4 authored by catch's avatar catch
Browse files

Issue #3260869 by lauriii, Wim Leers, bnjmnm, alexpott, catch: Resolve...

Issue #3260869 by lauriii, Wim Leers, bnjmnm, alexpott, catch: Resolve mismatch between <$block> interpretation by CKEditor 5 and Drupal

(cherry picked from commit 19eaa3ac)
parent 2855e443
Loading
Loading
Loading
Loading
+12 −1
Original line number Diff line number Diff line
@@ -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:
+30 −9
Original line number Diff line number Diff line
@@ -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)) {
+5 −1
Original line number Diff line number Diff line
@@ -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(),
      ],
    ];
  }
+40 −5
Original line number Diff line number Diff line
@@ -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);
  }

  /**
+13 −6
Original line number Diff line number Diff line
@@ -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;

}
Loading