From dd8c0e6b6caa98b1ca977fdc9384818e15b7fb0f Mon Sep 17 00:00:00 2001
From: Nathaniel Catchpole <catch@35733.no-reply.drupal.org>
Date: Fri, 11 Dec 2015 11:03:17 +0000
Subject: [PATCH] Issue #2625258 by alexpott:
 LocaleConfigManager::updateConfigTranslations() deletes translations if a
 config object's name happens to match that of a shipped configuration object

---
 core/config/schema/core.data_types.schema.yml | 12 ++++
 .../Drupal/Core/Config/ConfigInstaller.php    |  7 +++
 .../Core/Config/Entity/ConfigEntityBase.php   | 14 +++++
 .../Core/Config/Entity/ConfigEntityType.php   |  1 +
 .../config/src/Tests/ConfigCRUDTest.php       |  2 +
 .../config/src/Tests/ConfigDiffTest.php       |  5 +-
 .../ConfigInstallProfileOverrideTest.php      |  2 +
 .../config/src/Tests/ConfigSchemaTest.php     | 10 +++-
 .../Tests/ConfigSingleImportExportTest.php    |  2 +-
 core/modules/locale/locale.services.yml       |  2 +
 .../locale/src/LocaleConfigManager.php        | 59 ++++++++++++++++++-
 .../src/Tests/LocaleConfigManagerTest.php     | 53 ++++++++++++++++-
 .../block.block.test_default_config.yml       | 19 ++++++
 .../Tests/Migrate/d7/MigrateUserFloodTest.php |  3 +
 .../Drupal/KernelTests/AssertConfigTrait.php  |  4 ++
 .../Config/Entity/ConfigEntityTypeTest.php    |  1 +
 16 files changed, 186 insertions(+), 10 deletions(-)
 create mode 100644 core/modules/locale/tests/modules/locale_test/config/optional/block.block.test_default_config.yml

diff --git a/core/config/schema/core.data_types.schema.yml b/core/config/schema/core.data_types.schema.yml
index 3edb8057a346..4ab0fd769c3d 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 ae12c918607b..a77bb1c2f7dd 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,12 @@ protected function createConfiguration($collection, array $config_to_create) {
       }
       if ($config_to_create[$name] !== FALSE) {
         $new_config->setData($config_to_create[$name]);
+        // Add a hash to configuration created through the installer so it is
+        // possible to know if the configuration was created by installing an
+        // extension and to track which version of the default config was used.
+        if (!$this->isSyncing() && $collection == StorageInterface::DEFAULT_COLLECTION) {
+          $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 bb396e6d434e..8c0c376c8e0d 100644
--- a/core/lib/Drupal/Core/Config/Entity/ConfigEntityBase.php
+++ b/core/lib/Drupal/Core/Config/Entity/ConfigEntityBase.php
@@ -103,6 +103,17 @@ abstract class ConfigEntityBase extends Entity implements ConfigEntityInterface
    */
   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.
    *
@@ -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 527a336f3e91..785b314547ff 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/ConfigCRUDTest.php b/core/modules/config/src/Tests/ConfigCRUDTest.php
index e12789222cae..3d61c9e3f707 100644
--- a/core/modules/config/src/Tests/ConfigCRUDTest.php
+++ b/core/modules/config/src/Tests/ConfigCRUDTest.php
@@ -7,6 +7,7 @@
 
 namespace Drupal\config\Tests;
 
+use Drupal\Component\Utility\Crypt;
 use Drupal\Component\Utility\SafeMarkup;
 use Drupal\Core\Config\ConfigNameException;
 use Drupal\Core\Config\ConfigValueException;
@@ -263,6 +264,7 @@ public function testDataTypes() {
       'string' => 'string',
       'string_int' => '1',
     );
+    $data['_core']['default_config_hash'] = Crypt::hashBase64(serialize($data));
     $this->assertIdentical($config->get(), $data);
 
     // Re-set each key using Config::set().
diff --git a/core/modules/config/src/Tests/ConfigDiffTest.php b/core/modules/config/src/Tests/ConfigDiffTest.php
index efb5db33a7d6..372970f34168 100644
--- a/core/modules/config/src/Tests/ConfigDiffTest.php
+++ b/core/modules/config/src/Tests/ConfigDiffTest.php
@@ -35,13 +35,10 @@ function testDiff() {
     $add_key = 'biff';
     $add_data = 'bangpow';
     $change_data = 'foobar';
-    $original_data = array(
-      'foo' => 'bar',
-      '404' => 'herp',
-    );
 
     // Install the default config.
     $this->installConfig(array('config_test'));
+    $original_data = \Drupal::config($config_name)->get();
 
     // Change a configuration value in sync.
     $sync_data = $original_data;
diff --git a/core/modules/config/src/Tests/ConfigInstallProfileOverrideTest.php b/core/modules/config/src/Tests/ConfigInstallProfileOverrideTest.php
index e10dfe0b188d..36697a53e7e2 100644
--- a/core/modules/config/src/Tests/ConfigInstallProfileOverrideTest.php
+++ b/core/modules/config/src/Tests/ConfigInstallProfileOverrideTest.php
@@ -7,6 +7,7 @@
 
 namespace Drupal\config\Tests;
 
+use Drupal\Component\Utility\Crypt;
 use Drupal\Core\Config\InstallStorage;
 use Drupal\simpletest\WebTestBase;
 use Drupal\Core\Config\FileStorage;
@@ -48,6 +49,7 @@ function testInstallProfileConfigOverwrite() {
         'requirements_error' => 1209600,
       ),
     );
+    $expected_profile_data['_core']['default_config_hash'] = Crypt::hashBase64(serialize($expected_profile_data));
 
     // Verify that the original data matches. We have to read the module config
     // file directly, because the install profile default system.cron.yml
diff --git a/core/modules/config/src/Tests/ConfigSchemaTest.php b/core/modules/config/src/Tests/ConfigSchemaTest.php
index a94e26ad61d2..8735db16d9ea 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/config/src/Tests/ConfigSingleImportExportTest.php b/core/modules/config/src/Tests/ConfigSingleImportExportTest.php
index f08c7bd71970..088129afc901 100644
--- a/core/modules/config/src/Tests/ConfigSingleImportExportTest.php
+++ b/core/modules/config/src/Tests/ConfigSingleImportExportTest.php
@@ -202,7 +202,7 @@ public function testExport() {
     $this->assertIdentical($expected_options, array_intersect($expected_options, $options), 'The expected configuration files are listed.');
 
     $this->drupalGet('admin/config/development/configuration/single/export/system.simple/system.image');
-    $this->assertFieldByXPath('//textarea[@name="export"]', "toolkit: gd\n", 'The expected system configuration is displayed.');
+    $this->assertFieldByXPath('//textarea[@name="export"]', "toolkit: gd\n_core:\n  default_config_hash: durWHaKeBaq4d9Wpi4RqwADj1OufDepcnJuhVLmKN24\n", 'The expected system configuration is displayed.');
 
     $this->drupalGet('admin/config/development/configuration/single/export/date_format');
     $this->assertFieldByXPath('//select[@name="config_type"]//option[@selected="selected"]', t('Date format'), 'The date format entity type is selected when specified in the URL.');
diff --git a/core/modules/locale/locale.services.yml b/core/modules/locale/locale.services.yml
index 29603ee22aa5..1d55460bf06b 100644
--- a/core/modules/locale/locale.services.yml
+++ b/core/modules/locale/locale.services.yml
@@ -6,6 +6,8 @@ services:
   locale.config_manager:
     class: Drupal\locale\LocaleConfigManager
     arguments: ['@config.storage', '@locale.storage', '@config.factory', '@config.typed', '@language_manager', '@locale.default.config.storage']
+    calls:
+      - [_setConfigManager, ['@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 cab88166131d..db6c181f1460 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;
@@ -95,6 +96,17 @@ class LocaleConfigManager {
    */
   protected $defaultConfigStorage;
 
+  /**
+   * The configuration manager.
+   *
+   * @var \Drupal\Core\Config\ConfigManagerInterface
+   *
+   * @internal
+   *   Will be made protected and renamed to $configManager in 8.1.0.
+   *   https://www.drupal.org/node/2628132
+   */
+  private $_configManager;
+
   /**
    * Creates a new typed configuration manager.
    *
@@ -120,6 +132,36 @@ public function __construct(StorageInterface $config_storage, StringStorageInter
     $this->defaultConfigStorage = $default_config_storage;
   }
 
+  /**
+   * Sets the configuration manager service.
+   *
+   * @param \Drupal\Core\Config\ConfigManagerInterface $config_manager
+   *
+   * @internal
+   *   Will be replaced by constructor injection in 8.1.0.
+   *   https://www.drupal.org/node/2628132
+   */
+  public function _setConfigManager(ConfigManagerInterface $config_manager) {
+    $this->_configManager = $config_manager;
+  }
+
+  /**
+   * Gets the configuration manager service.
+   *
+   * @return \Drupal\Core\Config\ConfigManagerInterface
+   *   The config manager
+   *
+   * @internal
+   *   Will be replaced by constructor injection in 8.1.0.
+   *   https://www.drupal.org/node/2628132
+   */
+  private final function _getConfigManager() {
+    if (!isset($this->_configManager)) {
+      $this->_configManager = \Drupal::service('config.manager');
+    }
+    return $this->_configManager;
+  }
+
   /**
    * Gets array of translated strings for Locale translatable configuration.
    *
@@ -477,10 +519,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->_getConfigManager()->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 39f14cba357e..193f4facb828 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 000000000000..15f08072d093
--- /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/modules/user/src/Tests/Migrate/d7/MigrateUserFloodTest.php b/core/modules/user/src/Tests/Migrate/d7/MigrateUserFloodTest.php
index 786fcd26b083..020e15cc2233 100644
--- a/core/modules/user/src/Tests/Migrate/d7/MigrateUserFloodTest.php
+++ b/core/modules/user/src/Tests/Migrate/d7/MigrateUserFloodTest.php
@@ -35,6 +35,9 @@ public function testMigration() {
       'ip_window' => 7200,
       'user_limit' => 22,
       'user_window' => 86400,
+      '_core' => [
+        'default_config_hash' => 'UYfMzeP1S8jKm9PSvxf7nQNe8DsNS-3bc2WSNNXBQWs',
+      ],
     ];
     $this->assertIdentical($expected, $this->config('user.flood')->get());
   }
diff --git a/core/tests/Drupal/KernelTests/AssertConfigTrait.php b/core/tests/Drupal/KernelTests/AssertConfigTrait.php
index 24d393e77ce5..203e85f04b5b 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 97bf172c7a08..8104efc328a0 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',
       ],
-- 
GitLab