From 83874f2bcf32414048cc09df1571169ac645659b Mon Sep 17 00:00:00 2001 From: catch <catch@35733.no-reply.drupal.org> Date: Fri, 1 Mar 2024 14:02:18 +0000 Subject: [PATCH] Issue #3412361 by Wim Leers, phenaproxima, catch, effulgentsia: Mark Editor config schema as fully validatable --- .../Core/Config/Schema/SchemaCheckTrait.php | 7 + .../Controller/CKEditor5ImageController.php | 4 +- .../src/Plugin/CKEditor5Plugin/Image.php | 17 +- .../ckeditor5/src/Plugin/Editor/CKEditor5.php | 14 ++ ...extEditorObjectDependentValidatorTrait.php | 7 +- .../src/Functional/ImageUploadAccessTest.php | 19 +-- .../CKEditor5UpdateImageToolbarItemTest.php | 9 +- .../src/FunctionalJavascript/AdminUiTest.php | 49 ++++++ .../FunctionalJavascript/CKEditor5Test.php | 5 + .../src/Kernel/CKEditor5PluginManagerTest.php | 5 +- .../tests/src/Kernel/ValidatorsTest.php | 23 ++- .../editor/config/schema/editor.schema.yml | 96 ++++++++--- core/modules/editor/editor.module | 48 ++++++ core/modules/editor/editor.post_update.php | 23 +++ core/modules/editor/src/Entity/Editor.php | 15 ++ .../editor/src/Form/EditorImageDialog.php | 21 +-- .../tests/fixtures/update/editor-3412361.php | 41 +++++ .../update/editor.editor.umami_basic_html.yml | 64 +++++++ .../update/filter.format.umami_basic_html.yml | 55 ++++++ .../src/Functional/EditorDialogAccessTest.php | 19 ++- .../Rest/EditorResourceTestBase.php | 6 +- ...rSanitizeImageUploadSettingsUpdateTest.php | 104 ++++++++++++ .../src/Kernel/EditorImageDialogTest.php | 4 + .../tests/src/Kernel/EditorValidationTest.php | 157 +++++++++++++++++- .../tests/src/Functional/EditorTest.php | 8 +- .../install/editor.editor.basic_html.yml | 6 - .../install/editor.editor.full_html.yml | 2 +- .../install/editor.editor.basic_html.yml | 6 +- .../install/editor.editor.full_html.yml | 6 +- .../Tests/Component/Utility/BytesTest.php | 1 + 30 files changed, 751 insertions(+), 90 deletions(-) create mode 100644 core/modules/editor/tests/fixtures/update/editor-3412361.php create mode 100644 core/modules/editor/tests/fixtures/update/editor.editor.umami_basic_html.yml create mode 100644 core/modules/editor/tests/fixtures/update/filter.format.umami_basic_html.yml create mode 100644 core/modules/editor/tests/src/Functional/Update/EditorSanitizeImageUploadSettingsUpdateTest.php diff --git a/core/lib/Drupal/Core/Config/Schema/SchemaCheckTrait.php b/core/lib/Drupal/Core/Config/Schema/SchemaCheckTrait.php index 88682009ae22..fbc1edec5d85 100644 --- a/core/lib/Drupal/Core/Config/Schema/SchemaCheckTrait.php +++ b/core/lib/Drupal/Core/Config/Schema/SchemaCheckTrait.php @@ -52,6 +52,13 @@ trait SchemaCheckTrait { 'This value should not be blank.', ], ], + 'editor.editor.*' => [ + // @todo Fix stream wrappers not being available early enough in + // https://www.drupal.org/project/drupal/issues/3416735 + 'image_upload.scheme' => [ + '^The file storage you selected is not a visible, readable and writable stream wrapper\. Possible choices: <em class="placeholder"><\/em>\.$', + ], + ], ]; /** diff --git a/core/modules/ckeditor5/src/Controller/CKEditor5ImageController.php b/core/modules/ckeditor5/src/Controller/CKEditor5ImageController.php index 1a9cc15d1fb7..43f4d9b44cfc 100644 --- a/core/modules/ckeditor5/src/Controller/CKEditor5ImageController.php +++ b/core/modules/ckeditor5/src/Controller/CKEditor5ImageController.php @@ -175,7 +175,9 @@ public function upload(Request $request): Response { * Gets the image upload validators. */ protected function getImageUploadValidators(array $settings): array { - $max_filesize = min(Bytes::toNumber($settings['max_size']), Environment::getUploadMaxSize()); + $max_filesize = $settings['max_size'] + ? Bytes::toNumber($settings['max_size']) + : Environment::getUploadMaxSize(); $max_dimensions = 0; if (!empty($settings['max_dimensions']['width']) || !empty($settings['max_dimensions']['height'])) { $max_dimensions = $settings['max_dimensions']['width'] . 'x' . $settings['max_dimensions']['height']; diff --git a/core/modules/ckeditor5/src/Plugin/CKEditor5Plugin/Image.php b/core/modules/ckeditor5/src/Plugin/CKEditor5Plugin/Image.php index d897abd265f2..c243eaec6979 100644 --- a/core/modules/ckeditor5/src/Plugin/CKEditor5Plugin/Image.php +++ b/core/modules/ckeditor5/src/Plugin/CKEditor5Plugin/Image.php @@ -63,16 +63,27 @@ public function buildConfigurationForm(array $form, FormStateInterface $form_sta */ public function validateConfigurationForm(array &$form, FormStateInterface $form_state) { $form_state->setValue('status', (bool) $form_state->getValue('status')); - $form_state->setValue(['max_dimensions', 'width'], (int) $form_state->getValue(['max_dimensions', 'width'])); - $form_state->setValue(['max_dimensions', 'height'], (int) $form_state->getValue(['max_dimensions', 'height'])); + $directory = $form_state->getValue(['directory']); + $form_state->setValue(['directory'], trim($directory) === '' ? NULL : $directory); + $max_size = $form_state->getValue(['max_size']); + $form_state->setValue(['max_size'], trim($max_size) === '' ? NULL : $max_size); + $max_width = $form_state->getValue(['max_dimensions', 'width']); + $form_state->setValue(['max_dimensions', 'width'], trim($max_width) === '' ? NULL : (int) $max_width); + $max_height = $form_state->getValue(['max_dimensions', 'height']); + $form_state->setValue(['max_dimensions', 'height'], trim($max_height) === '' ? NULL : (int) $max_height); } /** * {@inheritdoc} */ public function submitConfigurationForm(array &$form, FormStateInterface $form_state) { + $settings = $form_state->getValues(); + if (!$settings['status']) { + // Remove all other settings to comply with config schema. + $settings = ['status' => FALSE]; + } // Store this configuration in its out-of-band location. - $form_state->get('editor')->setImageUploadSettings($form_state->getValues()); + $form_state->get('editor')->setImageUploadSettings($settings); } /** diff --git a/core/modules/ckeditor5/src/Plugin/Editor/CKEditor5.php b/core/modules/ckeditor5/src/Plugin/Editor/CKEditor5.php index b12841651f81..87d46bcc5e32 100644 --- a/core/modules/ckeditor5/src/Plugin/Editor/CKEditor5.php +++ b/core/modules/ckeditor5/src/Plugin/Editor/CKEditor5.php @@ -660,7 +660,13 @@ public function validateConfigurationForm(array &$form, FormStateInterface $form // All plugin settings have been collected, including defaults that depend // on visibility. Store the collected settings, throw away the interim state // that allowed determining which defaults to add. + // Create a new clone, because the plugins whose data is being stored + // out-of-band may have modified the Text Editor config entity in the form + // state. + // @see \Drupal\editor\EditorInterface::setImageUploadSettings() + // @see \Drupal\ckeditor5\Plugin\CKEditor5Plugin\Image::submitConfigurationForm() unset($eventual_editor_and_format_for_plugin_settings_visibility); + $submitted_editor = clone $form_state->get('editor'); $submitted_editor->setSettings($settings); // Validate the text editor + text format pair. @@ -903,6 +909,14 @@ protected static function mapPairViolationPropertyPathsToFormNames(string $prope return implode('][', array_merge(explode('.', $property_path), ['settings'])); } + // Image upload settings are stored out-of-band and may also trigger + // validation errors. + // @see \Drupal\ckeditor5\Plugin\CKEditor5Plugin\Image + if (str_starts_with($property_path, 'image_upload.')) { + $image_upload_setting_property_path = str_replace('image_upload.', '', $property_path); + return 'editor][settings][plugins][ckeditor5_image][' . implode('][', explode('.', $image_upload_setting_property_path)); + } + // Everything else is in the subform. return 'editor][' . static::mapViolationPropertyPathsToFormNames($property_path, $form); } diff --git a/core/modules/ckeditor5/src/Plugin/Validation/Constraint/TextEditorObjectDependentValidatorTrait.php b/core/modules/ckeditor5/src/Plugin/Validation/Constraint/TextEditorObjectDependentValidatorTrait.php index 63bf2f0dad60..9b334f742baa 100644 --- a/core/modules/ckeditor5/src/Plugin/Validation/Constraint/TextEditorObjectDependentValidatorTrait.php +++ b/core/modules/ckeditor5/src/Plugin/Validation/Constraint/TextEditorObjectDependentValidatorTrait.php @@ -29,8 +29,13 @@ private function createTextEditorObjectFromContext(): EditorInterface { ]); } else { - assert($this->context->getRoot()->getDataDefinition()->getDataType() === 'editor.editor.*'); + assert(in_array($this->context->getRoot()->getDataDefinition()->getDataType(), ['editor.editor.*', 'entity:editor'], TRUE)); $text_format = FilterFormat::load($this->context->getRoot()->get('format')->getValue()); + // This validator must not complain about a missing text format. + // @see \Drupal\Tests\editor\Kernel\EditorValidationTest::testInvalidFormat() + if ($text_format === NULL) { + $text_format = FilterFormat::create([]); + } } assert($text_format instanceof FilterFormatInterface); diff --git a/core/modules/ckeditor5/tests/src/Functional/ImageUploadAccessTest.php b/core/modules/ckeditor5/tests/src/Functional/ImageUploadAccessTest.php index de8bb0e6578f..5b224460e248 100644 --- a/core/modules/ckeditor5/tests/src/Functional/ImageUploadAccessTest.php +++ b/core/modules/ckeditor5/tests/src/Functional/ImageUploadAccessTest.php @@ -27,8 +27,14 @@ public function testCkeditor5ImageUploadRoute() { $response = $this->uploadRequest($url, $test_image, 'test.jpg'); $this->assertSame(404, $response->getStatusCode()); - $editor = $this->createEditorWithUpload([ - 'status' => FALSE, + $editor = $this->createEditorWithUpload(['status' => FALSE]); + + // Ensure that images cannot be uploaded when image upload is disabled. + $response = $this->uploadRequest($url, $test_image, 'test.jpg'); + $this->assertSame(403, $response->getStatusCode()); + + $editor->setImageUploadSettings([ + 'status' => TRUE, 'scheme' => 'public', 'directory' => 'inline-images', 'max_size' => '', @@ -36,14 +42,7 @@ public function testCkeditor5ImageUploadRoute() { 'width' => 0, 'height' => 0, ], - ]); - - // Ensure that images cannot be uploaded when image upload is disabled. - $response = $this->uploadRequest($url, $test_image, 'test.jpg'); - $this->assertSame(403, $response->getStatusCode()); - - $editor->setImageUploadSettings(['status' => TRUE] + $editor->getImageUploadSettings()) - ->save(); + ])->save(); $response = $this->uploadRequest($url, $test_image, 'test.jpg'); $this->assertSame(201, $response->getStatusCode()); diff --git a/core/modules/ckeditor5/tests/src/Functional/Update/CKEditor5UpdateImageToolbarItemTest.php b/core/modules/ckeditor5/tests/src/Functional/Update/CKEditor5UpdateImageToolbarItemTest.php index 483ebd6bf2bb..82379a24d877 100644 --- a/core/modules/ckeditor5/tests/src/Functional/Update/CKEditor5UpdateImageToolbarItemTest.php +++ b/core/modules/ckeditor5/tests/src/Functional/Update/CKEditor5UpdateImageToolbarItemTest.php @@ -138,7 +138,14 @@ public function test(bool $filter_html_is_enabled, bool $image_uploads_are_enabl function (ConstraintViolation $v) { return (string) $v->getMessage(); }, - iterator_to_array(CKEditor5::validatePair($editor_after, $filter_format_after)) + // @todo Fix stream wrappers not being available early enough in + // https://www.drupal.org/project/drupal/issues/3416735. Then remove the + // array_filter(). + // @see \Drupal\Core\Config\Schema\SchemaCheckTrait::$ignoredPropertyPaths + array_filter( + iterator_to_array(CKEditor5::validatePair($editor_after, $filter_format_after)), + fn(ConstraintViolation $v) => $v->getMessage() != 'The file storage you selected is not a visible, readable and writable stream wrapper. Possible choices: <em class="placeholder"></em>.', + ) )); } diff --git a/core/modules/ckeditor5/tests/src/FunctionalJavascript/AdminUiTest.php b/core/modules/ckeditor5/tests/src/FunctionalJavascript/AdminUiTest.php index c0b7a793194a..3bbf63a83090 100644 --- a/core/modules/ckeditor5/tests/src/FunctionalJavascript/AdminUiTest.php +++ b/core/modules/ckeditor5/tests/src/FunctionalJavascript/AdminUiTest.php @@ -177,6 +177,55 @@ public function testFilterCheckboxesToggleSettings() { $this->assertFalse($media_tab->isVisible(), 'Media settings should be removed when media filter disabled'); } + /** + * Tests that image upload settings (stored out of band) are validated too. + */ + public function testImageUploadSettingsAreValidated(): void { + $page = $this->getSession()->getPage(); + $assert_session = $this->assertSession(); + + $this->addNewTextFormat($page, $assert_session); + $this->drupalGet('admin/config/content/formats/manage/ckeditor5'); + + // Add the image plugin to the CKEditor 5 toolbar. + $this->assertNotEmpty($assert_session->waitForElement('css', '.ckeditor5-toolbar-item-drupalInsertImage')); + $this->triggerKeyUp('.ckeditor5-toolbar-item-drupalInsertImage', 'ArrowDown'); + $assert_session->assertExpectedAjaxRequest(1); + + // Open the vertical tab with its settings. + $page->find('css', '[href^="#edit-editor-settings-plugins-ckeditor5-image"]')->click(); + $this->assertTrue($assert_session->waitForText('Enable image uploads')); + + // Check the "Enable image uploads" checkbox. + $assert_session->checkboxNotChecked('editor[settings][plugins][ckeditor5_image][status]'); + $page->checkField('editor[settings][plugins][ckeditor5_image][status]'); + $assert_session->assertExpectedAjaxRequest(2); + + // Enter a nonsensical maximum file size. + $page->fillField('editor[settings][plugins][ckeditor5_image][max_size]', 'foobar'); + $this->assertNoRealtimeValidationErrors(); + + // Enable another toolbar item to trigger validation. + $this->triggerKeyUp('.ckeditor5-toolbar-item-sourceEditing', 'ArrowDown'); + $assert_session->assertExpectedAjaxRequest(3); + + // The expected validation error must be present. + $assert_session->elementExists('css', '[role=alert]:contains("This value must be a number of bytes, optionally with a unit such as "MB" or "megabytes".")'); + + // Enter no maximum file size because it is optional, this should result in + // no validation error and it being set to `null`. + $page->findField('editor[settings][plugins][ckeditor5_image][max_size]')->setValue(''); + + // Remove a toolbar item to trigger validation. + $this->triggerKeyUp('.ckeditor5-toolbar-item-sourceEditing', 'ArrowUp'); + $assert_session->assertExpectedAjaxRequest(4); + + // No more validation errors, let's save. + $this->assertNoRealtimeValidationErrors(); + $page->pressButton('Save configuration'); + $assert_session->pageTextContains('The text format ckeditor5 has been updated'); + } + /** * Ensure CKEditor 5 admin UI's real-time validation errors do not accumulate. */ diff --git a/core/modules/ckeditor5/tests/src/FunctionalJavascript/CKEditor5Test.php b/core/modules/ckeditor5/tests/src/FunctionalJavascript/CKEditor5Test.php index edc3b94576dc..3e17c31e00fa 100644 --- a/core/modules/ckeditor5/tests/src/FunctionalJavascript/CKEditor5Test.php +++ b/core/modules/ckeditor5/tests/src/FunctionalJavascript/CKEditor5Test.php @@ -95,6 +95,10 @@ public function testAttributeEncoding() { 'scheme' => 'public', 'directory' => 'inline-images', 'max_size' => '', + 'max_dimensions' => [ + 'width' => NULL, + 'height' => NULL, + ], ], ])->save(); $this->assertSame([], array_map( @@ -643,6 +647,7 @@ public function testListPlugin() { 'reversed' => FALSE, 'startIndex' => FALSE, ], + 'multiBlock' => TRUE, ], 'ckeditor5_sourceEditing' => [ 'allowed_tags' => [], diff --git a/core/modules/ckeditor5/tests/src/Kernel/CKEditor5PluginManagerTest.php b/core/modules/ckeditor5/tests/src/Kernel/CKEditor5PluginManagerTest.php index 1ae537c185b4..66a78c4f7cd5 100644 --- a/core/modules/ckeditor5/tests/src/Kernel/CKEditor5PluginManagerTest.php +++ b/core/modules/ckeditor5/tests/src/Kernel/CKEditor5PluginManagerTest.php @@ -1030,7 +1030,6 @@ public function testProvidedElementsInvalidElementSubset(array $configured_subse $sneaky_plugin_id => ['configured_subset' => $configured_subset], ], ], - 'image_upload' => [], ]); // Invalid subsets are allowed on unsaved Text Editor config entities, @@ -1257,7 +1256,9 @@ public function testProvidedElements(array $plugins, array $text_editor_settings 'format' => 'dummy', 'editor' => 'ckeditor5', 'settings' => $text_editor_settings, - 'image_upload' => [], + 'image_upload' => [ + 'status' => FALSE, + ], ]); FilterFormat::create([ 'format' => 'dummy', diff --git a/core/modules/ckeditor5/tests/src/Kernel/ValidatorsTest.php b/core/modules/ckeditor5/tests/src/Kernel/ValidatorsTest.php index bb18c3cef653..dac6a4095107 100644 --- a/core/modules/ckeditor5/tests/src/Kernel/ValidatorsTest.php +++ b/core/modules/ckeditor5/tests/src/Kernel/ValidatorsTest.php @@ -86,7 +86,9 @@ public function test(array $ckeditor5_settings, array $expected_violations) { 'format' => 'dummy', 'editor' => 'ckeditor5', 'settings' => $ckeditor5_settings, - 'image_upload' => [], + 'image_upload' => [ + 'status' => FALSE, + ], ]); $typed_config = $this->typedConfig->createFromNameAndData( @@ -182,7 +184,10 @@ public static function provider(): array { ], ], 'violations' => [ - 'settings.plugins.ckeditor5_language' => 'Configuration for the enabled plugin "<em class="placeholder">Language</em>" (<em class="placeholder">ckeditor5_language</em>) is missing.', + 'settings.plugins.ckeditor5_language' => [ + 'Configuration for the enabled plugin "<em class="placeholder">Language</em>" (<em class="placeholder">ckeditor5_language</em>) is missing.', + "'language_list' is a required key because settings.plugins.%key is ckeditor5_language (see config schema type ckeditor5.plugin.ckeditor5_language).", + ], ], ]; $data['valid language plugin configuration: un'] = [ @@ -1056,7 +1061,7 @@ public static function providerPair(): array { ], ], 'image_upload' => [ - 'status' => TRUE, + 'status' => FALSE, ], 'filters' => [], 'violations' => [ @@ -1102,7 +1107,7 @@ public static function providerPair(): array { ], ], 'image_upload' => [ - 'status' => TRUE, + 'status' => FALSE, ], 'filters' => [], 'violations' => [ @@ -1163,8 +1168,15 @@ public static function providerPair(): array { ], ], ], - 'image' => [ + 'image_upload' => [ 'status' => TRUE, + 'scheme' => 'public', + 'directory' => 'inline-images', + 'max_size' => NULL, + 'max_dimensions' => [ + 'width' => NULL, + 'height' => NULL, + ], ], 'filters' => [], 'violations' => [], @@ -1621,7 +1633,6 @@ public function testMultipleHtmlRestrictingFilters(): void { ], 'plugins' => [], ], - 'image_upload' => [], ]); $this->assertSame([], $this->validatePairToViolationsArray($text_editor, $text_format, TRUE)); diff --git a/core/modules/editor/config/schema/editor.schema.yml b/core/modules/editor/config/schema/editor.schema.yml index 0c0278b71551..ae035cc4cab0 100644 --- a/core/modules/editor/config/schema/editor.schema.yml +++ b/core/modules/editor/config/schema/editor.schema.yml @@ -7,6 +7,11 @@ editor.editor.*: format: type: string label: 'Name' + constraints: + # @see \Drupal\editor\Entity\Editor::getFilterFormat() + # @see \Drupal\editor\Entity\Editor::calculateDependencies() + ConfigExists: + prefix: 'filter.format.' editor: type: string label: 'Text editor' @@ -17,30 +22,71 @@ editor.editor.*: settings: type: editor.settings.[%parent.editor] image_upload: + type: editor.image_upload_settings.[status] + constraints: + FullyValidatable: ~ + +editor.image_upload_settings.*: + type: mapping + label: 'Image uploads' + constraints: + FullyValidatable: ~ + mapping: + status: + type: boolean + label: 'Status' + +editor.image_upload_settings.1: + type: editor.image_upload_settings.* + label: 'Image upload settings' + constraints: + FullyValidatable: ~ + mapping: + scheme: + type: string + label: 'File storage' + constraints: + Choice: + callback: \Drupal\editor\Entity\Editor::getValidStreamWrappers + message: 'The file storage you selected is not a visible, readable and writable stream wrapper. Possible choices: %choices.' + directory: + type: string + label: 'Upload directory' + nullable: true + constraints: + # `""` is not allowed, but `null` is. + NotBlank: + allowNull: true + Regex: + # Forbid any kind of control character. + # @see https://stackoverflow.com/a/66587087 + pattern: '/([^\PC])/u' + match: false + message: 'The image upload directory is not allowed to span multiple lines or contain control characters.' + max_size: + # @see \Drupal\file\Plugin\Validation\Constraint\FileSizeLimitConstraintValidator + type: bytes + label: 'Maximum file size' + nullable: true + max_dimensions: type: mapping - label: 'Image upload settings' + label: 'Maximum dimensions' mapping: - status: - type: boolean - label: 'Status' - scheme: - type: string - label: 'File storage' - directory: - type: string - label: 'Upload directory' - max_size: - type: string - label: 'Maximum file size' - max_dimensions: - type: mapping - label: 'Maximum dimensions' - mapping: - width: - type: integer - nullable: true - label: 'Maximum width' - height: - type: integer - nullable: true - label: 'Maximum height' + width: + type: integer + nullable: true + label: 'Maximum width' + constraints: + Range: + # @see editor_image_upload_settings_form() + min: 1 + max: 99999 + height: + type: integer + nullable: true + label: 'Maximum height' + constraints: + Range: + # @see editor_image_upload_settings_form() + min: 1 + max: 99999 diff --git a/core/modules/editor/editor.module b/core/modules/editor/editor.module index d1f3c5c21a29..6925eea7b140 100644 --- a/core/modules/editor/editor.module +++ b/core/modules/editor/editor.module @@ -8,6 +8,7 @@ use Drupal\Core\Url; use Drupal\Component\Utility\Html; use Drupal\Core\Form\SubformState; +use Drupal\editor\EditorInterface; use Drupal\editor\Entity\Editor; use Drupal\Core\Entity\FieldableEntityInterface; use Drupal\Core\Field\FieldDefinitionInterface; @@ -252,6 +253,15 @@ function editor_form_filter_admin_format_submit($form, FormStateInterface $form_ if ($settings = $form_state->getValue(['editor', 'settings'])) { $editor->setSettings($settings); } + // When image uploads are disabled (status = FALSE), the schema for image + // upload settings does not allow other keys to be present. + // @see editor.image_upload_settings.* + // @see editor.image_upload_settings.1 + // @see editor.schema.yml + $image_upload_settings = $editor->getImageUploadSettings(); + if (!$image_upload_settings['status']) { + $editor->setImageUploadSettings(['status' => FALSE]); + } $editor->save(); } } @@ -641,3 +651,41 @@ function editor_filter_format_presave(FilterFormatInterface $format) { $editor->setStatus($status)->save(); } } + +/** + * Implements hook_ENTITY_TYPE_presave(). + */ +function editor_editor_presave(EditorInterface $editor) { + // @see editor_post_update_sanitize_image_upload_settings() + $image_upload_settings = $editor->getImageUploadSettings(); + // When image uploads are disabled, then none of the other key-value pairs + // make sense. + // TRICKY: the configuration system has historically stored `type: boolean` + // not as `true` and `false`, but as `1` and `0`, so use `==`, not `===`. + // @see editor_post_update_sanitize_image_upload_settings() + if (!array_key_exists('status', $image_upload_settings) || $image_upload_settings['status'] == FALSE) { + $editor->setImageUploadSettings(['status' => FALSE]); + } + else { + // When image uploads are enabled, then some of the key-value pairs need + // some conversions to comply with the config schema. Note that all these + // keys SHOULD exist, but because validation has historically been absent, + // err on the side of caution. + // @see editor_post_update_sanitize_image_upload_settings() + if (array_key_exists('directory', $image_upload_settings) && $image_upload_settings['directory'] === '') { + $image_upload_settings['directory'] = NULL; + } + if (array_key_exists('max_size', $image_upload_settings) && $image_upload_settings['max_size'] === '') { + $image_upload_settings['max_size'] = NULL; + } + if (array_key_exists('max_dimensions', $image_upload_settings)) { + if (!array_key_exists('width', $image_upload_settings['max_dimensions']) || $image_upload_settings['max_dimensions']['width'] === 0) { + $image_upload_settings['max_dimensions']['width'] = NULL; + } + if (!array_key_exists('height', $image_upload_settings['max_dimensions']) || $image_upload_settings['max_dimensions']['height'] === 0) { + $image_upload_settings['max_dimensions']['height'] = NULL; + } + } + $editor->setImageUploadSettings($image_upload_settings); + } +} diff --git a/core/modules/editor/editor.post_update.php b/core/modules/editor/editor.post_update.php index 112409ecb0e5..c62567af7c8e 100644 --- a/core/modules/editor/editor.post_update.php +++ b/core/modules/editor/editor.post_update.php @@ -5,6 +5,8 @@ * Post update functions for Editor. */ +use Drupal\Core\Config\Entity\ConfigEntityUpdater; +use Drupal\editor\EditorInterface; use Drupal\filter\Entity\FilterFormat; use Drupal\filter\FilterFormatInterface; use Drupal\filter\FilterPluginCollection; @@ -44,3 +46,24 @@ function editor_post_update_image_lazy_load(): void { } } } + +/** + * Clean up image upload settings. + */ +function editor_post_update_sanitize_image_upload_settings(&$sandbox = []) { + $config_entity_updater = \Drupal::classResolver(ConfigEntityUpdater::class); + + $callback = function (EditorInterface $editor) { + $image_upload_settings = $editor->getImageUploadSettings(); + // Only update if the editor has image uploads: + // - empty image upload settings + // - disabled and >=1 other keys in its image upload settings + // - enabled (to tighten the key-value pairs in its settings). + // @see editor_editor_presave() + return !array_key_exists('status', $image_upload_settings) + || ($image_upload_settings['status'] == FALSE && count($image_upload_settings) >= 2) + || $image_upload_settings['status'] == TRUE; + }; + + $config_entity_updater->update($sandbox, 'editor', $callback); +} diff --git a/core/modules/editor/src/Entity/Editor.php b/core/modules/editor/src/Entity/Editor.php index b8c041df85b8..25ff5f8e9922 100644 --- a/core/modules/editor/src/Entity/Editor.php +++ b/core/modules/editor/src/Entity/Editor.php @@ -4,6 +4,7 @@ use Drupal\Component\Plugin\Exception\PluginNotFoundException; use Drupal\Core\Config\Entity\ConfigEntityBase; +use Drupal\Core\StreamWrapper\StreamWrapperInterface; use Drupal\editor\EditorInterface; /** @@ -207,4 +208,18 @@ public function setImageUploadSettings(array $image_upload_settings) { return $this; } + /** + * Computes all valid choices for the "image_upload.scheme" setting. + * + * @see editor.schema.yml + * + * @return string[] + * All valid choices. + * + * @internal + */ + public static function getValidStreamWrappers(): array { + return array_keys(\Drupal::service('stream_wrapper_manager')->getNames(StreamWrapperInterface::WRITE_VISIBLE)); + } + } diff --git a/core/modules/editor/src/Form/EditorImageDialog.php b/core/modules/editor/src/Form/EditorImageDialog.php index 6e4cf9db2705..323f24817ce5 100644 --- a/core/modules/editor/src/Form/EditorImageDialog.php +++ b/core/modules/editor/src/Form/EditorImageDialog.php @@ -96,25 +96,15 @@ public function buildForm(array $form, FormStateInterface $form_state, Editor $e // Construct strings to use in the upload validators. $image_upload = $editor->getImageUploadSettings(); - if (!empty($image_upload['max_dimensions']['width']) || !empty($image_upload['max_dimensions']['height'])) { - $max_dimensions = $image_upload['max_dimensions']['width'] . 'x' . $image_upload['max_dimensions']['height']; - } - else { - $max_dimensions = 0; - } - $max_filesize = min(Bytes::toNumber($image_upload['max_size']), Environment::getUploadMaxSize()); $existing_file = isset($image_element['data-entity-uuid']) ? \Drupal::service('entity.repository')->loadEntityByUuid('file', $image_element['data-entity-uuid']) : NULL; $fid = $existing_file ? $existing_file->id() : NULL; $form['fid'] = [ '#title' => $this->t('Image'), '#type' => 'managed_file', - '#upload_location' => $image_upload['scheme'] . '://' . $image_upload['directory'], '#default_value' => $fid ? [$fid] : NULL, '#upload_validators' => [ 'FileExtension' => ['extensions' => 'gif png jpg jpeg'], - 'FileSizeLimit' => ['fileLimit' => $max_filesize], - 'FileImageDimensions' => ['maxDimensions' => $max_dimensions], ], '#required' => TRUE, ]; @@ -132,6 +122,17 @@ public function buildForm(array $form, FormStateInterface $form_state, Editor $e if ($image_upload['status']) { $form['attributes']['src']['#access'] = FALSE; $form['attributes']['src']['#required'] = FALSE; + + if (!empty($image_upload['max_dimensions']['width']) || !empty($image_upload['max_dimensions']['height'])) { + $max_dimensions = $image_upload['max_dimensions']['width'] . 'x' . $image_upload['max_dimensions']['height']; + } + else { + $max_dimensions = 0; + } + $max_filesize = min(Bytes::toNumber($image_upload['max_size'] ?? 0), Environment::getUploadMaxSize()); + $form['fid']['#upload_location'] = $image_upload['scheme'] . '://' . ($image_upload['directory'] ?? ''); + $form['fid']['#upload_validators']['FileSizeLimit'] = ['fileLimit' => $max_filesize]; + $form['fid']['#upload_validators']['FileImageDimensions'] = ['maxDimensions' => $max_dimensions]; } else { $form['fid']['#access'] = FALSE; diff --git a/core/modules/editor/tests/fixtures/update/editor-3412361.php b/core/modules/editor/tests/fixtures/update/editor-3412361.php new file mode 100644 index 000000000000..390f07699e1c --- /dev/null +++ b/core/modules/editor/tests/fixtures/update/editor-3412361.php @@ -0,0 +1,41 @@ +<?php + +/** + * @file + * Test fixture. + */ + +use Drupal\Core\Database\Database; +use Drupal\Core\Serialization\Yaml; + +$connection = Database::getConnection(); + +$umami_basic_html_format = Yaml::decode(file_get_contents(__DIR__ . '/filter.format.umami_basic_html.yml')); +$umami_basic_html_format['format'] = 'umami_basic_html'; +$connection->insert('config') + ->fields([ + 'collection', + 'name', + 'data', + ]) + ->values([ + 'collection' => '', + 'name' => 'filter.format.umami_basic_html', + 'data' => serialize($umami_basic_html_format), + ]) + ->execute(); + +$umami_basic_html_editor = Yaml::decode(file_get_contents(__DIR__ . '/editor.editor.umami_basic_html.yml')); +$umami_basic_html_editor['format'] = 'umami_basic_html'; +$connection->insert('config') + ->fields([ + 'collection', + 'name', + 'data', + ]) + ->values([ + 'collection' => '', + 'name' => 'editor.editor.umami_basic_html', + 'data' => serialize($umami_basic_html_editor), + ]) + ->execute(); diff --git a/core/modules/editor/tests/fixtures/update/editor.editor.umami_basic_html.yml b/core/modules/editor/tests/fixtures/update/editor.editor.umami_basic_html.yml new file mode 100644 index 000000000000..5a0f96f3a680 --- /dev/null +++ b/core/modules/editor/tests/fixtures/update/editor.editor.umami_basic_html.yml @@ -0,0 +1,64 @@ +uuid: c82794ef-c451-49c6-be67-39e2b0649a47 +langcode: en +status: true +dependencies: + config: + - filter.format.basic_html + module: + - ckeditor5 +format: basic_html +editor: ckeditor5 +settings: + toolbar: + items: + - bold + - italic + - '|' + - link + - '|' + - bulletedList + - numberedList + - '|' + - blockQuote + - '|' + - heading + - '|' + - sourceEditing + - '|' + plugins: + ckeditor5_heading: + enabled_headings: + - heading2 + - heading3 + - heading4 + - heading5 + - heading6 + ckeditor5_list: + properties: + reversed: false + startIndex: true + multiBlock: false + ckeditor5_sourceEditing: + allowed_tags: + - '<cite>' + - '<dl>' + - '<dt>' + - '<dd>' + - '<a hreflang>' + - '<blockquote cite>' + - '<ul type>' + - '<ol type>' + - '<h2 id>' + - '<h3 id>' + - '<h4 id>' + - '<h5 id>' + - '<h6 id>' + - '<img src alt data-entity-type data-entity-uuid data-align data-caption width height loading>' +image_upload: + status: false + scheme: public + directory: inline-images + max_size: '' + max_dimensions: + width: null + height: null diff --git a/core/modules/editor/tests/fixtures/update/filter.format.umami_basic_html.yml b/core/modules/editor/tests/fixtures/update/filter.format.umami_basic_html.yml new file mode 100644 index 000000000000..db9282020ed5 --- /dev/null +++ b/core/modules/editor/tests/fixtures/update/filter.format.umami_basic_html.yml @@ -0,0 +1,55 @@ +uuid: 32fd1f3a-8ea1-44be-851f-64659c260bea +langcode: en +status: true +dependencies: + module: + - editor +name: 'Basic HTML' +format: basic_html +weight: 0 +filters: + editor_file_reference: + id: editor_file_reference + provider: editor + status: true + weight: 11 + settings: { } + filter_align: + id: filter_align + provider: filter + status: true + weight: 7 + settings: { } + filter_autop: + id: filter_autop + provider: filter + status: true + weight: 0 + settings: { } + filter_caption: + id: filter_caption + provider: filter + status: true + weight: 8 + settings: { } + filter_html: + id: filter_html + provider: filter + status: true + weight: -10 + settings: + allowed_html: '<a href hreflang> <em> <strong> <cite> <blockquote cite> <ul type> <ol start type> <li> <dl> <dt> <dd> <h2 id> <h3 id> <h4 id> <h5 id> <h6 id> <p> <br> <img src alt loading height width data-entity-type data-entity-uuid data-align data-caption> <drupal-media data-entity-type data-entity-uuid data-view-mode data-align data-caption alt title>' + filter_html_help: false + filter_html_nofollow: false + filter_html_image_secure: + id: filter_html_image_secure + provider: filter + status: true + weight: 9 + settings: { } + filter_image_lazy_load: + id: filter_image_lazy_load + provider: filter + status: true + weight: 15 + settings: { } diff --git a/core/modules/editor/tests/src/Functional/EditorDialogAccessTest.php b/core/modules/editor/tests/src/Functional/EditorDialogAccessTest.php index 7c0a689ae447..128ab2e7fdc8 100644 --- a/core/modules/editor/tests/src/Functional/EditorDialogAccessTest.php +++ b/core/modules/editor/tests/src/Functional/EditorDialogAccessTest.php @@ -45,13 +45,6 @@ public function testEditorImageDialogAccess() { 'format' => 'plain_text', 'image_upload' => [ 'status' => FALSE, - 'scheme' => 'public', - 'directory' => 'inline-images', - 'max_size' => '', - 'max_dimensions' => [ - 'width' => 0, - 'height' => 0, - ], ], ]); $editor->save(); @@ -63,8 +56,16 @@ public function testEditorImageDialogAccess() { // With image upload settings, expect a 200, and now there should be an // input[type=file]. - $editor->setImageUploadSettings(['status' => TRUE] + $editor->getImageUploadSettings()) - ->save(); + $editor->setImageUploadSettings([ + 'status' => TRUE, + 'scheme' => 'public', + 'directory' => 'inline-images', + 'max_size' => NULL, + 'max_dimensions' => [ + 'width' => NULL, + 'height' => NULL, + ], + ])->save(); $this->resetAll(); $this->drupalGet($url); $this->assertEmpty($this->cssSelect('input[type=text][name="attributes[src]"]'), 'Image uploads enabled: input[type=text][name="attributes[src]"] is absent.'); diff --git a/core/modules/editor/tests/src/Functional/Rest/EditorResourceTestBase.php b/core/modules/editor/tests/src/Functional/Rest/EditorResourceTestBase.php index 7b07ec45d07e..2eafeb051fed 100644 --- a/core/modules/editor/tests/src/Functional/Rest/EditorResourceTestBase.php +++ b/core/modules/editor/tests/src/Functional/Rest/EditorResourceTestBase.php @@ -66,7 +66,7 @@ protected function createEntity() { ]); $camelids ->setImageUploadSettings([ - 'status' => FALSE, + 'status' => TRUE, 'scheme' => 'public', 'directory' => 'inline-images', 'max_size' => '', @@ -96,10 +96,10 @@ protected function getExpectedNormalizedEntity() { 'editor' => 'ckeditor5', 'format' => 'llama', 'image_upload' => [ - 'status' => FALSE, + 'status' => TRUE, 'scheme' => 'public', 'directory' => 'inline-images', - 'max_size' => '', + 'max_size' => NULL, 'max_dimensions' => [ 'width' => NULL, 'height' => NULL, diff --git a/core/modules/editor/tests/src/Functional/Update/EditorSanitizeImageUploadSettingsUpdateTest.php b/core/modules/editor/tests/src/Functional/Update/EditorSanitizeImageUploadSettingsUpdateTest.php new file mode 100644 index 000000000000..a742ff3645d6 --- /dev/null +++ b/core/modules/editor/tests/src/Functional/Update/EditorSanitizeImageUploadSettingsUpdateTest.php @@ -0,0 +1,104 @@ +<?php + +declare(strict_types=1); + +namespace Drupal\Tests\editor\Functional\Update; + +use Drupal\FunctionalTests\Update\UpdatePathTestBase; + +/** + * @group Update + * @group editor + * @see editor_post_update_sanitize_image_upload_settings() + */ +class EditorSanitizeImageUploadSettingsUpdateTest extends UpdatePathTestBase { + + /** + * {@inheritdoc} + */ + protected $defaultTheme = 'stark'; + + /** + * {@inheritdoc} + */ + protected function setDatabaseDumpFiles(): void { + $this->databaseDumpFiles = [ + __DIR__ . '/../../../../../system/tests/fixtures/update/drupal-9.4.0.bare.standard.php.gz', + __DIR__ . '/../../../fixtures/update/editor-3412361.php', + ]; + } + + /** + * Ensure image upload settings for Text Editor config entities are corrected. + * + * @see editor_post_update_sanitize_image_upload_settings() + */ + public function testUpdateRemoveMeaninglessImageUploadSettings(): void { + $basic_html_before = $this->config('editor.editor.basic_html'); + $this->assertSame([ + 'status' => TRUE, + 'scheme' => 'public', + 'directory' => 'inline-images', + 'max_size' => '', + 'max_dimensions' => [ + 'width' => 0, + 'height' => 0, + ], + ], $basic_html_before->get('image_upload')); + $full_html_before = $this->config('editor.editor.full_html'); + $this->assertSame([ + 'status' => TRUE, + 'scheme' => 'public', + 'directory' => 'inline-images', + 'max_size' => '', + 'max_dimensions' => [ + 'width' => 0, + 'height' => 0, + ], + ], $full_html_before->get('image_upload')); + $umami_basic_html_before = $this->config('editor.editor.umami_basic_html'); + $this->assertSame([ + 'status' => FALSE, + 'scheme' => 'public', + 'directory' => 'inline-images', + 'max_size' => '', + 'max_dimensions' => [ + 'width' => NULL, + 'height' => NULL, + ], + ], $umami_basic_html_before->get('image_upload')); + + $this->runUpdates(); + + $basic_html_after = $this->config('editor.editor.basic_html'); + $this->assertNotSame($basic_html_before->get('image_upload'), $basic_html_after->get('image_upload')); + $this->assertSame([ + 'status' => TRUE, + 'scheme' => 'public', + 'directory' => 'inline-images', + 'max_size' => NULL, + 'max_dimensions' => [ + 'width' => NULL, + 'height' => NULL, + ], + ], $basic_html_after->get('image_upload')); + $full_html_after = $this->config('editor.editor.full_html'); + $this->assertNotSame($full_html_before->get('image_upload'), $full_html_after->get('image_upload')); + $this->assertSame([ + 'status' => TRUE, + 'scheme' => 'public', + 'directory' => 'inline-images', + 'max_size' => NULL, + 'max_dimensions' => [ + 'width' => NULL, + 'height' => NULL, + ], + ], $full_html_after->get('image_upload')); + $umami_basic_html_after = $this->config('editor.editor.umami_basic_html'); + $this->assertNotSame($umami_basic_html_before->get('image_upload'), $umami_basic_html_after->get('image_upload')); + $this->assertSame([ + 'status' => FALSE, + ], $umami_basic_html_after->get('image_upload')); + } + +} diff --git a/core/modules/editor/tests/src/Kernel/EditorImageDialogTest.php b/core/modules/editor/tests/src/Kernel/EditorImageDialogTest.php index 21fd39820407..5ba77d067ebf 100644 --- a/core/modules/editor/tests/src/Kernel/EditorImageDialogTest.php +++ b/core/modules/editor/tests/src/Kernel/EditorImageDialogTest.php @@ -68,6 +68,10 @@ protected function setUp(): void { 'max_size' => 100, 'scheme' => 'public', 'directory' => '', + 'max_dimensions' => [ + 'width' => NULL, + 'height' => NULL, + ], 'status' => TRUE, ], ]); diff --git a/core/modules/editor/tests/src/Kernel/EditorValidationTest.php b/core/modules/editor/tests/src/Kernel/EditorValidationTest.php index 29006ec44088..88035dae514e 100644 --- a/core/modules/editor/tests/src/Kernel/EditorValidationTest.php +++ b/core/modules/editor/tests/src/Kernel/EditorValidationTest.php @@ -2,6 +2,7 @@ namespace Drupal\Tests\editor\Kernel; +use Drupal\ckeditor5\Plugin\CKEditor5Plugin\Heading; use Drupal\editor\Entity\Editor; use Drupal\filter\Entity\FilterFormat; use Drupal\KernelTests\Core\Config\ConfigEntityValidationTestBase; @@ -16,7 +17,18 @@ class EditorValidationTest extends ConfigEntityValidationTestBase { /** * {@inheritdoc} */ - protected static $modules = ['editor', 'editor_test', 'filter']; + protected static $modules = ['ckeditor5', 'editor', 'filter']; + + /** + * {@inheritdoc} + */ + protected static array $propertiesWithRequiredKeys = [ + 'settings' => [ + "'toolbar' is a required key because editor is ckeditor5 (see config schema type editor.settings.ckeditor5).", + "'plugins' is a required key because editor is ckeditor5 (see config schema type editor.settings.ckeditor5).", + ], + 'image_upload' => "'status' is a required key.", + ]; /** * {@inheritdoc} @@ -32,11 +44,34 @@ protected function setUp(): void { $this->entity = Editor::create([ 'format' => $format->id(), - 'editor' => 'unicorn', + 'editor' => 'ckeditor5', + 'settings' => [ + // @see \Drupal\ckeditor5\Plugin\Editor\CKEditor5::getDefaultSettings() + 'toolbar' => [ + 'items' => ['heading', 'bold', 'italic'], + ], + 'plugins' => [ + 'ckeditor5_heading' => Heading::DEFAULT_CONFIGURATION, + ], + ], ]); $this->entity->save(); } + /** + * {@inheritdoc} + */ + public function testImmutableProperties(array $valid_values = [], ?array $additional_expected_validation_errors_when_missing = NULL): void { + // TRICKY: Every Text Editor is associated with a Text Format. It must exist + // to avoid triggering a validation error. + // @see \Drupal\editor\EditorInterface::hasAssociatedFilterFormat + FilterFormat::create([ + 'format' => 'another', + 'name' => 'Another', + ])->save(); + parent::testImmutableProperties(['format' => 'another']); + } + /** * Tests that validation fails if config dependencies are invalid. */ @@ -70,6 +105,17 @@ public function testInvalidPluginId(): void { $this->assertValidationErrors(['editor' => "The 'non_existent' plugin does not exist."]); } + /** + * Tests validating an editor with a non-existent `format`. + */ + public function testInvalidFormat(): void { + $this->entity->set('format', 'non_existent'); + $this->assertValidationErrors([ + '' => "The 'format' property cannot be changed.", + 'format' => "The 'filter.format.non_existent' config does not exist.", + ]); + } + /** * {@inheritdoc} */ @@ -79,6 +125,107 @@ public function testLabelValidation(): void { $this->markTestSkipped(); } + /** + * `image_upload.status = TRUE` must cause additional keys to be required. + */ + public function testImageUploadSettingsAreDynamicallyRequired(): void { + // When image uploads are disabled, no other key-value pairs are needed. + $this->entity->setImageUploadSettings(['status' => FALSE]); + $this->assertValidationErrors([]); + + // But when they are enabled, many others are needed. + $this->entity->setImageUploadSettings(['status' => TRUE]); + $this->assertValidationErrors([ + 'image_upload' => [ + "'scheme' is a required key because image_upload.status is 1 (see config schema type editor.image_upload_settings.1).", + "'directory' is a required key because image_upload.status is 1 (see config schema type editor.image_upload_settings.1).", + "'max_size' is a required key because image_upload.status is 1 (see config schema type editor.image_upload_settings.1).", + "'max_dimensions' is a required key because image_upload.status is 1 (see config schema type editor.image_upload_settings.1).", + ], + ]); + + // Specify all required keys, but forget one. + $this->entity->setImageUploadSettings([ + 'status' => TRUE, + 'scheme' => 'public', + 'directory' => 'uploaded-images', + 'max_size' => '5 MB', + ]); + $this->assertValidationErrors(['image_upload' => "'max_dimensions' is a required key because image_upload.status is 1 (see config schema type editor.image_upload_settings.1)."]); + + // Specify all required keys. + $this->entity->setImageUploadSettings([ + 'status' => TRUE, + 'scheme' => 'public', + 'directory' => 'uploaded-images', + 'max_size' => '5 MB', + 'max_dimensions' => [ + 'width' => 10000, + 'height' => 10000, + ], + ]); + $this->assertValidationErrors([]); + + // Specify all required keys … but now disable image uploads again. This + // should trigger a validation error from the ValidKeys constraint. + $this->entity->setImageUploadSettings([ + 'status' => FALSE, + 'scheme' => 'public', + 'directory' => 'uploaded-images', + 'max_size' => '5 MB', + 'max_dimensions' => [ + 'width' => 10000, + 'height' => 10000, + ], + ]); + $this->assertValidationErrors([ + 'image_upload' => [ + "'scheme' is an unknown key because image_upload.status is 0 (see config schema type editor.image_upload_settings.*).", + "'directory' is an unknown key because image_upload.status is 0 (see config schema type editor.image_upload_settings.*).", + "'max_size' is an unknown key because image_upload.status is 0 (see config schema type editor.image_upload_settings.*).", + "'max_dimensions' is an unknown key because image_upload.status is 0 (see config schema type editor.image_upload_settings.*).", + ], + ]); + + // Remove the values that the messages said are unknown. + $this->entity->setImageUploadSettings(['status' => FALSE]); + $this->assertValidationErrors([]); + + // Note how this is the same as the initial value. This proves that `status` + // being FALSE prevents any meaningless key-value pairs to be present, and + // `status` being TRUE requires those then meaningful pairs to be present. + } + + /** + * @testWith [{"scheme": "public"}, {}] + * [{"scheme": "private"}, {"image_upload.scheme": "The file storage you selected is not a visible, readable and writable stream wrapper. Possible choices: <em class=\"placeholder\">"public"</em>."}] + * [{"directory": null}, {}] + * [{"directory": ""}, {"image_upload.directory": "This value should not be blank."}] + * [{"directory": "inline\nimages"}, {"image_upload.directory": "The image upload directory is not allowed to span multiple lines or contain control characters."}] + * [{"directory": "foo\b\b\binline-images"}, {"image_upload.directory": "The image upload directory is not allowed to span multiple lines or contain control characters."}] + * [{"max_size": null}, {}] + * [{"max_size": "foo"}, {"image_upload.max_size": "This value must be a number of bytes, optionally with a unit such as \"MB\" or \"megabytes\". <em class=\"placeholder\">foo</em> does not represent a number of bytes."}] + * [{"max_size": ""}, {"image_upload.max_size": "This value must be a number of bytes, optionally with a unit such as \"MB\" or \"megabytes\". <em class=\"placeholder\"></em> does not represent a number of bytes."}] + * [{"max_size": "7 exabytes"}, {}] + * [{"max_dimensions": {"width": null, "height": 15}}, {}] + * [{"max_dimensions": {"width": null, "height": null}}, {}] + * [{"max_dimensions": {"width": null, "height": 0}}, {"image_upload.max_dimensions.height": "This value should be between <em class=\"placeholder\">1</em> and <em class=\"placeholder\">99999</em>."}] + * [{"max_dimensions": {"width": 100000, "height": 1}}, {"image_upload.max_dimensions.width": "This value should be between <em class=\"placeholder\">1</em> and <em class=\"placeholder\">99999</em>."}] + */ + public function testImageUploadSettingsValidation(array $invalid_setting, array $expected_message): void { + $this->entity->setImageUploadSettings($invalid_setting + [ + 'status' => TRUE, + 'scheme' => 'public', + 'directory' => 'uploaded-images', + 'max_size' => '5 MB', + 'max_dimensions' => [ + 'width' => 10000, + 'height' => 10000, + ], + ]); + $this->assertValidationErrors($expected_message); + } + /** * {@inheritdoc} */ @@ -89,6 +236,9 @@ public function testRequiredPropertyValuesMissing(?array $additional_expected_va // @see \Drupal\Core\Config\Plugin\Validation\Constraint\RequiredConfigDependenciesConstraintValidator '' => 'This text editor requires a text format.', ], + 'settings' => [ + 'settings.plugins.ckeditor5_heading' => 'Configuration for the enabled plugin "<em class="placeholder">Headings</em>" (<em class="placeholder">ckeditor5_heading</em>) is missing.', + ], ]); } @@ -102,6 +252,9 @@ public function testRequiredPropertyKeysMissing(?array $additional_expected_vali // @see \Drupal\Core\Config\Plugin\Validation\Constraint\RequiredConfigDependenciesConstraintValidator '' => 'This text editor requires a text format.', ], + 'settings' => [ + 'settings.plugins.ckeditor5_heading' => 'Configuration for the enabled plugin "<em class="placeholder">Headings</em>" (<em class="placeholder">ckeditor5_heading</em>) is missing.', + ], ]); } diff --git a/core/modules/jsonapi/tests/src/Functional/EditorTest.php b/core/modules/jsonapi/tests/src/Functional/EditorTest.php index d04bbd6267d5..1ee003232693 100644 --- a/core/modules/jsonapi/tests/src/Functional/EditorTest.php +++ b/core/modules/jsonapi/tests/src/Functional/EditorTest.php @@ -81,7 +81,7 @@ protected function createEntity() { ]); $camelids ->setImageUploadSettings([ - 'status' => FALSE, + 'status' => TRUE, 'scheme' => 'public', 'directory' => 'inline-images', 'max_size' => '', @@ -129,10 +129,10 @@ protected function getExpectedDocument() { ], 'editor' => 'ckeditor5', 'image_upload' => [ - 'status' => FALSE, + 'status' => TRUE, 'scheme' => 'public', 'directory' => 'inline-images', - 'max_size' => '', + 'max_size' => NULL, 'max_dimensions' => [ 'width' => NULL, 'height' => NULL, @@ -193,7 +193,7 @@ protected function createAnotherEntity($key) { ]); $entity->setImageUploadSettings([ - 'status' => FALSE, + 'status' => TRUE, 'scheme' => 'public', 'directory' => 'inline-images', 'max_size' => '', diff --git a/core/profiles/demo_umami/config/install/editor.editor.basic_html.yml b/core/profiles/demo_umami/config/install/editor.editor.basic_html.yml index d87f1cc4a3a5..60cd331cc875 100644 --- a/core/profiles/demo_umami/config/install/editor.editor.basic_html.yml +++ b/core/profiles/demo_umami/config/install/editor.editor.basic_html.yml @@ -59,9 +59,3 @@ settings: allow_view_mode_override: true image_upload: status: false - scheme: public - directory: inline-images - max_size: '' - max_dimensions: - width: null - height: null diff --git a/core/profiles/demo_umami/config/install/editor.editor.full_html.yml b/core/profiles/demo_umami/config/install/editor.editor.full_html.yml index dd7cf0a06571..307861f05c8b 100644 --- a/core/profiles/demo_umami/config/install/editor.editor.full_html.yml +++ b/core/profiles/demo_umami/config/install/editor.editor.full_html.yml @@ -51,7 +51,7 @@ image_upload: status: true scheme: public directory: inline-images - max_size: '' + max_size: null max_dimensions: width: null height: null diff --git a/core/profiles/standard/config/install/editor.editor.basic_html.yml b/core/profiles/standard/config/install/editor.editor.basic_html.yml index 07b13d8e16bd..a31e41506fd5 100644 --- a/core/profiles/standard/config/install/editor.editor.basic_html.yml +++ b/core/profiles/standard/config/install/editor.editor.basic_html.yml @@ -59,7 +59,7 @@ image_upload: status: true scheme: public directory: inline-images - max_size: '' + max_size: null max_dimensions: - width: 0 - height: 0 + width: null + height: null diff --git a/core/profiles/standard/config/install/editor.editor.full_html.yml b/core/profiles/standard/config/install/editor.editor.full_html.yml index a27ed46a5e3d..e30fc15eaf3c 100644 --- a/core/profiles/standard/config/install/editor.editor.full_html.yml +++ b/core/profiles/standard/config/install/editor.editor.full_html.yml @@ -96,7 +96,7 @@ image_upload: status: true scheme: public directory: inline-images - max_size: '' + max_size: null max_dimensions: - width: 0 - height: 0 + width: null + height: null diff --git a/core/tests/Drupal/Tests/Component/Utility/BytesTest.php b/core/tests/Drupal/Tests/Component/Utility/BytesTest.php index d8c9c5ce2222..e28b912fb5e2 100644 --- a/core/tests/Drupal/Tests/Component/Utility/BytesTest.php +++ b/core/tests/Drupal/Tests/Component/Utility/BytesTest.php @@ -89,6 +89,7 @@ public static function providerTestToNumber(): array { * * @dataProvider providerTestValidate * @covers ::validate + * @covers ::validateConstraint */ public function testValidate($string, bool $expected_result): void { $this->assertSame($expected_result, Bytes::validate($string)); -- GitLab