Commit 141cb6f3 authored by Dries's avatar Dries

Issue #2130811 by alexpott, Gábor Hojtsy, vijaycs85, sun, Wim Leers: Use...

Issue #2130811 by alexpott, Gábor Hojtsy, vijaycs85, sun, Wim Leers: Use config schema in saving and validating configuration form to ensure data is consistent and correct.
parent b3ac555f
......@@ -93,7 +93,7 @@ services:
class: Drupal\Core\Config\ConfigFactory
tags:
- { name: persist }
arguments: ['@config.storage', '@config.context']
arguments: ['@config.storage', '@config.context', '@config.typed']
config.storage.staging:
class: Drupal\Core\Config\FileStorage
factory_class: Drupal\Core\Config\FileStorageFactory
......
......@@ -67,6 +67,7 @@ function ($value) use ($name) {
$config_factory = Drupal::service('config.factory');
$context = new FreeConfigContext(Drupal::service('event_dispatcher'), Drupal::service('uuid'));
$target_storage = Drupal::service('config.storage');
$typed_config = Drupal::service('config.typed');
$config_factory->enterContext($context);
foreach ($config_to_install as $name) {
// Only import new config.
......@@ -74,7 +75,7 @@ function ($value) use ($name) {
continue;
}
$new_config = new Config($name, $target_storage, $context);
$new_config = new Config($name, $target_storage, $context, $typed_config);
$data = $source_storage->read($name);
if ($data !== FALSE) {
$new_config->setData($data);
......@@ -214,7 +215,7 @@ function config_get_entity_type_by_name($name) {
*
* @see \Drupal\Core\TypedData\TypedDataManager::create()
*
* @return \Drupal\Core\TypedData\TypedConfigManager
* @return \Drupal\Core\Config\TypedConfigManager
*/
function config_typed() {
return drupal_container()->get('config.typed');
......
......@@ -386,9 +386,16 @@ function install_begin_request(&$install_state) {
->setFactoryService(new Reference('config.context.factory'))
->setFactoryMethod('get');
$container->register('config.storage.schema', 'Drupal\Core\Config\Schema\SchemaStorage');
$container->register('config.typed', 'Drupal\Core\Config\TypedConfigManager')
->addArgument(new Reference('config.storage'))
->addArgument(new Reference('config.storage.schema'));
$container->register('config.factory', 'Drupal\Core\Config\ConfigFactory')
->addArgument(new Reference('config.storage'))
->addArgument(new Reference('config.context'));
->addArgument(new Reference('config.context'))
->addArgument(new Reference('config.typed'));
// Register the 'language_manager' service.
$container
......
......@@ -8,8 +8,12 @@
namespace Drupal\Core\Config;
use Drupal\Component\Utility\NestedArray;
use Drupal\Component\Utility\String;
use Drupal\Core\Config\ConfigNameException;
use Drupal\Core\Config\Context\ContextInterface;
use Drupal\Core\TypedData\PrimitiveInterface;
use Drupal\Core\TypedData\Type\FloatInterface;
use Drupal\Core\TypedData\Type\IntegerInterface;
/**
* Defines the default configuration object.
......@@ -77,6 +81,20 @@ class Config {
*/
protected $isLoaded = FALSE;
/**
* The config schema wrapper object for this configuration object.
*
* @var \Drupal\Core\Config\Schema\Element
*/
protected $schemaWrapper;
/**
* The typed config manager.
*
* @var \Drupal\Core\Config\TypedConfigManager
*/
protected $typedConfigManager;
/**
* Constructs a configuration object.
*
......@@ -87,11 +105,14 @@ class Config {
* configuration data.
* @param \Drupal\Core\Config\Context\ContextInterface $context
* The configuration context used for this configuration object.
* @param \Drupal\Core\Config\TypedConfigManager $typed_config
* The typed configuration manager service.
*/
public function __construct($name, StorageInterface $storage, ContextInterface $context) {
public function __construct($name, StorageInterface $storage, ContextInterface $context, TypedConfigManager $typed_config) {
$this->name = $name;
$this->storage = $storage;
$this->context = $context;
$this->typedConfigManager = $typed_config;
}
/**
......@@ -404,6 +425,17 @@ public function load() {
public function save() {
// 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 (!$this->isLoaded) {
$this->load();
}
......@@ -466,4 +498,110 @@ public function merge(array $data_to_merge) {
$this->replaceData(NestedArray::mergeDeepArray(array($this->data, $data_to_merge), TRUE));
return $this;
}
/**
* Gets the schema wrapper for the whole configuration object.
*
* The schema wrapper is dependent on the configuration name and the whole
* data structure, so if the name or the data changes in any way, the wrapper
* should be reset.
*
* @return \Drupal\Core\Config\Schema\Element
*/
protected function getSchemaWrapper() {
if (!isset($this->schemaWrapper)) {
$definition = $this->typedConfigManager->getDefinition($this->name);
$this->schemaWrapper = $this->typedConfigManager->create($definition, $this->data);
}
return $this->schemaWrapper;
}
/**
* Gets the definition for the configuration key.
*
* @param string $key
* A string that maps to a key within the configuration data.
*
* @return \Drupal\Core\Config\Schema\Element
*
* @throws \Drupal\Core\Config\ConfigException
* Thrown when schema is incomplete.
*/
protected function getSchemaForKey($key) {
$parts = explode('.', $key);
$schema_wrapper = $this->getSchemaWrapper();
if (count($parts) == 1) {
$schema = $schema_wrapper->get($key);
}
else {
$schema = clone $schema_wrapper;
foreach ($parts as $nested_key) {
if (!is_object($schema) || !method_exists($schema, 'get')) {
throw new ConfigException(String::format("Incomplete schema for !key key in configuration object !name.", array('!name' => $this->name, '!key' => $key)));
}
else {
$schema = $schema->get($nested_key);
}
}
}
return $schema;
}
/**
* Casts the value to correct data type using the configuration schema.
*
* @param string $key
* A string that maps to a key within the configuration data.
* @param string $value
* Value to associate with the key.
*
* @return mixed
* The value cast to the type indicated in the schema.
*/
protected function castValue($key, $value) {
if ($value === NULL) {
$value = NULL;
}
elseif (is_scalar($value)) {
try {
$element = $this->getSchemaForKey($key);
if ($element instanceof PrimitiveInterface) {
// Special handling for integers and floats since the configuration
// system is primarily concerned with saving values from the Form API
// we have to special case the meaning of an empty string for numeric
// types. In PHP this would be casted to a 0 but for the purposes of
// configuration we need to treat this as a NULL.
if ($value === '' && ($element instanceof IntegerInterface || $element instanceof FloatInterface)) {
$value = NULL;
}
else {
$value = $element->getCastedValue();
}
}
else {
// Config only supports primitive data types. If the config schema
// does define a type $element will be an instance of
// \Drupal\Core\Config\Schema\Property. Convert it to string since it
// is the safest possible type.
$value = $element->getString();
}
}
catch (\Exception $e) {
// @todo throw an exception due to an incomplete schema. Only possible
// once https://drupal.org/node/1910624 is complete.
}
}
else {
// Any non-scalar value must be an array.
if (!is_array($value)) {
$value = (array) $value;
}
// Recurse into any nested keys.
foreach ($value as $nested_value_key => $nested_value) {
$value[$nested_value_key] = $this->castValue($key . '.' . $nested_value_key, $nested_value);
}
}
return $value;
}
}
......@@ -8,6 +8,7 @@
namespace Drupal\Core\Config;
use Drupal\Core\Config\Context\ContextInterface;
use Drupal\Core\Config\TypedConfigManager;
/**
* Defines the configuration object factory.
......@@ -51,16 +52,26 @@ class ConfigFactory {
*/
protected $cache = array();
/**
* The typed config manager.
*
* @var \Drupal\Core\Config\TypedConfigManager
*/
protected $typedConfigManager;
/**
* Constructs the Config factory.
*
* @param \Drupal\Core\Config\StorageInterface
* @param \Drupal\Core\Config\StorageInterface $storage
* The configuration storage engine.
* @param \Drupal\Core\Config\Context\ContextInterface
* @param \Drupal\Core\Config\Context\ContextInterface $context
* Configuration context object.
* @param \Drupal\Core\Config\TypedConfigManager $typed_config
* The typed configuration manager.
*/
public function __construct(StorageInterface $storage, ContextInterface $context) {
public function __construct(StorageInterface $storage, ContextInterface $context, TypedConfigManager $typed_config) {
$this->storage = $storage;
$this->typedConfigManager = $typed_config;
$this->enterContext($context);
}
......@@ -80,7 +91,7 @@ public function get($name) {
return $this->cache[$cache_key];
}
$this->cache[$cache_key] = new Config($name, $this->storage, $context);
$this->cache[$cache_key] = new Config($name, $this->storage, $context, $this->typedConfigManager);
return $this->cache[$cache_key]->init();
}
......@@ -115,7 +126,7 @@ public function loadMultiple(array $names) {
$storage_data = $this->storage->readMultiple($names);
foreach ($storage_data as $name => $data) {
$cache_key = $this->getCacheKey($name, $context);
$this->cache[$cache_key] = new Config($name, $this->storage, $context);
$this->cache[$cache_key] = new Config($name, $this->storage, $context, $this->typedConfigManager);
$this->cache[$cache_key]->initWithData($data);
$list[$name] = $this->cache[$cache_key];
}
......@@ -172,7 +183,7 @@ public function rename($old_name, $new_name) {
}
else {
// Create the config object if it's not yet loaded into the static cache.
$config = new Config($old_name, $this->storage, $context);
$config = new Config($old_name, $this->storage, $context, $this->typedConfigManager);
}
$this->cache[$new_cache_key] = $config;
......
......@@ -8,6 +8,7 @@
namespace Drupal\Core\Config;
use Drupal\Core\Config\Context\FreeConfigContext;
use Drupal\Core\Config\TypedConfigManager;
use Drupal\Core\Entity\EntityManagerInterface;
use Drupal\Core\Lock\LockBackendInterface;
use Drupal\Component\Uuid\UuidInterface;
......@@ -102,6 +103,13 @@ class ConfigImporter {
*/
protected $uuidService;
/**
* The typed config manager.
*
* @var \Drupal\Core\Config\TypedConfigManager
*/
protected $typedConfigManager;
/**
* Constructs a configuration import object.
*
......@@ -118,14 +126,17 @@ class ConfigImporter {
* The lock backend to ensure multiple imports do not occur at the same time.
* @param \Drupal\Component\Uuid\UuidInterface $uuid_service
* The UUID service.
* @param \Drupal\Core\Config\TypedConfigManager $typed_config
* The typed configuration manager.
*/
public function __construct(StorageComparerInterface $storage_comparer, EventDispatcherInterface $event_dispatcher, ConfigFactory $config_factory, EntityManagerInterface $entity_manager, LockBackendInterface $lock, UuidInterface $uuid_service) {
public function __construct(StorageComparerInterface $storage_comparer, EventDispatcherInterface $event_dispatcher, ConfigFactory $config_factory, EntityManagerInterface $entity_manager, LockBackendInterface $lock, UuidInterface $uuid_service, TypedConfigManager $typed_config) {
$this->storageComparer = $storage_comparer;
$this->eventDispatcher = $event_dispatcher;
$this->configFactory = $config_factory;
$this->entityManager = $entity_manager;
$this->lock = $lock;
$this->uuidService = $uuid_service;
$this->typedConfigManager = $typed_config;
$this->processed = $this->storageComparer->getEmptyChangelist();
// Use an override free context for importing so that overrides to do not
// pollute the imported data. The context is hard coded to ensure this is
......@@ -264,7 +275,7 @@ public function validate() {
protected function importConfig() {
foreach (array('delete', 'create', 'update') as $op) {
foreach ($this->getUnprocessed($op) as $name) {
$config = new Config($name, $this->storageComparer->getTargetStorage(), $this->context);
$config = new Config($name, $this->storageComparer->getTargetStorage(), $this->context, $this->typedConfigManager);
if ($op == 'delete') {
$config->delete();
}
......@@ -297,11 +308,11 @@ protected function importInvokeOwner() {
// Validate the configuration object name before importing it.
// Config::validateName($name);
if ($entity_type = config_get_entity_type_by_name($name)) {
$old_config = new Config($name, $this->storageComparer->getTargetStorage(), $this->context);
$old_config = new Config($name, $this->storageComparer->getTargetStorage(), $this->context, $this->typedConfigManager);
$old_config->load();
$data = $this->storageComparer->getSourceStorage()->read($name);
$new_config = new Config($name, $this->storageComparer->getTargetStorage(), $this->context);
$new_config = new Config($name, $this->storageComparer->getTargetStorage(), $this->context, $this->typedConfigManager);
if ($data !== FALSE) {
$new_config->setData($data);
}
......
......@@ -27,7 +27,7 @@ class Mapping extends ArrayElement implements ComplexDataInterface {
protected function parse() {
$elements = array();
foreach ($this->definition['mapping'] as $key => $definition) {
if (isset($this->value[$key])) {
if (isset($this->value[$key]) || array_key_exists($key, $this->value)) {
$elements[$key] = $this->parseElement($key, $this->value[$key], $definition);
}
}
......
......@@ -49,4 +49,19 @@ public function onChange($delta) {
$this->parent->onChange($this->name);
}
}
/**
* Gets a typed configuration element from the sequence.
*
* @param string $key
* The key of the sequence to get.
*
* @return \Drupal\Core\Config\Schema\Element
* Typed configuration element.
*/
public function get($key) {
$elements = $this->parse();
return $elements[$key];
}
}
......@@ -87,4 +87,11 @@ public function getString() {
}
return $contents;
}
/**
* @inheritdoc
*/
public function getCastedValue() {
return $this->getValue();
}
}
......@@ -23,4 +23,10 @@
*/
class Boolean extends PrimitiveBase implements BooleanInterface {
/**
* @inheritdoc
*/
public function getCastedValue() {
return (bool) $this->value;
}
}
......@@ -23,4 +23,10 @@
*/
class Float extends PrimitiveBase implements FloatInterface {
/**
* @inheritdoc
*/
public function getCastedValue() {
return (float) $this->value;
}
}
......@@ -23,4 +23,10 @@
*/
class Integer extends PrimitiveBase implements IntegerInterface {
/**
* @inheritdoc
*/
public function getCastedValue() {
return (int) $this->value;
}
}
......@@ -23,4 +23,10 @@
*/
class String extends PrimitiveBase implements StringInterface {
/**
* @inheritdoc
*/
public function getCastedValue() {
return $this->getString();
}
}
......@@ -21,6 +21,6 @@
* label = @Translation("URI")
* )
*/
class Uri extends PrimitiveBase implements UriInterface {
class Uri extends String implements UriInterface {
}
......@@ -28,4 +28,10 @@ public function getValue();
*/
public function setValue($value);
/**
* Gets the primitive data value casted to the correct PHP type.
*
* @return mixed
*/
public function getCastedValue();
}
......@@ -87,6 +87,9 @@ block.block.*:
view_mode:
type: string
label: 'View mode'
module:
type: string
label: 'Module'
langcode:
type: string
label: 'Default language'
......@@ -96,7 +96,7 @@ protected function createTests() {
'status' => TRUE,
'langcode' => language_default()->id,
'theme' => 'stark',
'region' => -1,
'region' => '-1',
'plugin' => 'test_html_id',
'settings' => array(
'cache' => 1,
......@@ -106,7 +106,7 @@ protected function createTests() {
),
'visibility' => NULL,
);
$this->assertIdentical($actual_properties, $expected_properties, 'The block properties are exported correctly.');
$this->assertIdentical($actual_properties, $expected_properties);
$this->assertTrue($entity->getPlugin() instanceof TestHtmlIdBlock, 'The entity has an instance of the correct block plugin.');
}
......
......@@ -36,6 +36,9 @@ breakpoint.breakpoint.*.*.*:
langcode:
type: string
label: 'Default language'
status:
type: boolean
label: 'Enabled'
breakpoint.breakpoint_group.*.*.*:
type: mapping
......@@ -65,3 +68,9 @@ breakpoint.breakpoint_group.*.*.*:
sourceType:
type: string
label: 'Group source type'
langcode:
type: string
label: 'Default language'
status:
type: boolean
label: 'Enabled'
......@@ -82,7 +82,6 @@ public function settingsForm(array $form, array &$form_state, Editor $editor) {
*
* @see \Drupal\editor\Form\EditorImageDialog
* @see editor_image_upload_settings_form()
* @see editor_image_upload_settings_validate()
*/
function validateImageUploadSettings(array $element, array &$form_state) {
$settings = &$form_state['values']['editor']['settings']['plugins']['drupalimage']['image_upload'];
......
......@@ -16,6 +16,7 @@
use Drupal\Core\Config\ConfigImporter;
use Drupal\Core\Config\ConfigException;
use Drupal\Core\Config\ConfigFactory;
use Drupal\Core\Config\TypedConfigManager;
use Drupal\Core\Routing\UrlGeneratorInterface;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
......@@ -72,6 +73,13 @@ class ConfigSync extends FormBase {
*/
protected $uuidService;
/**
* The typed config manager.
*
* @var \Drupal\Core\Config\TypedConfigManager
*/
protected $typedConfigManager;
/**
* Constructs the object.
*
......@@ -90,9 +98,11 @@ class ConfigSync extends FormBase {
* @param \Drupal\Core\Routing\UrlGeneratorInterface $url_generator
* The url generator service.
* @param \Drupal\Component\Uuid\UuidInterface $uuid_service
* The UUID Service.
* The UUID Service.
* @param \Drupal\Core\Config\TypedConfigManager $typed_config
* The typed configuration manager.
*/
public function __construct(StorageInterface $sourceStorage, StorageInterface $targetStorage, LockBackendInterface $lock, EventDispatcherInterface $event_dispatcher, ConfigFactory $config_factory, EntityManagerInterface $entity_manager, UrlGeneratorInterface $url_generator, UuidInterface $uuid_service) {
public function __construct(StorageInterface $sourceStorage, StorageInterface $targetStorage, LockBackendInterface $lock, EventDispatcherInterface $event_dispatcher, ConfigFactory $config_factory, EntityManagerInterface $entity_manager, UrlGeneratorInterface $url_generator, UuidInterface $uuid_service, TypedConfigManager $typed_config) {
$this->sourceStorage = $sourceStorage;
$this->targetStorage = $targetStorage;
$this->lock = $lock;
......@@ -101,6 +111,7 @@ public function __construct(StorageInterface $sourceStorage, StorageInterface $t
$this->entity_manager = $entity_manager;
$this->urlGenerator = $url_generator;
$this->uuidService = $uuid_service;
$this->typedConfigManager = $typed_config;
}
/**
......@@ -115,7 +126,8 @@ public static function create(ContainerInterface $container) {
$container->get('config.factory'),
$container->get('entity.manager'),
$container->get('url_generator'),
$container->get('uuid')
$container->get('uuid'),
$container->get('config.typed')
);
}
......@@ -222,7 +234,8 @@ public function submitForm(array &$form, array &$form_state) {
$this->configFactory,
$this->entity_manager,
$this->lock,
$this->uuidService
$this->uuidService,
$this->typedConfigManager
);
if ($config_importer->alreadyImporting()) {
drupal_set_message($this->t('Another request may be synchronizing configuration already.'));
......
......@@ -61,7 +61,8 @@ function setUp() {
$this->container->get('config.factory'),
$this->container->get('entity.manager'),
$this->container->get('lock'),
$this->container->get('uuid')
$this->container->get('uuid'),
$this->container->get('config.typed')
);
$this->copyConfig($this->container->get('config.storage'), $this->container->get('config.storage.staging'));
}
......
......@@ -7,7 +7,6 @@
namespace Drupal\config\Tests;
use Drupal\Core\Config\TypedConfig;
use Drupal\Core\TypedData\Type\IntegerInterface;
use Drupal\Core\TypedData\Type\StringInterface;
use Drupal\simpletest\DrupalUnitTestBase;
......@@ -102,6 +101,8 @@ function testSchemaMapping() {
$expected['label'] = 'Image style';
$expected['class'] = '\Drupal\Core\Config\Schema\Mapping';
$expected['mapping']['name']['type'] = 'string';
$expected['mapping']['uuid']['label'] = 'UUID';
$expected['mapping']['uuid']['type'] = 'string';
$expected['mapping']['label']['type'] = 'label';
$expected['mapping']['effects']['type'] = 'sequence';
$expected['mapping']['effects']['sequence'][0]['type'] = 'mapping';
......@@ -111,6 +112,8 @@ function testSchemaMapping() {
$expected['mapping']['effects']['sequence'][0]['mapping']['uuid']['type'] = 'string';
$expected['mapping']['langcode']['label'] = 'Default language';
$expected['mapping']['langcode']['type'] = 'string';
$expected['mapping']['status']['label'] = 'Enabled';
$expected['mapping']['status']['type'] = 'boolean';
$this->assertEqual($definition, $expected, 'Retrieved the right metadata for image.style.large');
......@@ -242,4 +245,63 @@ function testSchemaData() {
$this->assertEqual($site_slogan->getValue(), $new_slogan, 'Successfully updated the contained configuration data');
}
/**
* Test configuration value data type enforcement using schemas.
*/
public function testConfigSaveWithSchema() {
$untyped_values = array(
'string' => 1,
'empty_string' => '',
'null_string' => NULL,
'integer' => '100',
'null_integer' => '',
'boolean' => 1,
// If the config schema doesn't have a type it should be casted to string.
'no_type' => 1,
'mapping' => array(
'string' => 1
),
'float' => '3.14',
'null_float' => '',
'sequence' => array (1, 0, 1),
// Not in schema and therefore should be left untouched.
'not_present_in_schema' => TRUE,
// Test a custom type.
'config_test_integer' => '1',
'config_test_integer_empty_string' => '',
);
$untyped_to_typed = $untyped_values;
$typed_values = array(
'string' => '1',
'empty_string' => '',
'null_string' => NULL,
'integer' => 100,
'null_integer' => NULL,
'boolean' => TRUE,
'no_type' => '1',
'mapping' => array(
'string' => '1'
),
'float' => 3.14,
'null_float' => NULL,
'sequence' => array (TRUE, FALSE, TRUE),
'not_present_in_schema' => TRUE,
'config_test_integer' => 1,
'config_test_integer_empty_string' => NULL,
);
// Save config which has a schema that enforces types.
\Drupal::config('config_test.schema_data_types')
->setData($untyped_to_typed)
->save();
$this->assertIdentical(\Drupal::config('config_test.schema_data_types')->get(), $typed_values);
// Save config which does not have a schema that enforces types.
\Drupal::config('config_test.no_schema_data_types')
->setData($untyped_values)
->save();
$this->assertIdentical(\Drupal::config('config_test.no_schema_data_types')->get(), $untyped_values);
}
}
......@@ -91,3 +91,43 @@ config_test.dynamic.*:
protected_property: