diff --git a/core/config/install/core.extension.yml b/core/config/install/core.extension.yml index ce74baed1829fe104bc1dc9df9543f24be8100d5..659446cc64c88900a59666f480bf3a475678b5dc 100644 --- a/core/config/install/core.extension.yml +++ b/core/config/install/core.extension.yml @@ -1,2 +1,3 @@ module: {} theme: {} +profile: '' diff --git a/core/config/schema/core.extension.schema.yml b/core/config/schema/core.extension.schema.yml index bdc84b9fee89f69e3c44dadd9105a3766001f545..087e2e3b9497f75ca86794bbd34796bb8b302350 100644 --- a/core/config/schema/core.extension.schema.yml +++ b/core/config/schema/core.extension.schema.yml @@ -14,3 +14,6 @@ core.extension: sequence: type: integer label: 'Weight' + profile: + type: string + label: 'Install profile' diff --git a/core/core.services.yml b/core/core.services.yml index 24c6cca28e91e2a5053cdc293e97bbbe3873a665..7d7449df03344a11cfb8339644154880bae607f4 100644 --- a/core/core.services.yml +++ b/core/core.services.yml @@ -303,7 +303,7 @@ services: - { name: event_subscriber } config.installer: class: Drupal\Core\Config\ConfigInstaller - arguments: ['@config.factory', '@config.storage', '@config.typed', '@config.manager', '@event_dispatcher'] + arguments: ['@config.factory', '@config.storage', '@config.typed', '@config.manager', '@event_dispatcher', '%install_profile%'] lazy: true config.storage: class: Drupal\Core\Config\CachedStorage @@ -328,7 +328,7 @@ services: - { name: backend_overridable } config.storage.schema: class: Drupal\Core\Config\ExtensionInstallStorage - arguments: ['@config.storage', 'config/schema'] + arguments: ['@config.storage', 'config/schema', '', true, '%install_profile%'] config.typed: class: Drupal\Core\Config\TypedConfigManager arguments: ['@config.storage', '@config.storage.schema', '@cache.discovery', '@module_handler'] diff --git a/core/includes/bootstrap.inc b/core/includes/bootstrap.inc index f74859e17185f93f9d459f9c7c830418f1b0f1c8..5b18c5cce02fdc864e575aba7d3bf5587f03d714 100644 --- a/core/includes/bootstrap.inc +++ b/core/includes/bootstrap.inc @@ -9,6 +9,7 @@ use Drupal\Component\Utility\Html; use Drupal\Component\Utility\SafeMarkup; use Drupal\Component\Utility\Unicode; +use Drupal\Core\Config\BootstrapConfigStorageFactory; use Drupal\Core\Logger\RfcLogLevel; use Drupal\Core\Render\Markup; use Drupal\Component\Render\MarkupInterface; @@ -727,12 +728,20 @@ function drupal_installation_attempted() { * When this function is called during Drupal's initial installation process, * the name of the profile that's about to be installed is stored in the global * installation state. At all other times, the "install_profile" setting will be - * available in settings.php. + * available in container as a parameter. * * @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. + * + * @deprecated in Drupal 8.3.0, will be removed before Drupal 9.0.0. + * Use the install_profile container parameter or \Drupal::installProfile() + * instead. If you are accessing the value before it is written to + * configuration during the installer use the $install_state global. If you + * need to access the value before container is available you can use + * BootstrapConfigStorageFactory to load the value directly from + * configuration. */ function drupal_get_profile() { global $install_state; @@ -747,8 +756,18 @@ function drupal_get_profile() { } } else { - // Fall back to NULL, if there is no 'install_profile' setting. - $profile = Settings::get('install_profile'); + if (\Drupal::hasContainer()) { + $profile = \Drupal::installProfile(); + } + else { + $profile = BootstrapConfigStorageFactory::getDatabaseStorage()->read('core.extension')['profile']; + } + + // A BC layer just in in case this only exists in Settings. Introduced in + // Drupal 8.3.x and will be removed before Drupal 9.0.0. + if (empty($profile)) { + $profile = Settings::get('install_profile'); + } } return $profile; diff --git a/core/includes/install.core.inc b/core/includes/install.core.inc index a016540328a4e2289e4ae676ea904fd5042f0575..6fcc2d26ed65b940b29f228d5a00c403328fc184 100644 --- a/core/includes/install.core.inc +++ b/core/includes/install.core.inc @@ -12,6 +12,7 @@ use Drupal\Core\Form\FormState; use Drupal\Core\Installer\Exception\AlreadyInstalledException; use Drupal\Core\Installer\Exception\InstallerException; +use Drupal\Core\Installer\Exception\InstallProfileMismatchException; use Drupal\Core\Installer\Exception\NoProfilesException; use Drupal\Core\Installer\InstallerKernel; use Drupal\Core\Language\Language; @@ -2188,13 +2189,23 @@ function install_display_requirements($install_state, $requirements) { } /** - * Installation task; ensures install profile is written to settings.php. + * Installation task; writes profile to settings.php if possible. * * @param array $install_state * An array of information about the current installation state. + * + * @see _install_select_profile() + * + * @throws \Drupal\Core\Installer\Exception\InstallProfileMismatchException + * + * @deprecated in Drupal 8.3.0 and will be removed before Drupal 9.0.0. The + * install profile is written to core.extension. */ function install_write_profile($install_state) { - if (Settings::get('install_profile') !== $install_state['parameters']['profile']) { + // Only need to write to settings.php if it is possible. The primary storage + // for the install profile is the core.extension configuration. + $settings_path = \Drupal::service('site.path') . '/settings.php'; + if (is_writable($settings_path)) { // Remember the profile which was used. $settings['settings']['install_profile'] = (object) array( 'value' => $install_state['parameters']['profile'], @@ -2202,4 +2213,7 @@ function install_write_profile($install_state) { ); drupal_rewrite_settings($settings); } + elseif (($settings_profile = Settings::get('install_profile')) && $settings_profile !== $install_state['parameters']['profile']) { + throw new InstallProfileMismatchException($install_state['parameters']['profile'], $settings_profile, $settings_path, \Drupal::translation()); + } } diff --git a/core/includes/install.inc b/core/includes/install.inc index 49dd4cb57f553f79537507629347093f40ad4529..6bb6c11a0d15e90bbefda09281c413fae591bcd0 100644 --- a/core/includes/install.inc +++ b/core/includes/install.inc @@ -621,6 +621,12 @@ function drupal_install_system($install_state) { // Install base system configuration. \Drupal::service('config.installer')->installDefaultConfig('core', 'core'); + // Store the installation profile in configuration to populate the + // 'install_profile' container parameter. + \Drupal::configFactory()->getEditable('core.extension') + ->set('profile', $install_state['parameters']['profile']) + ->save(); + // Install System module and rebuild the newly available routes. $kernel->getContainer()->get('module_installer')->install(array('system'), FALSE); \Drupal::service('router.builder')->rebuild(); diff --git a/core/lib/Drupal.php b/core/lib/Drupal.php index b301cd97763e221c4f93492c149e46a860463073..50027399d1178d6fd002b913572c9fb6552a3efa 100644 --- a/core/lib/Drupal.php +++ b/core/lib/Drupal.php @@ -181,6 +181,16 @@ public static function root() { return static::getContainer()->get('app.root'); } + /** + * Gets the active install profile. + * + * @return string|null + * The name of the active install profile. + */ + public static function installProfile() { + return static::getContainer()->getParameter('install_profile'); + } + /** * Indicates if there is a currently active request object. * diff --git a/core/lib/Drupal/Core/Config/ConfigInstaller.php b/core/lib/Drupal/Core/Config/ConfigInstaller.php index 2f30cce49a42d38a793efc9563373dfa3c32b821..5d50cd16ddeb03b5aa980d8433a12039004bbcf0 100644 --- a/core/lib/Drupal/Core/Config/ConfigInstaller.php +++ b/core/lib/Drupal/Core/Config/ConfigInstaller.php @@ -6,7 +6,6 @@ use Drupal\Component\Utility\Unicode; use Drupal\Core\Config\Entity\ConfigDependencyManager; use Drupal\Core\Config\Entity\ConfigEntityDependency; -use Drupal\Core\Site\Settings; use Symfony\Component\EventDispatcher\EventDispatcherInterface; class ConfigInstaller implements ConfigInstallerInterface { @@ -60,6 +59,13 @@ class ConfigInstaller implements ConfigInstallerInterface { */ protected $isSyncing = FALSE; + /** + * The name of the currently active installation profile. + * + * @var string + */ + protected $installProfile; + /** * Constructs the configuration installer. * @@ -73,13 +79,16 @@ class ConfigInstaller implements ConfigInstallerInterface { * The configuration manager. * @param \Symfony\Component\EventDispatcher\EventDispatcherInterface $event_dispatcher * The event dispatcher. + * @param string $install_profile + * The name of the currently active installation profile. */ - public function __construct(ConfigFactoryInterface $config_factory, StorageInterface $active_storage, TypedConfigManagerInterface $typed_config, ConfigManagerInterface $config_manager, EventDispatcherInterface $event_dispatcher) { + public function __construct(ConfigFactoryInterface $config_factory, StorageInterface $active_storage, TypedConfigManagerInterface $typed_config, ConfigManagerInterface $config_manager, EventDispatcherInterface $event_dispatcher, $install_profile) { $this->configFactory = $config_factory; $this->activeStorages[$active_storage->getCollectionName()] = $active_storage; $this->typedConfig = $typed_config; $this->configManager = $config_manager; $this->eventDispatcher = $event_dispatcher; + $this->installProfile = $install_profile; } /** @@ -140,7 +149,7 @@ public function installDefaultConfig($type, $name) { // Install any optional configuration entities whose dependencies can now // be met. This searches all the installed modules config/optional // directories. - $storage = new ExtensionInstallStorage($this->getActiveStorages(StorageInterface::DEFAULT_COLLECTION), InstallStorage::CONFIG_OPTIONAL_DIRECTORY, StorageInterface::DEFAULT_COLLECTION, FALSE); + $storage = new ExtensionInstallStorage($this->getActiveStorages(StorageInterface::DEFAULT_COLLECTION), InstallStorage::CONFIG_OPTIONAL_DIRECTORY, StorageInterface::DEFAULT_COLLECTION, FALSE, $this->installProfile); $this->installOptionalConfig($storage, [$type => $name]); } @@ -156,11 +165,11 @@ public function installOptionalConfig(StorageInterface $storage = NULL, $depende $optional_profile_config = []; 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); + $storage = new ExtensionInstallStorage($this->getActiveStorages(StorageInterface::DEFAULT_COLLECTION), InstallStorage::CONFIG_OPTIONAL_DIRECTORY, StorageInterface::DEFAULT_COLLECTION, TRUE, $this->installProfile); // The extension install storage ensures that overrides are used. $profile_storage = NULL; } - elseif (isset($profile)) { + elseif (!empty($profile)) { // Creates a profile storage to search for overrides. $profile_install_path = $this->drupalGetPath('module', $profile) . '/' . InstallStorage::CONFIG_OPTIONAL_DIRECTORY; $profile_storage = new FileStorage($profile_install_path, StorageInterface::DEFAULT_COLLECTION); @@ -331,7 +340,7 @@ protected function createConfiguration($collection, array $config_to_create) { * {@inheritdoc} */ public function installCollectionDefaultConfig($collection) { - $storage = new ExtensionInstallStorage($this->getActiveStorages(StorageInterface::DEFAULT_COLLECTION), InstallStorage::CONFIG_INSTALL_DIRECTORY, $collection, $this->drupalInstallationAttempted()); + $storage = new ExtensionInstallStorage($this->getActiveStorages(StorageInterface::DEFAULT_COLLECTION), InstallStorage::CONFIG_INSTALL_DIRECTORY, $collection, $this->drupalInstallationAttempted(), $this->installProfile); // Only install configuration for enabled extensions. $enabled_extensions = $this->getEnabledExtensions(); $config_to_install = array_filter($storage->listAll(), function ($config_name) use ($enabled_extensions) { @@ -672,9 +681,7 @@ protected function drupalGetPath($type, $name) { * 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'); + return $this->installProfile; } /** diff --git a/core/lib/Drupal/Core/Config/ExtensionInstallStorage.php b/core/lib/Drupal/Core/Config/ExtensionInstallStorage.php index 14e80dd8869efd1edd1e16d8abf08d756e44890f..16fecbc3966fc15880dda8480c28236b4eb38aba 100644 --- a/core/lib/Drupal/Core/Config/ExtensionInstallStorage.php +++ b/core/lib/Drupal/Core/Config/ExtensionInstallStorage.php @@ -2,7 +2,6 @@ namespace Drupal\Core\Config; -use Drupal\Core\Site\Settings; use Drupal\Core\Extension\ExtensionDiscovery; /** @@ -27,6 +26,13 @@ class ExtensionInstallStorage extends InstallStorage { */ protected $includeProfile = TRUE; + /** + * The name of the currently active installation profile. + * + * @var string + */ + protected $installProfile; + /** * Overrides \Drupal\Core\Config\InstallStorage::__construct(). * @@ -42,11 +48,19 @@ class ExtensionInstallStorage extends InstallStorage { * @param bool $include_profile * (optional) Whether to include the install profile in extensions to * search and to get overrides from. + * @param string $profile + * (optional) The current installation profile. This parameter will be + * mandatory in Drupal 9.0.0. In Drupal 8.3.0 not providing this parameter + * will trigger a silenced deprecation warning. */ - public function __construct(StorageInterface $config_storage, $directory = self::CONFIG_INSTALL_DIRECTORY, $collection = StorageInterface::DEFAULT_COLLECTION, $include_profile = TRUE) { + public function __construct(StorageInterface $config_storage, $directory = self::CONFIG_INSTALL_DIRECTORY, $collection = StorageInterface::DEFAULT_COLLECTION, $include_profile = TRUE, $profile = NULL) { parent::__construct($directory, $collection); $this->configStorage = $config_storage; $this->includeProfile = $include_profile; + if (is_null($profile)) { + @trigger_error('Install profile will be a mandatory parameter in Drupal 9.0.', E_USER_DEPRECATED); + } + $this->installProfile = $profile ?: \Drupal::installProfile(); } /** @@ -77,22 +91,20 @@ protected function getAllFolders() { $this->folders = array(); $this->folders += $this->getCoreNames(); - $install_profile = Settings::get('install_profile'); - $profile = drupal_get_profile(); $extensions = $this->configStorage->read('core.extension'); // @todo Remove this scan as part of https://www.drupal.org/node/2186491 $listing = new ExtensionDiscovery(\Drupal::root()); if (!empty($extensions['module'])) { $modules = $extensions['module']; // Remove the install profile as this is handled later. - unset($modules[$install_profile]); + unset($modules[$this->installProfile]); $profile_list = $listing->scan('profile'); - if ($profile && isset($profile_list[$profile])) { + if ($this->installProfile && isset($profile_list[$this->installProfile])) { // Prime the drupal_get_filename() static cache with the profile info // file location so we can use drupal_get_path() on the active profile // during the module scan. // @todo Remove as part of https://www.drupal.org/node/2186491 - drupal_get_filename('profile', $profile, $profile_list[$profile]->getPathname()); + drupal_get_filename('profile', $this->installProfile, $profile_list[$this->installProfile]->getPathname()); } $module_list_scan = $listing->scan('module'); $module_list = array(); @@ -117,12 +129,12 @@ protected function getAllFolders() { // 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. - if (isset($profile)) { + if ($this->installProfile) { if (!isset($profile_list)) { $profile_list = $listing->scan('profile'); } - if (isset($profile_list[$profile])) { - $profile_folders = $this->getComponentNames(array($profile_list[$profile])); + if (isset($profile_list[$this->installProfile])) { + $profile_folders = $this->getComponentNames(array($profile_list[$this->installProfile])); $this->folders = $profile_folders + $this->folders; } } diff --git a/core/lib/Drupal/Core/DrupalKernel.php b/core/lib/Drupal/Core/DrupalKernel.php index 7dad3c1064f242ca37923747e1f10b10dfb65b82..b8130b30bb725d39cecc02e67fb044bfee96a331 100644 --- a/core/lib/Drupal/Core/DrupalKernel.php +++ b/core/lib/Drupal/Core/DrupalKernel.php @@ -1204,6 +1204,7 @@ protected function compileContainer() { $container = $this->getContainerBuilder(); $container->set('kernel', $this); $container->setParameter('container.modules', $this->getModulesParameter()); + $container->setParameter('install_profile', $this->getInstallProfile()); // Get a list of namespaces and put it onto the container. $namespaces = $this->getModuleNamespacesPsr4($this->getModuleFileNames()); @@ -1561,4 +1562,25 @@ protected function addServiceFiles(array $service_yamls) { $this->serviceYamls['site'] = array_filter($service_yamls, 'file_exists'); } + /** + * Gets the active install profile. + * + * @return string|null + * The name of the any active install profile or distribution. + */ + protected function getInstallProfile() { + $config = $this->getConfigStorage()->read('core.extension'); + if (!empty($config['profile'])) { + $install_profile = $config['profile']; + } + // @todo https://www.drupal.org/node/2831065 remove the BC layer. + else { + // If system_update_8300() has not yet run fallback to using settings. + $install_profile = Settings::get('install_profile'); + } + + // Normalize an empty string to a NULL value. + return empty($install_profile) ? NULL : $install_profile; + } + } diff --git a/core/lib/Drupal/Core/EventSubscriber/ConfigImportSubscriber.php b/core/lib/Drupal/Core/EventSubscriber/ConfigImportSubscriber.php index ad2eb5e04e092ae5f02cf0e27066f0e3e0bc2110..24b4c70d5fb612ae10986296c7f7d1a5c5eeceaf 100644 --- a/core/lib/Drupal/Core/EventSubscriber/ConfigImportSubscriber.php +++ b/core/lib/Drupal/Core/EventSubscriber/ConfigImportSubscriber.php @@ -8,7 +8,6 @@ use Drupal\Core\Config\ConfigImportValidateEventSubscriberBase; use Drupal\Core\Config\ConfigNameException; use Drupal\Core\Extension\ThemeHandlerInterface; -use Drupal\Core\Site\Settings; /** * Config import subscriber for config import events. @@ -112,9 +111,10 @@ protected function validateModules(ConfigImporter $config_importer) { } } - // Settings is safe to use because settings.php is written before any module - // is installed. - $install_profile = Settings::get('install_profile'); + // Get the install profile from the site's configuration. + $current_core_extension = $config_importer->getStorageComparer()->getTargetStorage()->read('core.extension'); + $install_profile = isset($current_core_extension['profile']) ? $current_core_extension['profile'] : NULL; + // Ensure that all modules being uninstalled are not required by modules // that will be installed after the import. $uninstalls = $config_importer->getExtensionChangelist('module', 'uninstall'); @@ -129,10 +129,15 @@ protected function validateModules(ConfigImporter $config_importer) { } // Ensure that the install profile is not being uninstalled. - if (in_array($install_profile, $uninstalls)) { + if (in_array($install_profile, $uninstalls, TRUE)) { $profile_name = $module_data[$install_profile]->info['name']; $config_importer->logError($this->t('Unable to uninstall the %profile profile since it is the install profile.', array('%profile' => $profile_name))); } + + // Ensure the profile is not changing. + if ($install_profile !== $core_extension['profile']) { + $config_importer->logError($this->t('Cannot change the install profile from %new_profile to %profile once Drupal is installed.', array('%profile' => $install_profile, '%new_profile' => $core_extension['profile']))); + } } /** diff --git a/core/lib/Drupal/Core/Installer/Exception/InstallProfileMismatchException.php b/core/lib/Drupal/Core/Installer/Exception/InstallProfileMismatchException.php new file mode 100644 index 0000000000000000000000000000000000000000..d67978986abbaf552e8effe3aeafaf8561ccb27e --- /dev/null +++ b/core/lib/Drupal/Core/Installer/Exception/InstallProfileMismatchException.php @@ -0,0 +1,45 @@ +stringTranslation = $string_translation; + + $title = $this->t('Install profile mismatch'); + $message = $this->t( + 'The selected profile %profile does not match the install_profile setting, which is %settings_profile. Cannot write updated setting to %settings_file.', + [ + '%profile' => $selected_profile, + '%settings_profile' => $settings_profile, + '%settings_file' => $settings_file, + ] + ); + parent::__construct($message, $title); + } + +} diff --git a/core/lib/Drupal/Core/Installer/InstallerKernel.php b/core/lib/Drupal/Core/Installer/InstallerKernel.php index e00c215c09e82cd7ca6e1095b265cf53e6a6f289..adeb5c53eed62b1935dcd43c079b8d10a6abbe45 100644 --- a/core/lib/Drupal/Core/Installer/InstallerKernel.php +++ b/core/lib/Drupal/Core/Installer/InstallerKernel.php @@ -46,4 +46,24 @@ public function getConfigStorage() { return parent::getConfigStorage(); } + /** + * {@inheritdoc} + */ + public function getInstallProfile() { + global $install_state; + if ($install_state && empty($install_state['installation_finished'])) { + // If the profile has been selected return it. + if (isset($install_state['parameters']['profile'])) { + $profile = $install_state['parameters']['profile']; + } + else { + $profile = NULL; + } + } + else { + $profile = parent::getInstallProfile(); + } + return $profile; + } + } diff --git a/core/modules/locale/locale.services.yml b/core/modules/locale/locale.services.yml index e37cb48d1b554626e3cf8b153d08576c2b106dd2..b9f8fb807e7b72780b9d4ca086eca75256d12a40 100644 --- a/core/modules/locale/locale.services.yml +++ b/core/modules/locale/locale.services.yml @@ -1,7 +1,7 @@ services: locale.default.config.storage: class: Drupal\locale\LocaleDefaultConfigStorage - arguments: ['@config.storage', '@language_manager'] + arguments: ['@config.storage', '@language_manager', '%install_profile%'] public: false locale.config_manager: class: Drupal\locale\LocaleConfigManager diff --git a/core/modules/locale/src/LocaleDefaultConfigStorage.php b/core/modules/locale/src/LocaleDefaultConfigStorage.php index 29697a107653571713623a3d2530390a2ca6c314..fb4e352069e9ec4f5b027d8e1802fd253b9867c6 100644 --- a/core/modules/locale/src/LocaleDefaultConfigStorage.php +++ b/core/modules/locale/src/LocaleDefaultConfigStorage.php @@ -57,12 +57,12 @@ class LocaleDefaultConfigStorage { * @param \Drupal\language\ConfigurableLanguageManagerInterface $language_manager * The language manager. */ - public function __construct(StorageInterface $config_storage, ConfigurableLanguageManagerInterface $language_manager) { + public function __construct(StorageInterface $config_storage, ConfigurableLanguageManagerInterface $language_manager, $install_profile) { $this->configStorage = $config_storage; $this->languageManager = $language_manager; - $this->requiredInstallStorage = new ExtensionInstallStorage($this->configStorage); - $this->optionalInstallStorage = new ExtensionInstallStorage($this->configStorage, ExtensionInstallStorage::CONFIG_OPTIONAL_DIRECTORY); + $this->requiredInstallStorage = new ExtensionInstallStorage($this->configStorage, ExtensionInstallStorage::CONFIG_INSTALL_DIRECTORY, ExtensionInstallStorage::DEFAULT_COLLECTION, TRUE, $install_profile); + $this->optionalInstallStorage = new ExtensionInstallStorage($this->configStorage, ExtensionInstallStorage::CONFIG_OPTIONAL_DIRECTORY, ExtensionInstallStorage::DEFAULT_COLLECTION, TRUE, $install_profile); } /** diff --git a/core/modules/simpletest/src/KernelTestBase.php b/core/modules/simpletest/src/KernelTestBase.php index 0b5792f5fe0eb173cef266e80f243dbe848fdf46..c804218af60d81ea092a8107372cd96409a0e62d 100644 --- a/core/modules/simpletest/src/KernelTestBase.php +++ b/core/modules/simpletest/src/KernelTestBase.php @@ -232,7 +232,7 @@ protected function setUp() { // \Drupal\Core\Config\ConfigInstaller::installDefaultConfig() to work. // Write directly to active storage to avoid early instantiation of // the event dispatcher which can prevent modules from registering events. - \Drupal::service('config.storage')->write('core.extension', array('module' => array(), 'theme' => array())); + \Drupal::service('config.storage')->write('core.extension', array('module' => array(), 'theme' => array(), 'profile' => '')); // Collect and set a fixed module list. $class = get_class($this); diff --git a/core/modules/simpletest/src/Tests/KernelTestBaseTest.php b/core/modules/simpletest/src/Tests/KernelTestBaseTest.php index 15ecf1fe75b17197703a8d9a08f369efd3e2312a..472fdf9e45bc715cd387ea31f7dcc65f3634ee56 100644 --- a/core/modules/simpletest/src/Tests/KernelTestBaseTest.php +++ b/core/modules/simpletest/src/Tests/KernelTestBaseTest.php @@ -323,13 +323,13 @@ function testNoThemeByDefault() { } /** - * Tests that drupal_get_profile() returns NULL. + * Tests that \Drupal::installProfile() returns NULL. * * As the currently active installation profile is used when installing * configuration, for example, this is essential to ensure test isolation. */ public function testDrupalGetProfile() { - $this->assertNull(drupal_get_profile()); + $this->assertNull(\Drupal::installProfile()); } /** diff --git a/core/modules/system/src/Tests/Installer/DistributionProfileExistingSettingsTest.php b/core/modules/system/src/Tests/Installer/DistributionProfileExistingSettingsTest.php new file mode 100644 index 0000000000000000000000000000000000000000..2fe5d4f1bc33450c465b0f974972645d156e7865 --- /dev/null +++ b/core/modules/system/src/Tests/Installer/DistributionProfileExistingSettingsTest.php @@ -0,0 +1,132 @@ +info = [ + 'type' => 'profile', + 'core' => \Drupal::CORE_COMPATIBILITY, + 'name' => 'Distribution profile', + 'distribution' => [ + 'name' => 'My Distribution', + 'install' => [ + 'theme' => 'bartik', + ], + ], + ]; + // File API functions are not available yet. + $path = $this->siteDirectory . '/profiles/mydistro'; + mkdir($path, 0777, TRUE); + file_put_contents("$path/mydistro.info.yml", Yaml::encode($this->info)); + + // Pre-configure hash salt. + // Any string is valid, so simply use the class name of this test. + $this->settings['settings']['hash_salt'] = (object) [ + 'value' => __CLASS__, + 'required' => TRUE, + ]; + + // Pre-configure database credentials. + $connection_info = Database::getConnectionInfo(); + unset($connection_info['default']['pdo']); + unset($connection_info['default']['init_commands']); + + $this->settings['databases']['default'] = (object) [ + 'value' => $connection_info, + 'required' => TRUE, + ]; + + // Use the kernel to find the site path because the site.path service should + // not be available at this point in the install process. + $site_path = DrupalKernel::findSitePath(Request::createFromGlobals()); + // Pre-configure config directories. + $this->settings['config_directories'] = [ + CONFIG_SYNC_DIRECTORY => (object) [ + 'value' => $site_path . '/files/config_staging', + 'required' => TRUE, + ], + ]; + mkdir($this->settings['config_directories'][CONFIG_SYNC_DIRECTORY]->value, 0777, TRUE); + parent::setUp(); + } + + /** + * {@inheritdoc} + */ + protected function setUpLanguage() { + // Make settings file not writable. + $filename = $this->siteDirectory . '/settings.php'; + // Make the settings file read-only. + // Not using File API; a potential error must trigger a PHP warning. + chmod($filename, 0444); + + // Verify that the distribution name appears. + $this->assertRaw($this->info['distribution']['name']); + // Verify that the requested theme is used. + $this->assertRaw($this->info['distribution']['install']['theme']); + // Verify that the "Choose profile" step does not appear. + $this->assertNoText('profile'); + + parent::setUpLanguage(); + } + + /** + * {@inheritdoc} + */ + protected function setUpProfile() { + // This step is skipped, because there is a distribution profile. + } + + /** + * {@inheritdoc} + */ + protected function setUpSettings() { + // This step should not appear, since settings.php is fully configured + // already. + } + + /** + * Confirms that the installation succeeded. + */ + public function testInstalled() { + $this->assertUrl('user/1'); + $this->assertResponse(200); + // Confirm that we are logged-in after installation. + $this->assertText($this->rootUser->getUsername()); + + // Confirm that Drupal recognizes this distribution as the current profile. + $this->assertEqual(\Drupal::installProfile(), 'mydistro'); + $this->assertNull(Settings::get('install_profile'), 'The install profile has not been written to settings.php.'); + $this->assertEqual($this->config('core.extension')->get('profile'), 'mydistro', 'The install profile has been written to core.extension configuration.'); + + $this->rebuildContainer(); + $this->pass('Container can be rebuilt even though distribution is not written to settings.php.'); + $this->assertEqual(\Drupal::installProfile(), 'mydistro'); + } + +} diff --git a/core/modules/system/src/Tests/Installer/DistributionProfileTest.php b/core/modules/system/src/Tests/Installer/DistributionProfileTest.php index 0bb47acb44efcddff3c9e30e8f20187eb05ecaa5..569fe36212c2c98fa553bf9140039ce5fdaee490 100644 --- a/core/modules/system/src/Tests/Installer/DistributionProfileTest.php +++ b/core/modules/system/src/Tests/Installer/DistributionProfileTest.php @@ -3,6 +3,7 @@ namespace Drupal\system\Tests\Installer; use Drupal\Core\Serialization\Yaml; +use Drupal\Core\Site\Settings; use Drupal\simpletest\InstallerTestBase; /** @@ -68,6 +69,11 @@ public function testInstalled() { $this->assertResponse(200); // Confirm that we are logged-in after installation. $this->assertText($this->rootUser->getUsername()); + + // Confirm that Drupal recognizes this distribution as the current profile. + $this->assertEqual(\Drupal::installProfile(), 'mydistro'); + $this->assertEqual(Settings::get('install_profile'), 'mydistro', 'The install profile has been written to settings.php.'); + $this->assertEqual($this->config('core.extension')->get('profile'), 'mydistro', 'The install profile has been written to core.extension configuration.'); } } diff --git a/core/modules/system/src/Tests/Installer/InstallerExistingSettingsMismatchProfileBrokenTest.php b/core/modules/system/src/Tests/Installer/InstallerExistingSettingsMismatchProfileBrokenTest.php new file mode 100644 index 0000000000000000000000000000000000000000..6d866f0cf834c6118ca46a0008c3f736eb02bbca --- /dev/null +++ b/core/modules/system/src/Tests/Installer/InstallerExistingSettingsMismatchProfileBrokenTest.php @@ -0,0 +1,108 @@ +settings['settings']['hash_salt'] = (object) [ + 'value' => __CLASS__, + 'required' => TRUE, + ]; + + // Pre-configure database credentials. + $connection_info = Database::getConnectionInfo(); + unset($connection_info['default']['pdo']); + unset($connection_info['default']['init_commands']); + + $this->settings['databases']['default'] = (object) [ + 'value' => $connection_info, + 'required' => TRUE, + ]; + + // During interactive install we'll change this to a different profile and + // this test will ensure that the new value is written to settings.php. + $this->settings['settings']['install_profile'] = (object) [ + 'value' => 'minimal', + 'required' => TRUE, + ]; + + // Pre-configure config directories. + $site_path = DrupalKernel::findSitePath(Request::createFromGlobals()); + $this->settings['config_directories'] = [ + CONFIG_SYNC_DIRECTORY => (object) [ + 'value' => $site_path . '/files/config_staging', + 'required' => TRUE, + ], + ]; + mkdir($this->settings['config_directories'][CONFIG_SYNC_DIRECTORY]->value, 0777, TRUE); + + parent::setUp(); + } + + /** + * {@inheritdoc} + */ + protected function visitInstaller() { + // Make settings file not writable. This will break the installer. + $filename = $this->siteDirectory . '/settings.php'; + // Make the settings file read-only. + // Not using File API; a potential error must trigger a PHP warning. + chmod($filename, 0444); + + $this->drupalGet($GLOBALS['base_url'] . '/core/install.php?langcode=en&profile=testing'); + } + + /** + * {@inheritdoc} + */ + protected function setUpLanguage() { + // This step is skipped, because there is a lagcode as a query param. + } + + /** + * {@inheritdoc} + */ + protected function setUpProfile() { + // This step is skipped, because there is a profile as a query param. + } + + /** + * {@inheritdoc} + */ + protected function setUpSettings() { + // This step should not appear, since settings.php is fully configured + // already. + } + + protected function setUpSite() { + // This step should not appear, since settings.php could not be written. + } + + /** + * Verifies that installation did not succeed. + */ + public function testBrokenInstaller() { + $this->assertTitle('Install profile mismatch | Drupal'); + $this->assertText("The selected profile testing does not match the install_profile setting, which is minimal. Cannot write updated setting to {$this->siteDirectory}/settings.php."); + } + +} diff --git a/core/modules/system/src/Tests/Installer/InstallerExistingSettingsMismatchProfileTest.php b/core/modules/system/src/Tests/Installer/InstallerExistingSettingsMismatchProfileTest.php new file mode 100644 index 0000000000000000000000000000000000000000..a179039d0f92b7629a0d0fab54064be08d5e86e7 --- /dev/null +++ b/core/modules/system/src/Tests/Installer/InstallerExistingSettingsMismatchProfileTest.php @@ -0,0 +1,101 @@ +settings['settings']['hash_salt'] = (object) array( + 'value' => __CLASS__, + 'required' => TRUE, + ); + + // Pre-configure database credentials. + $connection_info = Database::getConnectionInfo(); + unset($connection_info['default']['pdo']); + unset($connection_info['default']['init_commands']); + + $this->settings['databases']['default'] = (object) array( + 'value' => $connection_info, + 'required' => TRUE, + ); + + // During interactive install we'll change this to a different profile and + // this test will ensure that the new value is written to settings.php. + $this->settings['settings']['install_profile'] = (object) [ + 'value' => 'minimal', + 'required' => TRUE, + ]; + + // Pre-configure config directories. + $this->settings['config_directories'] = array( + CONFIG_SYNC_DIRECTORY => (object) array( + 'value' => DrupalKernel::findSitePath(Request::createFromGlobals()) . '/files/config_sync', + 'required' => TRUE, + ), + ); + mkdir($this->settings['config_directories'][CONFIG_SYNC_DIRECTORY]->value, 0777, TRUE); + + parent::setUp(); + } + + /** + * {@inheritdoc} + */ + protected function visitInstaller() { + // Provide profile and language in query string to skip these pages. + $this->drupalGet($GLOBALS['base_url'] . '/core/install.php?langcode=en&profile=testing'); + } + + /** + * {@inheritdoc} + */ + protected function setUpLanguage() { + // This step is skipped, because there is a lagcode as a query param. + } + + /** + * {@inheritdoc} + */ + protected function setUpProfile() { + // This step is skipped, because there is a profile as a query param. + } + + /** + * {@inheritdoc} + */ + protected function setUpSettings() { + // This step should not appear, since settings.php is fully configured + // already. + } + + /** + * Verifies that installation succeeded. + */ + public function testInstaller() { + $this->assertUrl('user/1'); + $this->assertResponse(200); + $this->assertEqual('testing', \Drupal::installProfile()); + $this->assertEqual('testing', Settings::get('install_profile'), 'Profile was correctly changed to testing in Settings.php'); + } + +} diff --git a/core/modules/system/src/Tests/Installer/InstallerExistingSettingsNoProfileTest.php b/core/modules/system/src/Tests/Installer/InstallerExistingSettingsNoProfileTest.php index 84f861a751491d7823f71d0a14835d73b47b83cf..95173085ff9ab191017950d3fee3272e5ed1172b 100644 --- a/core/modules/system/src/Tests/Installer/InstallerExistingSettingsNoProfileTest.php +++ b/core/modules/system/src/Tests/Installer/InstallerExistingSettingsNoProfileTest.php @@ -3,7 +3,6 @@ namespace Drupal\system\Tests\Installer; use Drupal\Core\DrupalKernel; -use Drupal\Core\Site\Settings; use Drupal\simpletest\InstallerTestBase; use Drupal\Core\Database\Database; use Symfony\Component\HttpFoundation\Request; @@ -65,7 +64,7 @@ protected function setUpSettings() { public function testInstaller() { $this->assertUrl('user/1'); $this->assertResponse(200); - $this->assertEqual('testing', Settings::get('install_profile')); + $this->assertEqual('testing', \Drupal::installProfile()); } } diff --git a/core/modules/system/src/Tests/Installer/InstallerExistingSettingsTest.php b/core/modules/system/src/Tests/Installer/InstallerExistingSettingsTest.php index 6ecd9fdf34936e1f808e500039285d3cc1ccc5d1..eaca8fa3a4d91d532eae27c8f9f5fd30c80c102c 100644 --- a/core/modules/system/src/Tests/Installer/InstallerExistingSettingsTest.php +++ b/core/modules/system/src/Tests/Installer/InstallerExistingSettingsTest.php @@ -2,6 +2,7 @@ namespace Drupal\system\Tests\Installer; +use Drupal\Core\Site\Settings; use Drupal\simpletest\InstallerTestBase; use Drupal\Core\Database\Database; use Drupal\Core\DrupalKernel; @@ -74,7 +75,8 @@ protected function setUpSettings() { public function testInstaller() { $this->assertUrl('user/1'); $this->assertResponse(200); - $this->assertEqual('testing', drupal_get_profile(), 'Profile was changed from minimal to testing during interactive install.'); + $this->assertEqual('testing', \Drupal::installProfile(), 'Profile was changed from minimal to testing during interactive install.'); + $this->assertEqual('testing', Settings::get('install_profile'), 'Profile was correctly changed to testing in Settings.php'); } } diff --git a/core/modules/system/src/Tests/Installer/MultipleDistributionsProfileTest.php b/core/modules/system/src/Tests/Installer/MultipleDistributionsProfileTest.php new file mode 100644 index 0000000000000000000000000000000000000000..171a0afc317edf9280e4e1b6d5877dd6a7679eff --- /dev/null +++ b/core/modules/system/src/Tests/Installer/MultipleDistributionsProfileTest.php @@ -0,0 +1,91 @@ + 'profile', + 'core' => \Drupal::CORE_COMPATIBILITY, + 'name' => $name . ' profile', + 'distribution' => [ + 'name' => $name, + 'install' => [ + 'theme' => 'bartik', + ], + ], + ]; + // File API functions are not available yet. + $path = $this->siteDirectory . '/profiles/' . $name; + mkdir($path, 0777, TRUE); + file_put_contents("$path/$name.info.yml", Yaml::encode($info)); + } + // Install the first distribution. + $this->profile = 'distribution_one'; + + parent::setUp(); + } + + /** + * {@inheritdoc} + */ + protected function setUpLanguage() { + // Verify that the distribution name appears. + $this->assertRaw('distribution_one'); + // Verify that the requested theme is used. + $this->assertRaw('bartik'); + // Verify that the "Choose profile" step does not appear. + $this->assertNoText('profile'); + + parent::setUpLanguage(); + } + + /** + * {@inheritdoc} + */ + protected function setUpProfile() { + // This step is skipped, because there is a distribution profile. + } + + /** + * Confirms that the installation succeeded. + */ + public function testInstalled() { + $this->assertUrl('user/1'); + $this->assertResponse(200); + // Confirm that we are logged-in after installation. + $this->assertText($this->rootUser->getUsername()); + + // Confirm that Drupal recognizes this distribution as the current profile. + $this->assertEqual(\Drupal::installProfile(), 'distribution_one'); + $this->assertEqual(Settings::get('install_profile'), 'distribution_one', 'The install profile has been written to settings.php.'); + $this->assertEqual($this->config('core.extension')->get('profile'), 'distribution_one', 'The install profile has been written to core.extension configuration.'); + + $this->rebuildContainer(); + $this->pass('Container can be rebuilt as distribution is written to configuration.'); + $this->assertEqual(\Drupal::installProfile(), 'distribution_one'); + } + +} diff --git a/core/modules/system/src/Tests/Update/InstallProfileSystemInstall8300Test.php b/core/modules/system/src/Tests/Update/InstallProfileSystemInstall8300Test.php new file mode 100644 index 0000000000000000000000000000000000000000..f0eabd62ecfb195ef2baa023096c4d69cbcad96e --- /dev/null +++ b/core/modules/system/src/Tests/Update/InstallProfileSystemInstall8300Test.php @@ -0,0 +1,40 @@ +databaseDumpFiles = [ + __DIR__ . '/../../../tests/fixtures/update/drupal-8.bare.standard.php.gz', + ]; + } + + /** + * Ensures that the system_update_8300() runs as expected. + */ + public function testUpdate() { + // Ensure the BC layers work and settings.php and configuration is in the + // expected state before updating. + $this->assertEqual('standard', \Drupal::installProfile()); + $this->assertEqual('standard', Settings::get('install_profile'), 'The install profile has not been written to settings.php.'); + $this->assertFalse($this->config('core.extension')->get('profile'), 'The install profile is not present in core.extension configuration.'); + + $this->runUpdates(); + // Confirm that Drupal recognizes this distribution as the current profile. + $this->assertEqual('standard', \Drupal::installProfile()); + $this->assertEqual('standard', Settings::get('install_profile'), 'The install profile has not been written to settings.php.'); + $this->assertEqual('standard', $this->config('core.extension')->get('profile'), 'The install profile has been written to core.extension configuration.'); + } + +} diff --git a/core/modules/system/system.install b/core/modules/system/system.install index f1fcfba24870fca5de52d0c026dae6807b9d9633..ca9781c6072b668be64a9310590cc688957633ff 100644 --- a/core/modules/system/system.install +++ b/core/modules/system/system.install @@ -1799,6 +1799,15 @@ function system_update_8300() { ->save(TRUE); } +/** + * Add install profile to core.extension configuration. + */ +function system_update_8301() { + \Drupal::configFactory()->getEditable('core.extension') + ->set('profile', \Drupal::installProfile()) + ->save(); +} + /** * @} End of "addtogroup updates-8.3.0". */ diff --git a/core/tests/Drupal/KernelTests/Core/Bootstrap/GetFilenameTest.php b/core/tests/Drupal/KernelTests/Core/Bootstrap/GetFilenameTest.php index 6cdc4b39ead2529e813bec1f10b9fc38dce4339d..e2108b3cc09cf6a812972fd5d9c5f7e50d9de9a7 100644 --- a/core/tests/Drupal/KernelTests/Core/Bootstrap/GetFilenameTest.php +++ b/core/tests/Drupal/KernelTests/Core/Bootstrap/GetFilenameTest.php @@ -2,6 +2,7 @@ namespace Drupal\KernelTests\Core\Bootstrap; +use Drupal\Core\DependencyInjection\ContainerBuilder; use Drupal\KernelTests\KernelTestBase; /** @@ -16,15 +17,19 @@ class GetFilenameTest extends KernelTestBase { */ public static $modules = ['system']; + /** + * {@inheritdoc} + */ + public function register(ContainerBuilder $container) { + parent::register($container); + // Use the testing install profile. + $container->setParameter('install_profile', 'testing'); + } + /** * Tests that drupal_get_filename() works when the file is not in database. */ function testDrupalGetFilename() { - // drupal_get_profile() is using obtaining the profile from state if the - // install_state global is not set. - global $install_state; - $install_state['parameters']['profile'] = 'testing'; - // Rebuild system.module.files state data. // @todo Remove as part of https://www.drupal.org/node/2186491 drupal_static_reset('system_rebuild_module_data'); diff --git a/core/tests/Drupal/KernelTests/Core/Config/ConfigImporterTest.php b/core/tests/Drupal/KernelTests/Core/Config/ConfigImporterTest.php index 945e994e3ba7665866a25228193ad9a80e6151c8..bde5644ec6afed9002729ae9440c8fbec9e0b688 100644 --- a/core/tests/Drupal/KernelTests/Core/Config/ConfigImporterTest.php +++ b/core/tests/Drupal/KernelTests/Core/Config/ConfigImporterTest.php @@ -668,6 +668,34 @@ public function testInstallProfile() { } } + /** + * Tests install profile validation during configuration import. + * + * @see \Drupal\Core\EventSubscriber\ConfigImportSubscriber + */ + public function testInstallProfileMisMatch() { + $sync = $this->container->get('config.storage.sync'); + + $extensions = $sync->read('core.extension'); + // Change the install profile. + $extensions['profile'] = 'this_will_not_work'; + $sync->write('core.extension', $extensions); + + try { + $this->configImporter->reset()->import(); + $this->fail('ConfigImporterException not thrown; an invalid import was not stopped due to missing dependencies.'); + } + catch (ConfigImporterException $e) { + $this->assertEqual($e->getMessage(), 'There were errors validating the config synchronization.'); + $error_log = $this->configImporter->getErrors(); + // Install profiles can not be changed. Note that KernelTestBase currently + // does not use an install profile. This situation should be impossible + // to get in but site's can removed the install profile setting from + // settings.php so the test is valid. + $this->assertEqual(['Cannot change the install profile from this_will_not_work to once Drupal is installed.'], $error_log); + } + } + /** * Tests config_get_config_directory(). */ diff --git a/core/tests/Drupal/KernelTests/KernelTestBase.php b/core/tests/Drupal/KernelTests/KernelTestBase.php index f10cb14d099f92789fd6c7d93c766b1ceb5a500c..9f6b975d8e8401833fb568290fed5ca93b6a94ac 100644 --- a/core/tests/Drupal/KernelTests/KernelTestBase.php +++ b/core/tests/Drupal/KernelTests/KernelTestBase.php @@ -392,6 +392,7 @@ private function bootKernel() { $this->container->get('config.storage')->write('core.extension', array( 'module' => array_fill_keys($modules, 0), 'theme' => array(), + 'profile' => '', )); $settings = Settings::getAll(); diff --git a/core/tests/Drupal/Tests/Core/Extension/DefaultConfigTest.php b/core/tests/Drupal/Tests/Core/Extension/DefaultConfigTest.php index 879e7a00222182bcf718700a2d72019612a45019..56f913ae250eba8f2e30b0e6bea50b34ad9ba8bf 100644 --- a/core/tests/Drupal/Tests/Core/Extension/DefaultConfigTest.php +++ b/core/tests/Drupal/Tests/Core/Extension/DefaultConfigTest.php @@ -27,6 +27,7 @@ public function testConfigIsEmpty() { $expected = array( 'module' => array(), 'theme' => array(), + 'profile' => '', ); $this->assertEquals($expected, $config); } diff --git a/sites/default/default.settings.php b/sites/default/default.settings.php index 9ee73605217fb2b96068bb5b913772e21f6cdd2b..25d498e6f1d711cadb7f92d25668598f520a713f 100644 --- a/sites/default/default.settings.php +++ b/sites/default/default.settings.php @@ -270,6 +270,11 @@ * by the user. * * @see install_select_profile() + * + * @deprecated in Drupal 8.3.0 and will be removed before Drupal 9.0.0. The + * install profile is written to the core.extension configuration. If a + * service requires the install profile use the 'install_profile' container + * parameter. Functional code can use \Drupal::installProfile(). */ # $settings['install_profile'] = '';