From e8aaf3772e12c17a34be9abeda21984dca97939a Mon Sep 17 00:00:00 2001
From: Lauri Eskola <lauri.eskola@acquia.com>
Date: Thu, 17 Feb 2022 16:04:07 +0200
Subject: [PATCH] Issue #3224652 by Wim Leers, hooroomoo, vlyalko, lauriii,
 bnjmnm, huzooka: [drupalImage] Add ckeditor5-image's imageresize plugin to
 allow image resizing

---
 core/modules/ckeditor5/ckeditor5.api.php      |   2 +
 .../modules/ckeditor5/ckeditor5.ckeditor5.yml |  21 +++
 .../config/schema/ckeditor5.schema.yml        |  11 ++
 core/modules/ckeditor5/js/ckeditor5.es6.js    |   4 +
 core/modules/ckeditor5/js/ckeditor5.js        |   4 +
 .../Plugin/CKEditor5Plugin/ImageResize.php    |  58 +++++++
 .../src/Plugin/CKEditor5PluginDefinition.php  |  90 ++++++++---
 .../src/Plugin/CKEditor5PluginManager.php     |   4 +
 .../ckeditor5/src/Plugin/Editor/CKEditor5.php |  21 ++-
 .../FunctionalJavascript/CKEditor5Test.php    |   4 +-
 .../src/FunctionalJavascript/ImageTest.php    |  96 +++++++++++-
 .../src/Kernel/CKEditor5PluginManagerTest.php | 141 +++++++++++++++++-
 .../src/Kernel/ConfigurablePluginTest.php     |   3 +
 .../src/Kernel/SmartDefaultSettingsTest.php   |   7 +
 .../tests/src/Kernel/ValidatorsTest.php       |   6 +-
 15 files changed, 436 insertions(+), 36 deletions(-)
 create mode 100644 core/modules/ckeditor5/src/Plugin/CKEditor5Plugin/ImageResize.php

diff --git a/core/modules/ckeditor5/ckeditor5.api.php b/core/modules/ckeditor5/ckeditor5.api.php
index dce79025ebf5..763236299318 100644
--- a/core/modules/ckeditor5/ckeditor5.api.php
+++ b/core/modules/ckeditor5/ckeditor5.api.php
@@ -134,6 +134,8 @@
  *   - 'filter': a filter that must be enabled
  *   - 'imageUploadStatus': TRUE if image upload must be enabled, FALSE if it
  *      must not be enabled
+ *   - 'requiresConfiguration': a subset of the configuration for this plugin
+ *      that must match (exactly)
  *   - 'plugins': a list of CKEditor 5 Drupal plugin IDs that must be enabled
  *
  * All of these can be defined in YAML or annotations. A given plugin should
diff --git a/core/modules/ckeditor5/ckeditor5.ckeditor5.yml b/core/modules/ckeditor5/ckeditor5.ckeditor5.yml
index 8a588ee09ea0..894d6c4b49f6 100644
--- a/core/modules/ckeditor5/ckeditor5.ckeditor5.yml
+++ b/core/modules/ckeditor5/ckeditor5.ckeditor5.yml
@@ -430,6 +430,27 @@ ckeditor5_image:
       toolbarItem: uploadImage
       imageUploadStatus: true
 
+ckeditor5_imageResize:
+  ckeditor5:
+    plugins:
+      - image.ImageResize
+    config:
+      image:
+        resizeUnit: '%'
+        resizeOptions:
+          -
+            name: 'resizeImage:original'
+            value: null
+        toolbar: [resizeImage]
+  drupal:
+    label: Image resize
+    class: \Drupal\ckeditor5\Plugin\CKEditor5Plugin\ImageResize
+    elements: false
+    conditions:
+      requiresConfiguration:
+        allow_resize: true
+      plugins: [ckeditor5_image]
+
 ckeditor5_imageCaption:
   ckeditor5:
     plugins:
diff --git a/core/modules/ckeditor5/config/schema/ckeditor5.schema.yml b/core/modules/ckeditor5/config/schema/ckeditor5.schema.yml
index 29171f9e36d9..09b81657591d 100644
--- a/core/modules/ckeditor5/config/schema/ckeditor5.schema.yml
+++ b/core/modules/ckeditor5/config/schema/ckeditor5.schema.yml
@@ -62,6 +62,17 @@ ckeditor5.plugin.ckeditor5_heading:
           Choice:
             callback: \Drupal\ckeditor5\Plugin\CKEditor5Plugin\Heading::validChoices
 
+# Plugin \Drupal\ckeditor5\Plugin\CKEditor5Plugin\ImageResize
+ckeditor5.plugin.ckeditor5_imageResize:
+  type: mapping
+  label: Image Resize
+  mapping:
+    allow_resize:
+      type: boolean
+      label: 'Allow resize'
+      constraints:
+        NotNull: []
+
 ckeditor5.plugin.ckeditor5_sourceEditing:
   type: mapping
   label: Source Editing
diff --git a/core/modules/ckeditor5/js/ckeditor5.es6.js b/core/modules/ckeditor5/js/ckeditor5.es6.js
index e6916bc443e4..3ced334c3ac1 100644
--- a/core/modules/ckeditor5/js/ckeditor5.es6.js
+++ b/core/modules/ckeditor5/js/ckeditor5.es6.js
@@ -97,6 +97,10 @@
 
     return Object.entries(config).reduce((processed, [key, value]) => {
       if (typeof value === 'object') {
+        // Check for null values.
+        if (!value) {
+          return processed;
+        }
         if (value.hasOwnProperty('func')) {
           processed[key] = buildFunc(value);
         } else if (value.hasOwnProperty('regexp')) {
diff --git a/core/modules/ckeditor5/js/ckeditor5.js b/core/modules/ckeditor5/js/ckeditor5.js
index 2cc8deea9686..6784c686a87c 100644
--- a/core/modules/ckeditor5/js/ckeditor5.js
+++ b/core/modules/ckeditor5/js/ckeditor5.js
@@ -60,6 +60,10 @@
 
     return Object.entries(config).reduce((processed, [key, value]) => {
       if (typeof value === 'object') {
+        if (!value) {
+          return processed;
+        }
+
         if (value.hasOwnProperty('func')) {
           processed[key] = buildFunc(value);
         } else if (value.hasOwnProperty('regexp')) {
diff --git a/core/modules/ckeditor5/src/Plugin/CKEditor5Plugin/ImageResize.php b/core/modules/ckeditor5/src/Plugin/CKEditor5Plugin/ImageResize.php
new file mode 100644
index 000000000000..353cb2cbacc3
--- /dev/null
+++ b/core/modules/ckeditor5/src/Plugin/CKEditor5Plugin/ImageResize.php
@@ -0,0 +1,58 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Drupal\ckeditor5\Plugin\CKEditor5Plugin;
+
+use Drupal\ckeditor5\Plugin\CKEditor5PluginConfigurableTrait;
+use Drupal\ckeditor5\Plugin\CKEditor5PluginDefault;
+use Drupal\ckeditor5\Plugin\CKEditor5PluginConfigurableInterface;
+use Drupal\Core\Form\FormStateInterface;
+
+/**
+ * CKEditor 5 ImageResize plugin.
+ *
+ * @internal
+ *   Plugin classes are internal.
+ */
+class ImageResize extends CKEditor5PluginDefault implements CKEditor5PluginConfigurableInterface {
+
+  use CKEditor5PluginConfigurableTrait;
+
+  /**
+   * {@inheritdoc}
+   */
+  public function defaultConfiguration() {
+    return ['allow_resize' => TRUE];
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function buildConfigurationForm(array $form, FormStateInterface $form_state) {
+    $form['allow_resize'] = [
+      '#type' => 'checkbox',
+      '#title' => $this->t('Allow the user to resize images'),
+      '#default_value' => $this->configuration['allow_resize'],
+    ];
+
+    return $form;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function validateConfigurationForm(array &$form, FormStateInterface $form_state) {
+    // Match the config schema structure at ckeditor5.plugin.ckeditor5_imageResize.
+    $form_value = $form_state->getValue('allow_resize');
+    $form_state->setValue('allow_resize', (bool) $form_value);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function submitConfigurationForm(array &$form, FormStateInterface $form_state) {
+    $this->configuration['allow_resize'] = $form_state->getValue('allow_resize');
+  }
+
+}
diff --git a/core/modules/ckeditor5/src/Plugin/CKEditor5PluginDefinition.php b/core/modules/ckeditor5/src/Plugin/CKEditor5PluginDefinition.php
index 871e3bef7444..0222d76f2f09 100644
--- a/core/modules/ckeditor5/src/Plugin/CKEditor5PluginDefinition.php
+++ b/core/modules/ckeditor5/src/Plugin/CKEditor5PluginDefinition.php
@@ -10,6 +10,7 @@
 use Drupal\Component\Plugin\Definition\PluginDefinitionInterface;
 use Drupal\Component\Plugin\Exception\InvalidPluginDefinitionException;
 use Drupal\Core\Config\Schema\SchemaCheckTrait;
+use Drupal\Core\Config\TypedConfigManagerInterface;
 use Drupal\Core\StringTranslation\TranslatableMarkup;
 
 /**
@@ -160,33 +161,14 @@ private function validateDrupalAspects(string $id, array $definition): void {
       $default_configuration = (new \ReflectionClass($definition['drupal']['class']))
         ->newInstanceWithoutConstructor()
         ->defaultConfiguration();
-      $typed_config = \Drupal::service('config.typed');
       if (!empty($default_configuration)) {
         $configuration_name = sprintf("ckeditor5.plugin.%s", $definition['id']);
-        if (!$typed_config->hasConfigSchema($configuration_name)) {
+        if (!$this->getTypedConfig()->hasConfigSchema($configuration_name)) {
           throw new InvalidPluginDefinitionException($id, sprintf('The "%s" CKEditor 5 plugin definition is configurable, has non-empty default configuration but has no config schema. Config schema is required for validation.', $id));
         }
-        // TRICKY: SchemaCheckTrait::checkConfigSchema() dynamically adds a
-        // 'langcode' key-value pair that is irrelevant here. Also,
-        // ::checkValue() may (counter to its docs) trigger an exception.
-        $this->configName = 'STRIP';
-        $this->schema = $typed_config->createFromNameAndData($configuration_name, $default_configuration);
-        $schema_errors = [];
-        foreach ($default_configuration as $key => $value) {
-          try {
-            $schema_error = $this->checkValue($key, $value);
-          }
-          catch (\InvalidArgumentException $e) {
-            $schema_error = [$key => $e->getMessage()];
-          }
-          $schema_errors = array_merge($schema_errors, $schema_error);
-        }
-        $formatted_schema_errors = [];
-        foreach ($schema_errors as $key => $value) {
-          $formatted_schema_errors[] = sprintf("[%s] %s", str_replace('STRIP:', '', $key), trim($value, '.'));
-        }
-        if (!empty($schema_errors)) {
-          throw new InvalidPluginDefinitionException($id, sprintf('The "%s" CKEditor 5 plugin definition is configurable, but its default configuration does not match its config schema. The following errors were found: %s.', $id, implode(', ', $formatted_schema_errors)));
+        $error_message = $this->validateConfiguration($default_configuration);
+        if ($error_message) {
+          throw new InvalidPluginDefinitionException($id, sprintf('The "%s" CKEditor 5 plugin definition is configurable, but its default configuration does not match its config schema. %s', $id, $error_message));
         }
       }
     }
@@ -204,6 +186,16 @@ private function validateDrupalAspects(string $id, array $definition): void {
         'filter' => function ($value): ?string {
           return is_string($value) ? NULL : 'A string corresponding to a filter plugin ID must be specified.';
         },
+        'requiresConfiguration' => function ($required_configuration, array $definition): ?string {
+          if (!is_array($required_configuration)) {
+            return 'An array structure matching the required configuration for this plugin must be specified.';
+          }
+          if (!in_array(CKEditor5PluginConfigurableInterface::class, class_implements($definition['drupal']['class'], TRUE))) {
+            return 'This condition type is only available for CKEditor 5 plugins implementing CKEditor5PluginConfigurableInterface.';
+          }
+          $error_message = $this->validateConfiguration($required_configuration);
+          return is_string($error_message) ? sprintf('The required configuration does not match its config schema. %s', $error_message) : NULL;
+        },
         'plugins' => function ($value): ?string {
           return is_array($value) && Inspector::assertAllStrings($value) ? NULL : 'A list of strings, each corresponding to a CKEditor 5 plugin ID must be specified.';
         },
@@ -213,7 +205,7 @@ private function validateDrupalAspects(string $id, array $definition): void {
         throw new InvalidPluginDefinitionException($id, sprintf('The "%s" CKEditor 5 plugin definition has a "drupal.conditions" value that contains some unsupported condition types: "%s". Only the following conditions types are supported: "%s".', $id, implode(', ', $unsupported_condition_types), implode('", "', array_keys($supported_condition_types))));
       }
       foreach ($definition['drupal']['conditions'] as $condition_type => $value) {
-        $assessment = $supported_condition_types[$condition_type]($value);
+        $assessment = $supported_condition_types[$condition_type]($value, $definition);
         if (is_string($assessment)) {
           throw new InvalidPluginDefinitionException($id, sprintf('The "%s" CKEditor 5 plugin definition has an invalid "drupal.conditions" item. "%s" is set to an invalid value. %s', $id, $condition_type, $assessment));
         }
@@ -228,6 +220,56 @@ private function validateDrupalAspects(string $id, array $definition): void {
     }
   }
 
+  /**
+   * Returns the typed configuration service.
+   *
+   * @return \Drupal\Core\Config\TypedConfigManagerInterface
+   *   The typed configuration service.
+   */
+  private function getTypedConfig(): TypedConfigManagerInterface {
+    return \Drupal::service('config.typed');
+  }
+
+  /**
+   * Validates the given configuration array.
+   *
+   * @param array $configuration
+   *   The configuration to validate.
+   *
+   * @return string|null
+   *   NULL if there are no validation errors, a string containing the schema
+   *   violation error messages otherwise.
+   */
+  private function validateConfiguration(array $configuration): ?string {
+    if (!isset($this->schema)) {
+      $configuration_name = sprintf("ckeditor5.plugin.%s", $this->id);
+      // TRICKY: SchemaCheckTrait::checkConfigSchema() dynamically adds a
+      // 'langcode' key-value pair that is irrelevant here. Also,
+      // ::checkValue() may (counter to its docs) trigger an exception.
+      $this->configName = 'STRIP';
+      $this->schema = $this->getTypedConfig()->createFromNameAndData($configuration_name, $configuration);
+    }
+
+    $schema_errors = [];
+    foreach ($configuration as $key => $value) {
+      try {
+        $schema_error = $this->checkValue($key, $value);
+      }
+      catch (\InvalidArgumentException $e) {
+        $schema_error = [$key => $e->getMessage()];
+      }
+      $schema_errors = array_merge($schema_errors, $schema_error);
+    }
+    $formatted_schema_errors = [];
+    foreach ($schema_errors as $key => $value) {
+      $formatted_schema_errors[] = sprintf("[%s] %s", str_replace('STRIP:', '', $key), trim($value, '.'));
+    }
+    if (!empty($formatted_schema_errors)) {
+      return sprintf('The following errors were found: %s.', implode(', ', $formatted_schema_errors));
+    }
+    return NULL;
+  }
+
   /**
    * {@inheritdoc}
    *
diff --git a/core/modules/ckeditor5/src/Plugin/CKEditor5PluginManager.php b/core/modules/ckeditor5/src/Plugin/CKEditor5PluginManager.php
index 05290ebd6ff3..295699ee420b 100644
--- a/core/modules/ckeditor5/src/Plugin/CKEditor5PluginManager.php
+++ b/core/modules/ckeditor5/src/Plugin/CKEditor5PluginManager.php
@@ -380,6 +380,10 @@ protected function isPluginDisabled(CKEditor5PluginInterface $plugin, EditorInte
           }
           break;
 
+        case 'requiresConfiguration':
+          $intersection = array_intersect($plugin->getConfiguration(), $required_value);
+          return $intersection !== $required_value;
+
         case 'plugins':
           // Tricky: this cannot yet be evaluated here. It will evaluated later.
           // @see \Drupal\ckeditor5\Plugin\CKEditor5PluginManager::getEnabledDefinitions()
diff --git a/core/modules/ckeditor5/src/Plugin/Editor/CKEditor5.php b/core/modules/ckeditor5/src/Plugin/Editor/CKEditor5.php
index d824ddf429cc..8286c734f477 100644
--- a/core/modules/ckeditor5/src/Plugin/Editor/CKEditor5.php
+++ b/core/modules/ckeditor5/src/Plugin/Editor/CKEditor5.php
@@ -384,12 +384,19 @@ private function shouldHaveVisiblePluginSettingsForm(CKEditor5PluginDefinition $
     // due to isEnabled() returning false, that should still have its config
     // form provided:
     // 1 - A conditionally enabled plugin that does not depend on a toolbar item
-    // to be active.
+    // to be active AND the plugins it depends on are enabled.
     // 2 - A conditionally enabled plugin that does depend on a toolbar item,
     // and that toolbar item is active.
     if ($definition->hasConditions()) {
       $conditions = $definition->getConditions();
       if (!array_key_exists('toolbarItem', $conditions)) {
+        // The CKEditor 5 plugins this plugin depends on must be enabled.
+        if (array_key_exists('plugins', $conditions)) {
+          $all_plugins = $this->ckeditor5PluginManager->getDefinitions();
+          $dependencies = array_intersect_key($all_plugins, array_flip($conditions['plugins']));
+          $unmet_dependencies = array_diff_key($dependencies, $enabled_plugins);
+          return empty($unmet_dependencies);
+        }
         return TRUE;
       }
       elseif (in_array($conditions['toolbarItem'], $editor->getSettings()['toolbar']['items'], TRUE)) {
@@ -681,11 +688,19 @@ protected function getEventualEditorWithPrimedFilterFormat(SubformStateInterface
     $pair = static::createEphemeralPairedEditor($submitted_editor, $submitted_filter_format);
 
     // When CKEditor 5 plugins are disabled in the form-based admin UI, the
-    // associated settings (if any) should be omitted too.
+    // associated settings (if any) should be omitted too, except for plugins
+    // that are enabled using `requiresConfiguration` (because whether they are
+    // enabled or not depends on the associated settings).
     $original_settings = $pair->getSettings();
     $enabled_plugins = $this->ckeditor5PluginManager->getEnabledDefinitions($pair);
+    $config_enabled_plugins = [];
+    foreach ($this->ckeditor5PluginManager->getDefinitions() as $id => $definition) {
+      if ($definition->hasConditions() && isset($definition->getConditions()['requiresConfiguration'])) {
+        $config_enabled_plugins[$id] = TRUE;
+      }
+    }
     $updated_settings = [
-      'plugins' => array_intersect_key($original_settings['plugins'], $enabled_plugins),
+      'plugins' => array_intersect_key($original_settings['plugins'], $enabled_plugins + $config_enabled_plugins),
     ] + $original_settings;
     $pair->setSettings($updated_settings);
 
diff --git a/core/modules/ckeditor5/tests/src/FunctionalJavascript/CKEditor5Test.php b/core/modules/ckeditor5/tests/src/FunctionalJavascript/CKEditor5Test.php
index f68274ddc67b..ea7183764ecc 100644
--- a/core/modules/ckeditor5/tests/src/FunctionalJavascript/CKEditor5Test.php
+++ b/core/modules/ckeditor5/tests/src/FunctionalJavascript/CKEditor5Test.php
@@ -85,7 +85,7 @@ public function testAttributeEncoding() {
         'toolbar' => [
           'items' => ['uploadImage'],
         ],
-        'plugins' => [],
+        'plugins' => ['ckeditor5_imageResize' => ['allow_resize' => FALSE]],
       ],
       'image_upload' => [
         'status' => TRUE,
@@ -109,7 +109,7 @@ function (ConstraintViolation $v) {
     $this->assertNotEmpty($image_upload_field = $page->find('css', '.ck-file-dialog-button input[type="file"]'));
     $image = $this->getTestFiles('image')[0];
     $image_upload_field->attachFile($this->container->get('file_system')->realpath($image->uri));
-    $assert_session->assertWaitOnAjaxRequest();
+    $assert_session->waitForElementVisible('css', '.ck-widget.image');
 
     $this->click('.ck-widget.image');
     $balloon_panel = $page->find('css', '.ck-balloon-panel');
diff --git a/core/modules/ckeditor5/tests/src/FunctionalJavascript/ImageTest.php b/core/modules/ckeditor5/tests/src/FunctionalJavascript/ImageTest.php
index 3865884b873a..deb8733b23ae 100644
--- a/core/modules/ckeditor5/tests/src/FunctionalJavascript/ImageTest.php
+++ b/core/modules/ckeditor5/tests/src/FunctionalJavascript/ImageTest.php
@@ -5,18 +5,19 @@
 use Drupal\editor\Entity\Editor;
 use Drupal\file\Entity\File;
 use Drupal\filter\Entity\FilterFormat;
-use Drupal\FunctionalJavascriptTests\WebDriverTestBase;
 use Drupal\Tests\TestFileCreationTrait;
 use Drupal\Tests\ckeditor5\Traits\CKEditor5TestTrait;
 use Drupal\ckeditor5\Plugin\Editor\CKEditor5;
 use Symfony\Component\Validator\ConstraintViolation;
 
+// cspell:ignore imageresize imageupload
+
 /**
  * @coversDefaultClass \Drupal\ckeditor5\Plugin\CKEditor5Plugin\ImageUpload
  * @group ckeditor5
  * @internal
  */
-class ImageTest extends WebDriverTestBase {
+class ImageTest extends CKEditor5TestBase {
 
   use CKEditor5TestTrait;
   use TestFileCreationTrait;
@@ -91,6 +92,9 @@ protected function setUp(): void {
           'ckeditor5_sourceEditing' => [
             'allowed_tags' => [],
           ],
+          'ckeditor5_imageResize' => [
+            'allow_resize' => TRUE,
+          ],
         ],
       ],
       'image_upload' => [
@@ -113,6 +117,7 @@ function (ConstraintViolation $v) {
     $this->adminUser = $this->drupalCreateUser([
       'use text format test_format',
       'bypass node access',
+      'administer filters',
     ]);
 
     // Create a sample host entity to embed images in.
@@ -120,9 +125,8 @@ function (ConstraintViolation $v) {
       'uri' => $this->getTestFiles('image')[0]->uri,
     ]);
     $this->file->save();
-    $this->drupalCreateContentType(['type' => 'blog']);
     $this->host = $this->createNode([
-      'type' => 'blog',
+      'type' => 'page',
       'title' => 'Animals with strange names',
       'body' => [
         'value' => '<p>The pirate is irate.</p>',
@@ -327,7 +331,6 @@ public function testWidth(string $width): void {
     $this->assertNotEmpty($image_upload_field = $page->find('css', '.ck-file-dialog-button input[type="file"]'));
     $image = $this->getTestFiles('image')[0];
     $image_upload_field->attachFile($this->container->get('file_system')->realpath($image->uri));
-    $assert_session->assertWaitOnAjaxRequest();
     $this->assertNotEmpty($assert_session->waitForElementVisible('css', 'figure.image'));
 
     // Edit the source of the image through the UI.
@@ -370,4 +373,87 @@ public function providerWidth(): array {
     ];
   }
 
+  /**
+   * Tests the image resize plugin.
+   *
+   * Confirms that enabling the resize plugin introduces the resize class to
+   * images within CKEditor 5.
+   *
+   * @param bool $is_resize_enabled
+   *   Boolean flag to test enabled or disabled.
+   *
+   * @dataProvider providerResize
+   */
+  public function testResize(bool $is_resize_enabled): void {
+    // Disable resize plugin because it is enabled by default.
+    if (!$is_resize_enabled) {
+      Editor::load('test_format')->setSettings([
+        'toolbar' => [
+          'items' => [
+            'uploadImage',
+          ],
+        ],
+        'plugins' => [
+          'ckeditor5_imageResize' => [
+            'allow_resize' => FALSE,
+          ],
+        ],
+      ])->save();
+    }
+
+    $page = $this->getSession()->getPage();
+    $assert_session = $this->assertSession();
+    $this->drupalGet('node/add');
+    $page->fillField('title[0][value]', 'My test content');
+    $this->assertNotEmpty($image_upload_field = $page->find('css', '.ck-file-dialog-button input[type="file"]'));
+    $image = $this->getTestFiles('image')[0];
+    $image_upload_field->attachFile($this->container->get('file_system')->realpath($image->uri));
+    $image_figure = $assert_session->waitForElementVisible('css', 'figure');
+    $this->assertSame($is_resize_enabled, $image_figure->hasClass('ck-widget_with-resizer'));
+  }
+
+  /**
+   * Data provider for ::testResize().
+   *
+   * @return array
+   *   The test cases.
+   */
+  public function providerResize(): array {
+    return [
+      'Image resize is enabled' => [
+        'is_resize_enabled' => TRUE,
+      ],
+      'Image resize is disabled' => [
+        'is_resize_enabled' => FALSE,
+      ],
+    ];
+  }
+
+  /**
+   * Tests the ckeditor5_imageResize and ckeditor5_imageUpload settings forms.
+   */
+  public function testImageSettingsForm() {
+    $assert_session = $this->assertSession();
+
+    $this->drupalGet('admin/config/content/formats/manage/test_format');
+
+    // The image resize and upload plugin settings forms should be present.
+    $assert_session->elementExists('css', '[data-drupal-selector="edit-editor-settings-plugins-ckeditor5-imageresize"]');
+    $assert_session->elementExists('css', '[data-drupal-selector="edit-editor-settings-plugins-ckeditor5-imageupload"]');
+
+    // Removing the imageUpload button from the toolbar must remove the plugin
+    // settings forms too.
+    $this->triggerKeyUp('.ckeditor5-toolbar-item-uploadImage', 'ArrowUp');
+    $assert_session->assertWaitOnAjaxRequest();
+    $assert_session->elementNotExists('css', '[data-drupal-selector="edit-editor-settings-plugins-ckeditor5-imageresize"]');
+    $assert_session->elementNotExists('css', '[data-drupal-selector="edit-editor-settings-plugins-ckeditor5-imageupload"]');
+
+    // Re-adding the imageUpload button to the toolbar must re-add the plugin
+    // settings forms too.
+    $this->triggerKeyUp('.ckeditor5-toolbar-item-uploadImage', 'ArrowDown');
+    $assert_session->assertWaitOnAjaxRequest();
+    $assert_session->elementExists('css', '[data-drupal-selector="edit-editor-settings-plugins-ckeditor5-imageresize"]');
+    $assert_session->elementExists('css', '[data-drupal-selector="edit-editor-settings-plugins-ckeditor5-imageupload"]');
+  }
+
 }
diff --git a/core/modules/ckeditor5/tests/src/Kernel/CKEditor5PluginManagerTest.php b/core/modules/ckeditor5/tests/src/Kernel/CKEditor5PluginManagerTest.php
index 9c3b0d45dfff..e912ab75e480 100644
--- a/core/modules/ckeditor5/tests/src/Kernel/CKEditor5PluginManagerTest.php
+++ b/core/modules/ckeditor5/tests/src/Kernel/CKEditor5PluginManagerTest.php
@@ -352,7 +352,7 @@ public function providerTestInvalidPluginDefinitions(): \Generator {
     conditions:
       foo: bar
 YAML,
-      'The "ckeditor5_invalid_plugin_foo_bar" CKEditor 5 plugin definition has a "drupal.conditions" value that contains some unsupported condition types: "foo". Only the following conditions types are supported: "toolbarItem", "imageUploadStatus", "filter", "plugins".',
+      'The "ckeditor5_invalid_plugin_foo_bar" CKEditor 5 plugin definition has a "drupal.conditions" value that contains some unsupported condition types: "foo". Only the following conditions types are supported: "toolbarItem", "imageUploadStatus", "filter", "requiresConfiguration", "plugins".',
     ];
     yield 'invalid condition: toolbarItem' => [
       <<<YAML
@@ -833,6 +833,139 @@ public function buildConfigurationForm(array $form, FormStateInterface $form_sta
   public function validateConfigurationForm(array &$form, FormStateInterface $form_state) {}
   public function submitConfigurationForm(array &$form, FormStateInterface $form_state) {}
 }
+PHP,
+            ],
+          ],
+        ],
+      ],
+    ];
+
+    yield 'invalid condition: requiresConfiguration not specifying a configuration array' => [
+      <<<YAML
+ckeditor5_invalid_plugin_foo_bar:
+  ckeditor5:
+    plugins: {}
+  drupal:
+    label: "Foo bar"
+    elements: false
+    conditions:
+      requiresConfiguration: true
+YAML,
+      'The "ckeditor5_invalid_plugin_foo_bar" CKEditor 5 plugin definition has an invalid "drupal.conditions" item. "requiresConfiguration" is set to an invalid value. An array structure matching the required configuration for this plugin must be specified.',
+    ];
+
+    yield 'invalid condition: requiresConfiguration without configurable plugin' => [
+      <<<YAML
+ckeditor5_invalid_plugin_foo_bar:
+  ckeditor5:
+    plugins: {}
+  drupal:
+    label: "Foo bar"
+    elements: false
+    conditions:
+      requiresConfiguration:
+        allow_resize: true
+YAML,
+      'The "ckeditor5_invalid_plugin_foo_bar" CKEditor 5 plugin definition has an invalid "drupal.conditions" item. "requiresConfiguration" is set to an invalid value. This condition type is only available for CKEditor 5 plugins implementing CKEditor5PluginConfigurableInterface.',
+    ];
+
+    yield 'invalid condition: requiresConfiguration with configurable plugin but required configuration does not match config schema' => [
+      <<<YAML
+ckeditor5_invalid_plugin_foo_bar:
+  ckeditor5:
+    plugins: {}
+  drupal:
+    class: Drupal\ckeditor5_invalid_plugin\Plugin\CKEditor5Plugin\FooBar
+    label: "Foo bar"
+    elements: false
+    conditions:
+      requiresConfiguration:
+        allow_resize: true
+YAML,
+      'The "ckeditor5_invalid_plugin_foo_bar" CKEditor 5 plugin definition has an invalid "drupal.conditions" item. "requiresConfiguration" is set to an invalid value. The required configuration does not match its config schema. The following errors were found: [allow_resize] The configuration property allow_resize doesn\'t exist.',
+      [
+        'config' => [
+          'schema' => [
+            'ckeditor5_invalid_plugin.schema.yml' => <<<YAML
+ckeditor5.plugin.ckeditor5_invalid_plugin_foo_bar:
+  type: mapping
+  label: 'Foo Bar'
+  mapping:
+    foo:
+      type: boolean
+      label: 'Foo'
+YAML,
+          ],
+        ],
+        'src' => [
+          'Plugin' => [
+            'CKEditor5Plugin' => [
+              'FooBar.php' => <<<'PHP'
+<?php
+namespace Drupal\ckeditor5_invalid_plugin\Plugin\CKEditor5Plugin;
+use Drupal\ckeditor5\Plugin\CKEditor5PluginDefault;
+use Drupal\ckeditor5\Plugin\CKEditor5PluginConfigurableInterface;
+use Drupal\ckeditor5\Plugin\CKEditor5PluginConfigurableTrait;
+use Drupal\Core\Form\FormStateInterface;
+class FooBar extends CKEditor5PluginDefault implements CKEditor5PluginConfigurableInterface {
+  use CKEditor5PluginConfigurableTrait;
+  public function defaultConfiguration() { return ['foo' => FALSE]; }
+  public function buildConfigurationForm(array $form, FormStateInterface $form_state) { return []; }
+  public function validateConfigurationForm(array &$form, FormStateInterface $form_state) {}
+  public function submitConfigurationForm(array &$form, FormStateInterface $form_state) {}
+}
+PHP,
+            ],
+          ],
+        ],
+      ],
+    ];
+
+    yield 'valid condition: requiresConfiguration' => [
+      <<<YAML
+ckeditor5_invalid_plugin_foo_bar:
+  ckeditor5:
+    plugins: {}
+  drupal:
+    class: Drupal\ckeditor5_invalid_plugin\Plugin\CKEditor5Plugin\FooBar
+    label: "Foo bar"
+    elements: false
+    conditions:
+      requiresConfiguration:
+        foo: true
+YAML,
+      NULL,
+      [
+        'config' => [
+          'schema' => [
+            'ckeditor5_invalid_plugin.schema.yml' => <<<YAML
+ckeditor5.plugin.ckeditor5_invalid_plugin_foo_bar:
+  type: mapping
+  label: 'Foo Bar'
+  mapping:
+    foo:
+      type: boolean
+      label: 'Foo'
+YAML,
+          ],
+        ],
+        'src' => [
+          'Plugin' => [
+            'CKEditor5Plugin' => [
+              'FooBar.php' => <<<'PHP'
+<?php
+namespace Drupal\ckeditor5_invalid_plugin\Plugin\CKEditor5Plugin;
+use Drupal\ckeditor5\Plugin\CKEditor5PluginDefault;
+use Drupal\ckeditor5\Plugin\CKEditor5PluginConfigurableInterface;
+use Drupal\ckeditor5\Plugin\CKEditor5PluginConfigurableTrait;
+use Drupal\Core\Form\FormStateInterface;
+class FooBar extends CKEditor5PluginDefault implements CKEditor5PluginConfigurableInterface {
+  use CKEditor5PluginConfigurableTrait;
+  public function defaultConfiguration() { return ['foo' => FALSE]; }
+  public function buildConfigurationForm(array $form, FormStateInterface $form_state) { return []; }
+  public function validateConfigurationForm(array &$form, FormStateInterface $form_state) {}
+  public function submitConfigurationForm(array &$form, FormStateInterface $form_state) {}
+}
 PHP,
             ],
           ],
@@ -1060,6 +1193,12 @@ public function providerTestProvidedElements(): array {
         'expected_elements' => [],
         'expected_readable_string' => '',
       ],
+      'imageResize' => [
+        'plugins' => ['ckeditor5_imageResize'],
+        'text_editor_settings' => [],
+        'expected_elements' => [],
+        'expected_readable_string' => '',
+      ],
       'language' => [
         'plugins' => ['ckeditor5_language'],
         'text_editor_settings' => [],
diff --git a/core/modules/ckeditor5/tests/src/Kernel/ConfigurablePluginTest.php b/core/modules/ckeditor5/tests/src/Kernel/ConfigurablePluginTest.php
index 9a598b165804..d444f726b500 100644
--- a/core/modules/ckeditor5/tests/src/Kernel/ConfigurablePluginTest.php
+++ b/core/modules/ckeditor5/tests/src/Kernel/ConfigurablePluginTest.php
@@ -67,6 +67,9 @@ public function testDefaults() {
       'ckeditor5_sourceEditing' => [
         'allowed_tags' => [],
       ],
+      'ckeditor5_imageResize' => [
+        'allow_resize' => TRUE,
+      ],
       'ckeditor5_language' => [
         'language_list' => 'un',
       ],
diff --git a/core/modules/ckeditor5/tests/src/Kernel/SmartDefaultSettingsTest.php b/core/modules/ckeditor5/tests/src/Kernel/SmartDefaultSettingsTest.php
index a2ff35e0efc9..0f6eb4a625c9 100644
--- a/core/modules/ckeditor5/tests/src/Kernel/SmartDefaultSettingsTest.php
+++ b/core/modules/ckeditor5/tests/src/Kernel/SmartDefaultSettingsTest.php
@@ -399,6 +399,9 @@ public function provider() {
               'heading6',
             ],
           ],
+          'ckeditor5_imageResize' => [
+            'allow_resize' => TRUE,
+          ],
           'ckeditor5_language' => [
             'language_list' => 'un',
           ],
@@ -480,6 +483,7 @@ public function provider() {
               'heading5',
             ],
           ],
+          'ckeditor5_imageResize' => ['allow_resize' => TRUE],
           'ckeditor5_language' => $basic_html_test_case['expected_ckeditor5_settings']['plugins']['ckeditor5_language'],
         ],
       ],
@@ -677,6 +681,9 @@ public function provider() {
               'heading6',
             ],
           ],
+          'ckeditor5_imageResize' => [
+            'allow_resize' => TRUE,
+          ],
           'ckeditor5_sourceEditing' => [
             'allowed_tags' => [],
           ],
diff --git a/core/modules/ckeditor5/tests/src/Kernel/ValidatorsTest.php b/core/modules/ckeditor5/tests/src/Kernel/ValidatorsTest.php
index 48093d6d7701..b879e408e682 100644
--- a/core/modules/ckeditor5/tests/src/Kernel/ValidatorsTest.php
+++ b/core/modules/ckeditor5/tests/src/Kernel/ValidatorsTest.php
@@ -755,7 +755,11 @@ public function providerPair(): array {
             'uploadImage',
           ],
         ],
-        'plugins' => [],
+        'plugins' => [
+          'ckeditor5_imageResize' => [
+            'allow_resize' => FALSE,
+          ],
+        ],
       ],
       'image_upload' => [
         'status' => TRUE,
-- 
GitLab