diff --git a/core/config/schema/core.data_types.schema.yml b/core/config/schema/core.data_types.schema.yml index 3edb8057..4ab0fd7 100644 --- a/core/config/schema/core.data_types.schema.yml +++ b/core/config/schema/core.data_types.schema.yml @@ -87,12 +87,22 @@ color_hex: # Complex extended data types: # Root of a configuration object. + +_core_config_info: + type: mapping + mapping: + default_config_hash: + type: string + label: 'Default configuration hash' + config_object: type: mapping mapping: langcode: type: string label: 'Language code' + _core: + type: _core_config_info # Mail text with subject and body parts. mail: @@ -281,6 +291,8 @@ config_entity: label: 'Third party settings' sequence: type: '[%parent.%parent.%type].third_party.[%key]' + _core: + type: _core_config_info block_settings: type: mapping diff --git a/core/lib/Drupal/Core/Config/ConfigInstaller.php b/core/lib/Drupal/Core/Config/ConfigInstaller.php index ae12c91..04f517d 100644 --- a/core/lib/Drupal/Core/Config/ConfigInstaller.php +++ b/core/lib/Drupal/Core/Config/ConfigInstaller.php @@ -7,6 +7,7 @@ namespace Drupal\Core\Config; +use Drupal\Component\Utility\Crypt; use Drupal\Component\Utility\Unicode; use Drupal\Core\Config\Entity\ConfigDependencyManager; use Drupal\Core\Config\Entity\ConfigEntityDependency; @@ -279,6 +280,9 @@ protected function createConfiguration($collection, array $config_to_create) { } if ($config_to_create[$name] !== FALSE) { $new_config->setData($config_to_create[$name]); + if (!$this->isSyncing()) { + $new_config->set('_core.default_config_hash', Crypt::hashBase64(serialize($config_to_create[$name]))); + } } if ($collection == StorageInterface::DEFAULT_COLLECTION && $entity_type = $this->configManager->getEntityTypeIdByName($name)) { // If we are syncing do not create configuration entities. Pluggable diff --git a/core/lib/Drupal/Core/Config/Entity/ConfigEntityBase.php b/core/lib/Drupal/Core/Config/Entity/ConfigEntityBase.php index bb396e6..8c0c376 100644 --- a/core/lib/Drupal/Core/Config/Entity/ConfigEntityBase.php +++ b/core/lib/Drupal/Core/Config/Entity/ConfigEntityBase.php @@ -104,6 +104,17 @@ protected $third_party_settings = array(); /** + * Information maintained by Drupal core about configuration. + * + * Keys: + * - default_config_hash: A hash calculated by the config.installer service + * and added during installation. + * + * @var array + */ + protected $_core = []; + + /** * Trust supplied data and not use configuration schema on save. * * @var bool @@ -296,6 +307,9 @@ public function toArray() { if (empty($this->third_party_settings)) { unset($properties['third_party_settings']); } + if (empty($this->_core)) { + unset($properties['_core']); + } return $properties; } diff --git a/core/lib/Drupal/Core/Config/Entity/ConfigEntityType.php b/core/lib/Drupal/Core/Config/Entity/ConfigEntityType.php index 527a336..785b314 100644 --- a/core/lib/Drupal/Core/Config/Entity/ConfigEntityType.php +++ b/core/lib/Drupal/Core/Config/Entity/ConfigEntityType.php @@ -157,6 +157,7 @@ public function getPropertiesToExport() { 'status' => 'status', 'dependencies' => 'dependencies', 'third_party_settings' => 'third_party_settings', + '_core' => '_core', ]; foreach ($this->config_export as $property => $name) { if (is_numeric($property)) { diff --git a/core/modules/config/src/Tests/ConfigSchemaTest.php b/core/modules/config/src/Tests/ConfigSchemaTest.php index a94e26a..8735db1 100644 --- a/core/modules/config/src/Tests/ConfigSchemaTest.php +++ b/core/modules/config/src/Tests/ConfigSchemaTest.php @@ -63,6 +63,7 @@ function testSchemaMapping() { $expected['class'] = '\Drupal\Core\Config\Schema\Mapping'; $expected['mapping']['langcode']['type'] = 'string'; $expected['mapping']['langcode']['label'] = 'Language code'; + $expected['mapping']['_core']['type'] = '_core_config_info'; $expected['mapping']['testitem'] = array('label' => 'Test item'); $expected['mapping']['testlist'] = array('label' => 'Test list'); $expected['type'] = 'config_schema_test.someschema'; @@ -106,6 +107,7 @@ function testSchemaMapping() { 'label' => 'Language code', 'type' => 'string', ); + $expected['mapping']['_core']['type'] = '_core_config_info'; $expected['type'] = 'system.maintenance'; $expected['definition_class'] = '\Drupal\Core\TypedData\MapDataDefinition'; $this->assertEqual($definition, $expected, 'Retrieved the right metadata for system.maintenance'); @@ -120,6 +122,7 @@ function testSchemaMapping() { 'type' => 'string', 'label' => 'Language code', ); + $expected['mapping']['_core']['type'] = '_core_config_info'; $expected['mapping']['label'] = array( 'label' => 'Label', 'type' => 'label', @@ -179,6 +182,7 @@ function testSchemaMapping() { $expected['mapping']['third_party_settings']['type'] = 'sequence'; $expected['mapping']['third_party_settings']['label'] = 'Third party settings'; $expected['mapping']['third_party_settings']['sequence']['type'] = '[%parent.%parent.%type].third_party.[%key]'; + $expected['mapping']['_core']['type'] = '_core_config_info'; $expected['type'] = 'image.style.*'; $this->assertEqual($definition, $expected); @@ -231,6 +235,7 @@ function testSchemaMapping() { $expected['class'] = '\Drupal\Core\Config\Schema\Mapping'; $expected['mapping']['langcode']['type'] = 'string'; $expected['mapping']['langcode']['label'] = 'Language code'; + $expected['mapping']['_core']['type'] = '_core_config_info'; $expected['mapping']['testid']['type'] = 'string'; $expected['mapping']['testid']['label'] = 'ID'; $expected['mapping']['testdescription']['type'] = 'text'; @@ -386,7 +391,9 @@ public function testConfigSaveWithSchema() { $extension_path = drupal_get_path('module', 'config_schema_test'); $install_storage = new FileStorage($extension_path . '/' . InstallStorage::CONFIG_INSTALL_DIRECTORY); $original_data = $install_storage->read('config_schema_test.ignore'); - $this->assertIdentical($this->config('config_schema_test.ignore')->get(), $original_data); + $installed_data = $this->config('config_schema_test.ignore')->get(); + unset($installed_data['_core']); + $this->assertIdentical($installed_data, $original_data); } /** @@ -401,6 +408,7 @@ function testSchemaFallback() { $expected['definition_class'] = '\Drupal\Core\TypedData\MapDataDefinition'; $expected['mapping']['langcode']['type'] = 'string'; $expected['mapping']['langcode']['label'] = 'Language code'; + $expected['mapping']['_core']['type'] = '_core_config_info'; $expected['mapping']['testid']['type'] = 'string'; $expected['mapping']['testid']['label'] = 'ID'; $expected['mapping']['testdescription']['type'] = 'text'; diff --git a/core/modules/locale/locale.services.yml b/core/modules/locale/locale.services.yml index 29603ee..e37cb48 100644 --- a/core/modules/locale/locale.services.yml +++ b/core/modules/locale/locale.services.yml @@ -5,7 +5,7 @@ services: public: false locale.config_manager: class: Drupal\locale\LocaleConfigManager - arguments: ['@config.storage', '@locale.storage', '@config.factory', '@config.typed', '@language_manager', '@locale.default.config.storage'] + arguments: ['@config.storage', '@locale.storage', '@config.factory', '@config.typed', '@language_manager', '@locale.default.config.storage', '@config.manager'] locale.storage: class: Drupal\locale\StringDatabaseStorage arguments: ['@database'] diff --git a/core/modules/locale/src/LocaleConfigManager.php b/core/modules/locale/src/LocaleConfigManager.php index cab8816..d37c73b 100644 --- a/core/modules/locale/src/LocaleConfigManager.php +++ b/core/modules/locale/src/LocaleConfigManager.php @@ -9,6 +9,7 @@ use Drupal\Component\Utility\NestedArray; use Drupal\Core\Config\ConfigFactoryInterface; +use Drupal\Core\Config\ConfigManagerInterface; use Drupal\Core\Config\StorageInterface; use Drupal\Core\Config\TypedConfigManagerInterface; use Drupal\Core\StringTranslation\TranslatableMarkup; @@ -96,6 +97,13 @@ class LocaleConfigManager { protected $defaultConfigStorage; /** + * The configuration manager. + * + * @var \Drupal\Core\Config\ConfigManagerInterface + */ + protected $configManager; + + /** * Creates a new typed configuration manager. * * @param \Drupal\Core\Config\StorageInterface $config_storage @@ -111,13 +119,14 @@ class LocaleConfigManager { * @param \Drupal\locale\LocaleDefaultConfigStorage $default_config_storage * The locale default configuration storage. */ - public function __construct(StorageInterface $config_storage, StringStorageInterface $locale_storage, ConfigFactoryInterface $config_factory, TypedConfigManagerInterface $typed_config, ConfigurableLanguageManagerInterface $language_manager, LocaleDefaultConfigStorage $default_config_storage) { + public function __construct(StorageInterface $config_storage, StringStorageInterface $locale_storage, ConfigFactoryInterface $config_factory, TypedConfigManagerInterface $typed_config, ConfigurableLanguageManagerInterface $language_manager, LocaleDefaultConfigStorage $default_config_storage, ConfigManagerInterface $config_manager) { $this->configStorage = $config_storage; $this->localeStorage = $locale_storage; $this->configFactory = $config_factory; $this->typedConfigManager = $typed_config; $this->languageManager = $language_manager; $this->defaultConfigStorage = $default_config_storage; + $this->configManager = $config_manager; } /** @@ -477,10 +486,21 @@ public function hasTranslation($name, $langcode) { * configuration exists. */ public function getDefaultConfigLangcode($name) { - $shipped = $this->defaultConfigStorage->read($name); - if (!empty($shipped)) { - return !empty($shipped['langcode']) ? $shipped['langcode'] : 'en'; + // Config entities that do not have the 'default_config_hash' cannot be + // shipped configuration regardless of whether there is a name match. + // configurable_language entities are a special case since they can be + // translated regardless of whether they are shipped if they in the standard + // language list. + $config_entity_type = $this->configManager->getEntityTypeIdByName($name); + if (!$config_entity_type || $config_entity_type === 'configurable_language' + || !empty($this->configFactory->get($name)->get('_core.default_config_hash')) + ) { + $shipped = $this->defaultConfigStorage->read($name); + if (!empty($shipped)) { + return !empty($shipped['langcode']) ? $shipped['langcode'] : 'en'; + } } + return NULL; } /** diff --git a/core/modules/locale/src/Tests/LocaleConfigManagerTest.php b/core/modules/locale/src/Tests/LocaleConfigManagerTest.php index 39f14cb..193f4fa 100644 --- a/core/modules/locale/src/Tests/LocaleConfigManagerTest.php +++ b/core/modules/locale/src/Tests/LocaleConfigManagerTest.php @@ -7,6 +7,7 @@ namespace Drupal\locale\Tests; +use Drupal\block\Entity\Block; use Drupal\language\Entity\ConfigurableLanguage; use Drupal\simpletest\KernelTestBase; @@ -22,7 +23,14 @@ class LocaleConfigManagerTest extends KernelTestBase { * * @var array */ - public static $modules = array('language', 'locale', 'locale_test'); + public static $modules = array('system', 'language', 'locale', 'locale_test', 'block'); + + /** + * This test creates simple config on the fly breaking schema checking. + * + * @var bool + */ + protected $strictConfigSchema = FALSE; /** * Tests hasTranslation(). @@ -72,6 +80,49 @@ public function testGetDefaultConfigLangcode() { $this->assertNull(\Drupal::service('locale.config_manager')->getDefaultConfigLangcode('locale_test_translate.settings'), 'Before installing a module the locale config manager can not access the shipped configuration.'); \Drupal::service('module_installer')->install(['locale_test_translate']); $this->assertEqual('en', \Drupal::service('locale.config_manager')->getDefaultConfigLangcode('locale_test_translate.settings'), 'After installing a module the locale config manager can get the shipped configuration langcode.'); + + $simple_config = \Drupal::configFactory()->getEditable('locale_test_translate.simple_config_extra'); + $simple_config->set('foo', 'bar')->save(); + $this->assertNull(\Drupal::service('locale.config_manager')->getDefaultConfigLangcode($simple_config->getName()), 'Simple config created through the API is not treated as shipped configuration.'); + + $block = Block::create(array( + 'id' => 'test_default_config', + 'theme' => 'classy', + 'status' => TRUE, + 'region' => 'content', + 'plugin' => 'local_tasks_block', + 'settings' => [ + 'id' => 'local_tasks_block', + 'label' => $this->randomMachineName(), + 'provider' => 'core', + 'label_display' => FALSE, + 'primary' => TRUE, + 'secondary' => TRUE, + ], + )); + $block->save(); + + // Install the theme after creating the block as installing the theme will + // install the block provided by the locale_test module. + \Drupal::service('theme_installer')->install(['classy']); + + // The test_default_config block provided by the locale_test module will not + // be installed because a block with the same ID already exists. + $this->installConfig(['locale_test']); + $this->assertNull(\Drupal::service('locale.config_manager')->getDefaultConfigLangcode('block.block.test_default_config'), 'The block.block.test_default_config is not shipped configuration.'); + // Delete the block so we can install the one provided by the locale_test + // module. + $block->delete(); + $this->installConfig(['locale_test']); + $this->assertEqual('en', \Drupal::service('locale.config_manager')->getDefaultConfigLangcode('block.block.test_default_config'), 'The block.block.test_default_config is shipped configuration.'); + + // Test the special case for configurable_language config entities. + $fr_language = ConfigurableLanguage::createFromLangcode('fr'); + $fr_language->save(); + $this->assertEqual('en', \Drupal::service('locale.config_manager')->getDefaultConfigLangcode('language.entity.fr'), 'The language.entity.fr is treated as shipped configuration because it is a configurable_language config entity and in the standard language list.'); + $custom_language = ConfigurableLanguage::createFromLangcode('custom'); + $custom_language->save(); + $this->assertNull(\Drupal::service('locale.config_manager')->getDefaultConfigLangcode('language.entity.custom'), 'The language.entity.custom is not shipped configuration because it is not in the standard language list.'); } } diff --git a/core/modules/locale/tests/modules/locale_test/config/optional/block.block.test_default_config.yml b/core/modules/locale/tests/modules/locale_test/config/optional/block.block.test_default_config.yml new file mode 100644 index 0000000..15f0807 --- /dev/null +++ b/core/modules/locale/tests/modules/locale_test/config/optional/block.block.test_default_config.yml @@ -0,0 +1,19 @@ +langcode: en +status: true +dependencies: + theme: + - classy +id: test_default_config +theme: classy +region: content +weight: -40 +provider: null +plugin: local_tasks_block +settings: + id: local_tasks_block + label: Tabs + provider: core + label_display: '0' + primary: true + secondary: true +visibility: { } diff --git a/core/tests/Drupal/KernelTests/AssertConfigTrait.php b/core/tests/Drupal/KernelTests/AssertConfigTrait.php index 24d393e..203e85f 100644 --- a/core/tests/Drupal/KernelTests/AssertConfigTrait.php +++ b/core/tests/Drupal/KernelTests/AssertConfigTrait.php @@ -74,6 +74,10 @@ protected function assertConfigDiff(Diff $result, $config_name, array $skipped_c } break; case 'Drupal\Component\Diff\Engine\DiffOpAdd': + // The _core property does not exist in the default config. + if ($op->closing[0] === '_core:') { + continue; + } foreach ($op->closing as $closing) { // The UUIDs don't exist in the default config. if (strpos($closing, 'uuid: ') === 0) { diff --git a/core/tests/Drupal/Tests/Core/Config/Entity/ConfigEntityTypeTest.php b/core/tests/Drupal/Tests/Core/Config/Entity/ConfigEntityTypeTest.php index 97bf172..8104efc 100644 --- a/core/tests/Drupal/Tests/Core/Config/Entity/ConfigEntityTypeTest.php +++ b/core/tests/Drupal/Tests/Core/Config/Entity/ConfigEntityTypeTest.php @@ -163,6 +163,7 @@ public function providerGetPropertiesToExport() { 'status' => 'status', 'dependencies' => 'dependencies', 'third_party_settings' => 'third_party_settings', + '_core' => '_core', 'id' => 'id', 'custom_property' => 'customProperty', ],