Commit 45c63730 authored by Ben Mullins's avatar Ben Mullins
Browse files

Issue #3227822 by lauriii, Wim Leers: [GHS] Ensure GHS works with our custom...

Issue #3227822 by lauriii, Wim Leers: [GHS] Ensure GHS works with our custom plugins, to allow adding additional attributes
parent a031010e
Loading
Loading
Loading
Loading
+1 −1

File changed.

Preview size limit exceeded, changes collapsed.

+84 −16
Original line number Diff line number Diff line
/* eslint-disable import/no-extraneous-dependencies */
// cSpell:words conversionutils datafilter
// cSpell:words conversionutils datafilter eventinfo downcastdispatcher generalhtmlsupport
import { Plugin } from 'ckeditor5/src/core';
import { setViewAttributes } from '@ckeditor/ckeditor5-html-support/src/conversionutils';

@@ -38,6 +38,8 @@ function viewToModelDrupalMediaAttributeConverter(dataFilter) {
        const viewMediaElement = data.viewItem;
        const viewContainerElement = viewMediaElement.parent;

        preserveElementAttributes(viewMediaElement, 'htmlAttributes');

        if (viewContainerElement.is('element', 'a')) {
          preserveLinkAttributes(viewContainerElement);
        }
@@ -70,6 +72,26 @@ function getDescendantElement(writer, containerElement, elementName) {
  }
}

/**
 * Model to view converter for the Drupal Media wrapper attributes.
 *
 * @param {module:utils/eventinfo~EventInfo} evt
 *   An object containing information about the fired event.
 * @param {Object} data
 *   Additional information about the change.
 * @param {module:engine/conversion/downcastdispatcher~DowncastDispatcher} conversionApi
 *   Conversion interface to be used by the callback.
 */
function modelToDataAttributeConverter(evt, data, conversionApi) {
  if (!conversionApi.consumable.consume(data.item, evt.name)) {
    return;
  }

  const viewElement = conversionApi.mapper.toViewElement(data.item);

  setViewAttributes(conversionApi.writer, data.attributeNewValue, viewElement);
}

/**
 * Model to editing view attribute converter.
 *
@@ -77,7 +99,7 @@ function getDescendantElement(writer, containerElement, elementName) {
 *   A function that adds an event listener to downcastDispatcher.
 */
function modelToEditingViewAttributeConverter() {
  return (dispatcher) =>
  return (dispatcher) => {
    dispatcher.on(
      'attribute:linkHref:drupalMedia',
      (evt, data, conversionApi) => {
@@ -105,6 +127,16 @@ function modelToEditingViewAttributeConverter() {
      },
      { priority: 'low' },
    );

    // Render arbitrary attributes on the CKEditor 5 widget wrapper until
    // arbitrary attributes are included as part of the server rendered preview.
    // @see https://www.drupal.org/project/drupal/issues/3231337
    dispatcher.on(
      'attribute:htmlAttributes:drupalMedia',
      modelToDataAttributeConverter,
      { priority: 'low' },
    );
  };
}

/**
@@ -114,7 +146,7 @@ function modelToEditingViewAttributeConverter() {
 *   function that adds an event listener to downcastDispatcher.
 */
function modelToDataViewAttributeConverter() {
  return (dispatcher) =>
  return (dispatcher) => {
    dispatcher.on(
      'attribute:linkHref:drupalMedia',
      (evt, data, conversionApi) => {
@@ -137,6 +169,13 @@ function modelToDataViewAttributeConverter() {
      },
      { priority: 'low' },
    );

    dispatcher.on(
      'attribute:htmlAttributes:drupalMedia',
      modelToDataAttributeConverter,
      { priority: 'low' },
    );
  };
}

/**
@@ -148,20 +187,46 @@ export default class DrupalMediaGeneralHtmlSupport extends Plugin {
  /**
   * @inheritdoc
   */
  init() {
    const { editor } = this;
  constructor(editor) {
    super(editor);

    // This plugin is only needed if General HTML Support plugin is loaded.
    if (!editor.plugins.has('GeneralHtmlSupport')) {
      return;
    }
    // This plugin works only if `DataFilter` and `DataSchema` plugins are
    // loaded. These plugins are dependencies of `GeneralHtmlSupport` meaning
    // that these should be available always when `GeneralHtmlSupport` is
    // enabled.
    if (
      !editor.plugins.has('DataFilter') ||
      !editor.plugins.has('DataSchema')
    ) {
      console.error(
        'DataFilter and DataSchema plugins are required for Drupal Media to integrate with General HTML Support plugin.',
      );
    }

    const { schema } = editor.model;
    const { conversion } = editor;
    const dataFilter = editor.plugins.get('DataFilter');
    const dataFilter = this.editor.plugins.get('DataFilter');
    const dataSchema = this.editor.plugins.get('DataSchema');

    // This needs to be initialized in ::constructor() to ensure this runs
    // before the General HTML Support has been initialized.
    // @see module:html-support/generalhtmlsupport~GeneralHtmlSupport
    dataSchema.registerBlockElement({
      model: 'drupalMedia',
      view: 'drupal-media',
    });

    dataFilter.on('register:drupal-media', (evt, definition) => {
      if (definition.model !== 'drupalMedia') {
        return;
      }

      schema.extend('drupalMedia', {
      allowAttributes: ['htmlLinkAttributes'],
        allowAttributes: ['htmlLinkAttributes', 'htmlAttributes'],
      });

      conversion
@@ -171,6 +236,9 @@ export default class DrupalMediaGeneralHtmlSupport extends Plugin {
        .for('editingDowncast')
        .add(modelToEditingViewAttributeConverter());
      conversion.for('dataDowncast').add(modelToDataViewAttributeConverter());

      evt.stop();
    });
  }

  /**
+1 −1
Original line number Diff line number Diff line
@@ -127,7 +127,7 @@ function (ConstraintViolation $v) {
    $image_uuid = $uploaded_image->uuid();
    $image_url = $this->container->get('file_url_generator')->generateString($uploaded_image->getFileUri());
    $this->drupalGet('node/1');
    $assert_session->elementExists('xpath', sprintf('//img[@alt="</em> Kittens & llamas are cute" and @data-entity-uuid="%s" and @data-entity-type="file"]', $image_uuid));
    $this->assertNotEmpty($assert_session->waitForElement('xpath', sprintf('//img[@alt="</em> Kittens & llamas are cute" and @data-entity-uuid="%s" and @data-entity-type="file"]', $image_uuid)));

    // Drupal CKEditor 5 integrations overrides the CKEditor 5 HTML writer to
    // escape ampersand characters (&) and the angle brackets (< and >). This is
+164 −0
Original line number Diff line number Diff line
<?php

namespace Drupal\Tests\ckeditor5\FunctionalJavascript;

use Drupal\ckeditor5\Plugin\Editor\CKEditor5;
use Drupal\editor\Entity\Editor;
use Drupal\filter\Entity\FilterFormat;
use Drupal\FunctionalJavascriptTests\WebDriverTestBase;
use Drupal\Tests\ckeditor5\Traits\CKEditor5TestTrait;
use Symfony\Component\Validator\ConstraintViolation;

/**
 * Tests emphasis in CKEditor 5.
 *
 * CKEditor's use of <i> is converted to <em> in Drupal, so additional coverage
 * is provided here to verify successful conversion.
 *
 * @group ckeditor5
 * @internal
 */
class EmphasisTest extends WebDriverTestBase {
  use CKEditor5TestTrait;

  /**
   * The user to use during testing.
   *
   * @var \Drupal\user\UserInterface
   */
  protected $adminUser;

  /**
   * A host entity with a body field to use the <em> tag in.
   *
   * @var \Drupal\node\NodeInterface
   */
  protected $host;

  /**
   * {@inheritdoc}
   */
  protected static $modules = [
    'ckeditor5',
    'node',
    'text',
  ];

  /**
   * {@inheritdoc}
   */
  protected $defaultTheme = 'stark';

  /**
   * {@inheritdoc}
   */
  protected function setUp(): void {
    parent::setUp();

    FilterFormat::create([
      'format' => 'test_format',
      'name' => 'Test format',
      'filters' => [
        'filter_html' => [
          'status' => TRUE,
          'settings' => [
            'allowed_html' => '<p> <br> <em>',
          ],
        ],
      ],
    ])->save();
    Editor::create([
      'editor' => 'ckeditor5',
      'format' => 'test_format',
      'settings' => [
        'toolbar' => [
          'items' => [
            'italic',
            'sourceEditing',
          ],
        ],
        'plugins' => [
          'ckeditor5_sourceEditing' => [
            'allowed_tags' => [],
          ],
        ],
      ],
    ])->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')
      ))
    ));
    $this->adminUser = $this->drupalCreateUser([
      'use text format test_format',
      'bypass node access',
    ]);

    $this->drupalCreateContentType(['type' => 'blog']);
    $this->host = $this->createNode([
      'type' => 'blog',
      'title' => 'Animals with strange names',
      'body' => [
        'value' => '<p>This is a <em>test!</em></p>',
        'format' => 'test_format',
      ],
    ]);
    $this->host->save();

    $this->drupalLogin($this->adminUser);
  }

  /**
   * Ensures that CKEditor italic model is converted to em.
   */
  public function testEmphasis() {
    $page = $this->getSession()->getPage();
    $assert_session = $this->assertSession();

    $this->drupalGet($this->host->toUrl('edit-form'));
    $this->waitForEditor();

    $emphasis_element = $assert_session->waitForElementVisible('css', '.ck-content p em');
    $this->assertEquals('test!', $emphasis_element->getText());

    $xpath = new \DOMXPath($this->getEditorDataAsDom());
    $emphasis_source = $xpath->query('//p/em');
    $this->assertNotEmpty($emphasis_source);
    $this->assertEquals('test!', $emphasis_source[0]->textContent);
    $page->pressButton('Save');

    $assert_session->responseContains('<p>This is a <em>test!</em></p>');
  }

  /**
   * Tests that arbitrary attributes are allowed via GHS.
   */
  public function testEmphasisArbitraryHtml() {
    $assert_session = $this->assertSession();
    $editor = Editor::load('test_format');
    $settings = $editor->getSettings();

    // Allow the data-foo attribute in img via GHS.
    $settings['plugins']['ckeditor5_sourceEditing']['allowed_tags'] = ['<em data-foo>'];
    $editor->setSettings($settings);
    $editor->save();

    // Add data-foo use to an existing em tag.
    $original_value = $this->host->body->value;
    $this->host->body->value = str_replace('<em>', '<em data-foo="bar">', $original_value);
    $this->host->save();
    $this->drupalGet($this->host->toUrl('edit-form'));
    $this->waitForEditor();

    $emphasis_element = $assert_session->waitForElementVisible('css', '.ck-content p em');
    $this->assertEquals('bar', $emphasis_element->getAttribute('data-foo'));

    $xpath = new \DOMXPath($this->getEditorDataAsDom());
    $this->assertNotEmpty($xpath->query('//em[@data-foo="bar"]'));
  }

}
+41 −0
Original line number Diff line number Diff line
@@ -140,6 +140,47 @@ function (ConstraintViolation $v) {
    $this->drupalLogin($this->adminUser);
  }

  /**
   * Tests that arbitrary attributes are allowed via GHS.
   *
   * @dataProvider providerLinkability
   */
  public function testImageArbitraryHtml(string $image_type, bool $unrestricted) {
    $editor = Editor::load('test_format');
    $settings = $editor->getSettings();

    // Allow the data-foo attribute in img via GHS.
    $settings['plugins']['ckeditor5_sourceEditing']['allowed_tags'] = ['<img data-foo>'];
    $editor->setSettings($settings);
    $editor->save();

    // Disable filter_html.
    if ($unrestricted) {
      FilterFormat::load('test_format')
        ->setFilterConfig('filter_html', ['status' => FALSE])
        ->save();
    }

    // Make the test content have either a block image or an inline image.
    $img_tag = '<img data-foo="bar" alt="drupalimage test image" data-entity-type="file" data-entity-uuid="' . $this->file->uuid() . '" src="' . $this->file->createFileUrl() . '" />';
    $this->host->body->value .= $image_type === 'block'
      ? $img_tag
      : "<p>$img_tag</p>";
    $this->host->save();

    $expected_widget_selector = $image_type === 'block' ? 'image img' : 'image-inline';

    $this->drupalGet($this->host->toUrl('edit-form'));
    $this->waitForEditor();

    $drupalimage = $this->assertSession()->waitForElementVisible('css', ".ck-content .ck-widget.$expected_widget_selector");
    $this->assertNotEmpty($drupalimage);
    $this->assertEquals('bar', $drupalimage->getAttribute('data-foo'));

    $xpath = new \DOMXPath($this->getEditorDataAsDom());
    $this->assertNotEmpty($xpath->query('//img[@data-foo="bar"]'));
  }

  /**
   * Tests linkability of the image CKEditor widget.
   *
Loading