diff --git a/core/includes/bootstrap.inc b/core/includes/bootstrap.inc index 56e5f43..e32db3a 100644 --- a/core/includes/bootstrap.inc +++ b/core/includes/bootstrap.inc @@ -296,6 +296,20 @@ const DRUPAL_PHP_FUNCTION_PATTERN = '[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*'; /** + * $config_directories key for active directory. + * + * @see config_get_config_directory + */ +const CONFIG_ACTIVE_DIRECTORY = 'active'; + +/** + * $config_directories key for staging directory. + * + * @see config_get_config_directory + */ +const CONFIG_STAGING_DIRECTORY = 'staging'; + +/** * Starts the timer with the specified name. * * If you start and stop the same timer multiple times, the measured intervals @@ -469,20 +483,27 @@ function find_conf_path($http_host, $script_name, $require_settings = TRUE) { } /** - * Returns the path of the configuration directory. + * Returns the path of a configuration directory. + * + * @param string $type + * (optional) The type of config directory to return. Drupal core provides + * 'active' and 'staging'. Defaults to CONFIG_ACTIVE_DIRECTORY. * * @return string * The configuration directory path. */ -function config_get_config_directory() { - global $config_directory_name; +function config_get_config_directory($type = CONFIG_ACTIVE_DIRECTORY) { + global $config_directories; if ($test_prefix = drupal_valid_test_ua()) { // @see Drupal\simpletest\WebTestBase::setUp() - $path = conf_path() . '/files/simpletest/' . substr($test_prefix, 10) . '/config'; + $path = conf_path() . '/files/simpletest/' . substr($test_prefix, 10) . '/config_' . $type; + } + elseif (!empty($config_directories[$type])) { + $path = conf_path() . '/files/' . $config_directories[$type]; } else { - $path = conf_path() . '/files/' . $config_directory_name; + throw new \Exception(format_string('The configuration directory type %type does not exist.', array('%type' => $type))); } return $path; } @@ -675,7 +696,7 @@ function drupal_settings_initialize() { global $base_url, $base_path, $base_root, $script_path; // Export these settings.php variables to the global namespace. - global $databases, $cookie_domain, $conf, $installed_profile, $update_free_access, $db_url, $db_prefix, $drupal_hash_salt, $is_https, $base_secure_url, $base_insecure_url, $config_directory_name; + global $databases, $cookie_domain, $conf, $installed_profile, $update_free_access, $db_url, $db_prefix, $drupal_hash_salt, $is_https, $base_secure_url, $base_insecure_url, $config_directories; $conf = array(); // Make conf_path() available as local variable in settings.php. @@ -1676,7 +1697,9 @@ function watchdog($type, $message, array $variables = array(), $severity = WATCH // It is possible that the error handling will itself trigger an error. In that case, we could // end up in an infinite loop. To avoid that, we implement a simple static semaphore. - if (!$in_error_state && function_exists('module_implements')) { + // During early bootstrap of the installer, module_implements() might be + // loaded already, but the module system might be initialized yet. + if (!$in_error_state && function_exists('module_implements') && module_load_all(NULL)) { $in_error_state = TRUE; // The user object may not exist in all conditions, so 0 is substituted if needed. @@ -2400,20 +2423,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 @@ -2428,19 +2455,32 @@ 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(CONFIG_ACTIVE_DIRECTORY), + ), + '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%'); $container->register('config.subscriber.globalconf', 'Drupal\Core\EventSubscriber\ConfigGlobalOverrideSubscriber'); $container->register('dispatcher', 'Symfony\Component\EventDispatcher\EventDispatcher') ->addMethodCall('addSubscriber', array(new Reference('config.subscriber.globalconf'))); + // Register configuration object factory. $container->register('config.factory', 'Drupal\Core\Config\ConfigFactory') ->addArgument(new Reference('config.storage')) ->addArgument(new Reference('dispatcher')); + + // Register configuration state. + $container->setParameter('config.state.options', array( + 'directory' => config_get_config_directory(CONFIG_STAGING_DIRECTORY), + )); + $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 347bf85..c2b0f29 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 staging and the 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); @@ -214,12 +213,14 @@ function config_import_invoke_owner(array $config_changes, StorageInterface $sou } /** - * Exports configuration from the active store to FileStorage. + * Updates staging store 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 the active store and staging. $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.core.inc b/core/includes/install.core.inc index e53e895..cf3a605 100644 --- a/core/includes/install.core.inc +++ b/core/includes/install.core.inc @@ -5,6 +5,8 @@ use Drupal\Core\Database\Install\TaskException; use Drupal\Core\Language\Language; +use Symfony\Component\DependencyInjection\ContainerBuilder; +use Symfony\Component\DependencyInjection\Reference; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; @@ -276,6 +278,42 @@ function install_begin_request(&$install_state) { include_once DRUPAL_ROOT . '/core/includes/module.inc'; include_once DRUPAL_ROOT . '/core/includes/session.inc'; + // Determine whether the configuration system is ready to operate. + $install_state['config_verified'] = install_verify_config_directory(CONFIG_ACTIVE_DIRECTORY) && install_verify_config_directory(CONFIG_STAGING_DIRECTORY); + + // If it is not, replace the configuration storage with the InstallStorage + // implementation, for the following reasons: + // - The first call into drupal_container() will try to set up the regular + // runtime configuration storage, using the CachedFileStorage. The storage + // controller options require to pass in the config directory name, but that + // directory does not exist yet. + // - The installer outputs maintenance theme pages and performs many other + // operations, which try to load configuration. Since there is no active + // configuration yet, and because the configuration system does not have a + // notion of default values at runtime, data is missing in many places. The + // lack of data does not trigger errors, but results in a broken user + // interface (e.g., missing titles, etc). + // - The actual configuration data to read during installation is essentially + // the default configuration provided by the installation profile and + // modules (most notably System module). The InstallStorage therefore reads + // from the default configuration directories. + // This override is reverted as soon as the config directory has been set up + // successfully. + // @see drupal_install_config_directory() + // @todo Move this into a proper Drupal\Core\DependencyInjection\InstallContainerBuilder. + if (!$install_state['config_verified']) { + $container = new ContainerBuilder(); + + $container->register('dispatcher', 'Symfony\Component\EventDispatcher\EventDispatcher'); + + $container->register('config.storage', 'Drupal\Core\Config\InstallStorage'); + $container->register('config.factory', 'Drupal\Core\Config\ConfigFactory') + ->addArgument(new Reference('config.storage')) + ->addArgument(new Reference('dispatcher')); + + drupal_container($container); + } + // Set up $language, so t() caller functions will still work. drupal_language_initialize(); @@ -320,7 +358,6 @@ function install_begin_request(&$install_state) { // Check existing settings.php. $install_state['database_verified'] = install_verify_database_settings(); - $install_state['config_verified'] = install_ensure_config_directory(); $install_state['settings_verified'] = $install_state['config_verified'] && $install_state['database_verified']; if ($install_state['database_verified']) { @@ -1053,8 +1090,13 @@ function install_settings_form_submit($form, &$form_state) { drupal_rewrite_settings($settings); - // Add the config directory to settings.php. - drupal_install_config_directory(); + // Add the config directories to settings.php. + drupal_install_config_directories(); + + // We have a valid configuration directory in settings.php. + // Reset the service container, so the config.storage service will use the + // regular configuration storage for the remainder of the installation. + drupal_container(NULL, TRUE); // Indicate that the settings file has been verified, and check the database // for the last completed task, now that we have a valid connection. This diff --git a/core/includes/install.inc b/core/includes/install.inc index f1184a7..de32df2 100644 --- a/core/includes/install.inc +++ b/core/includes/install.inc @@ -254,48 +254,92 @@ function drupal_rewrite_settings($settings = array()) { * @see install_settings_form_submit() * @see update_prepare_d8_bootstrap() */ -function drupal_install_config_directory() { - global $config_directory_name; +function drupal_install_config_directories() { + global $config_directories; // Add a randomized config directory name to settings.php, unless it was // manually defined in the existing already. - if (!$config_directory_name) { - $settings['config_directory_name'] = array( - 'value' => 'config_' . drupal_hash_base64(drupal_random_bytes(55)), + if (empty($config_directories)) { + $settings['config_directories'] = array( + 'value' => array( + CONFIG_ACTIVE_DIRECTORY => 'config/active_' . drupal_hash_base64(drupal_random_bytes(55)), + CONFIG_STAGING_DIRECTORY => 'config/staging_' . drupal_hash_base64(drupal_random_bytes(55)), + ), 'required' => TRUE, ); // Rewrite settings.php, which also sets the value as global variable. drupal_rewrite_settings($settings); } - // Ensure that the config directory exists or can be created, and is writable. - if (!install_ensure_config_directory()) { + // Ensure the config directories exist or can be created, and are writable. + foreach (array(CONFIG_ACTIVE_DIRECTORY, CONFIG_STAGING_DIRECTORY) as $config_type) { // This should never fail, since if the config directory was specified in // settings.php it will have already been created and verified earlier, and // if it wasn't specified in settings.php, it is created here inside the // public files directory, which has already been verified to be writable // itself. But if it somehow fails anyway, the installation cannot proceed. // Bail out using a similar error message as in system_requirements(). - throw new Exception(st('The directory %directory could not be created or could not be made writable. To proceed with the installation, either create the directory and modify its permissions manually or ensure that the installer has the permissions to create it automatically. For more information, see the online handbook.', array( - '%directory' => config_get_config_directory(), - '@handbook_url' => 'http://drupal.org/server-permissions', - ))); + if (!install_ensure_config_directory($config_type)) { + throw new Exception(st('The directory %directory could not be created or could not be made writable. To proceed with the installation, either create the directory and modify its permissions manually or ensure that the installer has the permissions to create it automatically. For more information, see the online handbook.', array( + '%directory' => config_get_config_directory($config_type), + '@handbook_url' => 'http://drupal.org/server-permissions', + ))); + } } } /** + * Checks whether a config directory name is defined, and if so, whether it + * exists and is writable. + * + * This partically duplicates install_ensure_config_directory(), but is required + * since the installer would create the config directory too early in the + * installation process otherwise (e.g., when only visiting install.php when + * there is a settings.php already, but not actually executing the installation). + * + * @param string $type + * Type of config directory to return. Drupal core provides 'active' and + * 'staging'. + * + * @return bool + * TRUE if the config directory exists and is writable. + */ +function install_verify_config_directory($type) { + global $config_directories; + if (!isset($config_directories[$type])) { + return FALSE; + } + try { + $config_directory = config_get_config_directory($type); + if (is_dir($config_directory) && is_writable($config_directory)) { + return TRUE; + } + } + catch (\Exception $e) { + } + return FALSE; +} + +/** * Ensures that the config directory exists and is writable, or can be made so. + * + * @param string $type + * Type of config directory to return. Drupal core provides 'active' and + * 'staging'. + * + * @return bool + * TRUE if the config directory exists and is writable. */ -function install_ensure_config_directory() { +function install_ensure_config_directory($type) { // The config directory must be defined in settings.php. - global $config_directory_name; - if (empty($config_directory_name)) { + global $config_directories; + if (!isset($config_directories[$type])) { return FALSE; } // The logic here is similar to that used by system_requirements() for other // directories that the installer creates. else { - $config_directory = config_get_config_directory(); + $config_directory = config_get_config_directory($type); return file_prepare_directory($config_directory, FILE_CREATE_DIRECTORY | FILE_MODIFY_PERMISSIONS); } } 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/includes/theme.maintenance.inc b/core/includes/theme.maintenance.inc index 12de2a7..4b3e80c 100644 --- a/core/includes/theme.maintenance.inc +++ b/core/includes/theme.maintenance.inc @@ -122,7 +122,7 @@ function theme_task_list($variables) { } else { $class = $done ? 'done' : ''; - $status = $done ? '(' . t('done') . ')' : ''; + $status = $done ? '(' . $t('done') . ')' : ''; } $output .= ''; diff --git a/core/includes/update.inc b/core/includes/update.inc index 30be3cd..5582bb4 100644 --- a/core/includes/update.inc +++ b/core/includes/update.inc @@ -89,7 +89,7 @@ function update_prepare_d8_bootstrap() { drupal_bootstrap(DRUPAL_BOOTSTRAP_DATABASE); // Check whether settings.php needs to be rewritten. - $settings_exist = !empty($GLOBALS['config_directory_name']); + $settings_exist = !empty($GLOBALS['config_directories']); // If any of the required settings needs to be written, then settings.php // needs to be writable. @@ -139,11 +139,11 @@ function update_prepare_d8_bootstrap() { // Update the environment for the language bootstrap if needed. update_prepare_d8_language(); - // Ensure the configuration directory exists and is writable or create it. - // If no $config_directory_name has been specified in settings.php and - // created manually already, and the directory cannot be created by the + // Ensure the configuration directories exist and are writable, or create + // them. If the directories have not been specified in settings.php and + // created manually already, and either directory cannot be created by the // web server, an exception will be thrown, halting the update. - drupal_install_config_directory(); + drupal_install_config_directories(); // Change language column to langcode in url_alias. if (db_table_exists('url_alias') && db_field_exists('url_alias', 'language')) { diff --git a/core/lib/Drupal/Core/Cache/DatabaseBackend.php b/core/lib/Drupal/Core/Cache/DatabaseBackend.php index b44f18f..d4de8bf 100644 --- a/core/lib/Drupal/Core/Cache/DatabaseBackend.php +++ b/core/lib/Drupal/Core/Cache/DatabaseBackend.php @@ -7,6 +7,7 @@ namespace Drupal\Core\Cache; +use Drupal\Core\Database\Database; use Exception; /** @@ -53,14 +54,14 @@ function get($cid) { */ function getMultiple(&$cids) { try { - // When serving cached pages, the overhead of using db_select() was found + // When serving cached pages, the overhead of using ::select() was found // to add around 30% overhead to the request. Since $this->bin is a - // variable, this means the call to db_query() here uses a concatenated + // variable, this means the call to ::query() here uses a concatenated // string. This is highly discouraged under any other circumstances, and // is used here only due to the performance overhead we would incur // otherwise. When serving an uncached page, the overhead of using - // db_select() is a much smaller proportion of the request. - $result = db_query('SELECT cid, data, created, expire, serialized, tags, checksum FROM {' . db_escape_table($this->bin) . '} WHERE cid IN (:cids)', array(':cids' => $cids)); + // ::select() is a much smaller proportion of the request. + $result = Database::getConnection()->query('SELECT cid, data, created, expire, serialized, tags, checksum FROM {' . Database::getConnection()->escapeTable($this->bin) . '} WHERE cid IN (:cids)', array(':cids' => $cids)); $cache = array(); foreach ($result as $item) { $item = $this->prepareItem($item); @@ -135,7 +136,7 @@ function set($cid, $data, $expire = CacheBackendInterface::CACHE_PERMANENT, arra } try { - db_merge($this->bin) + Database::getConnection()->merge($this->bin) ->key(array('cid' => $cid)) ->fields($fields) ->execute(); @@ -149,7 +150,7 @@ function set($cid, $data, $expire = CacheBackendInterface::CACHE_PERMANENT, arra * Implements Drupal\Core\Cache\CacheBackendInterface::delete(). */ function delete($cid) { - db_delete($this->bin) + Database::getConnection()->delete($this->bin) ->condition('cid', $cid) ->execute(); } @@ -160,7 +161,7 @@ function delete($cid) { function deleteMultiple(array $cids) { // Delete in chunks when a large array is passed. do { - db_delete($this->bin) + Database::getConnection()->delete($this->bin) ->condition('cid', array_splice($cids, 0, 1000), 'IN') ->execute(); } @@ -171,8 +172,8 @@ function deleteMultiple(array $cids) { * Implements Drupal\Core\Cache\CacheBackendInterface::deletePrefix(). */ function deletePrefix($prefix) { - db_delete($this->bin) - ->condition('cid', db_like($prefix) . '%', 'LIKE') + Database::getConnection()->delete($this->bin) + ->condition('cid', Database::getConnection()->escapeLike($prefix) . '%', 'LIKE') ->execute(); } @@ -180,14 +181,14 @@ function deletePrefix($prefix) { * Implements Drupal\Core\Cache\CacheBackendInterface::flush(). */ function flush() { - db_truncate($this->bin)->execute(); + Database::getConnection()->truncate($this->bin)->execute(); } /** * Implements Drupal\Core\Cache\CacheBackendInterface::expire(). */ function expire() { - db_delete($this->bin) + Database::getConnection()->delete($this->bin) ->condition('expire', CacheBackendInterface::CACHE_PERMANENT, '<>') ->condition('expire', REQUEST_TIME, '<') ->execute(); @@ -250,7 +251,7 @@ protected function flattenTags(array $tags) { public function invalidateTags(array $tags) { foreach ($this->flattenTags($tags) as $tag) { unset(self::$tagCache[$tag]); - db_merge('cache_tags') + Database::getConnection()->merge('cache_tags') ->key(array('tag' => $tag)) ->fields(array('invalidations' => 1)) ->expression('invalidations', 'invalidations + 1') @@ -280,9 +281,14 @@ protected function checksumTags($tags) { } } if ($query_tags) { - if ($db_tags = db_query('SELECT tag, invalidations FROM {cache_tags} WHERE tag IN (:tags)', array(':tags' => $query_tags))->fetchAllKeyed()) { - self::$tagCache = array_merge(self::$tagCache, $db_tags); - $checksum += array_sum($db_tags); + try { + if ($db_tags = Database::getConnection()->query('SELECT tag, invalidations FROM {cache_tags} WHERE tag IN (:tags)', array(':tags' => $query_tags))->fetchAllKeyed()) { + self::$tagCache = array_merge(self::$tagCache, $db_tags); + $checksum += array_sum($db_tags); + } + } + catch (Exception $e) { + // The database may not be available, so we'll ignore cache_set requests. } } return $checksum; @@ -293,7 +299,7 @@ protected function checksumTags($tags) { */ function isEmpty() { $this->garbageCollection(); - $query = db_select($this->bin); + $query = Database::getConnection()->select($this->bin); $query->addExpression('1'); $result = $query->range(0, 1) ->execute() diff --git a/core/lib/Drupal/Core/Config/CacheStorage.php b/core/lib/Drupal/Core/Config/CacheStorage.php new file mode 100644 index 0000000..a2effc8 --- /dev/null +++ b/core/lib/Drupal/Core/Config/CacheStorage.php @@ -0,0 +1,143 @@ + '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, CacheBackendInterface::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::rename(). + */ + public function rename($name, $new_name) { + $this->getBackend()->delete($name); + $this->getBackend()->delete($new_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..0fc0cfe --- /dev/null +++ b/core/lib/Drupal/Core/Config/CachedFileStorage.php @@ -0,0 +1,134 @@ +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::rename(). + */ + public function rename($name, $new_name) { + $success = $this->storages['file']->rename($name, $new_name); + $this->storages['cache']->rename($name, $new_name); + 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/DatabaseStorage.php b/core/lib/Drupal/Core/Config/DatabaseStorage.php index ad8c155..a7b16a3 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 d96af7e..9a6ce66 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/InstallStorage.php b/core/lib/Drupal/Core/Config/InstallStorage.php new file mode 100644 index 0000000..740d583 --- /dev/null +++ b/core/lib/Drupal/Core/Config/InstallStorage.php @@ -0,0 +1,94 @@ +options = $options; + } + + /** + * Overrides Drupal\Core\Config\FileStorage::getFilePath(). + * + * Returns the path to the configuration file. + * + * This essentially attempts to determine the owner and path to the default + * configuration file of a requested config object name located in the + * installation profile, a module, or a theme. + * + * @return string + * The path to the configuration file. + * + * @todo Improve this when figuring out how we want to handle configuration in + * installation profiles. + */ + public function getFilePath($name) { + // Extract the owner. + $owner = strtok($name, '.'); + // Determine the path to the owner. + $path = FALSE; + foreach (array('profile', 'module', 'theme') as $type) { + if ($path = drupal_get_path($type, $owner)) { + break; + } + } + return $path . '/config/' . $name . '.' . self::getFileExtension(); + } + + /** + * Overrides Drupal\Core\Config\FileStorage::write(). + * + * @throws Drupal\Core\Config\StorageException + */ + public function write($name, array $data) { + throw new StorageException('Write operations are not allowed.'); + } + + /** + * Overrides Drupal\Core\Config\FileStorage::delete(). + * + * @throws Drupal\Core\Config\StorageException + */ + public function delete($name) { + throw new StorageException('Write operations are not allowed.'); + } + + /** + * Overrides Drupal\Core\Config\FileStorage::rename(). + * + * @throws Drupal\Core\Config\StorageException + */ + public function rename($name, $new_name) { + throw new StorageException('Write operations are not allowed.'); + } + + /** + * Implements Drupal\Core\Config\StorageInterface::listAll(). + * + * @throws Drupal\Core\Config\StorageException + */ + public function listAll($prefix = '') { + throw new StorageException('List operation is not supported.'); + } +} diff --git a/core/lib/Drupal/Core/Config/NullStorage.php b/core/lib/Drupal/Core/Config/NullStorage.php index ea21be1..aa81b73 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 a466538..688ae46 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/ConfigStorageController.php b/core/modules/config/lib/Drupal/config/ConfigStorageController.php index 57c2583..dcb7717 100644 --- a/core/modules/config/lib/Drupal/config/ConfigStorageController.php +++ b/core/modules/config/lib/Drupal/config/ConfigStorageController.php @@ -271,7 +271,8 @@ public function save(StorableInterface $entity) { // Configuration objects do not have a schema. Extract all key names from // class properties. $class_info = new \ReflectionClass($entity); - foreach ($class_info->getProperties(\ReflectionProperty::IS_PUBLIC) as $property) { + $properties = $class_info->getProperties(\ReflectionProperty::IS_PUBLIC); + foreach ($properties as $property) { $name = $property->getName(); $config->set($name, $entity->$name); } diff --git a/core/modules/config/lib/Drupal/config/Tests/ConfigCRUDTest.php b/core/modules/config/lib/Drupal/config/Tests/ConfigCRUDTest.php index a5d36a9..9eea495 100644 --- a/core/modules/config/lib/Drupal/config/Tests/ConfigCRUDTest.php +++ b/core/modules/config/lib/Drupal/config/Tests/ConfigCRUDTest.php @@ -7,7 +7,6 @@ namespace Drupal\config\Tests; -use Drupal\Core\Config\DatabaseStorage; use Drupal\simpletest\WebTestBase; /** @@ -26,7 +25,7 @@ public static function getInfo() { * Tests CRUD operations. */ function testCRUD() { - $storage = new DatabaseStorage(); + $storage = $this->container->get('config.storage'); $name = 'config_test.crud'; $config = config($name); diff --git a/core/modules/config/lib/Drupal/config/Tests/ConfigFileContentTest.php b/core/modules/config/lib/Drupal/config/Tests/ConfigFileContentTest.php index abbd2ae..7fadf31 100644 --- a/core/modules/config/lib/Drupal/config/Tests/ConfigFileContentTest.php +++ b/core/modules/config/lib/Drupal/config/Tests/ConfigFileContentTest.php @@ -7,7 +7,6 @@ namespace Drupal\config\Tests; -use Drupal\Core\Config\DatabaseStorage; use Drupal\Core\Config\FileStorage; use Drupal\simpletest\WebTestBase; @@ -23,15 +22,11 @@ public static function getInfo() { ); } - function setUp() { - parent::setUp(); - } - /** * Tests setting, writing, and reading of a configuration setting. */ function testReadWriteConfig() { - $database_storage = new DatabaseStorage(); + $storage = $this->container->get('config.storage'); $name = 'foo.bar'; $key = 'foo'; @@ -67,7 +62,7 @@ function testReadWriteConfig() { $this->assertEqual($config->get(), array(), t('New config object is empty.')); // Verify nothing was saved. - $db_data = $database_storage->read($name); + $db_data = $storage->read($name); $this->assertIdentical($db_data, FALSE); // Add a top level value @@ -94,7 +89,7 @@ function testReadWriteConfig() { $config->save(); // Verify the database entry exists. - $db_data = $database_storage->read($name); + $db_data = $storage->read($name); $this->assertTrue($db_data); // Read top level value @@ -152,27 +147,27 @@ function testReadWriteConfig() { $config->set($key, $value)->save(); // Verify the database entry exists from a chained save. - $db_data = $database_storage->read($chained_name); + $db_data = $storage->read($chained_name); $this->assertEqual($db_data, $config->get()); // Get file listing for all files starting with 'foo'. Should return // two elements. - $files = $database_storage->listAll('foo'); + $files = $storage->listAll('foo'); $this->assertEqual(count($files), 2, 'Two files listed with the prefix \'foo\'.'); // Get file listing for all files starting with 'biff'. Should return // one element. - $files = $database_storage->listAll('biff'); + $files = $storage->listAll('biff'); $this->assertEqual(count($files), 1, 'One file listed with the prefix \'biff\'.'); // Get file listing for all files starting with 'foo.bar'. Should return // one element. - $files = $database_storage->listAll('foo.bar'); + $files = $storage->listAll('foo.bar'); $this->assertEqual(count($files), 1, 'One file listed with the prefix \'foo.bar\'.'); // Get file listing for all files starting with 'bar'. Should return // an empty array. - $files = $database_storage->listAll('bar'); + $files = $storage->listAll('bar'); $this->assertEqual($files, array(), 'No files listed with the prefix \'bar\'.'); // Delete the configuration. @@ -180,7 +175,7 @@ function testReadWriteConfig() { $config->delete(); // Verify the database entry no longer exists. - $db_data = $database_storage->read($name); + $db_data = $storage->read($name); $this->assertIdentical($db_data, FALSE); } diff --git a/core/modules/config/lib/Drupal/config/Tests/ConfigImportTest.php b/core/modules/config/lib/Drupal/config/Tests/ConfigImportTest.php index 06df93f..09572df 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; /** @@ -64,6 +62,8 @@ function testNoImport() { 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); @@ -75,17 +75,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); @@ -99,6 +97,9 @@ function testDeleted() { $this->assertFalse(isset($GLOBALS['hook_config_test']['update'])); $this->assertTrue(isset($GLOBALS['hook_config_test']['predelete'])); $this->assertTrue(isset($GLOBALS['hook_config_test']['delete'])); + + // Verify that there is nothing more to import. + $this->assertFalse(config_sync_get_changes($state, $storage)); } /** @@ -107,34 +108,43 @@ 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.'); + 'langcode' => 'und', + 'style' => '', + 'uuid' => '30df59bd-7b03-4cf7-bb35-d42fc49f0651', + ); + $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']); // Verify that appropriate module API hooks have been invoked. $this->assertFalse(isset($GLOBALS['hook_config_test']['load'])); @@ -143,6 +153,9 @@ function testNew() { $this->assertFalse(isset($GLOBALS['hook_config_test']['update'])); $this->assertFalse(isset($GLOBALS['hook_config_test']['predelete'])); $this->assertFalse(isset($GLOBALS['hook_config_test']['delete'])); + + // Verify that there is nothing more to import. + $this->assertFalse(config_sync_get_changes($state, $storage)); } /** @@ -151,21 +164,27 @@ 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( - 'id' => 'default', - 'label' => 'Updated', - )); + ); + $state->write($name, $original_name_data); + $original_dynamic_data = $state->read($dynamic_name); + $original_dynamic_data['label'] = 'Updated'; + $state->write($dynamic_name, $original_dynamic_data); // Verify the active store still returns the default values. $config = config($name); @@ -182,6 +201,10 @@ function testUpdated() { $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); + // Verify that appropriate module API hooks have been invoked. $this->assertTrue(isset($GLOBALS['hook_config_test']['load'])); $this->assertTrue(isset($GLOBALS['hook_config_test']['presave'])); @@ -189,6 +212,9 @@ function testUpdated() { $this->assertTrue(isset($GLOBALS['hook_config_test']['update'])); $this->assertFalse(isset($GLOBALS['hook_config_test']['predelete'])); $this->assertFalse(isset($GLOBALS['hook_config_test']['delete'])); + + // Verify that there is nothing more to import. + $this->assertFalse(config_sync_get_changes($state, $storage)); } } 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..ea20651 --- /dev/null +++ b/core/modules/config/lib/Drupal/config/Tests/ConfigImportUITest.php @@ -0,0 +1,153 @@ + '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', + 'langcode' => 'und', + 'style' => '', + 'uuid' => '30df59bd-7b03-4cf7-bb35-d42fc49f0651', + ); + $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 d07b0b7..6b7a74f 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/config/lib/Drupal/config/Tests/Storage/DatabaseStorageTest.php b/core/modules/config/lib/Drupal/config/Tests/Storage/DatabaseStorageTest.php index 2e1599f..185921f 100644 --- a/core/modules/config/lib/Drupal/config/Tests/Storage/DatabaseStorageTest.php +++ b/core/modules/config/lib/Drupal/config/Tests/Storage/DatabaseStorageTest.php @@ -23,8 +23,34 @@ public static function getInfo() { function setUp() { parent::setUp(); + + $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'), + ); + db_create_table('config', $schema['config']); + $this->storage = new DatabaseStorage(); $this->invalidStorage = new DatabaseStorage(array('connection' => 'invalid')); + + // ::listAll() verifications require other configuration data to exist. + $this->storage->write('system.performance', array()); } protected function read($name) { diff --git a/core/modules/config/lib/Drupal/config/Tests/Storage/FileStorageTest.php b/core/modules/config/lib/Drupal/config/Tests/Storage/FileStorageTest.php index 092687f..0fd2ba4 100644 --- a/core/modules/config/lib/Drupal/config/Tests/Storage/FileStorageTest.php +++ b/core/modules/config/lib/Drupal/config/Tests/Storage/FileStorageTest.php @@ -25,7 +25,7 @@ public static function getInfo() { function setUp() { parent::setUp(); $this->storage = new FileStorage(); - $this->invalidStorage = new FileStorage(array('directory' => $this->configFileDirectory . '/nonexisting')); + $this->invalidStorage = new FileStorage(array('directory' => $this->configDirectories[CONFIG_ACTIVE_DIRECTORY] . '/nonexisting')); // FileStorage::listAll() requires other configuration data to exist. $this->storage->write('system.performance', config('system.performance')->get()); diff --git a/core/modules/entity/lib/Drupal/entity/StorableBase.php b/core/modules/entity/lib/Drupal/entity/StorableBase.php index 57bf352..c5d9867 100644 --- a/core/modules/entity/lib/Drupal/entity/StorableBase.php +++ b/core/modules/entity/lib/Drupal/entity/StorableBase.php @@ -45,7 +45,7 @@ * * @var bool */ - public $isCurrentRevision = TRUE; + protected $isCurrentRevision = TRUE; /** * Constructs a new entity object. @@ -280,7 +280,11 @@ public function getRevisionId() { /** * Implements Drupal\entity\StorableInterface::isCurrentRevision(). */ - public function isCurrentRevision() { - return $this->isCurrentRevision; + public function isCurrentRevision($new_value = NULL) { + $return = $this->isCurrentRevision; + if (isset($new_value)) { + $this->isCurrentRevision = (bool) $new_value; + } + return $return; } } diff --git a/core/modules/entity/lib/Drupal/entity/StorableInterface.php b/core/modules/entity/lib/Drupal/entity/StorableInterface.php index 23bc1ea..b784260 100644 --- a/core/modules/entity/lib/Drupal/entity/StorableInterface.php +++ b/core/modules/entity/lib/Drupal/entity/StorableInterface.php @@ -211,8 +211,13 @@ public function getRevisionId(); /** * Checks if this entity is the current revision. * + * @param bool $new_value + * (optional) A Boolean to (re)set the isCurrentRevision flag. + * * @return bool - * TRUE if the entity is the current revision, FALSE otherwise. + * TRUE if the entity is the current revision, FALSE otherwise. If + * $new_value was passed, the previous value is returned. */ - public function isCurrentRevision(); + public function isCurrentRevision($new_value = NULL); + } 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 e20eb9a..253d4df 100644 --- a/core/modules/simpletest/lib/Drupal/simpletest/TestBase.php +++ b/core/modules/simpletest/lib/Drupal/simpletest/TestBase.php @@ -689,7 +689,7 @@ protected function prepareEnvironment() { // Backup statics and globals. $this->originalContainer = clone drupal_container(); $this->originalLanguage = $language_interface; - $this->originalConfigDirectory = $GLOBALS['config_directory_name']; + $this->originalConfigDirectories = $GLOBALS['config_directories']; // Save further contextual information. $this->originalFileDirectory = variable_get('file_public_path', conf_path() . '/files'); @@ -717,13 +717,27 @@ 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. - // The child site automatically adjusts the global $config_directory_name to + // Create and set new configuration directories. The child site + // uses drupal_valid_test_ua() to adjust the config directory paths 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); + $GLOBALS['config_directories'] = array(); + foreach (array(CONFIG_ACTIVE_DIRECTORY, CONFIG_STAGING_DIRECTORY) as $type) { + $GLOBALS['config_directories'][$type] = 'simpletest/' . substr($this->databasePrefix, 10) . '/config_' . $type; + } + + // Reset and create a new service container. + drupal_container(NULL, TRUE); + $this->container = drupal_container(); + + $this->configDirectories = array(); + include_once DRUPAL_ROOT . '/core/includes/install.inc'; + foreach ($GLOBALS['config_directories'] as $type => $path) { + if (!install_ensure_config_directory($type)) { + return FALSE; + } + $this->configDirectories[$type] = $this->originalFileDirectory . '/' . $path; + } // Log fatal errors. ini_set('log_errors', 1); @@ -781,7 +795,7 @@ protected function tearDown() { // Restore original statics and globals. drupal_container($this->originalContainer); $language_interface = $this->originalLanguage; - $GLOBALS['config_directory_name'] = $this->originalConfigDirectory; + $GLOBALS['config_directories'] = $this->originalConfigDirectories; // Restore original shutdown callbacks. $callbacks = &drupal_register_shutdown_function(); diff --git a/core/modules/system/system.install b/core/modules/system/system.install index b8d402c..da76231 100644 --- a/core/modules/system/system.install +++ b/core/modules/system/system.install @@ -330,14 +330,15 @@ function system_requirements($phase) { // Check the config directory if it is defined in settings.php. If it isn't // defined, the installer will create a valid config directory later, but // during runtime we must always display an error. - if (!empty($GLOBALS['config_directory_name'])) { - $directories[] = config_get_config_directory(); + if (!empty($GLOBALS['config_directories'])) { + $directories[] = config_get_config_directory(CONFIG_ACTIVE_DIRECTORY); + $directories[] = config_get_config_directory(CONFIG_STAGING_DIRECTORY); } elseif ($phase != 'install') { - $requirements['config directory'] = array( - 'title' => $t('Configuration directory'), + $requirements['config directories'] = array( + 'title' => $t('Configuration directories'), 'value' => $t('Not present'), - 'description' => $t('Your %file file must define the $config_directory_name variable as the name of a directory in which configuration files can be written.', array('%file' => conf_path() . '/settings.php')), + 'description' => $t('Your %file file must define the $config_directories variable as an array containing the name of a directories in which configuration files can be written.', array('%file' => conf_path() . '/settings.php')), 'severity' => REQUIREMENT_ERROR, ); } @@ -723,6 +724,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 +735,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( @@ -1793,6 +1774,61 @@ function system_update_8003() { ), 'primary key' => array('name'), )); + + $schema['cache'] = array( + 'description' => 'Generic cache table for caching things not separated out into their own tables. Contributed modules may also use this to store cached items.', + 'fields' => array( + 'cid' => array( + 'description' => 'Primary Key: Unique cache ID.', + 'type' => 'varchar', + 'length' => 255, + 'not null' => TRUE, + 'default' => '', + ), + 'data' => array( + 'description' => 'A collection of data to cache.', + 'type' => 'blob', + 'not null' => FALSE, + 'size' => 'big', + ), + 'expire' => array( + 'description' => 'A Unix timestamp indicating when the cache entry should expire, or 0 for never.', + 'type' => 'int', + 'not null' => TRUE, + 'default' => 0, + ), + 'created' => array( + 'description' => 'A Unix timestamp indicating when the cache entry was created.', + 'type' => 'int', + 'not null' => TRUE, + 'default' => 0, + ), + 'serialized' => array( + 'description' => 'A flag to indicate whether content is serialized (1) or not (0).', + 'type' => 'int', + 'size' => 'small', + 'not null' => TRUE, + 'default' => 0, + ), + 'tags' => array( + 'description' => 'Space-separated list of cache tags for this entry.', + 'type' => 'text', + 'size' => 'big', + 'not null' => FALSE, + ), + 'checksum' => array( + 'description' => 'The tag invalidation sum when this entry was saved.', + 'type' => 'int', + 'not null' => TRUE, + 'default' => 0, + ), + ), + 'indexes' => array( + 'expire' => array('expire'), + ), + 'primary key' => array('cid'), + ); + db_create_table('cache_config', $schema['cache']); } /** @@ -1872,7 +1908,7 @@ function system_update_8007() { foreach ($tables as $table) { // Assume we have a valid cache table if there is both 'cid' and 'data' // columns. - if (db_field_exists($table, 'cid') && db_field_exists($table, 'data')) { + if (db_field_exists($table, 'cid') && db_field_exists($table, 'data') && !db_field_exists($table, 'tags')) { db_add_field($table, 'tags', array( 'description' => 'Space-separated list of cache tags for this entry.', 'type' => 'text', diff --git a/core/modules/system/system.module b/core/modules/system/system.module index 69a1f62..d0545a3 100644 --- a/core/modules/system/system.module +++ b/core/modules/system/system.module @@ -3403,7 +3403,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 diff --git a/sites/default/default.settings.php b/sites/default/default.settings.php index 2c12638..b063812 100755 --- a/sites/default/default.settings.php +++ b/sites/default/default.settings.php @@ -257,9 +257,14 @@ * @todo Flesh this out, provide more details, etc. * * Example: - * $config_directory_name = '/some/directory/outside/webroot'; + * @code + * $config_directories = array( + * CONFIG_ACTIVE_DIRECTORY => '/some/directory/outside/webroot', + * CONFIG_STAGING_DIRECTORY => '/another/directory/outside/webroot', + * ); + * @endcode */ -$config_directory_name = ''; +$config_directories = array(); /** * Base URL (optional).