Issue #3412361 by Wim Leers, phenaproxima, catch, effulgentsia: Mark Editor config schema as fully validatable

merge-requests/4139/merge
catch 2024-03-01 14:02:18 +00:00
parent 661f5453d0
commit 83874f2bcf
30 changed files with 751 additions and 90 deletions

View File

@ -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>\.$',
],
],
];
/**

View File

@ -175,7 +175,9 @@ class CKEditor5ImageController extends ControllerBase {
* 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'];

View File

@ -63,16 +63,27 @@ class Image extends CKEditor5PluginDefault implements CKEditor5PluginConfigurabl
*/
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);
}
/**

View File

@ -660,7 +660,13 @@ class CKEditor5 extends EditorBase implements ContainerFactoryPluginInterface {
// 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 @@ class CKEditor5 extends EditorBase implements ContainerFactoryPluginInterface {
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);
}

View File

@ -29,8 +29,13 @@ trait TextEditorObjectDependentValidatorTrait {
]);
}
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);

View File

@ -27,8 +27,14 @@ class ImageUploadAccessTest extends ImageUploadTest {
$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 @@ class ImageUploadAccessTest extends ImageUploadTest {
'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());

View File

@ -138,7 +138,14 @@ class CKEditor5UpdateImageToolbarItemTest extends UpdatePathTestBase {
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>.',
)
));
}

View File

@ -177,6 +177,55 @@ JS;
$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.
*/

View File

@ -95,6 +95,10 @@ class CKEditor5Test extends CKEditor5TestBase {
'scheme' => 'public',
'directory' => 'inline-images',
'max_size' => '',
'max_dimensions' => [
'width' => NULL,
'height' => NULL,
],
],
])->save();
$this->assertSame([], array_map(
@ -643,6 +647,7 @@ JS;
'reversed' => FALSE,
'startIndex' => FALSE,
],
'multiBlock' => TRUE,
],
'ckeditor5_sourceEditing' => [
'allowed_tags' => [],

View File

@ -1030,7 +1030,6 @@ PHP,
$sneaky_plugin_id => ['configured_subset' => $configured_subset],
],
],
'image_upload' => [],
]);
// Invalid subsets are allowed on unsaved Text Editor config entities,
@ -1257,7 +1256,9 @@ PHP,
'format' => 'dummy',
'editor' => 'ckeditor5',
'settings' => $text_editor_settings,
'image_upload' => [],
'image_upload' => [
'status' => FALSE,
],
]);
FilterFormat::create([
'format' => 'dummy',

View File

@ -86,7 +86,9 @@ class ValidatorsTest extends KernelTestBase {
'format' => 'dummy',
'editor' => 'ckeditor5',
'settings' => $ckeditor5_settings,
'image_upload' => [],
'image_upload' => [
'status' => FALSE,
],
]);
$typed_config = $this->typedConfig->createFromNameAndData(
@ -182,7 +184,10 @@ class ValidatorsTest extends KernelTestBase {
],
],
'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 @@ class ValidatorsTest extends KernelTestBase {
],
],
'image_upload' => [
'status' => TRUE,
'status' => FALSE,
],
'filters' => [],
'violations' => [
@ -1102,7 +1107,7 @@ class ValidatorsTest extends KernelTestBase {
],
],
'image_upload' => [
'status' => TRUE,
'status' => FALSE,
],
'filters' => [],
'violations' => [
@ -1163,8 +1168,15 @@ class ValidatorsTest extends KernelTestBase {
],
],
],
'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 @@ class ValidatorsTest extends KernelTestBase {
],
'plugins' => [],
],
'image_upload' => [],
]);
$this->assertSame([], $this->validatePairToViolationsArray($text_editor, $text_format, TRUE));

View File

@ -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

View File

@ -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);
}
}

View File

@ -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);
}

View File

@ -4,6 +4,7 @@ namespace Drupal\editor\Entity;
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 @@ class Editor extends ConfigEntityBase implements EditorInterface {
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));
}
}

View File

@ -96,25 +96,15 @@ class EditorImageDialog extends FormBase {
// 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 @@ class EditorImageDialog extends FormBase {
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;

View File

@ -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();

View File

@ -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

View File

@ -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: { }

View File

@ -45,13 +45,6 @@ class EditorDialogAccessTest extends BrowserTestBase {
'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 @@ class EditorDialogAccessTest extends BrowserTestBase {
// 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.');

View File

@ -66,7 +66,7 @@ abstract class EditorResourceTestBase extends ConfigEntityResourceTestBase {
]);
$camelids
->setImageUploadSettings([
'status' => FALSE,
'status' => TRUE,
'scheme' => 'public',
'directory' => 'inline-images',
'max_size' => '',
@ -96,10 +96,10 @@ abstract class EditorResourceTestBase extends ConfigEntityResourceTestBase {
'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,

View File

@ -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'));
}
}

View File

@ -68,6 +68,10 @@ class EditorImageDialogTest extends EntityKernelTestBase {
'max_size' => 100,
'scheme' => 'public',
'directory' => '',
'max_dimensions' => [
'width' => NULL,
'height' => NULL,
],
'status' => TRUE,
],
]);

View File

@ -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 @@ class EditorValidationTest extends ConfigEntityValidationTestBase {
$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 @@ class EditorValidationTest extends ConfigEntityValidationTestBase {
$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 @@ class EditorValidationTest extends ConfigEntityValidationTestBase {
$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\">&quot;public&quot;</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 @@ class EditorValidationTest extends ConfigEntityValidationTestBase {
// @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 @@ class EditorValidationTest extends ConfigEntityValidationTestBase {
// @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.',
],
]);
}

View File

@ -81,7 +81,7 @@ class EditorTest extends ConfigEntityResourceTestBase {
]);
$camelids
->setImageUploadSettings([
'status' => FALSE,
'status' => TRUE,
'scheme' => 'public',
'directory' => 'inline-images',
'max_size' => '',
@ -129,10 +129,10 @@ class EditorTest extends ConfigEntityResourceTestBase {
],
'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 @@ class EditorTest extends ConfigEntityResourceTestBase {
]);
$entity->setImageUploadSettings([
'status' => FALSE,
'status' => TRUE,
'scheme' => 'public',
'directory' => 'inline-images',
'max_size' => '',

View File

@ -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

View File

@ -51,7 +51,7 @@ image_upload:
status: true
scheme: public
directory: inline-images
max_size: ''
max_size: null
max_dimensions:
width: null
height: null

View File

@ -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

View File

@ -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

View File

@ -89,6 +89,7 @@ class BytesTest extends TestCase {
*
* @dataProvider providerTestValidate
* @covers ::validate
* @covers ::validateConstraint
*/
public function testValidate($string, bool $expected_result): void {
$this->assertSame($expected_result, Bytes::validate($string));