diff --git a/core/core.services.yml b/core/core.services.yml index 598754e10b..707a0fb0e2 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', '%install_profile%'] + arguments: ['@config.factory', '@config.storage', '@config.typed', '@config.manager', '@event_dispatcher', '%install_profile%', '@profile_handler'] lazy: true config.storage: class: Drupal\Core\Config\CachedStorage @@ -522,6 +522,9 @@ services: - { name: module_install.uninstall_validator } arguments: ['@string_translation'] lazy: true + profile_handler: + class: Drupal\Core\Extension\ProfileHandler + arguments: ['@app.root', '@info_parser'] theme_handler: class: Drupal\Core\Extension\ThemeHandler arguments: ['@app.root', '@config.factory', '@module_handler', '@state', '@info_parser'] diff --git a/core/includes/install.core.inc b/core/includes/install.core.inc index 6a6075f7d5..23105a3251 100644 --- a/core/includes/install.core.inc +++ b/core/includes/install.core.inc @@ -452,6 +452,12 @@ function install_begin_request($class_loader, &$install_state) { if (isset($install_state['profile_info']['distribution']['install']['theme'])) { $install_state['theme'] = $install_state['profile_info']['distribution']['install']['theme']; } + // Ensure all profile directories are registered. + $profiles = \Drupal::service('profile_handler')->getProfileInheritance($profile); + $profile_directories = array_map(function($extension) { + return $extension->getPath(); + }, $profiles); + $listing->setProfileDirectories($profile_directories); } // Use the language from the profile configuration, if available, to override @@ -1215,6 +1221,8 @@ function install_select_profile(&$install_state) { * - For non-interactive installations via install_drupal() settings. * - A discovered profile that is a distribution. If multiple profiles are * distributions, then the first discovered profile will be selected. + * If an inherited profile is detected that is a distribution, it will be + * chosen over its base profile. * - Only one visible profile is available. * * @param array $install_state @@ -1237,11 +1245,8 @@ function _install_select_profile(&$install_state) { } } // Check for a distribution profile. - foreach ($install_state['profiles'] as $profile) { - $profile_info = install_profile_info($profile->getName()); - if (!empty($profile_info['distribution'])) { - return $profile->getName(); - } + if ($distribution = \Drupal::service('profile_handler')->selectDistribution(array_keys($install_state['profiles']))) { + return $distribution; } // Get all visible (not hidden) profiles. @@ -1574,7 +1579,10 @@ function install_profile_themes(&$install_state) { * An array of information about the current installation state. */ function install_install_profile(&$install_state) { - \Drupal::service('module_installer')->install([drupal_get_profile()], FALSE); + // Install all the profiles. + $profiles = \Drupal::service('profile_handler')->getProfileInheritance(); + \Drupal::service('module_installer')->install(array_keys($profiles), 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 diff --git a/core/includes/install.inc b/core/includes/install.inc index 3529d51a98..3462250e81 100644 --- a/core/includes/install.inc +++ b/core/includes/install.inc @@ -1047,6 +1047,14 @@ function drupal_check_module($module) { * - install: Optional parameters to override the installer: * - theme: The machine name of a theme to use in the installer instead of * Drupal's default installer theme. + * - base profile: Existence of this key denotes that the installation profile + * depends on a parent installation profile. + * - name: The shortname of the base installation profile. + * - excluded_dependencies: An array of shortnames of other modules that have + * to be excluded from the base profile requirements. This allows e.g. to + * disable a demo module that would be installed by the base profile. + * If there are no excluded_dependencies, a shortcut of "base profile: name" + * can be used. * * Note that this function does an expensive file system scan to get info file * information for dependencies. If you only need information from the info @@ -1073,18 +1081,7 @@ function install_profile_info($profile, $langcode = 'en') { $cache = &drupal_static(__FUNCTION__, []); if (!isset($cache[$profile][$langcode])) { - // Set defaults for module info. - $defaults = [ - 'dependencies' => [], - 'themes' => ['stark'], - 'description' => '', - 'version' => NULL, - 'hidden' => FALSE, - 'php' => DRUPAL_MINIMUM_PHP, - ]; - $profile_file = drupal_get_path('profile', $profile) . "/$profile.info.yml"; - $info = \Drupal::service('info_parser')->parse($profile_file); - $info += $defaults; + $info = \Drupal::service('profile_handler')->getProfileInfo($profile); // drupal_required_modules() includes the current profile as a dependency. // Remove that dependency, since a module cannot depend on itself. diff --git a/core/lib/Drupal/Core/Config/ConfigInstaller.php b/core/lib/Drupal/Core/Config/ConfigInstaller.php index 242d92097e..134a34c535 100644 --- a/core/lib/Drupal/Core/Config/ConfigInstaller.php +++ b/core/lib/Drupal/Core/Config/ConfigInstaller.php @@ -6,6 +6,7 @@ use Drupal\Component\Utility\Unicode; use Drupal\Core\Config\Entity\ConfigDependencyManager; use Drupal\Core\Config\Entity\ConfigEntityDependency; +use Drupal\Core\Extension\ProfileHandlerInterface; use Symfony\Component\EventDispatcher\EventDispatcherInterface; class ConfigInstaller implements ConfigInstallerInterface { @@ -53,6 +54,13 @@ class ConfigInstaller implements ConfigInstallerInterface { protected $sourceStorage; /** + * The profile handler. + * + * @var \Drupal\Core\Extension\ProfileHandlerInterface + */ + protected $profileHandler; + + /** * Is configuration being created as part of a configuration sync. * * @var bool @@ -81,14 +89,17 @@ class ConfigInstaller implements ConfigInstallerInterface { * The event dispatcher. * @param string $install_profile * The name of the currently active installation profile. + * @param \Drupal\Core\Extension\ProfileHandlerInterface $profile_handler + * (optional) The profile handler. */ - public function __construct(ConfigFactoryInterface $config_factory, StorageInterface $active_storage, TypedConfigManagerInterface $typed_config, ConfigManagerInterface $config_manager, EventDispatcherInterface $event_dispatcher, $install_profile) { + public function __construct(ConfigFactoryInterface $config_factory, StorageInterface $active_storage, TypedConfigManagerInterface $typed_config, ConfigManagerInterface $config_manager, EventDispatcherInterface $event_dispatcher, $install_profile, ProfileHandlerInterface $profile_handler = NULL) { $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; + $this->profileHandler = $profile_handler ?: \Drupal::service('profile_handler'); } /** @@ -471,7 +482,8 @@ public function checkConfigurationToInstall($type, $name) { // 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()) { + $profiles = $this->profileHandler->getProfileInheritance(); + if (!isset($profiles[$name])) { // 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. diff --git a/core/lib/Drupal/Core/Config/ExtensionInstallStorage.php b/core/lib/Drupal/Core/Config/ExtensionInstallStorage.php index 9103ab8c07..c3a026b5a4 100644 --- a/core/lib/Drupal/Core/Config/ExtensionInstallStorage.php +++ b/core/lib/Drupal/Core/Config/ExtensionInstallStorage.php @@ -3,6 +3,7 @@ namespace Drupal\Core\Config; use Drupal\Core\Extension\ExtensionDiscovery; +use Drupal\Core\Extension\ProfileHandlerInterface; /** * Storage to access configuration and schema in enabled extensions. @@ -52,9 +53,11 @@ class ExtensionInstallStorage extends InstallStorage { * (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. + * @param \Drupal\Core\Extension\ProfileHandlerInterface $profile_handler + * (optional) The profile handler. */ - public function __construct(StorageInterface $config_storage, $directory = self::CONFIG_INSTALL_DIRECTORY, $collection = StorageInterface::DEFAULT_COLLECTION, $include_profile = TRUE, $profile = NULL) { - parent::__construct($directory, $collection); + public function __construct(StorageInterface $config_storage, $directory = self::CONFIG_INSTALL_DIRECTORY, $collection = StorageInterface::DEFAULT_COLLECTION, $include_profile = TRUE, $profile = NULL, ProfileHandlerInterface $profile_handler = NULL) { + parent::__construct($directory, $collection, $profile_handler); $this->configStorage = $config_storage; $this->includeProfile = $include_profile; if (is_null($profile)) { @@ -93,19 +96,11 @@ protected function getAllFolders() { $extensions = $this->configStorage->read('core.extension'); // @todo Remove this scan as part of https://www.drupal.org/node/2186491 - $listing = new ExtensionDiscovery(\Drupal::root()); + $listing = new ExtensionDiscovery(\Drupal::root(), TRUE, NULL, NULL, $this->profileHandler); if (!empty($extensions['module'])) { $modules = $extensions['module']; // Remove the install profile as this is handled later. unset($modules[$this->installProfile]); - $profile_list = $listing->scan('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', $this->installProfile, $profile_list[$this->installProfile]->getPathname()); - } $module_list_scan = $listing->scan('module'); $module_list = []; foreach (array_keys($modules) as $module) { @@ -126,18 +121,11 @@ protected function getAllFolders() { } 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. - if ($this->installProfile) { - if (!isset($profile_list)) { - $profile_list = $listing->scan('profile'); - } - if (isset($profile_list[$this->installProfile])) { - $profile_folders = $this->getComponentNames([$profile_list[$this->installProfile]]); - $this->folders = $profile_folders + $this->folders; - } - } + // The install profile (and any parent profiles) 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. + $this->folders += $this->getComponentNames($this->profileHandler->getProfileInheritance($this->installProfile)); } } return $this->folders; diff --git a/core/lib/Drupal/Core/Config/InstallStorage.php b/core/lib/Drupal/Core/Config/InstallStorage.php index c8d189e480..01fb7b6a0f 100644 --- a/core/lib/Drupal/Core/Config/InstallStorage.php +++ b/core/lib/Drupal/Core/Config/InstallStorage.php @@ -4,6 +4,7 @@ use Drupal\Core\Extension\ExtensionDiscovery; use Drupal\Core\Extension\Extension; +use Drupal\Core\Extension\ProfileHandlerInterface; /** * Storage used by the Drupal installer. @@ -48,6 +49,13 @@ class InstallStorage extends FileStorage { protected $directory; /** + * The profile handler used to find additional folders to scan for config. + * + * @var \Drupal\Core\Extension\ProfileHandlerInterface + */ + protected $profileHandler; + + /** * Constructs an InstallStorage object. * * @param string $directory @@ -56,9 +64,14 @@ class InstallStorage extends FileStorage { * @param string $collection * (optional) The collection to store configuration in. Defaults to the * default collection. + * @param \Drupal\Core\Extension\ProfileHandlerInterface $profile_handler + * (optional) The profile handler. */ - public function __construct($directory = self::CONFIG_INSTALL_DIRECTORY, $collection = StorageInterface::DEFAULT_COLLECTION) { + public function __construct($directory = self::CONFIG_INSTALL_DIRECTORY, $collection = StorageInterface::DEFAULT_COLLECTION, ProfileHandlerInterface $profile_handler = NULL) { parent::__construct($directory, $collection); + if (\Drupal::hasService('profile_handler')) { + $this->profileHandler = $profile_handler ?: \Drupal::service('profile_handler'); + } } /** @@ -151,21 +164,12 @@ protected function getAllFolders() { if (!isset($this->folders)) { $this->folders = []; $this->folders += $this->getCoreNames(); + // Get dependent profiles and add the extension components. + $this->folders += $this->getComponentNames($this->profileHandler->getProfileInheritance()); // Perform an ExtensionDiscovery scan as we cannot use drupal_get_path() // yet because the system module may not yet be enabled during install. // @todo Remove as part of https://www.drupal.org/node/2186491 $listing = new ExtensionDiscovery(\Drupal::root()); - if ($profile = drupal_get_profile()) { - $profile_list = $listing->scan('profile'); - if (isset($profile_list[$profile])) { - // 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()); - $this->folders += $this->getComponentNames([$profile_list[$profile]]); - } - } // @todo Remove as part of https://www.drupal.org/node/2186491 $this->folders += $this->getComponentNames($listing->scan('module')); $this->folders += $this->getComponentNames($listing->scan('theme')); diff --git a/core/lib/Drupal/Core/EventSubscriber/ConfigImportSubscriber.php b/core/lib/Drupal/Core/EventSubscriber/ConfigImportSubscriber.php index e948aa584c..1d7ec8ff13 100644 --- a/core/lib/Drupal/Core/EventSubscriber/ConfigImportSubscriber.php +++ b/core/lib/Drupal/Core/EventSubscriber/ConfigImportSubscriber.php @@ -91,12 +91,18 @@ protected function validateModules(ConfigImporter $config_importer) { $config_importer->logError($this->t('Unable to install the %module module since it does not exist.', ['%module' => $module])); } + // Get a list of parent profiles and the main profile. + /* @var $profiles \Drupal\Core\Extension\Extension[] */ + $profiles = \Drupal::service('profile_handler')->getProfileInheritance(); + /* @var $main_profile \Drupal\Core\Extension\Extension */ + $main_profile = end($profiles); + // Ensure that all modules being installed have their dependencies met. $installs = $config_importer->getExtensionChangelist('module', 'install'); foreach ($installs as $module) { $missing_dependencies = []; foreach (array_keys($module_data[$module]->requires) as $required_module) { - if (!isset($core_extension['module'][$required_module])) { + if (!isset($core_extension['module'][$required_module]) && !array_key_exists($module, $profiles)) { $missing_dependencies[] = $module_data[$required_module]->info['name']; } } @@ -111,33 +117,61 @@ protected function validateModules(ConfigImporter $config_importer) { } } - // Get the install profile from the site's configuration. + // Get the active 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; + if (isset($current_core_extension['profile'])) { + // Ensure the active profile is not changing. + if ($current_core_extension['profile'] !== $core_extension['profile']) { + $config_importer->logError($this->t('Cannot change the install profile from %new_profile to %profile once Drupal is installed.', ['%profile' => $current_core_extension['profile'], '%new_profile' => $core_extension['profile']])); + } + } // Ensure that all modules being uninstalled are not required by modules // that will be installed after the import. $uninstalls = $config_importer->getExtensionChangelist('module', 'uninstall'); foreach ($uninstalls as $module) { foreach (array_keys($module_data[$module]->required_by) as $dependent_module) { - if ($module_data[$dependent_module]->status && !in_array($dependent_module, $uninstalls, TRUE) && $dependent_module !== $install_profile) { - $module_name = $module_data[$module]->info['name']; - $dependent_module_name = $module_data[$dependent_module]->info['name']; - $config_importer->logError($this->t('Unable to uninstall the %module module since the %dependent_module module is installed.', ['%module' => $module_name, '%dependent_module' => $dependent_module_name])); + if ($module_data[$dependent_module]->status && !in_array($dependent_module, $uninstalls, TRUE)) { + if (!array_key_exists($dependent_module, $profiles)) { + $module_name = $module_data[$module]->info['name']; + $dependent_module_name = $module_data[$dependent_module]->info['name']; + $config_importer->logError($this->t('Unable to uninstall the %module module since the %dependent_module module is installed.', [ + '%module' => $module_name, + '%dependent_module' => $dependent_module_name + ])); + } } } } - // Ensure that the install profile is not being uninstalled. - 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.', ['%profile' => $profile_name])); - } + // Don't allow profiles to be uninstalled. It's possible for no profile to + // be set yet if the config is being imported during initial site install. + if ($main_profile instanceof \Drupal\core\Extension\Extension) { + if (in_array($main_profile->getName(), $uninstalls, TRUE)) { + // Ensure that the active profile is not being uninstalled. + $profile_name = $main_profile->info['name']; + $config_importer->logError($this->t('Unable to uninstall the %profile profile since it is the main install profile.', ['%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.', ['%profile' => $install_profile, '%new_profile' => $core_extension['profile']])); + if ($profile_uninstalls = array_intersect_key($profiles, array_flip($uninstalls))) { + // Ensure that none of the parent profiles are being uninstalled. + $profile_names = []; + foreach ($profile_uninstalls as $profile) { + if ($profile->getName() !== $main_profile->getName()) { + $profile_names[] = $module_data[$profile->getName()]->info['name']; + } + } + if (!empty($profile_names)) { + $message = $this->formatPlural(count($profile_names), + 'Unable to uninstall the :profile profile since it is a parent of another installed profile.', + 'Unable to uninstall the :profile profiles since they are parents of another installed profile.', + [':profile' => implode(', ', $profile_names)] + ); + $config_importer->logError($message); + } + } } + } /** diff --git a/core/lib/Drupal/Core/Extension/ExtensionDiscovery.php b/core/lib/Drupal/Core/Extension/ExtensionDiscovery.php index b74fc04f1d..6774594069 100644 --- a/core/lib/Drupal/Core/Extension/ExtensionDiscovery.php +++ b/core/lib/Drupal/Core/Extension/ExtensionDiscovery.php @@ -92,6 +92,15 @@ class ExtensionDiscovery { protected $sitePath; /** + * The profile handler. + * + * Used to determine the directories in which we want to scan for modules. + * + * @var \Drupal\Core\Extension\ProfileHandlerInterface|null + */ + protected $profileHandler; + + /** * Constructs a new ExtensionDiscovery object. * * @param string $root @@ -102,12 +111,27 @@ class ExtensionDiscovery { * The available profile directories * @param string $site_path * The path to the site. + * @param \Drupal\Core\Extension\ProfileHandlerInterface $profile_handler + * (optional) The profile handler. */ - public function __construct($root, $use_file_cache = TRUE, $profile_directories = NULL, $site_path = NULL) { + public function __construct($root, $use_file_cache = TRUE, $profile_directories = NULL, $site_path = NULL, ProfileHandlerInterface $profile_handler = NULL) { $this->root = $root; $this->fileCache = $use_file_cache ? FileCacheFactory::get('extension_discovery') : NULL; $this->profileDirectories = $profile_directories; $this->sitePath = $site_path; + + // ExtensionDiscovery can be used without a service container + // (@drupalKernel::moduleData), so create a fallback profile handler if the + // profile_handler service is unavailable. + if ($profile_handler) { + $this->profileHandler = $profile_handler; + } + elseif (\Drupal::hasService('profile_handler')) { + $this->profileHandler = \Drupal::service('profile_handler'); + } + else { + $this->profileHandler = new FallbackProfileHandler($root); + } } /** @@ -241,7 +265,11 @@ public function setProfileDirectoriesFromSettings() { // In case both profile directories contain the same extension, the actual // profile always has precedence. if ($profile) { - $this->profileDirectories[] = drupal_get_path('profile', $profile); + $profiles = $this->profileHandler->getProfileInheritance($profile); + $profile_directories = array_map(function($extension) { + return $extension->getPath(); + }, $profiles); + $this->profileDirectories = array_unique(array_merge($profile_directories, $this->profileDirectories)); } return $this; } diff --git a/core/lib/Drupal/Core/Extension/FallbackProfileHandler.php b/core/lib/Drupal/Core/Extension/FallbackProfileHandler.php new file mode 100644 index 0000000000..c8f18bec0d --- /dev/null +++ b/core/lib/Drupal/Core/Extension/FallbackProfileHandler.php @@ -0,0 +1,77 @@ +root = $root; + } + + /** + * The stored profile info. + * + * @var array[] + */ + protected $profileInfo = []; + + /** + * {@inheritdoc} + */ + public function getProfileInfo($profile) { + if (isset($this->profileInfo[$profile])) { + return $this->profileInfo[$profile]; + } + else { + throw new \InvalidArgumentException('The profile name is invalid.'); + } + } + + /** + * {@inheritdoc} + */ + public function setProfileInfo($profile, array $info) { + $this->profileInfo[$profile] = $info; + } + + /** + * {@inheritdoc} + */ + public function clearCache() { + unset($this->profileInfo); + } + + /** + * {@inheritdoc} + */ + public function getProfileInheritance($profile = NULL) { + $profile_path = drupal_get_path('profile', $profile); + return [ + $profile => new Extension($this->root, 'profile', $profile_path), + ]; + } + + /** + * {@inheritdoc} + */ + public function selectDistribution(array $profile_list) { + return NULL; + } + +} diff --git a/core/lib/Drupal/Core/Extension/ModuleInstaller.php b/core/lib/Drupal/Core/Extension/ModuleInstaller.php index 87924987c2..f3dc6a3c76 100644 --- a/core/lib/Drupal/Core/Extension/ModuleInstaller.php +++ b/core/lib/Drupal/Core/Extension/ModuleInstaller.php @@ -341,7 +341,7 @@ public function uninstall(array $module_list, $uninstall_dependents = TRUE) { if ($uninstall_dependents) { // Add dependent modules to the list. The new modules will be processed as // the while loop continues. - $profile = drupal_get_profile(); + $profiles = \Drupal::service('profile_handler')->getProfileInheritance(); while (list($module) = each($module_list)) { foreach (array_keys($module_data[$module]->required_by) as $dependent) { if (!isset($module_data[$dependent])) { @@ -349,8 +349,8 @@ public function uninstall(array $module_list, $uninstall_dependents = TRUE) { return FALSE; } - // Skip already uninstalled modules. - if (isset($installed_modules[$dependent]) && !isset($module_list[$dependent]) && $dependent != $profile) { + // Skip already uninstalled modules and dependencies of profiles. + if (isset($installed_modules[$dependent]) && !isset($module_list[$dependent]) && (!array_key_exists($dependent, $profiles))) { $module_list[$dependent] = $dependent; } } diff --git a/core/lib/Drupal/Core/Extension/ProfileHandler.php b/core/lib/Drupal/Core/Extension/ProfileHandler.php new file mode 100644 index 0000000000..fa2cfaf110 --- /dev/null +++ b/core/lib/Drupal/Core/Extension/ProfileHandler.php @@ -0,0 +1,302 @@ +root = $root; + $this->infoParser = $info_parser; + $this->extensionDiscovery = $extension_discovery ?: new ExtensionDiscovery($root, TRUE, NULL, NULL, $this); + } + + /** + * Return the full path to a profile. + * + * Wrapper around drupal_get_path. If profile path is not available yet we + * call scan('profile') and prime the cache. + * + * @param string $profile + * The name of the profile. + * + * @return string + * The full path to the profile. + */ + protected function getProfilePath($profile) { + // Check to see if system_rebuild_module_data cache is primed. + // @todo Remove as part of https://www.drupal.org/node/2186491. + $modules_cache = &drupal_static('system_rebuild_module_data'); + if (!$this->scanCache && !isset($modules_cache)) { + // Find installation profiles. This needs to happen before performing a + // module scan as the module scan requires knowing what the active profile + // is. + // @todo Remove as part of https://www.drupal.org/node/2186491. + $profiles = $this->extensionDiscovery->scan('profile'); + foreach ($profiles as $profile_name => $extension) { + // 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_name, $extension->getPathname()); + } + $this->scanCache = TRUE; + } + return drupal_get_path('profile', $profile); + } + + /** + * {@inheritdoc} + */ + public function getProfileInfo($profile) { + // Even though info_parser caches the info array, we need to also cache + // this since it is recursive. + if (!isset($this->infoCache[$profile])) { + // Set defaults for profile info. + $defaults = [ + 'dependencies' => [], + 'themes' => ['stark'], + 'description' => '', + 'version' => NULL, + 'hidden' => FALSE, + 'php' => DRUPAL_MINIMUM_PHP, + 'base profile' => [ + 'name' => '', + 'excluded_dependencies' => [], + 'excluded_themes' => [], + ], + ]; + + $profile_path = $this->getProfilePath($profile); + $profile_file = $profile_path . "/$profile.info.yml"; + $info = $this->infoParser->parse($profile_file) + $defaults; + + // Normalize any base profile info. + if (is_string($info['base profile'])) { + $info['base profile'] = [ + 'name' => $info['base profile'], + 'excluded_dependencies' => [], + 'excluded_themes' => [], + ]; + } + + $profile_list = []; + // Get the base profile dependencies. + if ($base_profile_name = $info['base profile']['name']) { + $base_info = $this->getProfileInfo($base_profile_name); + $profile_list += $base_info['profile_list']; + + // Ensure all dependencies are cleanly merged. + $info['dependencies'] = array_merge($info['dependencies'], $base_info['dependencies']); + + if (isset($info['base profile']['excluded_dependencies'])) { + // Apply excluded dependencies. + $info['dependencies'] = array_diff($info['dependencies'], $info['base profile']['excluded_dependencies']); + } + // Ensure there's no circular dependency. + $info['dependencies'] = array_diff($info['dependencies'], [$profile]); + + // Ensure all themes are cleanly merged. + $info['themes'] = array_unique(array_merge($info['themes'], $base_info['themes'])); + if (isset($info['base profile']['excluded_themes'])) { + // Apply excluded themes. + $info['themes'] = array_diff($info['themes'], $info['base profile']['excluded_themes']); + } + // Ensure each theme is listed only once. + $info['themes'] = array_unique($info['themes']); + + } + $profile_list[$profile] = $profile; + $info['profile_list'] = $profile_list; + + // Ensure the same dependency notation as in modules can be used. + array_walk($info['dependencies'], function(&$dependency) { + $dependency = ModuleHandler::parseDependency($dependency)['name']; + }); + + // Installation profiles are hidden by default, unless explicitly + // specified otherwise in the .info.yml file. + $info['hidden'] = isset($info['hidden']) ? $info['hidden'] : TRUE; + + $this->infoCache[$profile] = $info; + } + return $this->infoCache[$profile]; + } + + /** + * {@inheritdoc} + */ + public function setProfileInfo($profile, array $info) { + $this->infoCache[$profile] = $info; + // Also unset the cached profile extension so the updated info will + // be picked up. + unset($this->profilesWithParentsCache[$profile]); + } + + /** + * {@inheritdoc} + */ + public function clearCache() { + $this->profilesWithParentsCache = []; + $this->infoCache = []; + } + + /** + * Create an Extension object for a profile. + * + * @param string $profile + * The name of the profile. + * + * @return \Drupal\Core\Extension\Extension + * The extension object for the profile + * Properties added to extension: + * info: The parsed info.yml data. + * origin: The directory origin as used in ExtensionDiscovery. + */ + protected function getProfileExtension($profile) { + $profile_info = $this->getProfileInfo($profile); + + $type = $profile_info['type']; + $profile_path = $this->getProfilePath($profile); + $profile_file = $profile_path . "/$profile.info.yml"; + $filename = file_exists($profile_path . "/$profile.$type") ? "$profile.$type" : NULL; + $extension = new Extension($this->root, $type, $profile_file, $filename); + + $extension->info = $profile_info; + $extension->origin = ''; + + return $extension; + } + + /** + * Get a list of dependent profile names. + * + * @param string $profile + * Name of profile. + * + * @return string[] + * An associative array of profile names, keyed by profile name + * in descending order of their dependencies (parent profiles first, main + * profile last). + */ + protected function getProfileList($profile) { + $profile_info = $this->getProfileInfo($profile); + return $profile_info['profile_list']; + } + + /** + * {@inheritdoc} + */ + public function getProfileInheritance($profile = NULL) { + if (empty($profile)) { + $profile = drupal_get_profile(); + } + if (!isset($this->profilesWithParentsCache[$profile])) { + $profiles = []; + // Check if a valid profile name was given. + if (!empty($profile)) { + $list = $this->getProfileList($profile); + + // Starting weight for profiles ensures their hooks run last. + $weight = 1000; + + // Loop through profile list and create Extension objects. + $profiles = []; + foreach ($list as $profile_name) { + $extension = $this->getProfileExtension($profile_name); + $extension->weight = $weight; + $weight++; + $profiles[$profile_name] = $extension; + } + } + $this->profilesWithParentsCache[$profile] = $profiles; + } + return $this->profilesWithParentsCache[$profile]; + } + + /** + * {@inheritdoc} + */ + public function selectDistribution(array $profile_list) { + // First, find all profiles marked as distributions. + $distributions = []; + foreach ($profile_list as $profile_name) { + $profile_info = $this->getProfileInfo($profile_name); + if (!empty($profile_info['distribution'])) { + $distributions[$profile_name] = $profile_name; + } + } + // Remove any base profiles. + foreach ($profile_list as $profile_name) { + $profile_info = $this->getProfileInfo($profile_name); + if ($base_profile = $profile_info['base profile']['name']) { + unset($distributions[$base_profile]); + } + } + return !empty($distributions) ? current($distributions) : NULL; + } + +} diff --git a/core/lib/Drupal/Core/Extension/ProfileHandlerInterface.php b/core/lib/Drupal/Core/Extension/ProfileHandlerInterface.php new file mode 100644 index 0000000000..e2ce9b98fd --- /dev/null +++ b/core/lib/Drupal/Core/Extension/ProfileHandlerInterface.php @@ -0,0 +1,82 @@ +webUser = $this->drupalCreateUser(['synchronize configuration']); + $this->drupalLogin($this->webUser); + $this->copyConfig($this->container->get('config.storage'), $this->container->get('config.storage.sync')); + } + + /** + * Tests config importer cannot uninstall parent install profiles and + * dependencies of parent profiles can be uninstalled. + * + * @see \Drupal\Core\EventSubscriber\ConfigImportSubscriber + */ + public function testInstallParentProfileValidation() { + $sync = $this->container->get('config.storage.sync'); + $this->copyConfig($this->container->get('config.storage'), $sync); + $core = $sync->read('core.extension'); + + // Ensure that parent profile can not be uninstalled. + unset($core['module']['testing']); + $sync->write('core.extension', $core); + + $this->drupalPostForm('admin/config/development/configuration', [], t('Import all')); + $this->assertText('The configuration cannot be imported because it failed validation for the following reasons:'); + $this->assertText('Unable to uninstall the Testing profile since it is a parent of another installed profile.'); + + // Uninstall dependencies of parent profile. + $core['module']['testing'] = 0; + unset($core['module']['dynamic_page_cache']); + $sync->write('core.extension', $core); + $sync->deleteAll('dynamic_page_cache.'); + $this->drupalPostForm('admin/config/development/configuration', [], t('Import all')); + $this->assertText('The configuration was imported successfully.'); + $this->rebuildContainer(); + $this->assertFalse(\Drupal::moduleHandler()->moduleExists('dynamic_page_cache'), 'The dynamic_page_cache module has been uninstalled.'); + } + + /** + * Tests config importer cannot uninstall sub-profiles and dependencies of + * sub-profiles can be uninstalled. + * + * @see \Drupal\Core\EventSubscriber\ConfigImportSubscriber + */ + public function testInstallSubProfileValidation() { + $sync = $this->container->get('config.storage.sync'); + $this->copyConfig($this->container->get('config.storage'), $sync); + $core = $sync->read('core.extension'); + + // Ensure install sub-profiles can not be uninstalled. + unset($core['module']['testing_inherited']); + $sync->write('core.extension', $core); + + $this->drupalPostForm('admin/config/development/configuration', [], t('Import all')); + $this->assertText('The configuration cannot be imported because it failed validation for the following reasons:'); + $this->assertText('Unable to uninstall the Testing Inherited profile since it is the main install profile.'); + + // Uninstall dependencies of main profile. + $core['module']['testing_inherited'] = 0; + unset($core['module']['syslog']); + $sync->write('core.extension', $core); + $sync->deleteAll('syslog.'); + $this->drupalPostForm('admin/config/development/configuration', [], t('Import all')); + $this->assertText('The configuration was imported successfully.'); + $this->rebuildContainer(); + $this->assertFalse(\Drupal::moduleHandler()->moduleExists('syslog'), 'The syslog module has been uninstalled.'); + } + +} diff --git a/core/modules/config/tests/src/Functional/ConfigImportInstallProfileTest.php b/core/modules/config/tests/src/Functional/ConfigImportInstallProfileTest.php index 8b0787d6ee..d8d33da0e6 100644 --- a/core/modules/config/tests/src/Functional/ConfigImportInstallProfileTest.php +++ b/core/modules/config/tests/src/Functional/ConfigImportInstallProfileTest.php @@ -56,7 +56,7 @@ public function testInstallProfileValidation() { $this->drupalPostForm('admin/config/development/configuration', [], t('Import all')); $this->assertText('The configuration cannot be imported because it failed validation for the following reasons:'); - $this->assertText('Unable to uninstall the Testing config import profile since it is the install profile.'); + $this->assertText('Unable to uninstall the Testing config import profile since it is the main install profile.'); // Uninstall dependencies of testing_config_import. $core['module']['testing_config_import'] = 0; diff --git a/core/modules/system/src/Form/ModulesUninstallForm.php b/core/modules/system/src/Form/ModulesUninstallForm.php index 80348c59f1..2cff995d3a 100644 --- a/core/modules/system/src/Form/ModulesUninstallForm.php +++ b/core/modules/system/src/Form/ModulesUninstallForm.php @@ -114,12 +114,15 @@ public function buildForm(array $form, FormStateInterface $form_state) { return $form; } - $profile = drupal_get_profile(); + $profiles = \Drupal::service('profile_handler')->getProfileInheritance(); // Sort all modules by their name. uasort($uninstallable, 'system_sort_modules_by_info_name'); $validation_reasons = $this->moduleInstaller->validateUninstall(array_keys($uninstallable)); + // Remove any profiles from the list. + $uninstallable = array_diff_key($uninstallable, $profiles); + $form['uninstall'] = ['#tree' => TRUE]; foreach ($uninstallable as $module_key => $module) { $name = $module->info['name'] ?: $module->getName(); @@ -140,10 +143,10 @@ public function buildForm(array $form, FormStateInterface $form_state) { $form['uninstall'][$module->getName()]['#disabled'] = TRUE; } // All modules which depend on this one must be uninstalled first, before - // we can allow this module to be uninstalled. (The installation profile - // is excluded from this list.) + // we can allow this module to be uninstalled. (Installation profiles are + // excluded from this list.) foreach (array_keys($module->required_by) as $dependent) { - if ($dependent != $profile && drupal_get_installed_schema_version($dependent) != SCHEMA_UNINSTALLED) { + if (!in_array($dependent, array_keys($profiles)) && drupal_get_installed_schema_version($dependent) != SCHEMA_UNINSTALLED) { $name = isset($modules[$dependent]->info['name']) ? $modules[$dependent]->info['name'] : $dependent; $form['modules'][$module->getName()]['#required_by'][] = $name; $form['uninstall'][$module->getName()]['#disabled'] = TRUE; diff --git a/core/modules/system/system.module b/core/modules/system/system.module index ef6e3d5d55..1cb8092f2a 100644 --- a/core/modules/system/system.module +++ b/core/modules/system/system.module @@ -1002,27 +1002,13 @@ function system_get_info($type, $name = NULL) { function _system_rebuild_module_data() { $listing = new ExtensionDiscovery(\Drupal::root()); - // Find installation profiles. This needs to happen before performing a - // module scan as the module scan requires knowing what the active profile is. - // @todo Remove as part of https://www.drupal.org/node/2186491. - $profiles = $listing->scan('profile'); - $profile = drupal_get_profile(); - if ($profile && isset($profiles[$profile])) { - // 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, $profiles[$profile]->getPathname()); - } - // Find modules. $modules = $listing->scan('module'); - // Include the installation profile in modules that are loaded. - if ($profile) { - $modules[$profile] = $profiles[$profile]; - // Installation profile hooks are always executed last. - $modules[$profile]->weight = 1000; - } + + // Find profiles. + /** @var \Drupal\Core\Extension\ProfileHandlerInterface $profile_handler */ + $profile_handler = \Drupal::service('profile_handler'); + $modules = array_merge($modules, $profile_handler->getProfileInheritance()); // Set defaults for module info. $defaults = [ @@ -1036,7 +1022,14 @@ function _system_rebuild_module_data() { // Read info files for each module. foreach ($modules as $key => $module) { // Look for the info file. - $module->info = \Drupal::service('info_parser')->parse($module->getPathname()); + // @todo On the longrun we should leverage the extension lists services, + // see https://www.drupal.org/node/2208429. + if ($module->getType() === 'profile') { + $module->info = $profile_handler->getProfileInfo($module->getName()); + } + else { + $module->info = \Drupal::service('info_parser')->parse($module->getPathname()); + } // Add the info file modification time, so it becomes available for // contributed modules to use for ordering module lists. @@ -1045,12 +1038,6 @@ function _system_rebuild_module_data() { // Merge in defaults and save. $modules[$key]->info = $module->info + $defaults; - // Installation profiles are hidden by default, unless explicitly specified - // otherwise in the .info.yml file. - if ($key == $profile && !isset($modules[$key]->info['hidden'])) { - $modules[$key]->info['hidden'] = TRUE; - } - // Invoke hook_system_info_alter() to give installed modules a chance to // modify the data in the .info.yml files if necessary. // @todo Remove $type argument, obsolete with $module->getType(). @@ -1064,17 +1051,19 @@ function _system_rebuild_module_data() { _system_rebuild_module_data_ensure_required($module, $modules); } - if ($profile && isset($modules[$profile])) { - // The installation profile is required, if it's a valid module. - $modules[$profile]->info['required'] = TRUE; - // Add a default distribution name if the profile did not provide one. - // @see install_profile_info() - // @see drupal_install_profile_distribution_name() - if (!isset($modules[$profile]->info['distribution']['name'])) { - $modules[$profile]->info['distribution']['name'] = 'Drupal'; + $profiles = $profile_handler->getProfileInheritance(); + foreach ($profiles as $profile_name => $profile) { + if (isset($modules[$profile_name])) { + // The installation profile is required, if it's a valid module. + $modules[$profile_name]->info['required'] = TRUE; + // Add a default distribution name if the profile did not provide one. + // @see install_profile_info() + // @see drupal_install_profile_distribution_name() + if (!isset($modules[$profile_name]->info['distribution']['name'])) { + $modules[$profile_name]->info['distribution']['name'] = 'Drupal'; + } } } - return $modules; } diff --git a/core/modules/system/tests/modules/form_test/src/Form/FormTestElementSubmitForm.php b/core/modules/system/tests/modules/form_test/src/Form/FormTestElementSubmitForm.php new file mode 100644 index 0000000000..e9185e92b7 --- /dev/null +++ b/core/modules/system/tests/modules/form_test/src/Form/FormTestElementSubmitForm.php @@ -0,0 +1,102 @@ + 'textfield', + '#title' => 'Name', + '#default_value' => '', + '#element_validate' => [[$object, 'validateName']], + '#element_submit' => [[$object, 'submitName']], + ]; + $form['group_1'] = [ + '#type' => 'details', + '#tree' => TRUE, + ]; + $form['group_1']['name_1'] = [ + '#type' => 'textfield', + '#title' => 'Name 1', + '#default_value' => '', + '#element_validate' => [[$object, 'validateName']], + '#element_submit' => [[$object, 'submitName1']], + ]; + $form['group_1']['name_2'] = [ + '#type' => 'textfield', + '#title' => 'Name 2', + '#default_value' => '', + '#element_validate' => [[$object, 'validateName']], + '#element_submit' => [[$object, 'submitName2']], + ]; + $form['group_1']['submit_1'] = [ + '#type' => 'submit', + '#value' => 'Save group 1', + '#limit_element_submit' => [['group_1']], + '#submit' => [[$this, 'submitForm1']], + ]; + $form['group_2'] = [ + '#type' => 'details', + '#tree' => TRUE, + ]; + $form['group_2']['name_3'] = [ + '#type' => 'textfield', + '#title' => 'Name 3', + '#default_value' => '', + '#element_validate' => [[$object, 'validateName']], + '#element_submit' => [[$object, 'submitName3']], + ]; + + $form['submit'] = [ + '#type' => 'submit', + '#value' => 'Save', + ]; + + return $form; + } + + /** + * {@inheritdoc} + */ + public function validateForm(array &$form, FormStateInterface $form_state) { + } + + /** + * {@inheritdoc} + */ + public function submitForm(array &$form, FormStateInterface $form_state) { + // Output the method is executed. + drupal_set_message(t('Executed @method.', ['@method' => __METHOD__])); + } + + /** + * {@inheritdoc} + */ + public function submitForm1(array &$form, FormStateInterface $form_state) { + // Output the method is executed. + drupal_set_message(t('Executed @method.', ['@method' => __METHOD__])); + } + +} diff --git a/core/modules/system/tests/src/Functional/Form/ElementSubmitTest.php b/core/modules/system/tests/src/Functional/Form/ElementSubmitTest.php new file mode 100644 index 0000000000..05b569eb6e --- /dev/null +++ b/core/modules/system/tests/src/Functional/Form/ElementSubmitTest.php @@ -0,0 +1,57 @@ +drupalGet('form-test/element-submit'); + $edit = [ + 'name' => 'element_submit', + 'group_1[name_1]' => 'element_submit_1', + 'group_1[name_2]' => 'element_submit_2', + 'group_2[name_3]' => 'element_submit_3', + ]; + $this->drupalPostForm(NULL, $edit, 'Save'); + + $assert = $this->assertSession(); + $assert->pageTextContains('element_submit triggered'); + $assert->pageTextContains('element_submit group_1:name_1 triggered'); + $assert->pageTextContains('element_submit group_1:name_2 triggered'); + $assert->pageTextContains('element_submit group_2:name_3 triggered'); + $assert->pageTextContains('Executed Drupal\form_test\Form\FormTestElementSubmitForm::submitForm.'); + $assert->pageTextNotContains('Executed Drupal\form_test\Form\FormTestElementSubmitForm::submitForm1.'); + + $edit = [ + 'name' => 'element_submit', + 'group_1[name_1]' => 'element_submit_1', + 'group_1[name_2]' => 'element_submit_2', + 'group_2[name_3]' => 'element_submit_3', + ]; + $this->drupalPostForm(NULL, $edit, 'Save group 1'); + + $assert = $this->assertSession(); + $assert->pageTextNotContains('element_submit triggered'); + $assert->pageTextContains('element_submit group_1:name_1 triggered'); + $assert->pageTextContains('element_submit group_1:name_2 triggered'); + $assert->pageTextNotContains('element_submit group_2:name_3 triggered'); + $assert->pageTextNotContains('Executed Drupal\form_test\Form\FormTestElementSubmitForm::submitForm.'); + $assert->pageTextContains('Executed Drupal\form_test\Form\FormTestElementSubmitForm::submitForm1.'); + } + +} diff --git a/core/profiles/testing_inherited/config/install/block.block.stable_login.yml b/core/profiles/testing_inherited/config/install/block.block.stable_login.yml new file mode 100644 index 0000000000..3650c6c41a --- /dev/null +++ b/core/profiles/testing_inherited/config/install/block.block.stable_login.yml @@ -0,0 +1,19 @@ +langcode: en +status: true +dependencies: + module: + - user + theme: + - stable +id: stable_login +theme: stable +region: sidebar_first +weight: 0 +provider: null +plugin: user_login_block +settings: + id: user_login_block + label: 'User login' + provider: user + label_display: visible +visibility: { } diff --git a/core/profiles/testing_inherited/config/install/system.theme.yml b/core/profiles/testing_inherited/config/install/system.theme.yml new file mode 100644 index 0000000000..67aeeeeac7 --- /dev/null +++ b/core/profiles/testing_inherited/config/install/system.theme.yml @@ -0,0 +1,2 @@ +# @todo: Remove this file in https://www.drupal.org/node/2352949 +default: stable diff --git a/core/profiles/testing_inherited/testing_inherited.info.yml b/core/profiles/testing_inherited/testing_inherited.info.yml new file mode 100644 index 0000000000..d76332ddee --- /dev/null +++ b/core/profiles/testing_inherited/testing_inherited.info.yml @@ -0,0 +1,21 @@ +name: Testing Inherited +type: profile +description: 'Profile for testing base profile inheritance.' +version: VERSION +core: 8.x +hidden: true + +base profile: + name: testing + excluded_dependencies: + - page_cache + excluded_themes: + - classy + +dependencies: + - block + - config + - syslog + +themes: + - stable diff --git a/core/profiles/testing_inherited/tests/src/Functional/InheritedProfileTest.php b/core/profiles/testing_inherited/tests/src/Functional/InheritedProfileTest.php new file mode 100644 index 0000000000..dba4580ab5 --- /dev/null +++ b/core/profiles/testing_inherited/tests/src/Functional/InheritedProfileTest.php @@ -0,0 +1,40 @@ +assertInstanceOf(BlockInterface::class, Block::load('stable_login')); + + // Check that stable is the default theme. + $this->assertEquals('stable', $this->config('system.theme')->get('default')); + + // Check the excluded_dependencies flag on installation profiles. + $this->assertTrue(\Drupal::moduleHandler()->moduleExists('config')); + $this->assertFalse(\Drupal::moduleHandler()->moduleExists('page_cache')); + + // Check that all themes were installed, except excluded ones. + $this->assertTrue(\Drupal::service('theme_handler')->themeExists('stable')); + $this->assertFalse(\Drupal::service('theme_handler')->themeExists('classy')); + } + +} diff --git a/core/profiles/testing_subsubprofile/testing_subsubprofile.info.yml b/core/profiles/testing_subsubprofile/testing_subsubprofile.info.yml new file mode 100644 index 0000000000..74c0b8cf58 --- /dev/null +++ b/core/profiles/testing_subsubprofile/testing_subsubprofile.info.yml @@ -0,0 +1,14 @@ +name: Testing SubSubProfile +type: profile +description: 'Profile for testing deep profile inheritance.' +version: VERSION +core: 8.x +hidden: true + +base profile: + name: testing_inherited + excluded_dependencies: + - config + +dependencies: + - page_cache diff --git a/core/profiles/testing_subsubprofile/tests/src/Functional/DeepInheritedProfileTest.php b/core/profiles/testing_subsubprofile/tests/src/Functional/DeepInheritedProfileTest.php new file mode 100644 index 0000000000..6933de6214 --- /dev/null +++ b/core/profiles/testing_subsubprofile/tests/src/Functional/DeepInheritedProfileTest.php @@ -0,0 +1,35 @@ +assertEquals('stable', $this->config('system.theme')->get('default')); + + // page_cache was enabled in main profile, disabled in parent and enabled + // in this profile. + $this->assertTrue(\Drupal::moduleHandler()->moduleExists('page_cache')); + // block was enabled in parent profile. + $this->assertTrue(\Drupal::moduleHandler()->moduleExists('block')); + // config was enabled in parent profile and disabled in this. + $this->assertFalse(\Drupal::moduleHandler()->moduleExists('config')); + } + +} diff --git a/core/tests/Drupal/KernelTests/Core/Extension/ProfileHandlerTest.php b/core/tests/Drupal/KernelTests/Core/Extension/ProfileHandlerTest.php new file mode 100644 index 0000000000..92d553db9f --- /dev/null +++ b/core/tests/Drupal/KernelTests/Core/Extension/ProfileHandlerTest.php @@ -0,0 +1,139 @@ +container->get('profile_handler'); + $info = $profile_handler->getProfileInfo('testing_inherited'); + $this->assertNotEmpty($info); + $this->assertEquals($info['name'], 'Testing Inherited'); + $this->assertEquals($info['base profile']['name'], 'testing'); + $this->assertEquals($info['base profile']['excluded_dependencies'], ['page_cache']); + $this->assertTrue(in_array('config', $info['dependencies'], 'config should be found in dependencies')); + $this->assertFalse(in_array('page_cache', $info['dependencies'], 'page_cache should not be found in dependencies')); + $this->assertTrue($info['hidden'], 'Profiles should be hidden'); + $this->assertNotEmpty($info['profile_list']); + $profile_list = $info['profile_list']; + // Testing order of profile list. + $this->assertEquals($profile_list, [ + 'testing' => 'testing', + 'testing_inherited' => 'testing_inherited' + ]); + + // Test that profiles without any base return normalized info. + $info = $profile_handler->getProfileInfo('minimal'); + $this->assertInternalType('array', $info['base profile']); + + $this->assertArrayHasKey('name', $info['base profile']); + $this->assertEmpty($info['base profile']['name']); + + $this->assertArrayHasKey('excluded_dependencies', $info['base profile']); + $this->assertInternalType('array', $info['base profile']['excluded_dependencies']); + $this->assertEmpty($info['base profile']['excluded_dependencies']); + + $this->assertArrayHasKey('excluded_themes', $info['base profile']); + $this->assertInternalType('array', $info['base profile']['excluded_themes']); + $this->assertEmpty($info['base profile']['excluded_themes']); + + // Tests three levels profile inheritance. + $info = $profile_handler->getProfileInfo('testing_subsubprofile'); + $this->assertEquals($info['base profile']['name'], 'testing_inherited'); + $this->assertEquals($info['profile_list'], [ + 'testing' => 'testing', + 'testing_inherited' => 'testing_inherited', + 'testing_subsubprofile' => 'testing_subsubprofile', + ]); + } + + /** + * Tests getting profile dependency list. + * + * @covers ::getProfileInheritance + */ + public function testGetProfileInheritance() { + $profile_handler = $this->container->get('profile_handler'); + + $profiles = $profile_handler->getProfileInheritance('testing'); + $this->assertCount(1, $profiles); + + $profiles = $profile_handler->getProfileInheritance('testing_inherited'); + $this->assertCount(2, $profiles); + + $profiles = $profile_handler->getProfileInheritance('testing_subsubprofile'); + $this->assertCount(3, $profiles); + + $first_profile = current($profiles); + $this->assertEquals(get_class($first_profile), 'Drupal\Core\Extension\Extension'); + $this->assertEquals($first_profile->getName(), 'testing'); + $this->assertEquals($first_profile->weight, 1000); + $this->assertObjectHasAttribute('origin', $first_profile); + + $second_profile = next($profiles); + $this->assertEquals(get_class($second_profile), 'Drupal\Core\Extension\Extension'); + $this->assertEquals($second_profile->getName(), 'testing_inherited'); + $this->assertEquals($second_profile->weight, 1001); + $this->assertObjectHasAttribute('origin', $second_profile); + + $third_profile = next($profiles); + $this->assertEquals(get_class($third_profile), 'Drupal\Core\Extension\Extension'); + $this->assertEquals($third_profile->getName(), 'testing_subsubprofile'); + $this->assertEquals($third_profile->weight, 1002); + $this->assertObjectHasAttribute('origin', $third_profile); + } + + /** + * @covers ::selectDistribution + * @covers ::setProfileInfo + */ + public function testSelectDistribution() { + /** @var \Drupal\Core\Extension\ProfileHandler $profile_handler */ + $profile_handler = $this->container->get('profile_handler'); + $profiles = ['testing', 'testing_inherited']; + $base_info = $profile_handler->getProfileInfo('minimal'); + $profile_info = $profile_handler->getProfileInfo('testing_inherited'); + + // Neither profile has distribution set. + $distribution = $profile_handler->selectDistribution($profiles); + $this->assertEmpty($distribution, 'No distribution should be selected'); + + // Set base profile distribution. + $base_info['distribution']['name'] = 'Minimal'; + $profile_handler->setProfileInfo('minimal', $base_info); + // Base profile distribution should not be selected. + $distribution = $profile_handler->selectDistribution($profiles); + $this->assertEmpty($distribution, 'Base profile distribution should not be selected'); + + // Set main profile distribution. + $profile_info['distribution']['name'] = 'Testing Inherited'; + $profile_handler->setProfileInfo('testing_inherited', $profile_info); + // Main profile distribution should be selected. + $distribution = $profile_handler->selectDistribution($profiles); + $this->assertEquals($distribution, 'testing_inherited'); + } + +}