diff --git a/embed.module b/embed.module index 2834b39..3cb178d 100644 --- a/embed.module +++ b/embed.module @@ -6,8 +6,10 @@ use Drupal\Core\StringTranslation\TranslatableMarkup; /** * Implements hook_form_FORM_ID_alter() on behalf of ckeditor.module. + * + * @todo Remove this once CKEditor4 support is dropped. */ -function ckeditor_form_embed_button_add_form_alter(array &$form, FormStateInterface $form_state) { +function ckeditor_form_embed_button_add_form_alter(array &$form, FormStateInterface $form_state): void { $form['#validate'][] = 'ckeditor_form_embed_button_add_form_validate'; } @@ -16,8 +18,10 @@ function ckeditor_form_embed_button_add_form_alter(array &$form, FormStateInterf * * Checks to make sure that when adding a new embed button, its ID will not * conflict with any existing CKEditor buttons. + * + * @todo Remove this once CKEditor4 support is dropped. */ -function ckeditor_form_embed_button_add_form_validate(array &$form, FormStateInterface $form_state) { +function ckeditor_form_embed_button_add_form_validate(array &$form, FormStateInterface $form_state): void { /** @var \Drupal\ckeditor\CKEditorPluginManager $ckeditor_plugin_manager */ $ckeditor_plugin_manager = \Drupal::service('plugin.manager.ckeditor.plugin'); @@ -25,11 +29,11 @@ function ckeditor_form_embed_button_add_form_validate(array &$form, FormStateInt $button_ids = array_reduce($ckeditor_plugin_manager->getButtons(), function ($result, $item) { return array_merge($result, array_keys($item)); }, []); + $button_ids = array_map('mb_strtolower', $button_ids); // Ensure that button ID is unique. - // @todo Should this do a case-insensitive comparison? $button_id = $form_state->getValue('id'); - if (in_array($button_id, $button_ids)) { + if (in_array($button_id, $button_ids, TRUE)) { $form_state->setErrorByName('id', t('A CKEditor button with ID %id already exists.', ['%id' => $button_id])); } } @@ -50,8 +54,10 @@ function embed_form_filter_format_add_form_alter(array &$form) { /** * Validate callback for buttons that have a required_filter_plugin_id. + * + * @todo Remove this once CKEditor4 support is dropped. */ -function embed_filter_format_edit_form_validate($form, FormStateInterface $form_state) { +function embed_filter_format_edit_form_validate($form, FormStateInterface $form_state): void { if ($form_state->getTriggeringElement()['#name'] !== 'op') { return; } @@ -93,7 +99,7 @@ function embed_filter_format_edit_form_validate($form, FormStateInterface $form_ }; foreach ($buttons_to_validate as $button_id => $filter_plugin_id) { - list($plugin_id, $button_id) = explode(':', $button_id, 2); + [$plugin_id, $button_id] = explode(':', $button_id, 2); if (in_array($button_id, $selected_buttons, TRUE)) { $filter_enabled = $form_state->getValue([ 'filters', diff --git a/src/Access/EmbedButtonEditorAccessCheck.php b/src/Access/EmbedButtonEditorAccessCheck.php index 01463b6..fbc8b83 100644 --- a/src/Access/EmbedButtonEditorAccessCheck.php +++ b/src/Access/EmbedButtonEditorAccessCheck.php @@ -78,20 +78,29 @@ class EmbedButtonEditorAccessCheck implements AccessInterface { * currently only capable of detecting buttons used by CKEditor. */ protected function checkButtonEditorAccess(EmbedButtonInterface $embed_button, EditorInterface $editor) { - if ($editor->getEditor() !== 'ckeditor') { + if (!in_array($editor->getEditor(), ['ckeditor', 'ckeditor5'], TRUE)) { throw new HttpException(500, 'Currently, only CKEditor is supported.'); } $has_button = FALSE; $settings = $editor->getSettings(); - foreach ($settings['toolbar']['rows'] as $row) { - foreach ($row as $group) { - if (in_array($embed_button->id(), $group['items'])) { - $has_button = TRUE; - break 2; + if ($editor->getEditor() === 'ckeditor') { + foreach ($settings['toolbar']['rows'] as $row) { + foreach ($row as $group) { + if (in_array($embed_button->id(), $group['items'])) { + $has_button = TRUE; + break 2; + } } } } + elseif ($editor->getEditor() === 'ckeditor5') { + // The schema for CKEditor5 has changed, therefore we need to check for + // the toolbar items differently. + if ($settings['toolbar']['items'] && in_array($embed_button->id(), $settings['toolbar']['items'])) { + $has_button = TRUE; + } + } return AccessResult::allowedIf($has_button) ->addCacheableDependency($embed_button) diff --git a/src/Deriver/EmbedCKEditor5PluginDeriver.php b/src/Deriver/EmbedCKEditor5PluginDeriver.php new file mode 100644 index 0000000..b3f1f11 --- /dev/null +++ b/src/Deriver/EmbedCKEditor5PluginDeriver.php @@ -0,0 +1,75 @@ +entityTypeManager = $entity_type_manager; + } + + /** + * {@inheritdoc} + */ + public static function create(ContainerInterface $container, $base_plugin_id) { + return new static( + $container->get('entity_type.manager') + ); + } + + /** + * {@inheritdoc} + */ + public function getDerivativeDefinitions($base_plugin_definition) { + assert($base_plugin_definition instanceof CKEditor5PluginDefinition); + + /** @var \Drupal\embed\EmbedButtonInterface $embed_button */ + foreach ($this->entityTypeManager->getStorage('embed_button')->loadByProperties(['type' => $base_plugin_definition->id()]) as $embed_button) { + $embed_button_id = $embed_button->id(); + $embed_button_label = Html::escape($embed_button->label()); + $plugin_id = "{$base_plugin_definition->id()}_{$embed_button_id}"; + $definition = $base_plugin_definition->toArray(); + $definition['id'] .= $embed_button_id; + $definition['drupal']['label'] = $this->t('@base - @label', ['@base' => $base_plugin_definition->label(), '@label' => $embed_button_label])->render(); + $definition['drupal']['toolbar_items'] = [ + $embed_button_id => [ + 'label' => $embed_button_label, + ], + ]; + foreach ($definition['drupal']['elements'] as $element) { + if (str_contains('data-embed-button', $element)) { + $definition['drupal']['elements'][] = str_replace('data-embed-button', "data-embed-button=\"{$embed_button_id}\"", $element); + } + } + $this->derivatives[$plugin_id] = new CKEditor5PluginDefinition($definition); + } + + return $this->derivatives; + } + +} \ No newline at end of file diff --git a/src/Plugin/CKEditor5Plugin/EmbedCKEditor5PluginBase.php b/src/Plugin/CKEditor5Plugin/EmbedCKEditor5PluginBase.php new file mode 100644 index 0000000..a0da1fd --- /dev/null +++ b/src/Plugin/CKEditor5Plugin/EmbedCKEditor5PluginBase.php @@ -0,0 +1,138 @@ +", + * "", + * }, + * conditions = { + * "filter" = "your_embed_filter_id", + * }, + * ), + * @endcode + * + * @see \Drupal\ckeditor5\Annotation\CKEditor5Plugin + */ +abstract class EmbedCKEditor5PluginBase extends CKEditor5PluginDefault implements ContainerFactoryPluginInterface { + + /** + * The CSRF Token generator. + * + * @var \Drupal\Core\Access\CsrfTokenGenerator + */ + protected $csrfTokenGenerator; + + /** + * The entity type manager. + * + * @var \Drupal\Core\Entity\EntityTypeManagerInterface + */ + protected $entityTypeManager; + + /** + * DrupalEntity constructor. + * + * @param array $configuration + * A configuration array containing information about the plugin instance. + * @param string $plugin_id + * The plugin_id for the plugin instance. + * @param \Drupal\ckeditor5\Plugin\CKEditor5PluginDefinition $plugin_definition + * The plugin implementation definition. + * @param \Drupal\Core\Access\CsrfTokenGenerator $csrf_token_generator + * The CSRF Token generator service. + * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager + * The Entity Type Manager service. + */ + public function __construct(array $configuration, string $plugin_id, CKEditor5PluginDefinition $plugin_definition, CsrfTokenGenerator $csrf_token_generator, EntityTypeManagerInterface $entity_type_manager) { + parent::__construct($configuration, $plugin_id, $plugin_definition); + $this->csrfTokenGenerator = $csrf_token_generator; + $this->entityTypeManager = $entity_type_manager; + if (!isset($plugin_definition['embed_type_id'])) { + throw new InvalidPluginDefinitionException($plugin_id, sprintf('The %s plugin must define the embed_type_id property.', $plugin_id)); + } + } + + /** + * {@inheritDoc} + */ + public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) { + return new static( + $configuration, + $plugin_id, + $plugin_definition, + $container->get('csrf_token'), + $container->get('entity_type.manager') + ); + } + + /** + * {@inheritdoc} + */ + public function getButtons(): array { + $buttons = []; + + if ($embed_buttons = $this->entityTypeManager->getStorage('embed_button')->loadByProperties(['type_id' => $this->pluginDefinition['embed_type_id']])) { + foreach ($embed_buttons as $embed_button) { + $buttons[$embed_button->id()] = $this->getButton($embed_button); + } + } + + return $buttons; + } + + /** + * Build the information about the specific button. + * + * @param \Drupal\embed\EmbedButtonInterface $embed_button + * The embed button. + * + * @return array + * The array for use with getButtons(). + */ + protected function getButton(EmbedButtonInterface $embed_button): array { + $label = Html::escape($embed_button->label()); + + return [ + 'id' => $embed_button->id(), + 'name' => $label, + 'label' => $label, + 'image' => $embed_button->getIconUrl(), + ]; + } + + /** + * Get the embed preview route CSRF token. + */ + public function getEmbedPreviewCsrfToken(): string { + return $this->csrfTokenGenerator->get(EmbedController::PREVIEW_CSRF_TOKEN_NAME); + } + +} \ No newline at end of file diff --git a/tests/src/Functional/EmbedButtonEditorAccessCheckTest.php b/tests/src/Functional/EmbedButtonEditorAccessCheckTest.php index e8cdf78..2df5d86 100644 --- a/tests/src/Functional/EmbedButtonEditorAccessCheckTest.php +++ b/tests/src/Functional/EmbedButtonEditorAccessCheckTest.php @@ -2,29 +2,85 @@ namespace Drupal\Tests\embed\Functional; +use Drupal\editor\EditorInterface; use Drupal\editor\Entity\Editor; +use Drupal\embed\Access\EmbedButtonEditorAccessCheck; use Drupal\Tests\system\Functional\Cache\AssertPageCacheContextsAndTagsTrait; /** * Tests EmbedButtonEditorAccessCheck. * + * @coversDefaultClass \Drupal\embed\Access\EmbedButtonEditorAccessCheck * @group embed */ class EmbedButtonEditorAccessCheckTest extends EmbedTestBase { use AssertPageCacheContextsAndTagsTrait; - const SUCCESS = 'Success!'; - /** * {@inheritdoc} */ - protected $defaultTheme = 'stark'; + protected static $modules = [ + 'ckeditor', + 'ckeditor5', + ]; + + /** + * Sets up the editor for testing. + * + * @param string $editor_id + * The editor plugin ID. + * + * @return \Drupal\editor\EditorInterface + * The saved editor entity. + */ + public function setupEditor(string $editor_id): EditorInterface { + $editor = Editor::create([ + 'format' => 'embed_test', + 'editor' => $editor_id, + ]); + + switch ($editor_id) { + case 'ckeditor': + $editor->setSettings([ + 'toolbar' => [ + 'rows' => [[[ + 'name' => 'Embed', + 'items' => [ + 'embed_test_default', + ], + ]]], + ], + ]); + break; + + case 'ckeditor5': + $editor->setSettings([ + 'toolbar' => [ + 'items' => [ + 'embed_test_default', + ], + ], + ]); + break; + + default: + $this->fail("Unsupported editor plugin $editor_id."); + } + + $editor->save(); + return $editor; + } /** * Tests \Drupal\embed\Access\EmbedButtonEditorAccessCheck. + * + * @covers ::access + * @dataProvider providerEmbedbuttonEditorAccessCheck */ - public function testEmbedButtonEditorAccessCheck() { + public function testEmbedButtonEditorAccessCheck(string $editor) { + $this->setUpEditor($editor); + // The anonymous user should have access to the plain_text format, but it // hasn't been configured to use an editor yet. $this->getRoute('plain_text', 'embed_test_default'); @@ -52,7 +108,7 @@ class EmbedButtonEditorAccessCheckTest extends EmbedTestBase { // Add an empty configuration for the plain_text editor configuration. $editor = Editor::create([ 'format' => 'plain_text', - 'editor' => 'ckeditor', + 'editor' => $editor, ]); $editor->save(); $this->getRoute('plain_text', 'embed_test_default'); @@ -66,7 +122,7 @@ class EmbedButtonEditorAccessCheckTest extends EmbedTestBase { $this->assertCacheContext('route'); $this->assertSession()->responseHeaderContains('X-Drupal-Cache-Tags', 'config:editor.editor.embed_test'); $this->assertSession()->responseHeaderContains('X-Drupal-Cache-Tags', 'config:embed.button.embed_test_default'); - $this->assertSession()->pageTextContains(static::SUCCESS); + $this->assertSession()->pageTextContains('Success!'); // Test route with an empty request. $this->getRoute('embed_test', 'embed_test_default', ''); @@ -90,6 +146,15 @@ class EmbedButtonEditorAccessCheckTest extends EmbedTestBase { $this->assertSession()->responseHeaderNotContains('X-Drupal-Cache-Tags', 'config:embed.button.invalid_button'); } + /** + * Data provider for testEmbedbuttonEditorAccessCheck(). + */ + public function providerEmbedbuttonEditorAccessCheck(): array { + return array_map(static function (string $editor_id) { + return [$editor_id]; + }, ['ckeditor', 'ckeditor5']); + } + /** * Performs a request to the embed_test.test_access route. * @@ -106,7 +171,7 @@ class EmbedButtonEditorAccessCheckTest extends EmbedTestBase { public function getRoute($editor_id, $embed_button_id, $value = NULL) { $url = 'embed-test/access/' . $editor_id . '/' . $embed_button_id; if (!isset($value)) { - $value = static::SUCCESS; + $value = 'Success!'; } return $this->drupalGet($url, ['query' => ['value' => $value]]); } diff --git a/tests/src/Functional/EmbedTestBase.php b/tests/src/Functional/EmbedTestBase.php index bc86f60..16a921e 100644 --- a/tests/src/Functional/EmbedTestBase.php +++ b/tests/src/Functional/EmbedTestBase.php @@ -25,7 +25,6 @@ abstract class EmbedTestBase extends BrowserTestBase { 'embed', 'embed_test', 'editor', - 'ckeditor', ]; /** @@ -61,23 +60,6 @@ abstract class EmbedTestBase extends BrowserTestBase { ]); $format->save(); - $editor_group = [ - 'name' => 'Embed', - 'items' => [ - 'embed_test_default', - ], - ]; - $editor = Editor::create([ - 'format' => 'embed_test', - 'editor' => 'ckeditor', - 'settings' => [ - 'toolbar' => [ - 'rows' => [[$editor_group]], - ], - ], - ]); - $editor->save(); - // Create a user with required permissions. $this->adminUser = $this->drupalCreateUser([ 'administer embed buttons', diff --git a/tests/src/FunctionalJavascript/EmbedButtonAdminTest.php b/tests/src/FunctionalJavascript/EmbedButtonAdminTest.php index c4c9490..ee279c4 100644 --- a/tests/src/FunctionalJavascript/EmbedButtonAdminTest.php +++ b/tests/src/FunctionalJavascript/EmbedButtonAdminTest.php @@ -188,12 +188,13 @@ class EmbedButtonAdminTest extends WebDriverTestBase { $id = $assert_session->waitForField('id'); $this->assertNotEmpty($id); - $id->setValue('DrupalImage'); + $id->setValue('drupalimage'); $edit = [ 'type_id' => 'embed_test_default', ]; $this->submitForm($edit, 'Save'); + $assert_session->pageTextContains('A CKEditor button with ID DrupalImage already exists.'); } }