diff --git a/core/includes/bootstrap.inc b/core/includes/bootstrap.inc index e26cb36..da29fe0 100644 --- a/core/includes/bootstrap.inc +++ b/core/includes/bootstrap.inc @@ -2424,20 +2424,24 @@ function drupal_get_bootstrap_phase() { * * @see Drupal\Core\DrupalKernel * - * @param $reset - * A new container instance to reset the Drupal container to. + * @param Symfony\Component\DependencyInjection\Container $new_container + * A new container instance to replace the current. + * @param bool $reset + * (optional) Internal use only. Whether to enforce a reset of the statically + * cached container. Pass NULL for $new_container to recreate a new Container + * from scratch in a subsequent call to this function. Used by tests. * * @return Symfony\Component\DependencyInjection\Container * The instance of the Container used to set up and maintain object * instances. */ -function drupal_container(Container $reset = NULL) { +function drupal_container(Container $new_container = NULL, $reset = FALSE) { // We do not use drupal_static() here because we do not have a mechanism by // which to reinitialize the stored objects, so a drupal_static_reset() call // would leave Drupal in a nonfunctional state. static $container = NULL; - if (isset($reset)) { - $container = $reset; + if (isset($new_container) || $reset) { + $container = $new_container; } elseif (!isset($container)) { // Return a ContainerBuilder instance with the bare essentials needed for any @@ -2452,15 +2456,27 @@ function drupal_container(Container $reset = NULL) { // bootstrap configuration *file* to allow to set/override this very // lowest of low level configuration. $container->setParameter('config.storage.options', array( - 'connection' => 'default', - 'target' => 'default', + 'Drupal\Core\Config\FileStorage' => array( + 'directory' => config_get_config_directory() . '/active', + ), + 'Drupal\Core\Config\CacheStorage' => array( + 'backend' => 'Drupal\Core\Cache\DatabaseBackend', + 'bin' => 'config', + ), )); - $container->register('config.storage', 'Drupal\Core\Config\DatabaseStorage') + $container->register('config.storage', 'Drupal\Core\Config\CachedFileStorage') ->addArgument('%config.storage.options%'); // Register configuration object factory. $container->register('config.factory', 'Drupal\Core\Config\ConfigFactory') ->addArgument(new Reference('config.storage')); + + // Register configuration state. + $container->setParameter('config.state.options', array( + 'directory' => config_get_config_directory() . '/import', + )); + $container->register('config.state', 'Drupal\Core\Config\FileStorage') + ->addArgument('%config.state.options%'); } return $container; } diff --git a/core/includes/config.inc b/core/includes/config.inc index f2fcd39..14f41f9 100644 --- a/core/includes/config.inc +++ b/core/includes/config.inc @@ -17,9 +17,6 @@ * The extension type; e.g., 'module' or 'theme'. * @param string $name * The name of the module or theme to install default configuration for. - * - * @todo Make this acknowledge other storage engines rather than having - * SQL be hardcoded. */ function config_install_default_config($type, $name) { $config_dir = drupal_get_path($type, $name) . '/config'; @@ -43,7 +40,9 @@ function config_install_default_config($type, $name) { } /** - * @todo Modules need a way to access the active store, whatever it is. + * Gets configuration object names starting with a given prefix. + * + * @see Drupal\Core\Config\StorageInterface::listAll() */ function config_get_storage_names_with_prefix($prefix = '') { return drupal_container()->get('config.storage')->listAll($prefix); @@ -129,15 +128,15 @@ function config_sync_changes(array $config_changes, StorageInterface $source_sto } /** - * Imports configuration from FileStorage to the active store. + * Imports configuration into the active store. * * @return bool|null * TRUE if configuration was imported successfully, FALSE in case of a * synchronization error, or NULL if there are no changes to synchronize. */ function config_import() { - // Retrieve a list of differences between FileStorage and the active store. - $source_storage = new FileStorage(); + // Retrieve a list of differences between last known state and active store. + $source_storage = drupal_container()->get('config.state'); $target_storage = drupal_container()->get('config.storage'); $config_changes = config_sync_get_changes($source_storage, $target_storage); @@ -217,12 +216,14 @@ function config_import_invoke_owner(array $config_changes, StorageInterface $sou } /** - * Exports configuration from the active store to FileStorage. + * Updates the last known state with the active store configuration. + * + * @todo config_export() is a misnomer now. Rename to config_state_update(). */ function config_export() { - // Retrieve a list of differences between the active store and FileStorage. + // Retrieve a list of differences between active store and last known state. $source_storage = drupal_container()->get('config.storage'); - $target_storage = new FileStorage(); + $target_storage = drupal_container()->get('config.state'); $config_changes = config_sync_get_changes($source_storage, $target_storage); if (empty($config_changes)) { diff --git a/core/includes/install.inc b/core/includes/install.inc index 4c9c60c..f53c54e 100644 --- a/core/includes/install.inc +++ b/core/includes/install.inc @@ -296,7 +296,12 @@ function install_ensure_config_directory() { // directories that the installer creates. else { $config_directory = config_get_config_directory(); - return file_prepare_directory($config_directory, FILE_CREATE_DIRECTORY | FILE_MODIFY_PERMISSIONS); + $success = file_prepare_directory($config_directory, FILE_CREATE_DIRECTORY | FILE_MODIFY_PERMISSIONS); + foreach (array('active', 'import') as $subdir) { + $subdir = $config_directory . '/' . $subdir; + $success = $success && file_prepare_directory($subdir, FILE_CREATE_DIRECTORY | FILE_MODIFY_PERMISSIONS); + } + return $success; } } diff --git a/core/includes/module.inc b/core/includes/module.inc index ab45576..6c4f24d 100644 --- a/core/includes/module.inc +++ b/core/includes/module.inc @@ -6,7 +6,7 @@ */ use Drupal\Component\Graph\Graph; -use Drupal\Core\Config\DatabaseStorage; +use Drupal\Core\Config\NullStorage; /** * Load all the modules that have been enabled in the system table. @@ -622,19 +622,26 @@ function module_uninstall($module_list = array(), $uninstall_dependents = TRUE) $module_list = array_keys($module_list); } - $storage = new DatabaseStorage(); + $source_storage = new NullStorage(); + $target_storage = drupal_container()->get('config.storage'); foreach ($module_list as $module) { + // Remove all configuration belonging to the module. + $config_changes = $target_storage->listAll($module . '.'); + if (!empty($config_changes)) { + $config_changes = array( + 'delete' => $config_changes, + 'change' => array(), + 'create' => array(), + ); + $remaining_changes = config_import_invoke_owner($config_changes, $source_storage, $target_storage); + config_sync_changes($remaining_changes, $source_storage, $target_storage); + } + // Uninstall the module. module_load_install($module); module_invoke($module, 'uninstall'); drupal_uninstall_schema($module); - // Remove all configuration belonging to the module. - $config_names = $storage->listAll($module . '.'); - foreach ($config_names as $config_name) { - config($config_name)->delete(); - } - watchdog('system', '%module module uninstalled.', array('%module' => $module), WATCHDOG_INFO); drupal_set_installed_schema_version($module, SCHEMA_UNINSTALLED); } diff --git a/core/lib/Drupal/Core/Config/CacheStorage.php b/core/lib/Drupal/Core/Config/CacheStorage.php new file mode 100644 index 0000000..1f69c87 --- /dev/null +++ b/core/lib/Drupal/Core/Config/CacheStorage.php @@ -0,0 +1,132 @@ + 'Drupal\Core\Cache\DatabaseBackend', + 'bin' => 'config', + ); + $this->options = $options; + } + + /** + * Returns the instantiated Cache backend to use. + */ + protected function getBackend() { + if (!isset($this->storage)) { + $this->storage = new $this->options['backend']($this->options['bin']); + } + return $this->storage; + } + + /** + * Implements Drupal\Core\Config\StorageInterface::exists(). + */ + public function exists($name) { + return (bool) $this->getBackend()->get($name); + } + + /** + * Implements Drupal\Core\Config\StorageInterface::read(). + */ + public function read($name) { + if ($cache = $this->getBackend()->get($name)) { + // The cache backend supports primitive data types, but only an array + // represents valid config object data. + if (is_array($cache->data)) { + return $cache->data; + } + } + return FALSE; + } + + /** + * Implements Drupal\Core\Config\StorageInterface::write(). + */ + public function write($name, array $data) { + $this->getBackend()->set($name, $data, CACHE_PERMANENT, array('config' => array($name))); + return TRUE; + } + + /** + * Implements Drupal\Core\Config\StorageInterface::delete(). + */ + public function delete($name) { + $this->getBackend()->delete($name); + return TRUE; + } + + /** + * Implements Drupal\Core\Config\StorageInterface::encode(). + */ + public static function encode($data) { + return serialize($data); + } + + /** + * Implements Drupal\Core\Config\StorageInterface::decode(). + * + * @throws ErrorException + * unserialize() triggers E_NOTICE if the string cannot be unserialized. + */ + public static function decode($raw) { + $data = @unserialize($raw); + return is_array($data) ? $data : FALSE; + } + + /** + * Implements Drupal\Core\Config\StorageInterface::listAll(). + * + * Not supported by CacheBackendInterface. + */ + public function listAll($prefix = '') { + return array(); + } +} diff --git a/core/lib/Drupal/Core/Config/CachedFileStorage.php b/core/lib/Drupal/Core/Config/CachedFileStorage.php new file mode 100644 index 0000000..74eb99e --- /dev/null +++ b/core/lib/Drupal/Core/Config/CachedFileStorage.php @@ -0,0 +1,125 @@ +options = $options; + + $this->storages['file'] = new FileStorage($options['Drupal\Core\Config\FileStorage']); + + unset($options['Drupal\Core\Config\FileStorage']); + list($cache_class, $cache_options) = each($options); + $this->storages['cache'] = new $cache_class($cache_options); + } + + /** + * Implements Drupal\Core\Config\StorageInterface::exists(). + */ + public function exists($name) { + // A single filestat is faster than a complex cache lookup and possibly + // subsequent filestat. + return $this->storages['file']->exists($name); + } + + /** + * Implements Drupal\Core\Config\StorageInterface::read(). + */ + public function read($name) { + // Check the cache. + $data = $this->storages['cache']->read($name); + // If the cache returns no result, check the file storage. + if ($data === FALSE) { + $data = $this->storages['file']->read($name); + // @todo Should the config object be cached if it does not exist? + if ($data !== FALSE) { + $this->storages['cache']->write($name, $data); + } + } + return $data; + } + + /** + * Implements Drupal\Core\Config\StorageInterface::write(). + */ + public function write($name, array $data) { + $success = $this->storages['file']->write($name, $data); + $this->storages['cache']->delete($name); + return $success; + } + + /** + * Implements Drupal\Core\Config\StorageInterface::delete(). + */ + public function delete($name) { + $success = TRUE; + foreach ($this->storages as $storage) { + if (!$storage->delete($name)) { + $success = FALSE; + } + } + return $success; + } + + /** + * Implements Drupal\Core\Config\StorageInterface::encode(). + * + * @todo Remove encode() from StorageInterface. + */ + public static function encode($data) { + return $this->storages['file']->encode($data); + } + + /** + * Implements Drupal\Core\Config\StorageInterface::decode(). + * + * @todo Remove decode() from StorageInterface. + */ + public static function decode($raw) { + return $this->storages['file']->decode($raw); + } + + /** + * Implements Drupal\Core\Config\StorageInterface::listAll(). + */ + public function listAll($prefix = '') { + return $this->storages['file']->listAll($prefix); + } +} diff --git a/core/lib/Drupal/Core/Config/Config.php b/core/lib/Drupal/Core/Config/Config.php index 2f4d14a..c5423ca 100644 --- a/core/lib/Drupal/Core/Config/Config.php +++ b/core/lib/Drupal/Core/Config/Config.php @@ -270,6 +270,7 @@ public function sortByKey(array &$data) { * Deletes the configuration object. */ public function delete() { + // @todo Consider to remove the pruning of data for Config::delete(). $this->data = array(); $this->storage->delete($this->name); $this->isNew = TRUE; diff --git a/core/lib/Drupal/Core/Config/DatabaseStorage.php b/core/lib/Drupal/Core/Config/DatabaseStorage.php index 92aea45..8ab64f6 100644 --- a/core/lib/Drupal/Core/Config/DatabaseStorage.php +++ b/core/lib/Drupal/Core/Config/DatabaseStorage.php @@ -44,6 +44,15 @@ protected function getConnection() { } /** + * Implements Drupal\Core\Config\StorageInterface::exists(). + */ + public function exists($name) { + return (bool) $this->getConnection()->queryRange('SELECT 1 FROM {config} WHERE name = :name', 0, 1, array( + ':name' => $name, + ), $this->options)->fetchField(); + } + + /** * Implements Drupal\Core\Config\StorageInterface::read(). * * @throws PDOException diff --git a/core/lib/Drupal/Core/Config/FileStorage.php b/core/lib/Drupal/Core/Config/FileStorage.php index 033555d..727adef 100644 --- a/core/lib/Drupal/Core/Config/FileStorage.php +++ b/core/lib/Drupal/Core/Config/FileStorage.php @@ -54,10 +54,7 @@ public static function getFileExtension() { } /** - * Returns whether the configuration file exists. - * - * @return bool - * TRUE if the configuration file exists, FALSE otherwise. + * Implements Drupal\Core\Config\StorageInterface::exists(). */ public function exists($name) { return file_exists($this->getFilePath($name)); diff --git a/core/lib/Drupal/Core/Config/NullStorage.php b/core/lib/Drupal/Core/Config/NullStorage.php index fede4f0..2e1c06c 100644 --- a/core/lib/Drupal/Core/Config/NullStorage.php +++ b/core/lib/Drupal/Core/Config/NullStorage.php @@ -29,6 +29,13 @@ public function __construct(array $options = array()) { } /** + * Implements Drupal\Core\Config\StorageInterface::exists(). + */ + public function exists($name) { + return FALSE; + } + + /** * Implements Drupal\Core\Config\StorageInterface::read(). */ public function read($name) { diff --git a/core/lib/Drupal/Core/Config/StorageInterface.php b/core/lib/Drupal/Core/Config/StorageInterface.php index 806ee87..4fb9869 100644 --- a/core/lib/Drupal/Core/Config/StorageInterface.php +++ b/core/lib/Drupal/Core/Config/StorageInterface.php @@ -25,6 +25,17 @@ public function __construct(array $options = array()); /** + * Returns whether a configuration object exists. + * + * @param string $name + * The name of a configuration object to test. + * + * @return bool + * TRUE if the configuration object exists, FALSE otherwise. + */ + public function exists($name); + + /** * Reads configuration data from the storage. * * @param string $name diff --git a/core/modules/config/config.admin.inc b/core/modules/config/config.admin.inc new file mode 100644 index 0000000..eefa104 --- /dev/null +++ b/core/modules/config/config.admin.inc @@ -0,0 +1,132 @@ + t('There are no configuration changes.'), + ); + return $form; + } + + foreach ($config_changes as $config_change_type => $config_files) { + if (empty($config_files)) { + continue; + } + $form[$config_change_type] = array( + '#type' => 'fieldset', + '#title' => $config_change_type . ' (' . count($config_files) . ')', + '#collapsible' => TRUE, + ); + $form[$config_change_type]['config_files'] = array( + '#theme' => 'table', + '#header' => array('Name'), + ); + foreach ($config_files as $config_file) { + $form[$config_change_type]['config_files']['#rows'][] = array($config_file); + } + } +} + +/** + * Form constructor for configuration import form. + * + * @see config_admin_import_form_submit() + * @see config_import() + */ +function config_admin_import_form($form, &$form_state) { + // Retrieve a list of differences between last known state and active store. + $source_storage = drupal_container()->get('config.state'); + $target_storage = drupal_container()->get('config.storage'); + + // Prevent users from deleting all configuration. + // If the source storage is empty, that signals the unique condition of not + // having exported anything at all, and thus no valid storage to compare the + // active storage against. + // @todo StorageInterface::listAll() can easily yield hundreds or even + // thousands of entries; consider to add a dedicated isEmpty() method for + // storage controllers. + $all = $source_storage->listAll(); + if (empty($all)) { + form_set_error('', t('There is no base configuration. Export it first.', array( + '@export-url' => url('admin/config/development/sync/export'), + ))); + return $form; + } + + config_admin_sync_form($form, $form_state, $source_storage, $target_storage); + + $form['actions'] = array('#type' => 'actions'); + $form['actions']['submit'] = array( + '#type' => 'submit', + '#value' => t('Import'), + ); + return $form; +} + +/** + * Form submission handler for config_admin_import_form(). + */ +function config_admin_import_form_submit($form, &$form_state) { + if (config_import()) { + drupal_set_message(t('The configuration was imported successfully.')); + } + else { + // Another request may be synchronizing configuration already. Wait for it + // to complete before returning the error, so already synchronized changes + // do not appear again. + lock_wait(__FUNCTION__); + drupal_set_message(t('The import failed due to an error. Any errors have been logged.'), 'error'); + } +} + +/** + * Form constructor for configuration export form. + * + * @see config_admin_export_form_submit() + * @see config_export() + * + * @todo "export" is a misnomer with config.state. + */ +function config_admin_export_form($form, &$form_state) { + // Retrieve a list of differences between active store and last known state. + $source_storage = drupal_container()->get('config.storage'); + $target_storage = drupal_container()->get('config.state'); + + config_admin_sync_form($form, $form_state, $source_storage, $target_storage); + + $form['actions'] = array('#type' => 'actions'); + $form['actions']['submit'] = array( + '#type' => 'submit', + '#value' => t('Export'), + ); + return $form; +} + +/** + * Form submission handler for config_admin_export_form(). + */ +function config_admin_export_form_submit($form, &$form_state) { + config_export(); + drupal_set_message(t('The configuration was exported successfully.')); +} + diff --git a/core/modules/config/config.info b/core/modules/config/config.info index 380f17e..efab7a1 100644 --- a/core/modules/config/config.info +++ b/core/modules/config/config.info @@ -3,3 +3,4 @@ description = Allows administrators to manage configuration changes. package = Core version = VERSION core = 8.x +configure = admin/config/development/sync diff --git a/core/modules/config/config.module b/core/modules/config/config.module index b3d9bbc..fd38fd2 100644 --- a/core/modules/config/config.module +++ b/core/modules/config/config.module @@ -1 +1,46 @@ t('Synchronize configuration'), + 'restrict access' => TRUE, + ); + return $permissions; +} + +/** + * Implements hook_menu(). + */ +function config_menu() { + $items['admin/config/development/sync'] = array( + 'title' => 'Synchronize configuration', + 'description' => 'Synchronize configuration changes.', + 'page callback' => 'drupal_get_form', + 'page arguments' => array('config_admin_import_form'), + 'access arguments' => array('synchronize configuration'), + 'file' => 'config.admin.inc', + ); + $items['admin/config/development/sync/import'] = array( + 'title' => 'Import', + 'type' => MENU_DEFAULT_LOCAL_TASK, + 'weight' => -10, + ); + $items['admin/config/development/sync/export'] = array( + 'title' => 'Export', + 'page callback' => 'drupal_get_form', + 'page arguments' => array('config_admin_export_form'), + 'access arguments' => array('synchronize configuration'), + 'file' => 'config.admin.inc', + 'type' => MENU_LOCAL_TASK, + ); + return $items; +} + diff --git a/core/modules/config/lib/Drupal/config/Tests/ConfigImportTest.php b/core/modules/config/lib/Drupal/config/Tests/ConfigImportTest.php index ec92fdf..f834b72 100644 --- a/core/modules/config/lib/Drupal/config/Tests/ConfigImportTest.php +++ b/core/modules/config/lib/Drupal/config/Tests/ConfigImportTest.php @@ -7,8 +7,6 @@ namespace Drupal\config\Tests; -use Drupal\Core\Config\DatabaseStorage; -use Drupal\Core\Config\FileStorage; use Drupal\simpletest\WebTestBase; /** @@ -37,6 +35,8 @@ public static function getInfo() { function testDeleted() { $name = 'config_test.system'; $dynamic_name = 'config_test.dynamic.default'; + $storage = $this->container->get('config.storage'); + $state = $this->container->get('config.state'); // Verify the default configuration values exist. $config = config($name); @@ -48,17 +48,15 @@ function testDeleted() { config_export(); // Delete the configuration objects. - $file_storage = new FileStorage(); - $file_storage->delete($name); - $file_storage->delete($dynamic_name); + $state->delete($name); + $state->delete($dynamic_name); // Import. config_import(); // Verify the values have disappeared. - $database_storage = new DatabaseStorage(); - $this->assertIdentical($database_storage->read($name), FALSE); - $this->assertIdentical($database_storage->read($dynamic_name), FALSE); + $this->assertIdentical($storage->read($name), FALSE); + $this->assertIdentical($storage->read($dynamic_name), FALSE); $config = config($name); $this->assertIdentical($config->get('foo'), NULL); @@ -72,34 +70,40 @@ function testDeleted() { function testNew() { $name = 'config_test.new'; $dynamic_name = 'config_test.dynamic.new'; - - // Verify the configuration to create does not exist yet. - $file_storage = new FileStorage(); - $this->assertIdentical($file_storage->exists($name), FALSE, $name . ' not found.'); - $this->assertIdentical($file_storage->exists($dynamic_name), FALSE, $dynamic_name . ' not found.'); + $storage = $this->container->get('config.storage'); + $state = $this->container->get('config.state'); // Export. config_export(); + // Verify the configuration to create does not exist yet. + $this->assertIdentical($storage->exists($name), FALSE, $name . ' not found.'); + $this->assertIdentical($storage->exists($dynamic_name), FALSE, $dynamic_name . ' not found.'); + + $this->assertIdentical($state->exists($name), FALSE, $name . ' not found.'); + $this->assertIdentical($state->exists($dynamic_name), FALSE, $dynamic_name . ' not found.'); + // Create new configuration objects. - $file_storage->write($name, array( + $original_name_data = array( 'add_me' => 'new value', - )); - $file_storage->write($dynamic_name, array( + ); + $state->write($name, $original_name_data); + $original_dynamic_data = array( 'id' => 'new', 'label' => 'New', - )); - $this->assertIdentical($file_storage->exists($name), TRUE, $name . ' found.'); - $this->assertIdentical($file_storage->exists($dynamic_name), TRUE, $dynamic_name . ' found.'); + ); + $state->write($dynamic_name, $original_dynamic_data); + $this->assertIdentical($state->exists($name), TRUE, $name . ' found.'); + $this->assertIdentical($state->exists($dynamic_name), TRUE, $dynamic_name . ' found.'); // Import. config_import(); // Verify the values appeared. $config = config($name); - $this->assertIdentical($config->get('add_me'), 'new value'); + $this->assertIdentical($config->get('add_me'), $original_name_data['add_me']); $config = config($dynamic_name); - $this->assertIdentical($config->get('label'), 'New'); + $this->assertIdentical($config->get('label'), $original_dynamic_data['label']); } /** @@ -108,21 +112,29 @@ function testNew() { function testUpdated() { $name = 'config_test.system'; $dynamic_name = 'config_test.dynamic.default'; + $storage = $this->container->get('config.storage'); + $state = $this->container->get('config.state'); // Export. config_export(); + // Verify that the configuration objects to import exist. + $this->assertIdentical($storage->exists($name), TRUE, $name . ' found.'); + $this->assertIdentical($storage->exists($dynamic_name), TRUE, $dynamic_name . ' found.'); + + $this->assertIdentical($state->exists($name), TRUE, $name . ' found.'); + $this->assertIdentical($state->exists($dynamic_name), TRUE, $dynamic_name . ' found.'); + // Replace the file content of the existing configuration objects. - $file_storage = new FileStorage(); - $this->assertIdentical($file_storage->exists($name), TRUE, $name . ' found.'); - $this->assertIdentical($file_storage->exists($dynamic_name), TRUE, $dynamic_name . ' found.'); - $file_storage->write($name, array( + $original_name_data = array( 'foo' => 'beer', - )); - $file_storage->write($dynamic_name, array( + ); + $state->write($name, $original_name_data); + $original_dynamic_data = array( 'id' => 'default', 'label' => 'Updated', - )); + ); + $state->write($dynamic_name, $original_dynamic_data); // Verify the active store still returns the default values. $config = config($name); @@ -138,5 +150,9 @@ function testUpdated() { $this->assertIdentical($config->get('foo'), 'beer'); $config = config($dynamic_name); $this->assertIdentical($config->get('label'), 'Updated'); + + // Verify that the original file content is still the same. + $this->assertIdentical($state->read($name), $original_name_data); + $this->assertIdentical($state->read($dynamic_name), $original_dynamic_data); } } diff --git a/core/modules/config/lib/Drupal/config/Tests/ConfigImportUITest.php b/core/modules/config/lib/Drupal/config/Tests/ConfigImportUITest.php new file mode 100644 index 0000000..de0ccb2 --- /dev/null +++ b/core/modules/config/lib/Drupal/config/Tests/ConfigImportUITest.php @@ -0,0 +1,150 @@ + 'Import/Export UI', + 'description' => 'Tests the user interface for importing/exporting configuration.', + 'group' => 'Configuration', + ); + } + + function setUp() { + parent::setUp(); + + $this->web_user = $this->drupalCreateUser(array('synchronize configuration')); + $this->drupalLogin($this->web_user); + } + + /** + * Tests exporting configuration. + */ + function testExport() { + $name = 'config_test.system'; + $dynamic_name = 'config_test.dynamic.default'; + + // Verify the default configuration values exist. + $config = config($name); + $this->assertIdentical($config->get('foo'), 'bar'); + $config = config($dynamic_name); + $this->assertIdentical($config->get('id'), 'default'); + + // Verify that both appear as deleted by default. + $this->drupalGet('admin/config/development/sync/export'); + $this->assertText($name); + $this->assertText($dynamic_name); + + // Export and verify that both do not appear anymore. + $this->drupalPost(NULL, array(), t('Export')); + $this->assertUrl('admin/config/development/sync/export'); + $this->assertNoText($name); + $this->assertNoText($dynamic_name); + + // Verify that there are no further changes to export. + $this->assertText(t('There are no configuration changes.')); + + // Verify that the import screen shows no changes either. + $this->drupalGet('admin/config/development/sync'); + $this->assertText(t('There are no configuration changes.')); + } + + /** + * Tests importing configuration. + */ + function testImport() { + $name = 'config_test.new'; + $dynamic_name = 'config_test.dynamic.new'; + $storage = $this->container->get('config.storage'); + $state = $this->container->get('config.state'); + + // Verify the configuration to create does not exist yet. + $this->assertIdentical($storage->exists($name), FALSE, $name . ' not found.'); + $this->assertIdentical($storage->exists($dynamic_name), FALSE, $dynamic_name . ' not found.'); + $this->assertIdentical($state->exists($name), FALSE, $name . ' not found.'); + $this->assertIdentical($state->exists($dynamic_name), FALSE, $dynamic_name . ' not found.'); + + // Verify that the import UI does not allow to import without exported + // configuration. + $this->drupalGet('admin/config/development/sync'); + $this->assertText('There is no base configuration.'); + + // Verify that the Export link yields to the export UI page, and export. + $this->clickLink('Export'); + $this->drupalPost(NULL, array(), t('Export')); + + // Create new configuration objects. + $original_name_data = array( + 'add_me' => 'new value', + ); + $state->write($name, $original_name_data); + $original_dynamic_data = array( + 'id' => 'new', + 'label' => 'New', + ); + $state->write($dynamic_name, $original_dynamic_data); + $this->assertIdentical($state->exists($name), TRUE, $name . ' found.'); + $this->assertIdentical($state->exists($dynamic_name), TRUE, $dynamic_name . ' found.'); + + // Verify that both appear as new. + $this->drupalGet('admin/config/development/sync'); + $this->assertText($name); + $this->assertText($dynamic_name); + + // Import and verify that both do not appear anymore. + $this->drupalPost(NULL, array(), t('Import')); + $this->assertUrl('admin/config/development/sync'); + $this->assertNoText($name); + $this->assertNoText($dynamic_name); + + // Verify that there are no further changes to import. + $this->assertText(t('There are no configuration changes.')); + + // Verify that the export screen shows no changes either. + $this->drupalGet('admin/config/development/sync/export'); + $this->assertText(t('There are no configuration changes.')); + } + + /** + * Tests concurrent importing of configuration. + */ + function testImportLock() { + $name = 'config_test.new'; + $state = $this->container->get('config.state'); + + // Write a configuration object to import. + $state->write($name, array( + 'add_me' => 'new value', + )); + + // Verify that there are configuration differences to import. + $this->drupalGet('admin/config/development/sync'); + $this->assertNoText(t('There are no configuration changes.')); + + // Acquire a fake-lock on the import mechanism. + $lock_name = 'config_import'; + lock_acquire($lock_name); + + // Attempt to import configuration and verify that an error message appears. + $this->drupalPost(NULL, array(), t('Import')); + $this->assertUrl('admin/config/development/sync'); + $this->assertText(t('The import failed due to an error. Any errors have been logged.')); + + // Release the lock, just to keep testing sane. + lock_release($lock_name); + } +} diff --git a/core/modules/config/lib/Drupal/config/Tests/Storage/ConfigStorageTestBase.php b/core/modules/config/lib/Drupal/config/Tests/Storage/ConfigStorageTestBase.php index 2dbc627..e1d77be 100644 --- a/core/modules/config/lib/Drupal/config/Tests/Storage/ConfigStorageTestBase.php +++ b/core/modules/config/lib/Drupal/config/Tests/Storage/ConfigStorageTestBase.php @@ -32,6 +32,9 @@ function testCRUD() { $name = 'config_test.storage'; + // Checking whether a non-existing name exists returns FALSE. + $this->assertIdentical($this->storage->exists($name), FALSE); + // Reading a non-existing name returns FALSE. $data = $this->storage->read($name); $this->assertIdentical($data, FALSE); @@ -51,9 +54,13 @@ function testCRUD() { $data = array('foo' => 'bar'); $result = $this->storage->write($name, $data); $this->assertIdentical($result, TRUE); + $raw_data = $this->read($name); $this->assertIdentical($raw_data, $data); + // Checking whether an existing name exists returns TRUE. + $this->assertIdentical($this->storage->exists($name), TRUE); + // Writing the identical data again still returns TRUE. $result = $this->storage->write($name, $data); $this->assertIdentical($result, TRUE); diff --git a/core/modules/search/lib/Drupal/search/Tests/SearchExcerptTest.php b/core/modules/search/lib/Drupal/search/Tests/SearchExcerptTest.php index b96049e..99fd422 100644 --- a/core/modules/search/lib/Drupal/search/Tests/SearchExcerptTest.php +++ b/core/modules/search/lib/Drupal/search/Tests/SearchExcerptTest.php @@ -7,12 +7,14 @@ namespace Drupal\search\Tests; -use Drupal\simpletest\UnitTestBase; +use Drupal\simpletest\WebTestBase; /** * Tests the search_excerpt() function. */ -class SearchExcerptTest extends UnitTestBase { +class SearchExcerptTest extends WebTestBase { + public static $modules = array('search'); + public static function getInfo() { return array( 'name' => 'Search excerpt extraction', @@ -21,11 +23,6 @@ public static function getInfo() { ); } - function setUp() { - drupal_load('module', 'search'); - parent::setUp(); - } - /** * Tests search_excerpt() with several simulated search keywords. * diff --git a/core/modules/simpletest/lib/Drupal/simpletest/TestBase.php b/core/modules/simpletest/lib/Drupal/simpletest/TestBase.php index f58e1de..8f1fbd6 100644 --- a/core/modules/simpletest/lib/Drupal/simpletest/TestBase.php +++ b/core/modules/simpletest/lib/Drupal/simpletest/TestBase.php @@ -716,13 +716,22 @@ protected function prepareEnvironment() { file_prepare_directory($this->temp_files_directory, FILE_CREATE_DIRECTORY); $this->generatedTestFiles = FALSE; - // Create and set a new configuration directory and signature key. + // Set a new configuration directory name. // The child site automatically adjusts the global $config_directory_name to // a test-prefix-specific directory within the public files directory. // @see config_get_config_directory() $GLOBALS['config_directory_name'] = 'simpletest/' . substr($this->databasePrefix, 10) . '/config'; $this->configFileDirectory = $this->originalFileDirectory . '/' . $GLOBALS['config_directory_name']; - file_prepare_directory($this->configFileDirectory, FILE_CREATE_DIRECTORY | FILE_MODIFY_PERMISSIONS); + + // Reset and create a new service container. + drupal_container(NULL, TRUE); + $this->container = drupal_container(); + + // Create the new configuration directories. + include_once DRUPAL_ROOT . '/core/includes/install.inc'; + if (!install_ensure_config_directory()) { + return FALSE; + } // Log fatal errors. ini_set('log_errors', 1); diff --git a/core/modules/system/system.install b/core/modules/system/system.install index 0e42214..2c0b3d4 100644 --- a/core/modules/system/system.install +++ b/core/modules/system/system.install @@ -723,6 +723,8 @@ function system_schema() { ); $schema['cache_bootstrap'] = $schema['cache']; $schema['cache_bootstrap']['description'] = 'Cache table for data required to bootstrap Drupal, may be routed to a shared memory cache.'; + $schema['cache_config'] = $schema['cache']; + $schema['cache_config']['description'] = 'Cache table for configuration data.'; $schema['cache_form'] = $schema['cache']; $schema['cache_form']['description'] = 'Cache table for the form system to store recently built forms and their storage data, to be used in subsequent page requests.'; $schema['cache_page'] = $schema['cache']; @@ -732,28 +734,6 @@ function system_schema() { $schema['cache_path'] = $schema['cache']; $schema['cache_path']['description'] = 'Cache table for path alias lookup.'; - $schema['config'] = array( - 'description' => 'Default active store for the configuration system.', - 'fields' => array( - 'name' => array( - 'description' => 'The identifier for the configuration entry, such as module.example (the name of the file, minus the file extension).', - 'type' => 'varchar', - 'length' => 255, - 'not null' => TRUE, - 'default' => '', - ), - 'data' => array( - 'description' => 'The raw data for this configuration entry.', - 'type' => 'blob', - 'not null' => TRUE, - 'size' => 'big', - 'translatable' => TRUE, - ), - ), - 'primary key' => array('name'), - ); - - $schema['date_format_type'] = array( 'description' => 'Stores configured date format types.', 'fields' => array( diff --git a/core/modules/system/system.module b/core/modules/system/system.module index 983c317..fe6e5f36 100644 --- a/core/modules/system/system.module +++ b/core/modules/system/system.module @@ -3398,7 +3398,7 @@ function system_cron() { */ function system_cache_flush() { // Do NOT flush the 'form' cache bin to retain in-progress form submissions. - return array('bootstrap', 'cache', 'page', 'path'); + return array('bootstrap', 'config', 'cache', 'page', 'path'); } /** diff --git a/core/profiles/standard/standard.info b/core/profiles/standard/standard.info index 8b8a33b..c5240dd 100644 --- a/core/profiles/standard/standard.info +++ b/core/profiles/standard/standard.info @@ -6,6 +6,7 @@ dependencies[] = node dependencies[] = block dependencies[] = color dependencies[] = comment +dependencies[] = config dependencies[] = contextual dependencies[] = help dependencies[] = image