From 6f3215938f2543af92a0e7c4502ec2c255142329 Mon Sep 17 00:00:00 2001 From: Adam G-H <32250-phenaproxima@users.noreply.drupalcode.org> Date: Fri, 21 Jul 2023 21:58:09 +0000 Subject: [PATCH] Issue #3373653 by phenaproxima, Wim Leers, borisson_, penyaskito, andypost, lauriii, smustgrave, larowlan: Add a `langcode` data type to config schema --- core/config/schema/core.data_types.schema.yml | 15 ++++-- .../Plugin/DataType/LanguageReference.php | 31 +++++++++++ .../Core/Validation/ConstraintManager.php | 18 +++++-- core/modules/ckeditor5/ckeditor5.module | 19 ------- .../config/schema/language.schema.yml | 6 +-- .../system/config/schema/system.schema.yml | 2 +- .../Config/ConfigEntityValidationTestBase.php | 53 +++++++++++++++++++ .../Core/Config/ConfigSchemaTest.php | 18 +++---- 8 files changed, 119 insertions(+), 43 deletions(-) diff --git a/core/config/schema/core.data_types.schema.yml b/core/config/schema/core.data_types.schema.yml index 748500fca26..322a9514f2c 100644 --- a/core/config/schema/core.data_types.schema.yml +++ b/core/config/schema/core.data_types.schema.yml @@ -104,6 +104,15 @@ machine_name: # @see \Drupal\Core\Config\Entity\ConfigEntityStorage::MAX_ID_LENGTH max: 166 +# A language identifier. +langcode: + type: string + label: 'Language code' + constraints: + NotNull: [] + Choice: + callback: 'Drupal\Core\TypedData\Plugin\DataType\LanguageReference::getAllValidLangcodes' + # Complex extended data types: # Root of a configuration object. @@ -130,8 +139,7 @@ config_object: _core: type: _core_config_info langcode: - type: string - label: 'Language code' + type: langcode # Mail text with subject and body parts. mail: @@ -305,8 +313,7 @@ config_entity: type: uuid label: 'UUID' langcode: - type: string - label: 'Language code' + type: langcode status: type: boolean label: 'Status' diff --git a/core/lib/Drupal/Core/TypedData/Plugin/DataType/LanguageReference.php b/core/lib/Drupal/Core/TypedData/Plugin/DataType/LanguageReference.php index 4f76bda335f..c9caaec1733 100644 --- a/core/lib/Drupal/Core/TypedData/Plugin/DataType/LanguageReference.php +++ b/core/lib/Drupal/Core/TypedData/Plugin/DataType/LanguageReference.php @@ -2,6 +2,7 @@ namespace Drupal\Core\TypedData\Plugin\DataType; +use Drupal\Core\Language\LanguageInterface; use Drupal\Core\TypedData\DataReferenceBase; /** @@ -30,4 +31,34 @@ class LanguageReference extends DataReferenceBase { return isset($language) ? $language->id() : NULL; } + /** + * Returns all valid values for a `langcode` config value. + * + * @return string[] + * All possible valid langcodes. This includes all langcodes in the standard + * list of human languages, along with special langcodes like `und`, `zxx`, + * and `site_default`, which Drupal uses internally. If any custom languages + * are defined, they will be included as well. + * + * @see \Drupal\Core\Language\LanguageManagerInterface::getLanguages() + * @see \Drupal\Core\Language\LanguageManagerInterface::getStandardLanguageList() + */ + public static function getAllValidLangcodes(): array { + $language_manager = \Drupal::languageManager(); + + return array_unique([ + ...array_keys($language_manager::getStandardLanguageList()), + // We can't use LanguageInterface::STATE_ALL because it will exclude the + // site default language in certain situations. + // @see \Drupal\Core\Language\LanguageManager::filterLanguages() + ...array_keys($language_manager->getLanguages(LanguageInterface::STATE_LOCKED | LanguageInterface::STATE_CONFIGURABLE | LanguageInterface::STATE_SITE_DEFAULT)), + // Include special language codes used internally. + LanguageInterface::LANGCODE_NOT_APPLICABLE, + LanguageInterface::LANGCODE_SITE_DEFAULT, + LanguageInterface::LANGCODE_DEFAULT, + LanguageInterface::LANGCODE_SYSTEM, + LanguageInterface::LANGCODE_NOT_SPECIFIED, + ]); + } + } diff --git a/core/lib/Drupal/Core/Validation/ConstraintManager.php b/core/lib/Drupal/Core/Validation/ConstraintManager.php index c5faab67ed0..7d024e22bff 100644 --- a/core/lib/Drupal/Core/Validation/ConstraintManager.php +++ b/core/lib/Drupal/Core/Validation/ConstraintManager.php @@ -7,6 +7,11 @@ use Drupal\Core\Cache\CacheBackendInterface; use Drupal\Core\Extension\ModuleHandlerInterface; use Drupal\Core\Plugin\DefaultPluginManager; use Drupal\Core\StringTranslation\TranslatableMarkup; +use Drupal\Core\Validation\Plugin\Validation\Constraint\EmailConstraint; +use Symfony\Component\Validator\Constraints\Blank; +use Symfony\Component\Validator\Constraints\Callback; +use Symfony\Component\Validator\Constraints\Choice; +use Symfony\Component\Validator\Constraints\NotBlank; /** * Constraint plugin manager. @@ -87,24 +92,29 @@ class ConstraintManager extends DefaultPluginManager { public function registerDefinitions() { $this->getDiscovery()->setDefinition('Callback', [ 'label' => new TranslatableMarkup('Callback'), - 'class' => '\Symfony\Component\Validator\Constraints\Callback', + 'class' => Callback::class, 'type' => FALSE, ]); $this->getDiscovery()->setDefinition('Blank', [ 'label' => new TranslatableMarkup('Blank'), - 'class' => '\Symfony\Component\Validator\Constraints\Blank', + 'class' => Blank::class, 'type' => FALSE, ]); $this->getDiscovery()->setDefinition('NotBlank', [ 'label' => new TranslatableMarkup('Not blank'), - 'class' => '\Symfony\Component\Validator\Constraints\NotBlank', + 'class' => NotBlank::class, 'type' => FALSE, ]); $this->getDiscovery()->setDefinition('Email', [ 'label' => new TranslatableMarkup('Email'), - 'class' => '\Drupal\Core\Validation\Plugin\Validation\Constraint\EmailConstraint', + 'class' => EmailConstraint::class, 'type' => ['string'], ]); + $this->getDiscovery()->setDefinition('Choice', [ + 'label' => new TranslatableMarkup('Choice'), + 'class' => Choice::class, + 'type' => FALSE, + ]); } /** diff --git a/core/modules/ckeditor5/ckeditor5.module b/core/modules/ckeditor5/ckeditor5.module index e2574b833c3..a3daee5f264 100644 --- a/core/modules/ckeditor5/ckeditor5.module +++ b/core/modules/ckeditor5/ckeditor5.module @@ -23,7 +23,6 @@ use Drupal\Core\Render\Element; use Drupal\Core\Routing\RouteMatchInterface; use Drupal\Core\Url; use Drupal\editor\EditorInterface; -use Symfony\Component\Validator\Constraints\Choice; /** * Implements hook_help(). @@ -568,24 +567,6 @@ function ckeditor5_js_alter(&$javascript, AttachedAssetsInterface $assets, Langu } } -/** - * Implements hook_validation_constraint_alter(). - */ -function ckeditor5_validation_constraint_alter(array &$definitions) { - // Add the Symfony validation constraints that Drupal core does not add in - // \Drupal\Core\Validation\ConstraintManager::registerDefinitions() for - // unknown reasons. Do it defensively, to not break when this changes. - if (!isset($definitions['Choice'])) { - $definitions['Choice'] = [ - 'label' => 'Choice', - 'class' => Choice::class, - 'type' => FALSE, - 'provider' => 'core', - 'id' => 'Choice', - ]; - } -} - /** * Implements hook_config_schema_info_alter(). */ diff --git a/core/modules/language/config/schema/language.schema.yml b/core/modules/language/config/schema/language.schema.yml index 91ce2d19026..4516a6fe057 100644 --- a/core/modules/language/config/schema/language.schema.yml +++ b/core/modules/language/config/schema/language.schema.yml @@ -71,7 +71,7 @@ language.negotiation: type: string label: 'Domain' selected_langcode: - type: string + type: langcode label: 'Selected language' language.mappings: @@ -118,7 +118,7 @@ language.content_settings.*.*: type: string label: 'Bundle' default_langcode: - type: string + type: langcode label: 'Default language' language_alterable: type: boolean @@ -130,7 +130,7 @@ condition.plugin.language: langcodes: type: sequence sequence: - type: string + type: langcode field.widget.settings.language_select: type: mapping diff --git a/core/modules/system/config/schema/system.schema.yml b/core/modules/system/config/schema/system.schema.yml index 7dcfd4db58e..2c01a98156c 100644 --- a/core/modules/system/config/schema/system.schema.yml +++ b/core/modules/system/config/schema/system.schema.yml @@ -39,7 +39,7 @@ system.site: type: integer label: 'Weight element maximum value' default_langcode: - type: string + type: langcode label: 'Site default language code' mail_notification: type: string diff --git a/core/tests/Drupal/KernelTests/Core/Config/ConfigEntityValidationTestBase.php b/core/tests/Drupal/KernelTests/Core/Config/ConfigEntityValidationTestBase.php index 2aa8cedb874..b0eafccf14d 100644 --- a/core/tests/Drupal/KernelTests/Core/Config/ConfigEntityValidationTestBase.php +++ b/core/tests/Drupal/KernelTests/Core/Config/ConfigEntityValidationTestBase.php @@ -4,7 +4,11 @@ namespace Drupal\KernelTests\Core\Config; use Drupal\Component\Utility\NestedArray; use Drupal\Core\Config\Entity\ConfigEntityInterface; +use Drupal\Core\Language\LanguageInterface; +use Drupal\Core\Language\LanguageManager; +use Drupal\Core\TypedData\Plugin\DataType\LanguageReference; use Drupal\KernelTests\KernelTestBase; +use Drupal\language\Entity\ConfigurableLanguage; /** * Base class for testing validation of config entities. @@ -311,4 +315,53 @@ abstract class ConfigEntityValidationTestBase extends KernelTestBase { $this->assertSame($expected_messages, $actual_messages); } + /** + * Tests that the config entity's langcode is validated. + */ + public function testLangcode(): void { + $this->entity->set('langcode', NULL); + $this->assertValidationErrors([ + 'langcode' => 'This value should not be null.', + ]); + + // A langcode from the standard list should always be acceptable. + $standard_languages = LanguageManager::getStandardLanguageList(); + $this->assertNotEmpty($standard_languages); + $this->entity->set('langcode', key($standard_languages)); + $this->assertValidationErrors([]); + + // All special, internal langcodes should be acceptable. + $system_langcodes = [ + LanguageInterface::LANGCODE_NOT_SPECIFIED, + LanguageInterface::LANGCODE_NOT_APPLICABLE, + LanguageInterface::LANGCODE_DEFAULT, + LanguageInterface::LANGCODE_SITE_DEFAULT, + LanguageInterface::LANGCODE_SYSTEM, + ]; + foreach ($system_langcodes as $langcode) { + $this->entity->set('langcode', $langcode); + $this->assertValidationErrors([]); + } + + // An invalid langcode should be unacceptable, even if it "looks" right. + $fake_langcode = 'definitely-not-a-language'; + $this->assertArrayNotHasKey($fake_langcode, LanguageReference::getAllValidLangcodes()); + $this->entity->set('langcode', $fake_langcode); + $this->assertValidationErrors([ + 'langcode' => 'The value you selected is not a valid choice.', + ]); + + // If a new configurable language is created with a non-standard langcode, + // it should be acceptable. + $this->enableModules(['language']); + // The language doesn't exist yet, so it shouldn't be a valid choice. + $this->entity->set('langcode', 'kthxbai'); + $this->assertValidationErrors([ + 'langcode' => 'The value you selected is not a valid choice.', + ]); + // Once we create the language, it should be a valid choice. + ConfigurableLanguage::createFromLangcode('kthxbai')->save(); + $this->assertValidationErrors([]); + } + } diff --git a/core/tests/Drupal/KernelTests/Core/Config/ConfigSchemaTest.php b/core/tests/Drupal/KernelTests/Core/Config/ConfigSchemaTest.php index 05e7b54c79a..f04e330a420 100644 --- a/core/tests/Drupal/KernelTests/Core/Config/ConfigSchemaTest.php +++ b/core/tests/Drupal/KernelTests/Core/Config/ConfigSchemaTest.php @@ -68,8 +68,7 @@ class ConfigSchemaTest extends KernelTestBase { $expected = []; $expected['label'] = 'Schema test data'; $expected['class'] = Mapping::class; - $expected['mapping']['langcode']['type'] = 'string'; - $expected['mapping']['langcode']['label'] = 'Language code'; + $expected['mapping']['langcode']['type'] = 'langcode'; $expected['mapping']['_core']['type'] = '_core_config_info'; $expected['mapping']['testitem'] = ['label' => 'Test item']; $expected['mapping']['testlist'] = ['label' => 'Test list']; @@ -115,8 +114,7 @@ class ConfigSchemaTest extends KernelTestBase { 'type' => 'text', ]; $expected['mapping']['langcode'] = [ - 'label' => 'Language code', - 'type' => 'string', + 'type' => 'langcode', ]; $expected['mapping']['_core']['type'] = '_core_config_info'; $expected['type'] = 'system.maintenance'; @@ -131,8 +129,7 @@ class ConfigSchemaTest extends KernelTestBase { $expected['class'] = Mapping::class; $expected['definition_class'] = '\Drupal\Core\TypedData\MapDataDefinition'; $expected['mapping']['langcode'] = [ - 'type' => 'string', - 'label' => 'Language code', + 'type' => 'langcode', ]; $expected['mapping']['_core']['type'] = '_core_config_info'; $expected['mapping']['label'] = [ @@ -179,8 +176,7 @@ class ConfigSchemaTest extends KernelTestBase { $expected['mapping']['name']['type'] = 'machine_name'; $expected['mapping']['uuid']['type'] = 'uuid'; $expected['mapping']['uuid']['label'] = 'UUID'; - $expected['mapping']['langcode']['type'] = 'string'; - $expected['mapping']['langcode']['label'] = 'Language code'; + $expected['mapping']['langcode']['type'] = 'langcode'; $expected['mapping']['status']['type'] = 'boolean'; $expected['mapping']['status']['label'] = 'Status'; $expected['mapping']['dependencies']['type'] = 'config_dependencies'; @@ -247,8 +243,7 @@ class ConfigSchemaTest extends KernelTestBase { $expected = []; $expected['label'] = 'Schema multiple filesystem marker test'; $expected['class'] = Mapping::class; - $expected['mapping']['langcode']['type'] = 'string'; - $expected['mapping']['langcode']['label'] = 'Language code'; + $expected['mapping']['langcode']['type'] = 'langcode'; $expected['mapping']['_core']['type'] = '_core_config_info'; $expected['mapping']['testid']['type'] = 'string'; $expected['mapping']['testid']['label'] = 'ID'; @@ -518,8 +513,7 @@ class ConfigSchemaTest extends KernelTestBase { $expected['class'] = Mapping::class; $expected['definition_class'] = '\Drupal\Core\TypedData\MapDataDefinition'; $expected['unwrap_for_canonical_representation'] = TRUE; - $expected['mapping']['langcode']['type'] = 'string'; - $expected['mapping']['langcode']['label'] = 'Language code'; + $expected['mapping']['langcode']['type'] = 'langcode'; $expected['mapping']['_core']['type'] = '_core_config_info'; $expected['mapping']['testid']['type'] = 'string'; $expected['mapping']['testid']['label'] = 'ID';