Commit e0aae8c2 authored by catch's avatar catch

Issue #2432791 by alexpott, vijaycs85, tim.plunkett, joshtaylor, Fabianx,...

Issue #2432791 by alexpott, vijaycs85, tim.plunkett, joshtaylor, Fabianx, Berdir, yched, bojanz: Skip Config::save schema validation of config data for trusted data.
parent f8f024e3
......@@ -1215,8 +1215,9 @@ function file_directory_temp() {
// everything to use slash which is supported on all platforms.
$temporary_directory = str_replace('\\', '/', $temporary_directory);
}
// Save the path of the discovered directory.
$config->set('path.temporary', $temporary_directory)->save();
// Save the path of the discovered directory. Do not check config schema on
// save.
$config->set('path.temporary', (string) $temporary_directory)->save(TRUE);
}
return $temporary_directory;
......
......@@ -619,9 +619,9 @@ function drupal_install_system($install_state) {
// Ensure default language is saved.
if (isset($install_state['parameters']['langcode'])) {
\Drupal::configFactory()->getEditable('system.site')
->set('langcode', $install_state['parameters']['langcode'])
->set('default_langcode', $install_state['parameters']['langcode'])
->save();
->set('langcode', (string) $install_state['parameters']['langcode'])
->set('default_langcode', (string) $install_state['parameters']['langcode'])
->save(TRUE);
}
}
......
......@@ -179,10 +179,12 @@ function drupal_required_modules() {
function module_set_weight($module, $weight) {
$extension_config = \Drupal::configFactory()->getEditable('core.extension');
if ($extension_config->get("module.$module") !== NULL) {
// Pre-cast the $weight to an integer so that we can save this without using
// schema. This is a performance improvement for module installation.
$extension_config
->set("module.$module", $weight)
->set("module.$module", (int) $weight)
->set('module', module_config_sort($extension_config->get('module')))
->save();
->save(TRUE);
// Prepare the new module list, sorted by weight, including filenames.
// @see \Drupal\Core\Extension\ModuleHandler::install()
......
......@@ -203,22 +203,24 @@ public function clear($key) {
/**
* {@inheritdoc}
*/
public function save() {
public function save($has_trusted_data = FALSE) {
// Validate the configuration object name before saving.
static::validateName($this->name);
// If there is a schema for this configuration object, cast all values to
// conform to the schema.
if ($this->typedConfigManager->hasConfigSchema($this->name)) {
// Ensure that the schema wrapper has the latest data.
$this->schemaWrapper = NULL;
foreach ($this->data as $key => $value) {
$this->data[$key] = $this->castValue($key, $value);
if (!$has_trusted_data) {
if ($this->typedConfigManager->hasConfigSchema($this->name)) {
// Ensure that the schema wrapper has the latest data.
$this->schemaWrapper = NULL;
foreach ($this->data as $key => $value) {
$this->data[$key] = $this->castValue($key, $value);
}
}
}
else {
foreach ($this->data as $key => $value) {
$this->validateValue($key, $value);
else {
foreach ($this->data as $key => $value) {
$this->validateValue($key, $value);
}
}
}
......@@ -229,6 +231,9 @@ public function save() {
$this->isNew = FALSE;
$this->eventDispatcher->dispatch(ConfigEvents::SAVE, new ConfigCrudEvent($this));
$this->originalData = $this->data;
// Potentially configuration schema could have changed the underlying data's
// types.
$this->resetOverriddenData();
return $this;
}
......@@ -302,4 +307,5 @@ public function getOriginal($key = '', $apply_overrides = TRUE) {
}
}
}
}
......@@ -304,11 +304,11 @@ protected function createConfiguration($collection, array $config_to_create) {
$entity = $entity_storage->createFromStorageRecord($new_config->get());
}
if ($entity->isInstallable()) {
$entity->save();
$entity->trustData()->save();
}
}
else {
$new_config->save();
$new_config->save(TRUE);
}
}
}
......
......@@ -105,6 +105,13 @@ abstract class ConfigEntityBase extends Entity implements ConfigEntityInterface
*/
protected $third_party_settings = array();
/**
* Trust supplied data and not use configuration schema on save.
*
* @var bool
*/
protected $trustedData = FALSE;
/**
* Overrides Entity::__construct().
*/
......@@ -265,22 +272,31 @@ public static function sort(ConfigEntityInterface $a, ConfigEntityInterface $b)
*/
public function toArray() {
$properties = array();
$config_name = $this->getEntityType()->getConfigPrefix() . '.' . $this->id();
$definition = $this->getTypedConfig()->getDefinition($config_name);
if (!isset($definition['mapping'])) {
throw new SchemaIncompleteException(SafeMarkup::format('Incomplete or missing schema for @config_name', array('@config_name' => $config_name)));
/** @var \Drupal\Core\Config\Entity\ConfigEntityTypeInterface $entity_type */
$entity_type = $this->getEntityType();
$properties_to_export = $entity_type->getPropertiesToExport();
if (empty($properties_to_export)) {
$config_name = $entity_type->getConfigPrefix() . '.' . $this->id();
$definition = $this->getTypedConfig()->getDefinition($config_name);
if (!isset($definition['mapping'])) {
throw new SchemaIncompleteException(SafeMarkup::format('Incomplete or missing schema for @config_name', array('@config_name' => $config_name)));
}
$properties_to_export = array_combine(array_keys($definition['mapping']), array_keys($definition['mapping']));
}
$id_key = $this->getEntityType()->getKey('id');
foreach (array_keys($definition['mapping']) as $name) {
$id_key = $entity_type->getKey('id');
foreach ($properties_to_export as $property_name => $export_name) {
// Special handling for IDs so that computed compound IDs work.
// @see \Drupal\Core\Entity\EntityDisplayBase::id()
if ($name == $id_key) {
$properties[$name] = $this->id();
if ($property_name == $id_key) {
$properties[$export_name] = $this->id();
}
else {
$properties[$name] = $this->get($name);
$properties[$export_name] = $this->get($property_name);
}
}
if (empty($this->third_party_settings)) {
unset($properties['third_party_settings']);
}
......@@ -328,7 +344,7 @@ public function preSave(EntityStorageInterface $storage) {
throw new ConfigDuplicateUUIDException(SafeMarkup::format('Attempt to save a configuration entity %id with UUID %uuid when this entity already exists with UUID %original_uuid', array('%id' => $this->id(), '%uuid' => $this->uuid(), '%original_uuid' => $original->uuid())));
}
}
if (!$this->isSyncing()) {
if (!$this->isSyncing() && !$this->trustedData) {
// Ensure the correct dependencies are present. If the configuration is
// being written during a configuration synchronization then there is no
// need to recalculate the dependencies.
......@@ -572,4 +588,28 @@ public function isInstallable() {
return TRUE;
}
/**
* {@inheritdoc}
*/
public function trustData() {
$this->trustedData = TRUE;
return $this;
}
/**
* {@inheritdoc}
*/
public function hasTrustedData() {
return $this->trustedData;
}
/**
* {@inheritdoc}
*/
public function save() {
$return = parent::save();
$this->trustedData = FALSE;
return $return;
}
}
......@@ -203,4 +203,27 @@ public function getDependencies();
*/
public function isInstallable();
/**
* Sets that the data should be trusted.
*
* If the data is trusted then dependencies will not be calculated on save and
* schema will not be used to cast the values. Generally this is only used
* during module and theme installation. Once the config entity has been saved
* the data will no longer be marked as trusted. This is an optimization for
* creation of configuration during installation.
*
* @return $this
*
* @see \Drupal\Core\Config\ConfigInstaller::createConfiguration()
*/
public function trustData();
/**
* Gets whether on not the data is trusted.
*
* @return bool
* TRUE if the configuration data is trusted, FALSE if not.
*/
public function hasTrustedData();
}
......@@ -256,7 +256,19 @@ protected function doSave($id, EntityInterface $entity) {
// Retrieve the desired properties and set them in config.
$config->setData($this->mapToStorageRecord($entity));
$config->save();
$config->save($entity->hasTrustedData());
// Update the entity with the values stored in configuration. It is possible
// that configuration schema has casted some of the values.
if (!$entity->hasTrustedData()) {
$data = $this->mapFromStorageRecords(array($config->get()));
$updated_entity = current($data);
foreach (array_keys($config->get()) as $property) {
$value = $updated_entity->get($property);
$entity->set($property, $value);
}
}
return $is_new ? SAVED_NEW : SAVED_UPDATED;
}
......
......@@ -34,6 +34,22 @@ class ConfigEntityType extends EntityType implements ConfigEntityTypeInterface {
*/
protected $static_cache = FALSE;
/**
* The list of configuration entity properties to export from the annotation.
*
* @var array
*/
protected $config_export = [];
/**
* The result of merging config_export annotation with the defaults.
*
* This is stored on the class so that it does not have to be recalculated.
*
* @var array
*/
protected $mergedConfigExport = [];
/**
* {@inheritdoc}
*
......@@ -146,4 +162,32 @@ protected function checkStorageClass($class) {
}
}
/**
* {@inheritdoc}
*/
public function getPropertiesToExport() {
if (!empty($this->config_export)) {
if (empty($this->mergedConfigExport)) {
// Always add default properties to be exported.
$this->mergedConfigExport = [
'uuid' => 'uuid',
'langcode' => 'langcode',
'status' => 'status',
'dependencies' => 'dependencies',
'third_party_settings' => 'third_party_settings',
];
foreach ($this->config_export as $property => $name) {
if (is_numeric($property)) {
$this->mergedConfigExport[$name] = $name;
}
else {
$this->mergedConfigExport[$property] = $name;
}
}
}
return $this->mergedConfigExport;
}
return NULL;
}
}
......@@ -61,4 +61,13 @@ interface ConfigEntityTypeInterface extends EntityTypeInterface {
*/
public function getConfigPrefix();
/**
* Gets the config entity properties to export if declared on the annotation.
*
* @return array|NULL
* The properties to export or NULL if they can not be determine from the
* config entity type annotation.
*/
public function getPropertiesToExport();
}
......@@ -44,7 +44,7 @@ public function clear($key) {
/**
* {@inheritdoc}
*/
public function save() {
public function save($has_trusted_data = FALSE) {
throw new ImmutableConfigException(SafeMarkup::format('Can not save immutable configuration !name. Use \Drupal\Core\Config\ConfigFactoryInterface::getEditable() to retrieve a mutable configuration object', ['!name' => $this->getName()]));
}
......
......@@ -66,11 +66,18 @@ abstract class StorableConfigBase extends ConfigBase {
/**
* Saves the configuration object.
*
* @param bool $has_trusted_data
* Set to TRUE is the configuration data has already been checked to ensure
* it conforms to schema. Generally this is only used during module and
* theme installation.
*
* Must invalidate the cache tags associated with the configuration object.
*
* @return $this
*
* @see \Drupal\Core\Config\ConfigInstaller::createConfiguration()
*/
abstract public function save();
abstract public function save($has_trusted_data = FALSE);
/**
* Deletes the configuration object.
......
......@@ -25,7 +25,13 @@
* "label" = "label"
* },
* admin_permission = "administer site configuration",
* list_cache_tags = { "rendered" }
* list_cache_tags = { "rendered" },
* config_export = {
* "id",
* "label",
* "locked",
* "pattern",
* }
* )
*/
class DateFormat extends ConfigEntityBase implements DateFormatInterface {
......
......@@ -23,6 +23,14 @@
* entity_keys = {
* "id" = "id",
* "status" = "status"
* },
* config_export = {
* "id",
* "targetEntityType",
* "bundle",
* "mode",
* "content",
* "hidden",
* }
* )
*/
......
......@@ -33,6 +33,12 @@
* entity_keys = {
* "id" = "id",
* "label" = "label"
* },
* config_export = {
* "id",
* "label",
* "targetEntityType",
* "cache",
* }
* )
*/
......
......@@ -24,6 +24,14 @@
* entity_keys = {
* "id" = "id",
* "status" = "status"
* },
* config_export = {
* "id",
* "targetEntityType",
* "bundle",
* "mode",
* "content",
* "hidden",
* }
* )
*/
......
......@@ -35,6 +35,12 @@
* entity_keys = {
* "id" = "id",
* "label" = "label"
* },
* config_export = {
* "id",
* "label",
* "targetEntityType",
* "cache",
* }
* )
*/
......
......@@ -155,10 +155,12 @@ public function install(array $module_list, $enable_dependencies = TRUE) {
// exceptions if the configuration is not valid.
$config_installer->checkConfigurationToInstall('module', $module);
// Save this data without checking schema. This is a performance
// improvement for module installation.
$extension_config
->set("module.$module", 0)
->set('module', module_config_sort($extension_config->get('module')))
->save();
->save(TRUE);
// Prepare the new module list, sorted by weight, including filenames.
// This list is used for both the ModuleHandler and DrupalKernel. It
......@@ -385,8 +387,9 @@ public function uninstall(array $module_list, $uninstall_dependents = TRUE) {
// Remove the schema.
drupal_uninstall_schema($module);
// Remove the module's entry from the config.
\Drupal::configFactory()->getEditable('core.extension')->clear("module.$module")->save();
// Remove the module's entry from the config. Don't check schema when
// uninstalling a module since we are only clearing a key.
\Drupal::configFactory()->getEditable('core.extension')->clear("module.$module")->save(TRUE);
// Update the module handler to remove the module.
// The current ModuleHandler instance is obsolete with the kernel rebuild
......
......@@ -260,10 +260,11 @@ public function install(array $theme_list, $install_dependencies = TRUE) {
// configuration then stop installing.
$this->configInstaller->checkConfigurationToInstall('theme', $key);
// The value is not used; the weight is ignored for themes currently.
// The value is not used; the weight is ignored for themes currently. Do
// not check schema when saving the configuration.
$extension_config
->set("theme.$key", 0)
->save();
->save(TRUE);
// Add the theme to the current list.
// @todo Remove all code that relies on $status property.
......@@ -358,7 +359,9 @@ public function uninstall(array $theme_list) {
$this->configManager->uninstall('theme', $key);
}
$extension_config->save();
// Don't check schema when uninstalling a theme since we are only clearing
// keys.
$extension_config->save(TRUE);
$this->state->set('system.theme.data', $current_theme_data);
$this->resetSystem();
......
......@@ -28,6 +28,20 @@
* entity_keys = {
* "id" = "id",
* "label" = "label"
* },
* config_export = {
* "id",
* "field_name",
* "entity_type",
* "bundle",
* "label",
* "description",
* "required",
* "translatable",
* "default_value",
* "default_value_callback",
* "settings",
* "field_type",
* }
* )
*/
......
......@@ -260,14 +260,14 @@ public function validateForm(array &$form, FormStateInterface $form_state) {
*/
public function submitForm(array &$form, FormStateInterface $form_state) {
$this->config('system.site')
->set('name', $form_state->getValue('site_name'))
->set('mail', $form_state->getValue('site_mail'))
->save();
->set('name', (string) $form_state->getValue('site_name'))
->set('mail', (string) $form_state->getValue('site_mail'))
->save(TRUE);
$this->config('system.date')
->set('timezone.default', $form_state->getValue('date_default_timezone'))
->set('country.default', $form_state->getValue('site_default_country'))
->save();
->set('timezone.default', (string) $form_state->getValue('date_default_timezone'))
->set('country.default', (string) $form_state->getValue('site_default_country'))
->save(TRUE);
$account_values = $form_state->getValue('account');
......@@ -281,7 +281,7 @@ public function submitForm(array &$form, FormStateInterface $form_state) {
if ($update_status_module[2]) {
// Reset the configuration factory so it is updated with the new module.
$this->resetConfigFactory();
$this->config('update.settings')->set('notification.emails', array($account_values['mail']))->save();
$this->config('update.settings')->set('notification.emails', array($account_values['mail']))->save(TRUE);
}
}
......
......@@ -130,20 +130,29 @@ public function loadMultipleOverrides(array $ids) {
public function saveOverride($id, array $definition) {
// Only allow to override a specific subset of the keys.
$expected = array(
'menu_name' => 1,
'parent' => 1,
'weight' => 1,
'expanded' => 1,
'enabled' => 1,
'menu_name' => '',
'parent' => '',
'weight' => 0,
'expanded' => FALSE,
'enabled' => FALSE,
);
// Filter the overrides to only those that are expected.
$definition = array_intersect_key($definition, $expected);
// Ensure all values are set.
$definition = $definition + $expected;
if ($definition) {
// Cast keys to avoid config schema during save.
$definition['menu_name'] = (string) $definition['menu_name'];
$definition['parent'] = (string) $definition['parent'];
$definition['weight'] = (int) $definition['weight'];
$definition['expanded'] = (bool) $definition['expanded'];
$definition['enabled'] = (bool) $definition['enabled'];
$id = static::encodeId($id);
$all_overrides = $this->getConfig()->get('definitions');
// Combine with any existing data.
$all_overrides[$id] = $definition + $this->loadOverride($id);
$this->getConfig()->set('definitions', $all_overrides)->save();
$this->getConfig()->set('definitions', $all_overrides)->save(TRUE);
}
return array_keys($definition);
}
......
......@@ -38,6 +38,16 @@
* links = {
* "delete-form" = "/admin/structure/block/manage/{block}/delete",
* "edit-form" = "/admin/structure/block/manage/{block}"
* },
* config_export = {
* "id",
* "theme",
* "region",
* "weight",
* "provider",
* "plugin",
* "settings",
* "visibility",
* }
* )
*/
......
......@@ -38,6 +38,12 @@
* "delete-form" = "/admin/structure/block/block-content/manage/{block_content_type}/delete",
* "edit-form" = "/admin/structure/block/block-content/manage/{block_content_type}",
* "collection" = "/admin/structure/block/block-content/types",
* },
* config_export = {
* "id",
* "label",
* "revision",
* "description",
* }
* )
*/
......
langcode: en
status: true
dependencies:
module:
- book
enforced:
module:
- book
......
......@@ -38,6 +38,12 @@
* "edit-form" = "/admin/structure/comment/manage/{comment_type}",
* "add-form" = "/admin/structure/comment/types/add",
* "collection" = "/admin/structure/comment/types",
* },
* config_export = {
* "id",
* "label",
* "target_entity_type_id",
* "description",
* }
* )
*/
......
......@@ -275,6 +275,14 @@ public function testDataTypes() {
$this->assertIdentical($config->get(), $data);
$this->assertIdentical($storage->read($name), $data);
// Test that schema type enforcement can be overridden by trusting the data.
$this->assertIdentical(99, $config->get('int'));
$config->set('int', '99')->save(TRUE);
$this->assertIdentical('99', $config->get('int'));
// Test that re-saving without testing the data enforces the schema type.
$config->save();
$this->assertIdentical($data, $config->get());
// Test that setting an unsupported type for a config object with a schema
// fails.
try {
......
......@@ -17,6 +17,15 @@
*/
class ConfigEntityUnitTest extends KernelTestBase {
/**
* Exempt from strict schema checking.
*
* @see \Drupal\Core\Config\Testing\ConfigSchemaChecker
*
* @var bool
*/
protected $strictConfigSchema = FALSE;
/**
* Modules to enable.
*
......@@ -89,6 +98,20 @@ public function testStorageMethods() {
foreach ($entities as $entity) {
$this->assertIdentical($entity->get('style'), $style, 'The loaded entity has the correct style value specified.');
}
// Test that schema type enforcement can be overridden by trusting the data.
$entity = $this->storage->create(array(
'id' => $this->randomMachineName(),
'label' => $this->randomString(),
'style' => 999
));
$entity->save();
$this->assertIdentical('999', $entity->style);
$entity->style = 999