diff --git a/core/includes/install.core.inc b/core/includes/install.core.inc index 06d4f0819573..7e3398645c48 100644 --- a/core/includes/install.core.inc +++ b/core/includes/install.core.inc @@ -741,6 +741,8 @@ function install_tasks($install_state) { ), 'install_profile_themes' => array( ), + 'install_install_profile' => array( + ), 'install_import_translations' => array( 'display_name' => t('Set up translations'), 'display' => $needs_translations, @@ -1022,10 +1024,6 @@ function install_base_system(&$install_state) { // State can be set to the database now that system.module is installed. $modules = $install_state['profile_info']['dependencies']; - // The installation profile is also a module, which needs to be installed - // after all the dependencies have been installed. - $modules[] = drupal_get_profile(); - \Drupal::state()->set('install_profile_modules', array_diff($modules, array('system'))); $install_state['base_system_verified'] = TRUE; } @@ -1571,10 +1569,7 @@ function install_profile_modules(&$install_state) { // the modules. $required = array(); $non_required = array(); - // Although the profile module is marked as required, it needs to go after - // every dependency, including non-required ones. So clear its required - // flag for now to allow it to install late. - $files[$install_state['parameters']['profile']]->info['required'] = FALSE; + // Add modules that other modules depend on. foreach ($modules as $module) { if ($files[$module]->requires) { @@ -1633,6 +1628,24 @@ function install_profile_themes(&$install_state) { } } +/** + * Installs the install profile. + * + * @param $install_state + * An array of information about the current installation state. + */ +function install_install_profile(&$install_state) { + \Drupal::service('module_installer')->install(array(drupal_get_profile()), FALSE); + // Install all available optional config. During installation the module order + // is determined by dependencies. If there are no dependencies between modules + // then the order in which they are installed is dependent on random factors + // like PHP version. Optional configuration therefore might or might not be + // created depending on this order. Ensuring that we have installed all of the + // optional configuration whose dependencies can be met at this point removes + // any disparities that this creates. + \Drupal::service('config.installer')->installOptionalConfig(); +} + /** * Prepares the system for import and downloads additional translations. * diff --git a/core/lib/Drupal/Core/Config/ConfigImporter.php b/core/lib/Drupal/Core/Config/ConfigImporter.php index 715138f0d488..3f7db462f190 100644 --- a/core/lib/Drupal/Core/Config/ConfigImporter.php +++ b/core/lib/Drupal/Core/Config/ConfigImporter.php @@ -786,8 +786,7 @@ class ConfigImporter { $this->setProcessedExtension($type, $op, $name); \Drupal::service('config.installer') - ->setSyncing(FALSE) - ->resetSourceStorage(); + ->setSyncing(FALSE); } /** diff --git a/core/lib/Drupal/Core/Config/ConfigInstaller.php b/core/lib/Drupal/Core/Config/ConfigInstaller.php index 8065613fc1ad..ffdc7609641c 100644 --- a/core/lib/Drupal/Core/Config/ConfigInstaller.php +++ b/core/lib/Drupal/Core/Config/ConfigInstaller.php @@ -9,6 +9,7 @@ namespace Drupal\Core\Config; use Drupal\Component\Utility\Unicode; use Drupal\Core\Config\Entity\ConfigDependencyManager; +use Drupal\Core\Entity\EntityTypeInterface; use Drupal\Core\Site\Settings; use Symfony\Component\EventDispatcher\EventDispatcherInterface; @@ -89,36 +90,66 @@ class ConfigInstaller implements ConfigInstallerInterface { * {@inheritdoc} */ public function installDefaultConfig($type, $name) { - $extension_path = drupal_get_path($type, $name); + $extension_path = $this->drupalGetPath($type, $name); // Refresh the schema cache if the extension provides configuration schema // or is a theme. if (is_dir($extension_path . '/' . InstallStorage::CONFIG_SCHEMA_DIRECTORY) || $type == 'theme') { $this->typedConfig->clearCachedDefinitions(); } - // Gather information about all the supported collections. - $collection_info = $this->configManager->getConfigCollectionInfo(); + $default_install_path = $this->getDefaultConfigDirectory($type, $name); + if (is_dir($default_install_path)) { + if (!$this->isSyncing()) { + $storage = new FileStorage($default_install_path, StorageInterface::DEFAULT_COLLECTION); + $prefix = ''; + } + else { + // The configuration importer sets the source storage on the config + // installer. The configuration importer handles all of the + // configuration entity imports. We only need to ensure that simple + // configuration is created when the extension is installed. + $storage = $this->getSourceStorage(); + $prefix = $name . '.'; + } - // Read enabled extensions directly from configuration to avoid circular - // dependencies with ModuleHandler and ThemeHandler. - $extension_config = $this->configFactory->get('core.extension'); - $modules = (array) $extension_config->get('module'); - // Unless we are installing the profile, remove it from the list. - if ($install_profile = Settings::get('install_profile')) { - if ($name !== $install_profile) { - unset($modules[$install_profile]); + // Gets a profile storage to search for overrides if necessary. + $profile_storage = $this->getProfileStorage($name); + + // Gather information about all the supported collections. + $collection_info = $this->configManager->getConfigCollectionInfo(); + foreach ($collection_info->getCollectionNames() as $collection) { + $config_to_create = $this->getConfigToCreate($storage, $collection, $prefix, $profile_storage); + // If we're installing a profile ensure configuration that is overriding + // is excluded. + if ($name == $this->drupalGetProfile()) { + $existing_configuration = $this->getActiveStorages($collection)->listAll(); + $config_to_create = array_diff_key($config_to_create, array_flip($existing_configuration)); + } + if (!empty($config_to_create)) { + $this->createConfiguration($collection, $config_to_create); + } } } - $enabled_extensions = array_keys($modules); - $enabled_extensions += array_keys((array) $extension_config->get('theme')); - // Core can provide configuration. - $enabled_extensions[] = 'core'; - - foreach ($collection_info->getCollectionNames(TRUE) as $collection) { - $config_to_install = $this->listDefaultConfigToInstall($type, $name, $collection, $enabled_extensions); - if (!empty($config_to_install)) { - $this->createConfiguration($collection, $config_to_install); + // During a drupal installation optional configuration is installed at the + // end of the installation process. + // @see install_install_profile() + if (!$this->isSyncing() && !$this->drupalInstallationAttempted()) { + $optional_install_path = $extension_path . '/' . InstallStorage::CONFIG_OPTIONAL_DIRECTORY; + if (is_dir($optional_install_path)) { + // Install any optional config the module provides. + $storage = new FileStorage($optional_install_path, StorageInterface::DEFAULT_COLLECTION); + $this->installOptionalConfig($storage, ''); + } + // Install any optional configuration entities whose type this extension + // provides. This searches all the installed modules config/optional + // directories. + $provides_config_entity_type = array_reduce($this->configManager->getEntityManager()->getDefinitions(), function ($return, EntityTypeInterface $entity_type) use ($name) { + return $return ?: $entity_type->getProvider() && $entity_type->getConfigPrefix(); + }, FALSE); + if ($provides_config_entity_type) { + $storage = new ExtensionInstallStorage($this->getActiveStorages(StorageInterface::DEFAULT_COLLECTION), InstallStorage::CONFIG_OPTIONAL_DIRECTORY, StorageInterface::DEFAULT_COLLECTION, FALSE); + $this->installOptionalConfig($storage, $name . '.'); } } @@ -127,49 +158,76 @@ class ConfigInstaller implements ConfigInstallerInterface { } /** - * Lists default configuration for an extension that is available to install. - * - * This looks in the extension's config/install directory and all of the - * currently enabled extensions config/install directories for configuration - * that begins with the extension's name. - * - * @param string $type - * The extension type; e.g., 'module' or 'theme'. - * @param string $name - * The name of the module or theme to install default configuration for. - * @param string $collection - * The configuration collection to install. - * @param array $enabled_extensions - * A list of all the currently enabled modules and themes. - * - * @return array - * The list of configuration objects to create. + * {@inheritdoc} */ - protected function listDefaultConfigToInstall($type, $name, $collection, array $enabled_extensions) { - // Get all default configuration owned by this extension. - $source_storage = $this->getSourceStorage($collection); - $config_to_install = $source_storage->listAll($name . '.'); - - // If not installing the core base system default configuration, work out if - // this extension provides default configuration for any other enabled - // extensions. - $extension_path = drupal_get_path($type, $name); - if ($type !== 'core' && is_dir($extension_path . '/' . InstallStorage::CONFIG_INSTALL_DIRECTORY)) { - $default_storage = new FileStorage($extension_path . '/' . InstallStorage::CONFIG_INSTALL_DIRECTORY, $collection); - $extension_provided_config = array_filter($default_storage->listAll(), function ($config_name) use ($config_to_install, $enabled_extensions) { - // Ensure that we have not already discovered the config to install. - if (in_array($config_name, $config_to_install)) { - return FALSE; - } - // Ensure the configuration is provided by an enabled module. - $provider = Unicode::substr($config_name, 0, strpos($config_name, '.')); - return in_array($provider, $enabled_extensions); - }); - - $config_to_install = array_merge($config_to_install, $extension_provided_config); + public function installOptionalConfig(StorageInterface $storage = NULL, $prefix = '') { + if (!$storage) { + // Search the install profile's optional configuration too. + $storage = new ExtensionInstallStorage($this->getActiveStorages(StorageInterface::DEFAULT_COLLECTION), InstallStorage::CONFIG_OPTIONAL_DIRECTORY, StorageInterface::DEFAULT_COLLECTION, TRUE); + // The extension install storage ensures that overrides are used. + $profile_storage = NULL; + } + else { + // Creates a profile storage to search for overrides. + $profile_install_path = $this->drupalGetPath('module', $this->drupalGetProfile()) . '/' . InstallStorage::CONFIG_OPTIONAL_DIRECTORY; + $profile_storage = new FileStorage($profile_install_path, StorageInterface::DEFAULT_COLLECTION); } - return $config_to_install; + $collection_info = $this->configManager->getConfigCollectionInfo(); + $enabled_extensions = $this->getEnabledExtensions(); + + foreach ($collection_info->getCollectionNames() as $collection) { + if (!$this->configManager->supportsConfigurationEntities($collection)) { + continue; + } + $existing_config = $this->getActiveStorages($collection)->listAll($prefix); + $config_to_create = $this->getConfigToCreate($storage, $collection, $prefix, $profile_storage); + $all_config = array_merge($existing_config, array_keys($config_to_create)); + foreach ($config_to_create as $config_name => $data) { + // Exclude configuration that: + // - already exists + // - is a not configuration entity + // - or its dependencies cannot be met. + if (in_array($config_name, $existing_config) || + !$this->configManager->getEntityTypeIdByName($config_name) || + !$this->validateDependencies($config_name, $data, $enabled_extensions, $all_config)) { + unset($config_to_create[$config_name]); + } + } + if (!empty($config_to_create)) { + $this->createConfiguration($collection, $config_to_create, TRUE); + } + } + } + + /** + * Gets configuration data from the provided storage to create. + * + * @param StorageInterface $storage + * The configuration storage to read configuration from. + * @param string $collection + * The configuration collection to use. + * @param string $prefix + * (optional) Limit to configuration starting with the provided string. + * + * @return array + * An array of configuration data read from the source storage keyed by the + * configuration object name. + */ + protected function getConfigToCreate(StorageInterface $storage, $collection, $prefix = '', StorageInterface $profile_storage = NULL) { + if ($storage->getCollectionName() != $collection) { + $storage = $storage->createCollection($collection); + } + $data = $storage->readMultiple($storage->listAll($prefix)); + + // Check to see if the corresponding override storage has any overrides. + if ($profile_storage) { + if ($profile_storage->getCollectionName() != $collection) { + $profile_storage = $profile_storage->createCollection($collection); + } + $data = $profile_storage->readMultiple(array_keys($data)) + $data; + } + return $data; } /** @@ -177,24 +235,23 @@ class ConfigInstaller implements ConfigInstallerInterface { * * @param string $collection * The configuration collection. - * @param array $config_to_install - * A list of configuration object names to create. + * @param array $config_to_create + * An array of configuration data to create, keyed by name. */ - protected function createConfiguration($collection, array $config_to_install) { + protected function createConfiguration($collection, array $config_to_create) { // Order the configuration to install in the order of dependencies. - $data = $this->getSourceStorage($collection)->readMultiple($config_to_install); $config_entity_support = $this->configManager->supportsConfigurationEntities($collection); if ($config_entity_support) { $dependency_manager = new ConfigDependencyManager(); - $config_to_install = $dependency_manager - ->setData($data) + $config_names = $dependency_manager + ->setData($config_to_create) ->sortAll(); } + else { + $config_names = array_keys($config_to_create); + } - // Remove configuration that already exists in the active storage. - $config_to_install = array_diff($config_to_install, $this->getActiveStorages($collection)->listAll()); - - foreach ($config_to_install as $name) { + foreach ($config_names as $name) { // Allow config factory overriders to use a custom configuration object if // they are responsible for the collection. $overrider = $this->configManager->getConfigCollectionInfo()->getOverrideService($collection); @@ -204,18 +261,17 @@ class ConfigInstaller implements ConfigInstallerInterface { else { $new_config = new Config($name, $this->getActiveStorages($collection), $this->eventDispatcher, $this->typedConfig); } - if ($data[$name] !== FALSE) { - $new_config->setData($data[$name]); + if ($config_to_create[$name] !== FALSE) { + $new_config->setData($config_to_create[$name]); } if ($config_entity_support && $entity_type = $this->configManager->getEntityTypeIdByName($name)) { - // If we are syncing do not create configuration entities. Pluggable // configuration entities can have dependencies on modules that are // not yet enabled. This approach means that any code that expects // default configuration entities to exist will be unstable after the // module has been enabled and before the config entity has been // imported. - if ($this->isSyncing) { + if ($this->isSyncing()) { continue; } /** @var \Drupal\Core\Config\Entity\ConfigEntityStorageInterface $entity_storage */ @@ -246,16 +302,15 @@ class ConfigInstaller implements ConfigInstallerInterface { * {@inheritdoc} */ public function installCollectionDefaultConfig($collection) { - $config_to_install = $this->getSourceStorage($collection)->listAll(); - $extension_config = $this->configFactory->get('core.extension'); - $enabled_extensions = array_keys((array) $extension_config->get('module')); - $enabled_extensions += array_keys((array) $extension_config->get('theme')); - $config_to_install = array_filter($config_to_install, function ($config_name) use ($enabled_extensions) { + $storage = new ExtensionInstallStorage($this->getActiveStorages(StorageInterface::DEFAULT_COLLECTION), InstallStorage::CONFIG_INSTALL_DIRECTORY, $collection, $this->drupalInstallationAttempted()); + // Only install configuration for enabled extensions. + $enabled_extensions = $this->getEnabledExtensions(); + $config_to_install = array_filter($storage->listAll(), function ($config_name) use ($enabled_extensions) { $provider = Unicode::substr($config_name, 0, strpos($config_name, '.')); return in_array($provider, $enabled_extensions); }); if (!empty($config_to_install)) { - $this->createConfiguration($collection, $config_to_install); + $this->createConfiguration($collection, $storage->readMultiple($config_to_install)); // Reset all the static caches and list caches. $this->configFactory->reset(); } @@ -269,34 +324,14 @@ class ConfigInstaller implements ConfigInstallerInterface { return $this; } - /** - * {@inheritdoc} - */ - public function resetSourceStorage() { - $this->sourceStorage = null; - return $this; - } - /** * Gets the configuration storage that provides the default configuration. * - * @param string $collection - * (optional) The configuration collection. Defaults to the default - * collection. - * - * @return \Drupal\Core\Config\StorageInterface + * @return \Drupal\Core\Config\StorageInterface|null * The configuration storage that provides the default configuration. + * Returns null if the source storage has not been set. */ - public function getSourceStorage($collection = StorageInterface::DEFAULT_COLLECTION) { - if (!isset($this->sourceStorage)) { - // Default to using the ExtensionInstallStorage which searches extension's - // config directories for default configuration. Only include the profile - // configuration during Drupal installation. - $this->sourceStorage = new ExtensionInstallStorage($this->getActiveStorages(StorageInterface::DEFAULT_COLLECTION), InstallStorage::CONFIG_INSTALL_DIRECTORY, $collection, drupal_installation_attempted()); - } - if ($this->sourceStorage->getCollectionName() != $collection) { - $this->sourceStorage = $this->sourceStorage->createCollection($collection); - } + public function getSourceStorage() { return $this->sourceStorage; } @@ -321,6 +356,9 @@ class ConfigInstaller implements ConfigInstallerInterface { * {@inheritdoc} */ public function setSyncing($status) { + if (!$status) { + $this->sourceStorage = NULL; + } $this->isSyncing = $status; return $this; } @@ -333,24 +371,30 @@ class ConfigInstaller implements ConfigInstallerInterface { } /** - * {@inheritdoc} + * Finds pre-existing configuration objects for the provided extension. + * + * Extensions can not be installed if configuration objects exist in the + * active storage with the same names. This can happen in a number of ways, + * commonly: + * - if a user has created configuration with the same name as that provided + * by the extension. + * - if the extension provides default configuration that does not depend on + * it and the extension has been uninstalled and is about to the + * reinstalled. + * + * @return array + * Array of configuration object names that already exist keyed by + * collection. */ - public function findPreExistingConfiguration($type, $name) { + protected function findPreExistingConfiguration(StorageInterface $storage) { $existing_configuration = array(); // Gather information about all the supported collections. $collection_info = $this->configManager->getConfigCollectionInfo(); - // Read enabled extensions directly from configuration to avoid circular - // dependencies on ModuleHandler and ThemeHandler. - $extension_config = $this->configFactory->get('core.extension'); - $enabled_extensions = array_keys((array) $extension_config->get('module')); - $enabled_extensions += array_keys((array) $extension_config->get('theme')); - // Add the extension that will be enabled to the list of enabled extensions. - $enabled_extensions[] = $name; - foreach ($collection_info->getCollectionNames(TRUE) as $collection) { - $config_to_install = $this->listDefaultConfigToInstall($type, $name, $collection, $enabled_extensions); + foreach ($collection_info->getCollectionNames() as $collection) { + $config_to_create = array_keys($this->getConfigToCreate($storage, $collection)); $active_storage = $this->getActiveStorages($collection); - foreach ($config_to_install as $config_name) { + foreach ($config_to_create as $config_name) { if ($active_storage->exists($config_name)) { $existing_configuration[$collection][] = $config_name; } @@ -358,4 +402,225 @@ class ConfigInstaller implements ConfigInstallerInterface { } return $existing_configuration; } + + /** + * {@inheritdoc} + */ + public function checkConfigurationToInstall($type, $name) { + if ($this->isSyncing()) { + // Configuration is assumed to already be checked by the config importer + // validation events. + return; + } + $config_install_path = $this->getDefaultConfigDirectory($type, $name); + if (!is_dir($config_install_path)) { + return; + } + + $storage = new FileStorage($config_install_path, StorageInterface::DEFAULT_COLLECTION); + + $enabled_extensions = $this->getEnabledExtensions(); + // Add the extension that will be enabled to the list of enabled extensions. + $enabled_extensions[] = $name; + // Gets a profile storage to search for overrides if necessary. + $profile_storage = $this->getProfileStorage($name); + + // Check the dependencies of configuration provided by the module. + $invalid_default_config = $this->findDefaultConfigWithUnmetDependencies($storage, $enabled_extensions, $profile_storage); + if (!empty($invalid_default_config)) { + throw UnmetDependenciesException::create($name, $invalid_default_config); + } + + // Install profiles can not have config clashes. Configuration that + // has the same name as a module's configuration will be used instead. + if ($name != $this->drupalGetProfile()) { + // Throw an exception if the module being installed contains configuration + // that already exists. Additionally, can not continue installing more + // modules because those may depend on the current module being installed. + $existing_configuration = $this->findPreExistingConfiguration($storage); + if (!empty($existing_configuration)) { + throw PreExistingConfigException::create($name, $existing_configuration); + } + } + } + + /** + * Finds default configuration with unmet dependencies. + * + * @param array $enabled_extensions + * A list of all the currently enabled modules and themes. + * + * @return array + * List of configuration that has unmet dependencies + */ + protected function findDefaultConfigWithUnmetDependencies(StorageInterface $storage, array $enabled_extensions, StorageInterface $profile_storage = NULL) { + $config_to_create = $this->getConfigToCreate($storage, StorageInterface::DEFAULT_COLLECTION, '', $profile_storage); + $all_config = array_merge($this->configFactory->listAll(), array_keys($config_to_create)); + return array_filter(array_keys($config_to_create), function($config_name) use ($enabled_extensions, $all_config, $config_to_create) { + return !$this->validateDependencies($config_name, $config_to_create[$config_name], $enabled_extensions, $all_config); + }); + } + + /** + * Validates an array of config data that contains dependency information. + * + * @param string $config_name + * The name of the configuration object that is being validated. + * @param array $data + * Configuration data. + * @param array $enabled_extensions + * A list of all the currently enabled modules and themes. + * @param array $all_config + * A list of all the active configuration names. + * + * @return bool + * TRUE if the dependencies are met, FALSE if not. + */ + protected function validateDependencies($config_name, array $data, array $enabled_extensions, array $all_config) { + // All the migrate tests will fail if we check since they install the + // migrate_drupal module but only set up the dependencies for the single + // migration they are testing. + if (strpos($config_name, 'migrate.migration.') === 0) { + return TRUE; + } + if (isset($data['dependencies'])) { + $all_dependencies = $data['dependencies']; + + // Ensure enforced dependencies are included. + if (isset($all_dependencies['enforced'])) { + $all_dependencies = array_merge($all_dependencies, $data['dependencies']['enforced']); + unset($all_dependencies['enforced']); + } + // Ensure the configuration entity type provider is in the list of + // dependencies. + list($provider) = explode('.', $config_name, 2); + if (!isset($all_dependencies['module'])) { + $all_dependencies['module'][] = $provider; + } + elseif (!in_array($provider, $all_dependencies['module'])) { + $all_dependencies['module'][] = $provider; + } + + foreach ($all_dependencies as $type => $dependencies) { + $list_to_check = []; + switch ($type) { + case 'module': + case 'theme': + $list_to_check = $enabled_extensions; + break; + case 'config': + $list_to_check = $all_config; + break; + } + if (!empty($list_to_check)) { + $missing = array_diff($dependencies, $list_to_check); + if (!empty($missing)) { + return FALSE; + } + } + } + } + return TRUE; + } + + /** + * Gets the list of enabled extensions including both modules and themes. + * + * @return array + * A list of enabled extensions which includes both modules and themes. + */ + protected function getEnabledExtensions() { + // Read enabled extensions directly from configuration to avoid circular + // dependencies on ModuleHandler and ThemeHandler. + $extension_config = $this->configFactory->get('core.extension'); + $enabled_extensions = (array) $extension_config->get('module'); + $enabled_extensions += (array) $extension_config->get('theme'); + // Core can provide configuration. + $enabled_extensions['core'] = 'core'; + return array_keys($enabled_extensions); + } + + /** + * Gets the profile storage to use to check for profile overrides. + * + * @param string $installing_name + * (optional) The name of the extension currently being installed. + * + * @return \Drupal\Core\Config\StorageInterface|null + * A storage to access configuration from the installation profile. If a + * Drupal installation is not in progress or we're installing the profile + * itself, then it will return NULL as the profile storage should not be + * used. + */ + protected function getProfileStorage($installing_name = '') { + $profile = $this->drupalGetProfile(); + if ($this->drupalInstallationAttempted() && $profile != $installing_name) { + // Profiles should not contain optional configuration so always use the + // install directory. + $profile_install_path = $this->getDefaultConfigDirectory('module', $profile); + $profile_storage = new FileStorage($profile_install_path, StorageInterface::DEFAULT_COLLECTION); + } + else { + $profile_storage = NULL; + } + return $profile_storage; + } + + /** + * Gets an extension's default configuration directory. + * + * @param string $type + * Type of extension to install. + * @param string $name + * Name of extension to install. + * + * @return string + * The extension's default configuration directory. + */ + protected function getDefaultConfigDirectory($type, $name) { + return $this->drupalGetPath($type, $name) . '/' . InstallStorage::CONFIG_INSTALL_DIRECTORY; + } + + /** + * Wrapper for drupal_get_path(). + * + * @param $type + * The type of the item; one of 'core', 'profile', 'module', 'theme', or + * 'theme_engine'. + * @param $name + * The name of the item for which the path is requested. Ignored for + * $type 'core'. + * + * @return string + * The path to the requested item or an empty string if the item is not + * found. + */ + protected function drupalGetPath($type, $name) { + return drupal_get_path($type, $name); + } + + /** + * Gets the install profile from settings. + * + * @return string|null $profile + * The name of the installation profile or NULL if no installation profile + * is currently active. This is the case for example during the first steps + * of the installer or during unit tests. + */ + protected function drupalGetProfile() { + // Settings is safe to use because settings.php is written before any module + // is installed. + return Settings::get('install_profile'); + } + + /** + * Wrapper for drupal_installation_attempted(). + * + * @return bool + * TRUE if a Drupal installation is currently being attempted. + */ + protected function drupalInstallationAttempted() { + return drupal_installation_attempted(); + } + } diff --git a/core/lib/Drupal/Core/Config/ConfigInstallerInterface.php b/core/lib/Drupal/Core/Config/ConfigInstallerInterface.php index 1d1e7e0d1112..6d8e489c3892 100644 --- a/core/lib/Drupal/Core/Config/ConfigInstallerInterface.php +++ b/core/lib/Drupal/Core/Config/ConfigInstallerInterface.php @@ -37,6 +37,24 @@ interface ConfigInstallerInterface { */ public function installDefaultConfig($type, $name); + /** + * Installs optional configuration. + * + * Optional configuration is only installed if: + * - the configuration does not exist already. + * - it's a configuration entity. + * - its dependencies can be met. + * + * @param \Drupal\Core\Config\StorageInterface + * (optional) The configuration storage to search for optional + * configuration. If not provided, all enabled extension's optional + * configuration directories will be searched. + * @param string $prefix + * (optional) If set, limits the installed configuration to only + * configuration beginning with the provided value. + */ + public function installOptionalConfig(StorageInterface $storage = NULL, $prefix = ''); + /** * Installs all default configuration in the specified collection. * @@ -59,13 +77,6 @@ interface ConfigInstallerInterface { */ public function setSourceStorage(StorageInterface $storage); - /** - * Resets the configuration storage that provides the default configuration. - * - * @return $this - */ - public function resetSourceStorage(); - /** * Sets the status of the isSyncing flag. * @@ -85,25 +96,16 @@ interface ConfigInstallerInterface { public function isSyncing(); /** - * Finds pre-existing configuration objects for the provided extension. - * - * Extensions can not be installed if configuration objects exist in the - * active storage with the same names. This can happen in a number of ways, - * commonly: - * - if a user has created configuration with the same name as that provided - * by the extension. - * - if the extension provides default configuration that does not depend on - * it and the extension has been uninstalled and is about to the - * reinstalled. + * Checks the configuration that will be installed for an extension. * * @param string $type * Type of extension to install. * @param string $name * Name of extension to install. * - * @return array - * Array of configuration objects that already exist keyed by collection. + * @throws \Drupal\Core\Config\UnmetDependenciesException + * @throws \Drupal\Core\Config\PreExistingConfigException */ - public function findPreExistingConfiguration($type, $name); + public function checkConfigurationToInstall($type, $name); } diff --git a/core/lib/Drupal/Core/Config/ExtensionInstallStorage.php b/core/lib/Drupal/Core/Config/ExtensionInstallStorage.php index cb2833e4a948..15490b497b3c 100644 --- a/core/lib/Drupal/Core/Config/ExtensionInstallStorage.php +++ b/core/lib/Drupal/Core/Config/ExtensionInstallStorage.php @@ -45,7 +45,7 @@ class ExtensionInstallStorage extends InstallStorage { * default collection. * @param bool $include_profile * (optional) Whether to include the install profile in extensions to - * search. + * search and to get overrides from. */ public function __construct(StorageInterface $config_storage, $directory = self::CONFIG_INSTALL_DIRECTORY, $collection = StorageInterface::DEFAULT_COLLECTION, $include_profile = TRUE) { $this->configStorage = $config_storage; @@ -82,27 +82,24 @@ class ExtensionInstallStorage extends InstallStorage { $this->folders = array(); $this->folders += $this->getComponentNames('core', array('core')); + $install_profile = Settings::get('install_profile'); $extensions = $this->configStorage->read('core.extension'); if (!empty($extensions['module'])) { $modules = $extensions['module']; - if (!$this->includeProfile) { - if ($install_profile = Settings::get('install_profile')) { - unset($modules[$install_profile]); - } - } + // Remove the install profile as this is handled later. + unset($modules[$install_profile]); $this->folders += $this->getComponentNames('module', array_keys($modules)); } if (!empty($extensions['theme'])) { $this->folders += $this->getComponentNames('theme', array_keys($extensions['theme'])); } - // The install profile can override module default configuration. We do - // this by replacing the config file path from the module/theme with the - // install profile version if there are any duplicates. - $profile_folders = $this->getComponentNames('profile', array(drupal_get_profile())); - $folders_to_replace = array_intersect_key($profile_folders, $this->folders); - if (!empty($folders_to_replace)) { - $this->folders = array_merge($this->folders, $folders_to_replace); + if ($this->includeProfile) { + // The install profile can override module default configuration. We do + // this by replacing the config file path from the module/theme with the + // install profile version if there are any duplicates. + $profile_folders = $this->getComponentNames('profile', array(drupal_get_profile())); + $this->folders = $profile_folders + $this->folders; } } return $this->folders; diff --git a/core/lib/Drupal/Core/Config/InstallStorage.php b/core/lib/Drupal/Core/Config/InstallStorage.php index d4e7d5c77f84..06618d7e79d2 100644 --- a/core/lib/Drupal/Core/Config/InstallStorage.php +++ b/core/lib/Drupal/Core/Config/InstallStorage.php @@ -27,6 +27,11 @@ class InstallStorage extends FileStorage { */ const CONFIG_INSTALL_DIRECTORY = 'config/install'; + /** + * Extension sub-directory containing optional configuration for installation. + */ + const CONFIG_OPTIONAL_DIRECTORY = 'config/optional'; + /** * Extension sub-directory containing configuration schema. */ diff --git a/core/lib/Drupal/Core/Config/UnmetDependenciesException.php b/core/lib/Drupal/Core/Config/UnmetDependenciesException.php new file mode 100644 index 000000000000..6a10003f0fc3 --- /dev/null +++ b/core/lib/Drupal/Core/Config/UnmetDependenciesException.php @@ -0,0 +1,95 @@ +configObjects; + } + + /** + * Gets the name of the extension that is being installed. + * + * @return string + * The name of the extension that is being installed. + */ + public function getExtension() { + return $this->extension; + } + + /** + * Gets a translated message from the exception. + * + * @param \Drupal\Core\StringTranslation\TranslationInterface $string_translation + * The string translation service. + * + * @return string + */ + public function getTranslatedMessage(TranslationInterface $string_translation, $extension) { + return $string_translation->formatPlural( + count($this->getConfigObjects()), + 'Unable to install @extension, %config_names has unmet dependencies.', + 'Unable to install @extension, %config_names have unmet dependencies.', + [ + '%config_names' => implode(', ', $this->getConfigObjects()), + '@extension' => $extension, + ] + ); + } + + /** + * Creates an exception for an extension and a list of configuration objects. + * + * @param $extension + * The name of the extension that is being installed. + * @param array $config_objects + * A list of configuration object names that have unmet dependencies + * + * @return \Drupal\Core\Config\PreExistingConfigException + */ + public static function create($extension, array $config_objects) { + $message = String::format('Configuration objects (@config_names) provided by @extension have unmet dependencies', + array( + '@config_names' => implode(', ', $config_objects), + '@extension' => $extension + ) + ); + $e = new static($message); + $e->configObjects = $config_objects; + $e->extension = $extension; + return $e; + } + +} diff --git a/core/lib/Drupal/Core/Extension/ModuleInstaller.php b/core/lib/Drupal/Core/Extension/ModuleInstaller.php index 412a7b31aef5..172bfc06f3c1 100644 --- a/core/lib/Drupal/Core/Extension/ModuleInstaller.php +++ b/core/lib/Drupal/Core/Extension/ModuleInstaller.php @@ -151,17 +151,9 @@ class ModuleInstaller implements ModuleInstallerInterface { ))); } - // Install profiles can not have config clashes. Configuration that - // has the same name as a module's configuration will be used instead. - if ($module != drupal_get_profile()) { - // Validate default configuration of this module. Bail if unable to - // install. Should not continue installing more modules because those - // may depend on this one. - $existing_configuration = $config_installer->findPreExistingConfiguration('module', $module); - if (!empty($existing_configuration)) { - throw PreExistingConfigException::create($module, $existing_configuration); - } - } + // Check the validity of the default configuration. This will throw + // exceptions if the configuration is not valid. + $config_installer->checkConfigurationToInstall('module', $module); $extension_config ->set("module.$module", 0) @@ -249,12 +241,6 @@ class ModuleInstaller implements ModuleInstallerInterface { ->setSyncing(TRUE) ->setSourceStorage($source_storage); } - else { - // If we're not in a config synchronization reset the source storage - // so that the extension install storage will pick up the new - // configuration. - $config_installer->resetSourceStorage(); - } \Drupal::service('config.installer')->installDefaultConfig('module', $module); // If the module has no current updates, but has some that were diff --git a/core/lib/Drupal/Core/Extension/ThemeHandler.php b/core/lib/Drupal/Core/Extension/ThemeHandler.php index dcb1fe3a2f3e..0c7ade428025 100644 --- a/core/lib/Drupal/Core/Extension/ThemeHandler.php +++ b/core/lib/Drupal/Core/Extension/ThemeHandler.php @@ -258,10 +258,7 @@ class ThemeHandler implements ThemeHandlerInterface { // Validate default configuration of the theme. If there is existing // configuration then stop installing. - $existing_configuration = $this->configInstaller->findPreExistingConfiguration('theme', $key); - if (!empty($existing_configuration)) { - throw PreExistingConfigException::create($key, $existing_configuration); - } + $this->configInstaller->checkConfigurationToInstall('theme', $key); // The value is not used; the weight is ignored for themes currently. $extension_config @@ -288,16 +285,6 @@ class ThemeHandler implements ThemeHandlerInterface { // Only install default configuration if this theme has not been installed // already. if (!isset($installed_themes[$key])) { - // The default config installation storage only knows about the - // currently installed list of themes, so it has to be reset in order to - // pick up the default config of the newly installed theme. However, do - // not reset the source storage when synchronizing configuration, since - // that would needlessly trigger a reload of the whole configuration to - // be imported. - if (!$this->configInstaller->isSyncing()) { - $this->configInstaller->resetSourceStorage(); - } - // Install default configuration of the theme. $this->configInstaller->installDefaultConfig('theme', $key); } diff --git a/core/modules/aggregator/config/install/views.view.aggregator_rss_feed.yml b/core/modules/aggregator/config/optional/views.view.aggregator_rss_feed.yml similarity index 100% rename from core/modules/aggregator/config/install/views.view.aggregator_rss_feed.yml rename to core/modules/aggregator/config/optional/views.view.aggregator_rss_feed.yml diff --git a/core/modules/aggregator/config/install/views.view.aggregator_sources.yml b/core/modules/aggregator/config/optional/views.view.aggregator_sources.yml similarity index 100% rename from core/modules/aggregator/config/install/views.view.aggregator_sources.yml rename to core/modules/aggregator/config/optional/views.view.aggregator_sources.yml diff --git a/core/modules/block/src/Tests/BlockStorageUnitTest.php b/core/modules/block/src/Tests/BlockStorageUnitTest.php index ff4ba29a2d35..eb3f8ebfa736 100644 --- a/core/modules/block/src/Tests/BlockStorageUnitTest.php +++ b/core/modules/block/src/Tests/BlockStorageUnitTest.php @@ -149,6 +149,7 @@ class BlockStorageUnitTest extends KernelTestBase { * Tests the installation of default blocks. */ public function testDefaultBlocks() { + \Drupal::service('theme_handler')->install(['classy']); $entities = $this->controller->loadMultiple(); $this->assertTrue(empty($entities), 'There are no blocks initially.'); diff --git a/core/modules/block/tests/modules/block_test/block_test.info.yml b/core/modules/block/tests/modules/block_test/block_test.info.yml index 9a8895f9bbff..fceed3644819 100644 --- a/core/modules/block/tests/modules/block_test/block_test.info.yml +++ b/core/modules/block/tests/modules/block_test/block_test.info.yml @@ -4,3 +4,5 @@ description: 'Provides test blocks.' package: Testing version: VERSION core: 8.x +dependencies: + - block diff --git a/core/modules/block_content/src/Tests/BlockContentPageViewTest.php b/core/modules/block_content/src/Tests/BlockContentPageViewTest.php index 90adab712533..0564c6019317 100644 --- a/core/modules/block_content/src/Tests/BlockContentPageViewTest.php +++ b/core/modules/block_content/src/Tests/BlockContentPageViewTest.php @@ -19,7 +19,7 @@ class BlockContentPageViewTest extends BlockContentTestBase { * * @var array */ - public static $modules = array('block', 'block_content', 'block_content_test'); + public static $modules = array('block_content_test'); /** * Checks block edit and fallback functionality. diff --git a/core/modules/block_content/tests/modules/block_content_test/block_content_test.info.yml b/core/modules/block_content/tests/modules/block_content_test/block_content_test.info.yml index 747efa4dd99d..16e4bc7c948a 100644 --- a/core/modules/block_content/tests/modules/block_content_test/block_content_test.info.yml +++ b/core/modules/block_content/tests/modules/block_content_test/block_content_test.info.yml @@ -4,3 +4,5 @@ description: "Support module for custom block related testing." package: Testing version: VERSION core: 8.x +dependencies: + - block_content diff --git a/core/modules/block_content/tests/modules/block_content_test/config/install/block.block.foobargorilla.yml b/core/modules/block_content/tests/modules/block_content_test/config/install/block.block.foobargorilla.yml index ed83291500b6..4cc2360220b1 100644 --- a/core/modules/block_content/tests/modules/block_content_test/config/install/block.block.foobargorilla.yml +++ b/core/modules/block_content/tests/modules/block_content_test/config/install/block.block.foobargorilla.yml @@ -4,7 +4,7 @@ dependencies: module: - block_content theme: - - stark + - classy id: foobargorilla theme: classy region: content diff --git a/core/modules/book/config/install/core.entity_view_mode.node.print.yml b/core/modules/book/config/install/core.entity_view_mode.node.print.yml index 512d5814ca9c..de47b0cbe3d6 100644 --- a/core/modules/book/config/install/core.entity_view_mode.node.print.yml +++ b/core/modules/book/config/install/core.entity_view_mode.node.print.yml @@ -5,4 +5,8 @@ cache: true targetEntityType: node dependencies: module: + - book - node + enforced: + module: + - book diff --git a/core/modules/comment/config/install/views.view.comments_recent.yml b/core/modules/comment/config/optional/views.view.comments_recent.yml similarity index 100% rename from core/modules/comment/config/install/views.view.comments_recent.yml rename to core/modules/comment/config/optional/views.view.comments_recent.yml diff --git a/core/modules/comment/src/Tests/CommentDefaultFormatterCacheTagsTest.php b/core/modules/comment/src/Tests/CommentDefaultFormatterCacheTagsTest.php index c04b9395c3cf..67cd4a7bf188 100644 --- a/core/modules/comment/src/Tests/CommentDefaultFormatterCacheTagsTest.php +++ b/core/modules/comment/src/Tests/CommentDefaultFormatterCacheTagsTest.php @@ -42,7 +42,7 @@ class CommentDefaultFormatterCacheTagsTest extends EntityUnitTestBase { // Install tables and config needed to render comments. $this->installSchema('comment', array('comment_entity_statistics')); - $this->installConfig(array('system', 'filter')); + $this->installConfig(array('system', 'filter', 'comment')); // Comment rendering generates links, so build the router. $this->installSchema('system', array('router')); diff --git a/core/modules/comment/src/Tests/CommentFieldAccessTest.php b/core/modules/comment/src/Tests/CommentFieldAccessTest.php index d1be85d2fca1..7faecadd1414 100644 --- a/core/modules/comment/src/Tests/CommentFieldAccessTest.php +++ b/core/modules/comment/src/Tests/CommentFieldAccessTest.php @@ -79,7 +79,7 @@ class CommentFieldAccessTest extends EntityUnitTestBase { */ protected function setUp() { parent::setUp(); - $this->installConfig(array('user')); + $this->installConfig(array('user', 'comment')); $this->installSchema('comment', array('comment_entity_statistics')); } diff --git a/core/modules/config/src/Tests/ConfigEntityListTest.php b/core/modules/config/src/Tests/ConfigEntityListTest.php index 275136d63e81..9287174cdcbd 100644 --- a/core/modules/config/src/Tests/ConfigEntityListTest.php +++ b/core/modules/config/src/Tests/ConfigEntityListTest.php @@ -25,6 +25,16 @@ class ConfigEntityListTest extends WebTestBase { */ public static $modules = array('config_test'); + /** + * {@inheritdoc} + */ + protected function setUp() { + parent::setUp(); + // Delete the override config_test entity since it is not required by this + // test. + \Drupal::entityManager()->getStorage('config_test')->load('override')->delete(); + } + /** * Tests entity list builder methods. */ diff --git a/core/modules/config/src/Tests/ConfigImportRecreateTest.php b/core/modules/config/src/Tests/ConfigImportRecreateTest.php index 74aea5f9210a..83ea2ee35d1b 100644 --- a/core/modules/config/src/Tests/ConfigImportRecreateTest.php +++ b/core/modules/config/src/Tests/ConfigImportRecreateTest.php @@ -38,7 +38,7 @@ class ConfigImportRecreateTest extends KernelTestBase { parent::setUp(); $this->installEntitySchema('node'); - $this->installConfig(array('field')); + $this->installConfig(array('field', 'node')); $this->copyConfig($this->container->get('config.storage'), $this->container->get('config.storage.staging')); @@ -94,8 +94,8 @@ class ConfigImportRecreateTest extends KernelTestBase { // will be recreated. $creates = $this->configImporter->getUnprocessedConfiguration('create'); $deletes = $this->configImporter->getUnprocessedConfiguration('delete'); - $this->assertEqual(4, count($creates), 'There are 4 configuration items to create.'); - $this->assertEqual(4, count($deletes), 'There are 4 configuration items to delete.'); + $this->assertEqual(5, count($creates), 'There are 5 configuration items to create.'); + $this->assertEqual(5, count($deletes), 'There are 5 configuration items to delete.'); $this->assertEqual(0, count($this->configImporter->getUnprocessedConfiguration('update')), 'There are no configuration items to update.'); $this->assertIdentical($creates, array_reverse($deletes), 'Deletes and creates contain the same configuration names in opposite orders due to dependencies.'); diff --git a/core/modules/config/src/Tests/ConfigImporterTest.php b/core/modules/config/src/Tests/ConfigImporterTest.php index 1be23dee5607..a6b231284706 100644 --- a/core/modules/config/src/Tests/ConfigImporterTest.php +++ b/core/modules/config/src/Tests/ConfigImporterTest.php @@ -32,7 +32,7 @@ class ConfigImporterTest extends KernelTestBase { * * @var array */ - public static $modules = array('config_test', 'system', 'config_import_test', 'config_test_language'); + public static $modules = array('config_test', 'system', 'config_import_test'); protected function setUp() { parent::setUp(); @@ -541,24 +541,4 @@ class ConfigImporterTest extends KernelTestBase { $this->assertTrue($this->container->get('config.storage')->exists($config_name)); } - /** - * Tests imported configuration entities with and without language information. - */ - function testLanguage() { - // Test imported configuration with implicit language code. - $data = $this->container->get('config.storage.installer')->read('config_test.dynamic.dotted.english'); - $this->assertTrue(!isset($data['langcode'])); - $this->assertEqual( - $this->config('config_test.dynamic.dotted.english')->get('langcode'), - 'en' - ); - - // Test imported configuration with explicit language code. - $data = $this->container->get('config.storage.installer')->read('config_test.dynamic.dotted.french'); - $this->assertEqual($data['langcode'], 'fr'); - $this->assertEqual( - $this->config('config_test.dynamic.dotted.french')->get('langcode'), - 'fr' - ); - } } diff --git a/core/modules/config/src/Tests/ConfigInstallProfileOverrideTest.php b/core/modules/config/src/Tests/ConfigInstallProfileOverrideTest.php new file mode 100644 index 000000000000..3e2c7bfce595 --- /dev/null +++ b/core/modules/config/src/Tests/ConfigInstallProfileOverrideTest.php @@ -0,0 +1,120 @@ + array( + 'autorun' => 0, + 'requirements_warning' => 172800, + 'requirements_error' => 1209600, + ), + ); + // The expected active configuration altered by the install profile. + $expected_profile_data = array( + 'threshold' => array( + 'autorun' => 0, + 'requirements_warning' => 259200, + 'requirements_error' => 1209600, + ), + ); + + // Verify that the original data matches. We have to read the module config + // file directly, because the install profile default system.cron.yml + // configuration file was used to create the active configuration. + $config_dir = drupal_get_path('module', 'system') . '/'. InstallStorage::CONFIG_INSTALL_DIRECTORY; + $this->assertTrue(is_dir($config_dir)); + $source_storage = new FileStorage($config_dir); + $data = $source_storage->read($config_name); + $this->assertIdentical($data, $expected_original_data); + + // Verify that active configuration matches the expected data, which was + // created from the testing install profile's system.cron.yml file. + $config = $this->config($config_name); + $this->assertIdentical($config->get(), $expected_profile_data); + + // Ensure that the configuration entity has the expected dependencies and + // overrides. + $action = Action::load('user_block_user_action'); + $this->assertEqual($action->label(), 'Overridden block the selected user(s)'); + $action = Action::load('user_cancel_user_action'); + $this->assertEqual($action->label(), 'Cancel the selected user account(s)', 'Default configuration that is not overridden is not affected.'); + + // Ensure that optional configuration can be overridden. + $tour = Tour::load('language'); + $this->assertEqual(count($tour->getTips()), 1, 'Optional configuration can be overridden. The language tour only has one tip'); + $tour = Tour::load('language-add'); + $this->assertEqual(count($tour->getTips()), 3, 'Optional configuration that is not overridden is not affected.'); + + // Ensure that optional configuration from a profile is created if + // dependencies are met. + $this->assertEqual(Tour::load('testing_config_overrides')->label(), 'Config override test'); + + // Ensure that optional configuration from a profile is not created if + // dependencies are not met. Cannot use the entity system since the entity + // type does not exist. + $optional_dir = drupal_get_path('module', 'testing_config_overrides') . '/' . InstallStorage::CONFIG_OPTIONAL_DIRECTORY; + $optional_storage = new FileStorage($optional_dir); + foreach (['config_test.dynamic.dotted.default', 'config_test.dynamic.override','config_test.dynamic.override_unmet'] as $id) { + $this->assertTrue(\Drupal::config($id)->isNew(), "The config_test entity $id contained in the profile's optional directory does not exist."); + // Make that we don't get false positives from the assertion above. + $this->assertTrue($optional_storage->exists($id), "The config_test entity $id does exist in the profile's optional directory."); + } + + // Install the config_test module and ensure that the override from the + // install profile is not used. Optional configuration can not override + // configuration in a modules config/install directory. + $this->container->get('module_installer')->install(['config_test']); + $this->rebuildContainer(); + $config_test_storage = \Drupal::entityManager()->getStorage('config_test'); + $this->assertEqual($config_test_storage->load('dotted.default')->label(), 'Default', 'The config_test entity is not overridden by the profile optional configuration.'); + // Test that override of optional configuration does work. + $this->assertEqual($config_test_storage->load('override')->label(), 'Override', 'The optional config_test entity is overridden by the profile optional configuration.'); + // Test that override of optional configuration which introduces an unmet + // dependency does not get created. + $this->assertNull($config_test_storage->load('override_unmet'), 'The optional config_test entity with unmet dependencies is not created.'); + + $this->container->get('module_installer')->install(['dblog']); + $this->rebuildContainer(); + // Just installing db_log does not create the optional configuration. + $this->assertNull($config_test_storage->load('override_unmet'), 'The optional config_test entity with unmet dependencies is not created.'); + // Install all available optional configuration. + $this->container->get('config.installer')->installOptionalConfig(); + $this->assertEqual($config_test_storage->load('override_unmet')->label(), 'Override', 'The optional config_test entity is overridden by the profile optional configuration.'); + + + } + +} diff --git a/core/modules/config/src/Tests/ConfigInstallProfileUnmetDependenciesTest.php b/core/modules/config/src/Tests/ConfigInstallProfileUnmetDependenciesTest.php new file mode 100644 index 000000000000..e7e8f6cab183 --- /dev/null +++ b/core/modules/config/src/Tests/ConfigInstallProfileUnmetDependenciesTest.php @@ -0,0 +1,100 @@ +siteDirectory . '/profiles/testing_config_overrides'; + mkdir($dest, 0777, TRUE); + $source = DRUPAL_ROOT . '/core/profiles/testing_config_overrides'; + $iterator = new \RecursiveIteratorIterator(new \RecursiveDirectoryIterator($source, \RecursiveDirectoryIterator::SKIP_DOTS), \RecursiveIteratorIterator::SELF_FIRST); + foreach ($iterator as $item) { + if ($item->isDir()) { + mkdir($dest . DIRECTORY_SEPARATOR . $iterator->getSubPathName()); + } + else { + copy($item, $dest . DIRECTORY_SEPARATOR . $iterator->getSubPathName()); + } + } + + // Add a dependency that can not be met because User is installed before + // Action. + $config_file = $dest . DIRECTORY_SEPARATOR . InstallStorage::CONFIG_INSTALL_DIRECTORY . DIRECTORY_SEPARATOR . 'system.action.user_block_user_action.yml'; + $action = Yaml::decode(file_get_contents($config_file)); + $action['dependencies']['module'][] = 'action'; + file_put_contents($config_file, Yaml::encode($action)); + + parent::setUp(); + } + + /** + * {@inheritdoc} + * + * Override the error method so we can test for the expected exception. + */ + protected function error($message = '', $group = 'Other', array $caller = NULL) { + if ($group == 'User notice') { + // Since 'User notice' is set by trigger_error() which is used for debug + // set the message to a status of 'debug'. + return $this->assert('debug', $message, 'Debug', $caller); + } + if ($group == 'Drupal\Core\Config\UnmetDependenciesException') { + $this->expectedException = TRUE; + return FALSE; + } + return $this->assert('exception', $message, $group, $caller); + } + + /** + * {@inheritdoc} + */ + protected function setUpSite() { + // This step is not reached due to the exception. + } + + + /** + * Confirms that the installation succeeded. + */ + public function testInstalled() { + if ($this->expectedException) { + $this->pass('Expected Drupal\Core\Config\UnmetDependenciesException exception thrown'); + } + else { + $this->fail('Expected Drupal\Core\Config\UnmetDependenciesException exception thrown'); + } + } + +} diff --git a/core/modules/config/src/Tests/ConfigInstallTest.php b/core/modules/config/src/Tests/ConfigInstallTest.php index 2da9e53de516..9a903a1cd0fd 100644 --- a/core/modules/config/src/Tests/ConfigInstallTest.php +++ b/core/modules/config/src/Tests/ConfigInstallTest.php @@ -9,6 +9,7 @@ namespace Drupal\config\Tests; use Drupal\Core\Config\PreExistingConfigException; use Drupal\Core\Config\StorageInterface; +use Drupal\Core\Config\UnmetDependenciesException; use Drupal\simpletest\KernelTestBase; /** @@ -19,11 +20,17 @@ use Drupal\simpletest\KernelTestBase; */ class ConfigInstallTest extends KernelTestBase { + /** + * {@inheritdoc} + */ + public static $modules = ['system']; + /** * {@inheritdoc} */ protected function setUp() { parent::setUp(); + $this->installSchema('system', ['router']); // Ensure the global variable being asserted by this test does not exist; // a previous test executed in this request/process might have set it. @@ -48,11 +55,7 @@ class ConfigInstallTest extends KernelTestBase { $this->assertFalse(\Drupal::service('config.typed')->hasConfigSchema('config_schema_test.schema_in_install'), 'Configuration schema for config_schema_test.schema_in_install does not exist.'); // Install the test module. - $this->enableModules(array('config_test', 'config_schema_test')); - $this->installConfig(array('config_test', 'config_schema_test')); - - // After module installation the new schema should exist. - $this->assertTrue(\Drupal::service('config.typed')->hasConfigSchema('config_schema_test.schema_in_install'), 'Configuration schema for config_schema_test.schema_in_install exists.'); + $this->installModules(array('config_test')); // Verify that default module config exists. \Drupal::configFactory()->reset($default_config); @@ -71,6 +74,13 @@ class ConfigInstallTest extends KernelTestBase { $this->assertFalse(isset($GLOBALS['hook_config_test']['predelete'])); $this->assertFalse(isset($GLOBALS['hook_config_test']['delete'])); + // Install the schema test module. + $this->enableModules(array('config_schema_test')); + $this->installConfig(array('config_schema_test')); + + // After module installation the new schema should exist. + $this->assertTrue(\Drupal::service('config.typed')->hasConfigSchema('config_schema_test.schema_in_install'), 'Configuration schema for config_schema_test.schema_in_install exists.'); + // Ensure that data type casting is applied during config installation. $config = $this->config('config_schema_test.schema_in_install'); $this->assertIdentical($config->get('integer'), 1); @@ -170,8 +180,7 @@ class ConfigInstallTest extends KernelTestBase { ); \Drupal::state()->set('config_collection_install_test.collection_names', $collections); // Install the test module. - $this->enableModules(array('config_test', 'config_collection_install_test')); - $this->installConfig(array('config_test')); + $this->installModules(array('config_test', 'config_collection_install_test')); /** @var \Drupal\Core\Config\StorageInterface $active_storage */ $active_storage = \Drupal::service('config.storage'); $this->assertEqual($collections, $active_storage->getAllCollectionNames()); @@ -189,4 +198,56 @@ class ConfigInstallTest extends KernelTestBase { $this->assertIdentical(array('label' => 'entity'), $data); } + /** + * Tests the configuration with unmet dependencies is not installed. + */ + public function testDependencyChecking() { + $this->installModules(['config_test']); + try { + $this->installModules(['config_install_dependency_test']); + $this->fail('Expected UnmetDependenciesException not thrown.'); + } + catch (UnmetDependenciesException $e) { + $this->assertEqual($e->getExtension(), 'config_install_dependency_test'); + $this->assertEqual($e->getConfigObjects(), ['config_test.dynamic.other_module_test_with_dependency']); + $this->assertEqual($e->getMessage(), 'Configuration objects (config_test.dynamic.other_module_test_with_dependency) provided by config_install_dependency_test have unmet dependencies'); + } + $this->installModules(['config_other_module_config_test']); + $this->installModules(['config_install_dependency_test']); + $this->assertTrue(entity_load('config_test', 'other_module_test_with_dependency'), 'The config_test.dynamic.other_module_test_with_dependency configuration has been created during install.'); + } + + /** + * Tests imported configuration entities with and without language information. + */ + function testLanguage() { + $this->installModules(['config_test_language']); + // Test imported configuration with implicit language code. + $data = $this->container->get('config.storage.installer')->read('config_test.dynamic.dotted.english'); + $this->assertTrue(!isset($data['langcode'])); + $this->assertEqual( + $this->config('config_test.dynamic.dotted.english')->get('langcode'), + 'en' + ); + + // Test imported configuration with explicit language code. + $data = $this->container->get('config.storage.installer')->read('config_test.dynamic.dotted.french'); + $this->assertEqual($data['langcode'], 'fr'); + $this->assertEqual( + $this->config('config_test.dynamic.dotted.french')->get('langcode'), + 'fr' + ); + } + + /** + * Installs a module. + * + * @param array $modules + * The module names. + */ + protected function installModules(array $modules) { + $this->container->get('module_installer')->install($modules); + $this->container = \Drupal::getContainer(); + } + } diff --git a/core/modules/config/src/Tests/ConfigInstallWebTest.php b/core/modules/config/src/Tests/ConfigInstallWebTest.php index b04ce39ad704..cff60a450feb 100644 --- a/core/modules/config/src/Tests/ConfigInstallWebTest.php +++ b/core/modules/config/src/Tests/ConfigInstallWebTest.php @@ -7,12 +7,10 @@ namespace Drupal\config\Tests; -use Drupal\Core\Config\InstallStorage; use Drupal\Core\Config\PreExistingConfigException; use Drupal\Core\Config\StorageInterface; use Drupal\language\Entity\ConfigurableLanguage; use Drupal\simpletest\WebTestBase; -use Drupal\Core\Config\FileStorage; /** * Tests installation and removal of configuration objects in install, disable @@ -119,43 +117,6 @@ class ConfigInstallWebTest extends WebTestBase { $this->assertIdentical($config_entity->get('label'), 'Default integration config label'); } - /** - * Tests install profile config changes. - */ - function testInstallProfileConfigOverwrite() { - $config_name = 'system.cron'; - // The expected configuration from the system module. - $expected_original_data = array( - 'threshold' => array( - 'autorun' => 0, - 'requirements_warning' => 172800, - 'requirements_error' => 1209600, - ), - ); - // The expected active configuration altered by the install profile. - $expected_profile_data = array( - 'threshold' => array( - 'autorun' => 0, - 'requirements_warning' => 259200, - 'requirements_error' => 1209600, - ), - ); - - // Verify that the original data matches. We have to read the module config - // file directly, because the install profile default system.cron.yml - // configuration file was used to create the active configuration. - $config_dir = drupal_get_path('module', 'system') . '/'. InstallStorage::CONFIG_INSTALL_DIRECTORY; - $this->assertTrue(is_dir($config_dir)); - $source_storage = new FileStorage($config_dir); - $data = $source_storage->read($config_name); - $this->assertIdentical($data, $expected_original_data); - - // Verify that active configuration matches the expected data, which was - // created from the testing install profile's system.cron.yml file. - $config = $this->config($config_name); - $this->assertIdentical($config->get(), $expected_profile_data); - } - /** * Tests pre-existing configuration detection. */ @@ -214,4 +175,21 @@ class ConfigInstallWebTest extends WebTestBase { $this->assertEqual($e->getMessage(), 'Configuration objects (config_test.dynamic.dotted.default, language/fr/config_test.dynamic.dotted.default) provided by config_clash_test_theme already exist in active configuration'); } } + + /** + * Tests unmet dependencies detection. + */ + public function testUnmetDependenciesInstall() { + $this->drupalLogin($this->adminUser); + // We need to install separately since config_install_dependency_test does + // not depend on config_test and order is important. + $this->drupalPostForm('admin/modules', array('modules[Testing][config_test][enable]' => TRUE), t('Save configuration')); + $this->drupalPostForm('admin/modules', array('modules[Testing][config_install_dependency_test][enable]' => TRUE), t('Save configuration')); + $this->assertRaw('Unable to install Config install dependency test, config_test.dynamic.other_module_test_with_dependency has unmet dependencies.'); + + $this->drupalPostForm('admin/modules', array('modules[Testing][config_other_module_config_test][enable]' => TRUE), t('Save configuration')); + $this->drupalPostForm('admin/modules', array('modules[Testing][config_install_dependency_test][enable]' => TRUE), t('Save configuration')); + $this->rebuildContainer(); + $this->assertTrue(entity_load('config_test', 'other_module_test_with_dependency'), 'The config_test.dynamic.other_module_test_with_dependency configuration has been created during install.'); + } } diff --git a/core/modules/config/src/Tests/ConfigOtherModuleTest.php b/core/modules/config/src/Tests/ConfigOtherModuleTest.php index 082e48547eed..ee033170a410 100644 --- a/core/modules/config/src/Tests/ConfigOtherModuleTest.php +++ b/core/modules/config/src/Tests/ConfigOtherModuleTest.php @@ -7,8 +7,6 @@ namespace Drupal\config\Tests; -use Drupal\Core\Config\PreExistingConfigException; -use Drupal\Core\Config\StorageInterface; use Drupal\simpletest\WebTestBase; /** @@ -59,18 +57,10 @@ class ConfigOtherModuleTest extends WebTestBase { // Default configuration provided by config_test should still exist. $this->assertTrue(entity_load('config_test', 'dotted.default', TRUE), 'The configuration is not deleted.'); - // Re-enable module to test that pre-existing default configuration throws - // an error. - $msg = "The expected PreExistingConfigException is thrown by reinstalling config_other_module_config_test."; - try { - $this->installModule('config_other_module_config_test'); - $this->fail($msg); - } - catch (PreExistingConfigException $e) { - $this->pass($msg); - $this->assertEqual($e->getExtension(), 'config_other_module_config_test'); - $this->assertEqual($e->getConfigObjects(), [StorageInterface::DEFAULT_COLLECTION => ['config_test.dynamic.other_module_test']]); - } + // Re-enable module to test that pre-existing optional configuration does + // not throw an error. + $this->installModule('config_other_module_config_test'); + $this->assertTrue(\Drupal::moduleHandler()->moduleExists('config_other_module_config_test'), 'The config_other_module_config_test module is installed.'); } /** diff --git a/core/modules/config/tests/config_install_dependency_test/config/install/config_test.dynamic.other_module_test_with_dependency.yml b/core/modules/config/tests/config_install_dependency_test/config/install/config_test.dynamic.other_module_test_with_dependency.yml new file mode 100644 index 000000000000..69470f818f2c --- /dev/null +++ b/core/modules/config/tests/config_install_dependency_test/config/install/config_test.dynamic.other_module_test_with_dependency.yml @@ -0,0 +1,11 @@ +id: other_module_test_with_dependency +label: 'Other module test with dependency' +weight: 0 +style: '' +status: true +langcode: en +protected_property: Default +dependencies: + enforced: + module: + - config_other_module_config_test diff --git a/core/modules/config/tests/config_install_dependency_test/config_install_dependency_test.info.yml b/core/modules/config/tests/config_install_dependency_test/config_install_dependency_test.info.yml new file mode 100644 index 000000000000..e9b3c724562a --- /dev/null +++ b/core/modules/config/tests/config_install_dependency_test/config_install_dependency_test.info.yml @@ -0,0 +1,5 @@ +name: 'Config install dependency test' +type: module +package: Testing +version: VERSION +core: 8.x diff --git a/core/modules/config/tests/config_other_module_config_test/config/install/config_test.dynamic.other_module_test.yml b/core/modules/config/tests/config_other_module_config_test/config/optional/config_test.dynamic.other_module_test.yml similarity index 100% rename from core/modules/config/tests/config_other_module_config_test/config/install/config_test.dynamic.other_module_test.yml rename to core/modules/config/tests/config_other_module_config_test/config/optional/config_test.dynamic.other_module_test.yml diff --git a/core/modules/config/tests/config_test/config/optional/config_test.dynamic.override.yml b/core/modules/config/tests/config_test/config/optional/config_test.dynamic.override.yml new file mode 100644 index 000000000000..972119095caf --- /dev/null +++ b/core/modules/config/tests/config_test/config/optional/config_test.dynamic.override.yml @@ -0,0 +1,6 @@ +# See \Drupal\config\Tests\ConfigInstallProfileOverrideTest +id: override +label: Default +weight: 0 +protected_property: Default +status: 1 diff --git a/core/modules/config/tests/config_test/config/optional/config_test.dynamic.override_unmet.yml b/core/modules/config/tests/config_test/config/optional/config_test.dynamic.override_unmet.yml new file mode 100644 index 000000000000..7fa7ae40d319 --- /dev/null +++ b/core/modules/config/tests/config_test/config/optional/config_test.dynamic.override_unmet.yml @@ -0,0 +1,10 @@ +# See \Drupal\config\Tests\ConfigInstallProfileOverrideTest +# This configuration entity has dependencies which would be met. +id: override_unmet +label: Default +weight: 0 +protected_property: Default +status: 1 +dependencies: + module: + - tour diff --git a/core/modules/config/tests/config_test_language/config_test_language.info.yml b/core/modules/config/tests/config_test_language/config_test_language.info.yml index 28d720b67fdf..527270f6460e 100644 --- a/core/modules/config/tests/config_test_language/config_test_language.info.yml +++ b/core/modules/config/tests/config_test_language/config_test_language.info.yml @@ -3,3 +3,5 @@ type: module package: Testing version: VERSION core: 8.x +dependencies: + - config_test \ No newline at end of file diff --git a/core/modules/contact/src/Tests/MessageEntityTest.php b/core/modules/contact/src/Tests/MessageEntityTest.php index 52c78a27228d..7d9b9b2af6af 100644 --- a/core/modules/contact/src/Tests/MessageEntityTest.php +++ b/core/modules/contact/src/Tests/MessageEntityTest.php @@ -32,7 +32,7 @@ class MessageEntityTest extends EntityUnitTestBase { protected function setUp() { parent::setUp(); - $this->installConfig(array('contact')); + $this->installConfig(array('contact', 'contact_test')); } /** diff --git a/core/modules/editor/src/Tests/EditorFileUsageTest.php b/core/modules/editor/src/Tests/EditorFileUsageTest.php index 3e66cefccd07..7b5210e50e99 100644 --- a/core/modules/editor/src/Tests/EditorFileUsageTest.php +++ b/core/modules/editor/src/Tests/EditorFileUsageTest.php @@ -28,6 +28,7 @@ class EditorFileUsageTest extends EntityUnitTestBase { $this->installEntitySchema('file'); $this->installSchema('node', array('node_access')); $this->installSchema('file', array('file_usage')); + $this->installConfig(['node']); // Add text formats. $filtered_html_format = entity_create('filter_format', array( diff --git a/core/modules/entity_reference/tests/modules/entity_reference_test/entity_reference_test.info.yml b/core/modules/entity_reference/tests/modules/entity_reference_test/entity_reference_test.info.yml index 67a5f4876fa2..e46551972baf 100644 --- a/core/modules/entity_reference/tests/modules/entity_reference_test/entity_reference_test.info.yml +++ b/core/modules/entity_reference/tests/modules/entity_reference_test/entity_reference_test.info.yml @@ -6,3 +6,6 @@ package: Testing version: VERSION dependencies: - entity_reference + - node + - user + - views diff --git a/core/modules/field/src/Tests/FieldImportChangeTest.php b/core/modules/field/src/Tests/FieldImportChangeTest.php index 85416644d5d5..c34ff3126edc 100644 --- a/core/modules/field/src/Tests/FieldImportChangeTest.php +++ b/core/modules/field/src/Tests/FieldImportChangeTest.php @@ -31,6 +31,7 @@ class FieldImportChangeTest extends FieldUnitTestBase { * Tests importing an updated field. */ function testImportChange() { + $this->installConfig(['field_test_config']); $field_storage_id = 'field_test_import'; $field_id = "entity_test.entity_test.$field_storage_id"; $field_config_name = "field.field.$field_id"; diff --git a/core/modules/field/src/Tests/FieldImportDeleteTest.php b/core/modules/field/src/Tests/FieldImportDeleteTest.php index 030adfb964ea..04eaecc4c7d7 100644 --- a/core/modules/field/src/Tests/FieldImportDeleteTest.php +++ b/core/modules/field/src/Tests/FieldImportDeleteTest.php @@ -33,6 +33,7 @@ class FieldImportDeleteTest extends FieldUnitTestBase { * Tests deleting field storages and fields as part of config import. */ public function testImportDelete() { + $this->installConfig(['field_test_config']); // At this point there are 5 field configuration objects in the active // storage. // - field.storage.entity_test.field_test_import diff --git a/core/modules/field_ui/src/Tests/EntityDisplayTest.php b/core/modules/field_ui/src/Tests/EntityDisplayTest.php index 9cedc2d93d98..af63002069f2 100644 --- a/core/modules/field_ui/src/Tests/EntityDisplayTest.php +++ b/core/modules/field_ui/src/Tests/EntityDisplayTest.php @@ -28,7 +28,7 @@ class EntityDisplayTest extends KernelTestBase { parent::setUp(); $this->installSchema('system', array('router')); $this->installEntitySchema('node'); - $this->installConfig(array('field')); + $this->installConfig(array('field', 'node')); } /** diff --git a/core/modules/file/config/install/views.view.files.yml b/core/modules/file/config/optional/views.view.files.yml similarity index 100% rename from core/modules/file/config/install/views.view.files.yml rename to core/modules/file/config/optional/views.view.files.yml diff --git a/core/modules/filter/src/Tests/FilterAPITest.php b/core/modules/filter/src/Tests/FilterAPITest.php index 3ceb5eb4a7ad..12efba397fff 100644 --- a/core/modules/filter/src/Tests/FilterAPITest.php +++ b/core/modules/filter/src/Tests/FilterAPITest.php @@ -29,7 +29,7 @@ class FilterAPITest extends EntityUnitTestBase { parent::setUp(); $this->installSchema('system', array('router')); - $this->installConfig(array('system', 'filter')); + $this->installConfig(array('system', 'filter', 'filter_test')); } /** diff --git a/core/modules/filter/tests/filter_test/filter_test.info.yml b/core/modules/filter/tests/filter_test/filter_test.info.yml index dcefe8eeb3b5..6e377bc0097c 100644 --- a/core/modules/filter/tests/filter_test/filter_test.info.yml +++ b/core/modules/filter/tests/filter_test/filter_test.info.yml @@ -4,3 +4,5 @@ description: 'Tests filter hooks and functions.' package: Testing version: VERSION core: 8.x +dependencies: + - filter diff --git a/core/modules/forum/config/install/rdf.mapping.node.forum.yml b/core/modules/forum/config/optional/rdf.mapping.node.forum.yml similarity index 100% rename from core/modules/forum/config/install/rdf.mapping.node.forum.yml rename to core/modules/forum/config/optional/rdf.mapping.node.forum.yml diff --git a/core/modules/forum/config/install/rdf.mapping.taxonomy_term.forums.yml b/core/modules/forum/config/optional/rdf.mapping.taxonomy_term.forums.yml similarity index 100% rename from core/modules/forum/config/install/rdf.mapping.taxonomy_term.forums.yml rename to core/modules/forum/config/optional/rdf.mapping.taxonomy_term.forums.yml diff --git a/core/modules/hal/src/Tests/EntityTest.php b/core/modules/hal/src/Tests/EntityTest.php index a1b221ddeb79..9fb10147915a 100644 --- a/core/modules/hal/src/Tests/EntityTest.php +++ b/core/modules/hal/src/Tests/EntityTest.php @@ -35,6 +35,7 @@ class EntityTest extends NormalizerTestBase { $this->installSchema('system', array('sequences')); $this->installSchema('comment', array('comment_entity_statistics')); $this->installEntitySchema('taxonomy_term'); + $this->installConfig(['node', 'comment']); } /** diff --git a/core/modules/language/config/install/tour.tour.language-add.yml b/core/modules/language/config/optional/tour.tour.language-add.yml similarity index 100% rename from core/modules/language/config/install/tour.tour.language-add.yml rename to core/modules/language/config/optional/tour.tour.language-add.yml diff --git a/core/modules/language/config/install/tour.tour.language-edit.yml b/core/modules/language/config/optional/tour.tour.language-edit.yml similarity index 100% rename from core/modules/language/config/install/tour.tour.language-edit.yml rename to core/modules/language/config/optional/tour.tour.language-edit.yml diff --git a/core/modules/language/config/install/tour.tour.language.yml b/core/modules/language/config/optional/tour.tour.language.yml similarity index 100% rename from core/modules/language/config/install/tour.tour.language.yml rename to core/modules/language/config/optional/tour.tour.language.yml diff --git a/core/modules/locale/config/install/tour.tour.locale.yml b/core/modules/locale/config/optional/tour.tour.locale.yml similarity index 100% rename from core/modules/locale/config/install/tour.tour.locale.yml rename to core/modules/locale/config/optional/tour.tour.locale.yml diff --git a/core/modules/node/config/install/search.page.node_search.yml b/core/modules/node/config/optional/search.page.node_search.yml similarity index 100% rename from core/modules/node/config/install/search.page.node_search.yml rename to core/modules/node/config/optional/search.page.node_search.yml diff --git a/core/modules/node/config/install/views.view.archive.yml b/core/modules/node/config/optional/views.view.archive.yml similarity index 100% rename from core/modules/node/config/install/views.view.archive.yml rename to core/modules/node/config/optional/views.view.archive.yml diff --git a/core/modules/node/config/install/views.view.content.yml b/core/modules/node/config/optional/views.view.content.yml similarity index 100% rename from core/modules/node/config/install/views.view.content.yml rename to core/modules/node/config/optional/views.view.content.yml diff --git a/core/modules/node/config/install/views.view.content_recent.yml b/core/modules/node/config/optional/views.view.content_recent.yml similarity index 99% rename from core/modules/node/config/install/views.view.content_recent.yml rename to core/modules/node/config/optional/views.view.content_recent.yml index c769ff4460f4..7cd39e1e19c7 100644 --- a/core/modules/node/config/install/views.view.content_recent.yml +++ b/core/modules/node/config/optional/views.view.content_recent.yml @@ -427,3 +427,5 @@ display: id: block_1 display_title: Block position: 1 + display_options: + display_extenders: { } diff --git a/core/modules/node/config/install/views.view.frontpage.yml b/core/modules/node/config/optional/views.view.frontpage.yml similarity index 100% rename from core/modules/node/config/install/views.view.frontpage.yml rename to core/modules/node/config/optional/views.view.frontpage.yml diff --git a/core/modules/node/config/install/views.view.glossary.yml b/core/modules/node/config/optional/views.view.glossary.yml similarity index 100% rename from core/modules/node/config/install/views.view.glossary.yml rename to core/modules/node/config/optional/views.view.glossary.yml diff --git a/core/modules/node/src/Tests/NodeBodyFieldStorageTest.php b/core/modules/node/src/Tests/NodeBodyFieldStorageTest.php index 6e606f54a7d3..2153c7f14314 100644 --- a/core/modules/node/src/Tests/NodeBodyFieldStorageTest.php +++ b/core/modules/node/src/Tests/NodeBodyFieldStorageTest.php @@ -37,7 +37,7 @@ class NodeBodyFieldStorageTest extends KernelTestBase { $this->installSchema('user', 'users_data'); $this->installEntitySchema('user'); $this->installEntitySchema('node'); - $this->installConfig(array('field')); + $this->installConfig(array('field', 'node')); } /** diff --git a/core/modules/node/src/Tests/NodeTokenReplaceTest.php b/core/modules/node/src/Tests/NodeTokenReplaceTest.php index 7c0dfe76b832..7431f06034cd 100644 --- a/core/modules/node/src/Tests/NodeTokenReplaceTest.php +++ b/core/modules/node/src/Tests/NodeTokenReplaceTest.php @@ -30,7 +30,7 @@ class NodeTokenReplaceTest extends TokenReplaceUnitTestBase { */ protected function setUp() { parent::setUp(); - $this->installConfig(array('filter')); + $this->installConfig(array('filter', 'node')); $node_type = entity_create('node_type', array('type' => 'article', 'name' => 'Article')); $node_type->save(); diff --git a/core/modules/simpletest/src/Tests/KernelTestBaseTest.php b/core/modules/simpletest/src/Tests/KernelTestBaseTest.php index e808cd2aae32..3024e43690b3 100644 --- a/core/modules/simpletest/src/Tests/KernelTestBaseTest.php +++ b/core/modules/simpletest/src/Tests/KernelTestBaseTest.php @@ -211,6 +211,8 @@ EOS; * Tests expected behavior of installConfig(). */ function testInstallConfig() { + // The user module has configuration that depends on system. + $this->enableModules(array('system')); $module = 'user'; // Verify that default config can only be installed for enabled modules. diff --git a/core/modules/system/src/Controller/ThemeController.php b/core/modules/system/src/Controller/ThemeController.php index dd5f74984bfb..5f56052b3d6a 100644 --- a/core/modules/system/src/Controller/ThemeController.php +++ b/core/modules/system/src/Controller/ThemeController.php @@ -9,6 +9,7 @@ namespace Drupal\system\Controller; use Drupal\Core\Config\ConfigFactoryInterface; use Drupal\Core\Config\PreExistingConfigException; +use Drupal\Core\Config\UnmetDependenciesException; use Drupal\Core\Controller\ControllerBase; use Drupal\Core\Extension\ThemeHandlerInterface; use Drupal\Core\Routing\RouteBuilderInterface; @@ -144,6 +145,9 @@ class ThemeController extends ControllerBase { 'error' ); } + catch (UnmetDependenciesException $e) { + drupal_set_message($e->getTranslatedMessage($this->getStringTranslation(), $theme), 'error'); + } return $this->redirect('system.themes_page'); } diff --git a/core/modules/system/src/Form/ModulesListConfirmForm.php b/core/modules/system/src/Form/ModulesListConfirmForm.php index b3c797704ca0..5449b410841f 100644 --- a/core/modules/system/src/Form/ModulesListConfirmForm.php +++ b/core/modules/system/src/Form/ModulesListConfirmForm.php @@ -8,6 +8,7 @@ namespace Drupal\system\Form; use Drupal\Core\Config\PreExistingConfigException; +use Drupal\Core\Config\UnmetDependenciesException; use Drupal\Core\Extension\ModuleHandlerInterface; use Drupal\Core\Extension\ModuleInstallerInterface; use Drupal\Core\Form\ConfirmFormBase; @@ -176,6 +177,13 @@ class ModulesListConfirmForm extends ConfirmFormBase { ); return; } + catch (UnmetDependenciesException $e) { + drupal_set_message( + $e->getTranslatedMessage($this->getStringTranslation(), $this->modules['install'][$e->getExtension()]), + 'error' + ); + return; + } } // Gets module list after install process, flushes caches and displays a diff --git a/core/modules/system/src/Form/ModulesListForm.php b/core/modules/system/src/Form/ModulesListForm.php index 2e804824eb20..90a6e9f71da0 100644 --- a/core/modules/system/src/Form/ModulesListForm.php +++ b/core/modules/system/src/Form/ModulesListForm.php @@ -10,6 +10,7 @@ namespace Drupal\system\Form; use Drupal\Component\Utility\String; use Drupal\Component\Utility\Unicode; use Drupal\Core\Config\PreExistingConfigException; +use Drupal\Core\Config\UnmetDependenciesException; use Drupal\Core\Controller\TitleResolverInterface; use Drupal\Core\Access\AccessManagerInterface; use Drupal\Core\Entity\EntityManagerInterface; @@ -536,6 +537,13 @@ class ModulesListForm extends FormBase { ); return; } + catch (UnmetDependenciesException $e) { + drupal_set_message( + $e->getTranslatedMessage($this->getStringTranslation(), $modules['install'][$e->getExtension()]), + 'error' + ); + return; + } } // Gets module list after install process, flushes caches and displays a diff --git a/core/modules/system/src/Tests/Entity/EntityCrudHookTest.php b/core/modules/system/src/Tests/Entity/EntityCrudHookTest.php index 93869835ffeb..deff706fb20a 100644 --- a/core/modules/system/src/Tests/Entity/EntityCrudHookTest.php +++ b/core/modules/system/src/Tests/Entity/EntityCrudHookTest.php @@ -52,6 +52,7 @@ class EntityCrudHookTest extends EntityUnitTestBase { $this->installSchema('file', array('file_usage')); $this->installSchema('node', array('node_access')); $this->installSchema('comment', array('comment_entity_statistics')); + $this->installConfig(['node', 'comment']); } /** diff --git a/core/modules/system/src/Tests/Installer/InstallerExistingSettingsTest.php b/core/modules/system/src/Tests/Installer/InstallerExistingSettingsTest.php index 4645668c844d..1951f5727bb0 100644 --- a/core/modules/system/src/Tests/Installer/InstallerExistingSettingsTest.php +++ b/core/modules/system/src/Tests/Installer/InstallerExistingSettingsTest.php @@ -31,6 +31,15 @@ class InstallerExistingSettingsTest extends InstallerTestBase { 'required' => TRUE, ); + // Actually the install profile should be skipped to because it is written + // to settings.php. + // @todo https://www.drupal.org/node/2451369 Fix install_profile so that it + // is written to an existing settings.php if possible or if set used. + $this->settings['settings']['install_profile'] = (object) array( + 'value' => 'testing', + 'required' => TRUE, + ); + // Pre-configure database credentials. $connection_info = Database::getConnectionInfo(); unset($connection_info['default']['pdo']); diff --git a/core/modules/system/tests/themes/test_basetheme/config/install/core.date_format.fancy.yml b/core/modules/system/tests/themes/test_basetheme/config/install/core.date_format.fancy.yml index 37a0bf83ec5e..05d5e2774e87 100644 --- a/core/modules/system/tests/themes/test_basetheme/config/install/core.date_format.fancy.yml +++ b/core/modules/system/tests/themes/test_basetheme/config/install/core.date_format.fancy.yml @@ -7,3 +7,7 @@ status: true langcode: en locked: false pattern: 'U' +dependencies: + enforced: + theme: + - test_basetheme diff --git a/core/modules/taxonomy/config/install/views.view.taxonomy_term.yml b/core/modules/taxonomy/config/optional/views.view.taxonomy_term.yml similarity index 100% rename from core/modules/taxonomy/config/install/views.view.taxonomy_term.yml rename to core/modules/taxonomy/config/optional/views.view.taxonomy_term.yml diff --git a/core/modules/user/config/install/rdf.mapping.user.user.yml b/core/modules/user/config/optional/rdf.mapping.user.user.yml similarity index 100% rename from core/modules/user/config/install/rdf.mapping.user.user.yml rename to core/modules/user/config/optional/rdf.mapping.user.user.yml diff --git a/core/modules/user/config/install/search.page.user_search.yml b/core/modules/user/config/optional/search.page.user_search.yml similarity index 100% rename from core/modules/user/config/install/search.page.user_search.yml rename to core/modules/user/config/optional/search.page.user_search.yml diff --git a/core/modules/user/config/install/views.view.user_admin_people.yml b/core/modules/user/config/optional/views.view.user_admin_people.yml similarity index 100% rename from core/modules/user/config/install/views.view.user_admin_people.yml rename to core/modules/user/config/optional/views.view.user_admin_people.yml diff --git a/core/modules/user/config/install/views.view.who_s_new.yml b/core/modules/user/config/optional/views.view.who_s_new.yml similarity index 100% rename from core/modules/user/config/install/views.view.who_s_new.yml rename to core/modules/user/config/optional/views.view.who_s_new.yml diff --git a/core/modules/user/config/install/views.view.who_s_online.yml b/core/modules/user/config/optional/views.view.who_s_online.yml similarity index 100% rename from core/modules/user/config/install/views.view.who_s_online.yml rename to core/modules/user/config/optional/views.view.who_s_online.yml diff --git a/core/modules/user/user.info.yml b/core/modules/user/user.info.yml index 45a421a5ade5..8aec70e116ff 100644 --- a/core/modules/user/user.info.yml +++ b/core/modules/user/user.info.yml @@ -6,3 +6,5 @@ version: VERSION core: 8.x required: true configure: user.admin_index +dependencies: + - system diff --git a/core/modules/views/src/Tests/ModuleTest.php b/core/modules/views/src/Tests/ModuleTest.php index e801a09f4806..cbb067e9eca5 100644 --- a/core/modules/views/src/Tests/ModuleTest.php +++ b/core/modules/views/src/Tests/ModuleTest.php @@ -142,8 +142,8 @@ class ModuleTest extends ViewUnitTestBase { * Tests the load wrapper/helper functions. */ public function testLoadFunctions() { - $this->enableModules(['text', 'node']); - $this->installConfig(['node']); + $this->enableModules(array('field', 'text', 'node')); + $this->installConfig(array('node')); $storage = $this->container->get('entity.manager')->getStorage('view'); // Test views_view_is_enabled/disabled. diff --git a/core/modules/views/src/Tests/ViewExecutableTest.php b/core/modules/views/src/Tests/ViewExecutableTest.php index b39abefa4ec0..6555c5b75b4d 100644 --- a/core/modules/views/src/Tests/ViewExecutableTest.php +++ b/core/modules/views/src/Tests/ViewExecutableTest.php @@ -84,7 +84,7 @@ class ViewExecutableTest extends ViewUnitTestBase { $this->installEntitySchema('node'); $this->installEntitySchema('comment'); $this->installSchema('comment', array('comment_entity_statistics')); - $this->installConfig(array('field')); + $this->installConfig(array('system', 'field', 'node', 'comment')); entity_create('node_type', array( 'type' => 'page', diff --git a/core/modules/views_ui/config/install/tour.tour.views-ui.yml b/core/modules/views_ui/config/optional/tour.tour.views-ui.yml similarity index 100% rename from core/modules/views_ui/config/install/tour.tour.views-ui.yml rename to core/modules/views_ui/config/optional/tour.tour.views-ui.yml diff --git a/core/profiles/minimal/minimal.info.yml b/core/profiles/minimal/minimal.info.yml index 628dcd3d6dc5..d1f83de86ea6 100644 --- a/core/profiles/minimal/minimal.info.yml +++ b/core/profiles/minimal/minimal.info.yml @@ -7,3 +7,5 @@ dependencies: - node - block - dblog +themes: + - stark diff --git a/core/profiles/testing/config/install/locale.settings.yml b/core/profiles/testing/config/optional/locale.settings.yml similarity index 100% rename from core/profiles/testing/config/install/locale.settings.yml rename to core/profiles/testing/config/optional/locale.settings.yml diff --git a/core/profiles/testing_config_overrides/config/install/system.action.user_block_user_action.yml b/core/profiles/testing_config_overrides/config/install/system.action.user_block_user_action.yml new file mode 100644 index 000000000000..f902ff1a68f5 --- /dev/null +++ b/core/profiles/testing_config_overrides/config/install/system.action.user_block_user_action.yml @@ -0,0 +1,9 @@ +id: user_block_user_action +label: 'Overridden block the selected user(s)' +status: true +langcode: en +type: user +plugin: user_block_user_action +dependencies: + module: + - user diff --git a/core/profiles/testing/config/install/system.cron.yml b/core/profiles/testing_config_overrides/config/install/system.cron.yml similarity index 100% rename from core/profiles/testing/config/install/system.cron.yml rename to core/profiles/testing_config_overrides/config/install/system.cron.yml diff --git a/core/profiles/testing_config_overrides/config/install/tour.tour.language.yml b/core/profiles/testing_config_overrides/config/install/tour.tour.language.yml new file mode 100644 index 000000000000..6f9024076341 --- /dev/null +++ b/core/profiles/testing_config_overrides/config/install/tour.tour.language.yml @@ -0,0 +1,13 @@ +id: language +module: language +label: Language +langcode: en +routes: + - route_name: entity.configurable_language.collection +tips: + language-overview: + id: language-overview + plugin: text + label: Languages + body: '
The "Languages" page allows you to add, edit, delete, and reorder languages for the site.
' + weight: 1 diff --git a/core/profiles/testing_config_overrides/config/optional/config_test.dynamic.dotted.default.yml b/core/profiles/testing_config_overrides/config/optional/config_test.dynamic.dotted.default.yml new file mode 100644 index 000000000000..db97f0f85db2 --- /dev/null +++ b/core/profiles/testing_config_overrides/config/optional/config_test.dynamic.dotted.default.yml @@ -0,0 +1,6 @@ +id: dotted.default +label: 'Default install profile override' +weight: 0 +protected_property: Default +# Intentionally commented out to verify default status behavior. +# status: 1 diff --git a/core/profiles/testing_config_overrides/config/optional/config_test.dynamic.override.yml b/core/profiles/testing_config_overrides/config/optional/config_test.dynamic.override.yml new file mode 100644 index 000000000000..4a54a333c049 --- /dev/null +++ b/core/profiles/testing_config_overrides/config/optional/config_test.dynamic.override.yml @@ -0,0 +1,8 @@ +id: override +label: Override +weight: 0 +protected_property: Default +status: 1 +dependencies: + module: + - tour diff --git a/core/profiles/testing_config_overrides/config/optional/config_test.dynamic.override_unmet.yml b/core/profiles/testing_config_overrides/config/optional/config_test.dynamic.override_unmet.yml new file mode 100644 index 000000000000..f20365f7d268 --- /dev/null +++ b/core/profiles/testing_config_overrides/config/optional/config_test.dynamic.override_unmet.yml @@ -0,0 +1,8 @@ +id: override_unmet +label: Override +weight: 0 +protected_property: Default +status: 1 +dependencies: + module: + - dblog diff --git a/core/profiles/testing_config_overrides/config/optional/tour.tour.testing_config_overrides.yml b/core/profiles/testing_config_overrides/config/optional/tour.tour.testing_config_overrides.yml new file mode 100644 index 000000000000..f0ef399bcfd8 --- /dev/null +++ b/core/profiles/testing_config_overrides/config/optional/tour.tour.testing_config_overrides.yml @@ -0,0 +1,13 @@ +id: testing_config_overrides +module: testing_config_overrides +label: Config override test +langcode: en +routes: + - route_name: entity.configurable_language.collection +tips: + language-overview: + id: language-overview + plugin: text + label: Languages + body: 'The "Languages" page allows you to add, edit, delete, and reorder languages for the site.
' + weight: 1 diff --git a/core/profiles/testing_config_overrides/testing_config_overrides.info.yml b/core/profiles/testing_config_overrides/testing_config_overrides.info.yml new file mode 100644 index 000000000000..f88724c5104c --- /dev/null +++ b/core/profiles/testing_config_overrides/testing_config_overrides.info.yml @@ -0,0 +1,10 @@ +name: Testing config overrides +type: profile +description: 'Minimal profile for running tests with config overrides in a profile.' +version: VERSION +core: 8.x +hidden: true +dependencies: + - action + - language + - tour