diff --git a/core/core.services.yml b/core/core.services.yml index 0694089def..59a51bda3f 100644 --- a/core/core.services.yml +++ b/core/core.services.yml @@ -550,7 +550,7 @@ services: lazy: true theme_handler: class: Drupal\Core\Extension\ThemeHandler - arguments: ['@app.root', '@config.factory', '@extension.list.theme'] + arguments: ['@app.root', '@config.factory', '@extension.list.theme', '@state'] theme_installer: class: Drupal\Core\Extension\ThemeInstaller arguments: ['@theme_handler', '@config.factory', '@config.installer', '@module_handler', '@config.manager', '@asset.css.collection_optimizer', '@router.builder', '@logger.channel.default', '@state', '@module_installer'] diff --git a/core/lib/Drupal/Core/Extension/ModuleHandler.php b/core/lib/Drupal/Core/Extension/ModuleHandler.php index 3b60d528bf..3639d48dbc 100644 --- a/core/lib/Drupal/Core/Extension/ModuleHandler.php +++ b/core/lib/Drupal/Core/Extension/ModuleHandler.php @@ -221,6 +221,17 @@ protected function add($type, $name, $path) { * {@inheritdoc} */ public function buildModuleDependencies(array $modules) { + $themes = \Drupal::state()->get('theme.list', []); + $modules_required_by_themes = []; + foreach ($themes as $theme_name => $theme) { + if (!empty($theme->info['dependencies'])) { + foreach ($theme->info['dependencies'] as $dependency) { + if (isset($modules[$dependency]) && $modules[$dependency]->getType() === 'module') { + $modules_required_by_themes[$dependency][$theme_name] = $theme; + } + } + } + } foreach ($modules as $module) { $graph[$module->getName()]['edges'] = []; if (isset($module->info['dependencies']) && is_array($module->info['dependencies'])) { @@ -236,6 +247,13 @@ public function buildModuleDependencies(array $modules) { $modules[$module_name]->required_by = isset($data['reverse_paths']) ? $data['reverse_paths'] : []; $modules[$module_name]->requires = isset($data['paths']) ? $data['paths'] : []; $modules[$module_name]->sort = $data['weight']; + // This prevents uninstalling a module required by a theme via drush, + // but the output of the command is not great at the moment. + if (isset($modules_required_by_themes[$module_name])) { + foreach ($modules_required_by_themes[$module_name] as $theme_name => $theme) { + $modules[$module_name]->required_by[$theme_name] = $theme; + } + } } return $modules; } diff --git a/core/lib/Drupal/Core/Extension/ModuleInstaller.php b/core/lib/Drupal/Core/Extension/ModuleInstaller.php index 62dbb6c9b0..413187e5a0 100644 --- a/core/lib/Drupal/Core/Extension/ModuleInstaller.php +++ b/core/lib/Drupal/Core/Extension/ModuleInstaller.php @@ -355,6 +355,7 @@ public function uninstall(array $module_list, $uninstall_dependents = TRUE) { // Get all module data so we can find dependencies and sort. $module_data = \Drupal::service('extension.list.module')->getList(); $module_list = $module_list ? array_combine($module_list, $module_list) : []; + $theme_list = \Drupal::state()->get('theme.list', []); if (array_diff_key($module_list, $module_data)) { // One or more of the given modules doesn't exist. return FALSE; @@ -372,7 +373,7 @@ public function uninstall(array $module_list, $uninstall_dependents = TRUE) { // the foreach loop continues. foreach ($module_list as $module => $value) { foreach (array_keys($module_data[$module]->required_by) as $dependent) { - if (!isset($module_data[$dependent])) { + if (!isset($module_data[$dependent]) && !isset($theme_list[$dependent])) { // The dependent module does not exist. return FALSE; } diff --git a/core/lib/Drupal/Core/Extension/ThemeExtensionList.php b/core/lib/Drupal/Core/Extension/ThemeExtensionList.php index 2c35f0535b..a86c12d285 100644 --- a/core/lib/Drupal/Core/Extension/ThemeExtensionList.php +++ b/core/lib/Drupal/Core/Extension/ThemeExtensionList.php @@ -151,7 +151,6 @@ protected function doList() { } $themes[$key]->module_dependencies = isset($theme->base_themes) ? array_diff_key($theme->requires, $theme->base_themes) : $theme->requires; } - return $themes; } diff --git a/core/lib/Drupal/Core/Extension/ThemeHandler.php b/core/lib/Drupal/Core/Extension/ThemeHandler.php index a1f43654ab..d376c7037f 100644 --- a/core/lib/Drupal/Core/Extension/ThemeHandler.php +++ b/core/lib/Drupal/Core/Extension/ThemeHandler.php @@ -5,6 +5,7 @@ use Drupal\Core\Config\ConfigFactoryInterface; use Drupal\Core\Extension\Exception\UninstalledExtensionException; use Drupal\Core\Extension\Exception\UnknownExtensionException; +use Drupal\Core\State\StateInterface; /** * Default theme handler using the config system to store installation statuses. @@ -39,6 +40,13 @@ class ThemeHandler implements ThemeHandlerInterface { */ protected $root; + /** + * The state service. + * + * @var \Drupal\Core\State\StateInterface + */ + protected $state; + /** * Constructs a new ThemeHandler. * @@ -49,10 +57,11 @@ class ThemeHandler implements ThemeHandlerInterface { * @param \Drupal\Core\Extension\ThemeExtensionList $theme_list * A extension discovery instance. */ - public function __construct($root, ConfigFactoryInterface $config_factory, ThemeExtensionList $theme_list) { + public function __construct($root, ConfigFactoryInterface $config_factory, ThemeExtensionList $theme_list, StateInterface $state) { $this->root = $root; $this->configFactory = $config_factory; $this->themeList = $theme_list; + $this->state = $state; } /** @@ -161,7 +170,12 @@ public function reset() { * {@inheritdoc} */ public function rebuildThemeData() { - return $this->themeList->reset()->getList(); + $theme_list = $this->themeList->reset()->getList(); + // Store a list of themes in state so other classes in Drupal\Core\Extension + // can access this information without recursion problems. + $this->state->set('theme.list', $theme_list); + return $theme_list; + // return $this->themeList->reset()->getList(); } /** diff --git a/core/modules/system/src/Controller/SystemController.php b/core/modules/system/src/Controller/SystemController.php index b355b60a4d..4783cc26d4 100644 --- a/core/modules/system/src/Controller/SystemController.php +++ b/core/modules/system/src/Controller/SystemController.php @@ -252,6 +252,8 @@ public function themesPage() { $theme->incompatible_engine = isset($theme->info['engine']) && !isset($theme->owner); // Confirm that module dependencies are available. $theme->incompatible_module = FALSE; + // Confirm that the user has permission to enable modules. + $theme->insufficient_module_permissions = FALSE; } // Check module dependencies. @@ -274,6 +276,12 @@ public function themesPage() { $name = $modules[$dependency]->info['name']; $theme->module_dependencies[$dependency] = $modules[$dependency]->status ? $this->t('@module', ['@module' => $name]) : $this->t('@module (disabled)', ['@module' => $name]); + // Create an additional property that contains only disabled module + // dependencies. This will determine if a confirmation form is needed + // when enabling a theme. + if (!$modules[$dependency]->status) { + $theme->module_dependencies_disabled[$dependency] = $name; + } } } @@ -319,18 +327,47 @@ public function themesPage() { $admin_theme_options[$theme->getName()] = $theme->info['name'] . ($theme->is_experimental ? ' (' . t('Experimental') . ')' : ''); } else { - $theme->operations[] = [ - 'title' => $this->t('Install'), - 'url' => Url::fromRoute('system.theme_install'), - 'query' => $query, - 'attributes' => ['title' => $this->t('Install @theme theme', ['@theme' => $theme->info['name']])], - ]; - $theme->operations[] = [ - 'title' => $this->t('Install and set as default'), - 'url' => Url::fromRoute('system.theme_set_default'), - 'query' => $query, - 'attributes' => ['title' => $this->t('Install @theme as default theme', ['@theme' => $theme->info['name']])], - ]; + if (!empty($theme->module_dependencies_disabled)) { + // If there are module dependencies that are not enabled, the + // operations links direct to a confirmation form that lists the + // modules that will be enabled. + // If the user does not have the `administer modules` permission, + // no operations links will be available. + if ($this->currentUser()->hasPermission('administer modules')) { + $query += [ + 'modules' => array_keys($theme->module_dependencies_disabled), + ]; + $theme->operations[] = [ + 'title' => $this->t('Install'), + 'url' => Url::fromRoute('system.theme_install_confirm'), + 'query' => $query, + 'attributes' => ['title' => $this->t('Install @theme theme', ['@theme' => $theme->info['name']])], + ]; + $theme->operations[] = [ + 'title' => $this->t('Install and set as default'), + 'url' => Url::fromRoute('system.theme_install_confirm'), + 'query' => $query + ['set_default' => TRUE], + 'attributes' => ['title' => $this->t('Install @theme as default theme', ['@theme' => $theme->info['name']])], + ]; + } + else { + $theme->insufficient_module_permissions = TRUE; + } + } + else { + $theme->operations[] = [ + 'title' => $this->t('Install'), + 'url' => Url::fromRoute('system.theme_install'), + 'query' => $query, + 'attributes' => ['title' => $this->t('Install @theme theme', ['@theme' => $theme->info['name']])], + ]; + $theme->operations[] = [ + 'title' => $this->t('Install and set as default'), + 'url' => Url::fromRoute('system.theme_set_default'), + 'query' => $query, + 'attributes' => ['title' => $this->t('Install @theme as default theme', ['@theme' => $theme->info['name']])], + ]; + } } } diff --git a/core/modules/system/src/Form/ModulesListForm.php b/core/modules/system/src/Form/ModulesListForm.php index d6cba92c71..1e22f4b536 100644 --- a/core/modules/system/src/Form/ModulesListForm.php +++ b/core/modules/system/src/Form/ModulesListForm.php @@ -6,6 +6,7 @@ use Drupal\Core\Config\PreExistingConfigException; use Drupal\Core\Config\UnmetDependenciesException; use Drupal\Core\Access\AccessManagerInterface; +use Drupal\Core\Extension\Dependency; use Drupal\Core\Extension\Extension; use Drupal\Core\Extension\InfoParserException; use Drupal\Core\Extension\ModuleExtensionList; @@ -74,6 +75,13 @@ class ModulesListForm extends FormBase { */ protected $moduleExtensionList; + /** + * A array keyed by modules that required by themes. + * + * @var array + */ + protected $modulesRequiredByThemes = []; + /** * {@inheritdoc} */ @@ -166,6 +174,19 @@ public function buildForm(array $form, FormStateInterface $form_state) { $modules = []; } + // Get a list of themes and determine which depend on modules. + $theme_list = \Drupal::service('extension.list.theme'); + $themes = $theme_list->getList(); + foreach ($themes as $theme_name => $theme) { + if (!empty($theme->info['dependencies'])) { + foreach ($theme->info['dependencies'] as $dependency) { + if (isset($modules[$dependency])) { + $this->modulesRequiredByThemes[$dependency][] = $theme; + } + } + } + } + // Iterate over each of the modules. $form['modules']['#tree'] = TRUE; foreach ($modules as $filename => $module) { @@ -217,13 +238,13 @@ public function buildForm(array $form, FormStateInterface $form_state) { * The list of existing modules. * @param string $dependency * The module dependency to check. - * @param array $dependency_object + * @param \Drupal\Core\Extension\Dependency $dependency_object * Dependency object used for comparing version requirement data. * * @return string|null * NULL if compatible, otherwise a string describing the incompatibility. */ - public static function checkDependencyMessage(array $modules, $dependency, $dependency_object) { + public static function checkDependencyMessage(array $modules, $dependency, Dependency $dependency_object) { if (!isset($modules[$dependency])) { return t('@module (missing)', ['@module' => Unicode::ucfirst($dependency)]); } @@ -397,6 +418,21 @@ protected function buildRow(array $modules, Extension $module, $distribution) { } } + // If a module is required by a theme, that theme should be added to the + // `Required by` list. + if (isset($this->modulesRequiredByThemes[$module->getName()])) { + foreach ($this->modulesRequiredByThemes[$module->getName()] as $theme) { + $theme_name = $theme->info['name']; + if ($theme->status === 1) { + $row['#required_by'][$module->getName()] = $this->t('@theme_name', ['@theme_name' => $theme_name]); + $row['enable']['#disabled'] = TRUE; + } + else { + $row['#required_by'][$module->getName()] = $this->t('@theme_name (Theme) (disabled)', ['@theme_name' => $theme_name]); + } + } + } + return $row; } diff --git a/core/modules/system/src/Form/ModulesUninstallForm.php b/core/modules/system/src/Form/ModulesUninstallForm.php index 9370050d15..0e85d8933c 100644 --- a/core/modules/system/src/Form/ModulesUninstallForm.php +++ b/core/modules/system/src/Form/ModulesUninstallForm.php @@ -5,6 +5,7 @@ use Drupal\Core\Extension\ModuleExtensionList; use Drupal\Core\Extension\ModuleHandlerInterface; use Drupal\Core\Extension\ModuleInstallerInterface; +use Drupal\Core\Extension\ThemeExtensionList; use Drupal\Core\Form\FormBase; use Drupal\Core\Form\FormStateInterface; use Drupal\Core\KeyValueStore\KeyValueStoreExpirableInterface; @@ -45,6 +46,13 @@ class ModulesUninstallForm extends FormBase { */ protected $moduleExtensionList; + /** + * The theme extension list. + * + * @var \Drupal\Core\Extension\ThemeExtensionList + */ + protected $themeExtensionList; + /** * {@inheritdoc} */ @@ -53,7 +61,8 @@ public static function create(ContainerInterface $container) { $container->get('module_handler'), $container->get('module_installer'), $container->get('keyvalue.expirable')->get('modules_uninstall'), - $container->get('extension.list.module') + $container->get('extension.list.module'), + $container->get('extension.list.theme') ); } @@ -68,12 +77,19 @@ public static function create(ContainerInterface $container) { * The key value expirable factory. * @param \Drupal\Core\Extension\ModuleExtensionList $extension_list_module * The module extension list. + * @param \Drupal\Core\Extension\ThemeExtensionList $theme_extension_list + * The theme extension list. */ - public function __construct(ModuleHandlerInterface $module_handler, ModuleInstallerInterface $module_installer, KeyValueStoreExpirableInterface $key_value_expirable, ModuleExtensionList $extension_list_module) { + public function __construct(ModuleHandlerInterface $module_handler, ModuleInstallerInterface $module_installer, KeyValueStoreExpirableInterface $key_value_expirable, ModuleExtensionList $extension_list_module, ThemeExtensionList $theme_extension_list = NULL) { $this->moduleExtensionList = $extension_list_module; $this->moduleHandler = $module_handler; $this->moduleInstaller = $module_installer; $this->keyValueExpirable = $key_value_expirable; + if (is_null($theme_extension_list)) { + @trigger_error('The extension.list.theme service must be passed to \Drupal\system\Form\ModulesUninstallForm::__construct(). It was added in Drupal 8.9.0 and will be required before Drupal 9.0.0.', E_USER_DEPRECATED); + $theme_extension_list = \Drupal::service('extension.list.theme'); + } + $this->themeExtensionList = $theme_extension_list; } /** @@ -131,6 +147,19 @@ public function buildForm(array $form, FormStateInterface $form_state) { uasort($uninstallable, 'system_sort_modules_by_info_name'); $validation_reasons = $this->moduleInstaller->validateUninstall(array_keys($uninstallable)); + $themes = $this->themeExtensionList->getList(); + + $modules_required_by_themes = []; + foreach ($themes as $theme_name => $theme) { + if (!empty($theme->info['dependencies'])) { + foreach ($theme->info['dependencies'] as $dependency) { + if (isset($uninstallable[$dependency]) && $theme->status === 1) { + $modules_required_by_themes[$dependency][] = $theme; + } + } + } + } + $form['uninstall'] = ['#tree' => TRUE]; foreach ($uninstallable as $module_key => $module) { $name = $module->info['name'] ?: $module->getName(); @@ -159,6 +188,15 @@ public function buildForm(array $form, FormStateInterface $form_state) { $form['uninstall'][$module->getName()]['#disabled'] = TRUE; } } + // Modules required by an active theme should not be allowed to be + // uninstalled. + if (isset($modules_required_by_themes[$module->getName()])) { + foreach ($modules_required_by_themes[$module->getName()] as $theme) { + $theme_name = $theme->getName(); + $form['modules'][$module->getName()]['#required_by'][] = $this->t('@theme_name (Theme)', ['@theme_name' => $theme_name]); + $form['uninstall'][$module->getName()]['#disabled'] = TRUE; + } + } } $form['#attached']['library'][] = 'system/drupal.system.modules'; diff --git a/core/modules/system/src/Form/ThemeInstallConfirmForm.php b/core/modules/system/src/Form/ThemeInstallConfirmForm.php new file mode 100644 index 0000000000..a6d7be7f89 --- /dev/null +++ b/core/modules/system/src/Form/ThemeInstallConfirmForm.php @@ -0,0 +1,168 @@ +themeExtensionList = $theme_extension_list; + $this->moduleExtensionList = $module_extension_list; + $query = $this->getRequest()->query; + $this->modulesToBeInstalled = $query->get('modules'); + $this->theme = $query->get('theme'); + $this->setDefault = $query->get('set_default'); + $themes = $this->themeExtensionList->getList(); + $this->themeName = $themes[$this->theme]->info['name']; + } + + /** + * {@inheritdoc} + */ + public static function create(ContainerInterface $container) { + return new static( + $container->get('extension.list.module'), + $container->get('extension.list.theme') + ); + } + + /** + * {@inheritdoc} + */ + public function getFormId() { + return 'system_modules_confirm_form'; + } + + /** + * {@inheritdoc} + */ + public function submitForm(array &$form, FormStateInterface $form_state) { + } + + /** + * {@inheritdoc} + */ + public function getDescription() { + $modules = $this->moduleExtensionList->getList(); + $modules_to_be_installed = $this->modulesToBeInstalled; + $theme_name = $this->themeName; + $module_names = array_map(function ($module_key) use ($modules) { + return $modules[$module_key]->info['name']; + }, $modules_to_be_installed); + $module_string = implode(", ", $module_names); + return $this->formatPlural(count($modules_to_be_installed), + 'Enabling the %theme_name theme will also enable the module: %module_string.', + 'Enabling the %theme_name theme will also enable these modules: %module_string.', + [ + '%theme_name' => $theme_name, + '%module_string' => $module_string, + ] + ); + } + + /** + * {@inheritdoc} + */ + public function buildForm(array $form, FormStateInterface $form_state) { + $form = parent::buildForm($form, $form_state); + + // The route is different if the theme is also to be set to default. + $route = $this->setDefault ? 'system.theme_set_default' : 'system.theme_install'; + + // Change to a link with the necessary CSRF token. + $form['actions']['submit'] = [ + '#type' => 'link', + '#url' => Url::fromRoute($route, [], [ + 'query' => ['theme' => $this->theme], + ]), + '#title' => $this->t('Confirm'), + '#attributes' => [ + 'title' => $this->t('Confirm installation of @theme theme', ['@theme' => $this->theme]), + 'class' => ['button', 'button--primary'], + ], + ]; + return $form; + } + + /** + * {@inheritdoc} + */ + public function getQuestion() { + $theme_name = $this->themeName; + if ($this->setDefault) { + return $this->t('Enable @theme_name and make it the default theme', ['@theme_name' => $theme_name]); + } + else { + return $this->t('Enable @theme_name', ['@theme_name' => $theme_name]); + } + } + + /** + * {@inheritdoc} + */ + public function getCancelUrl() { + return Url::fromRoute('system.themes_page'); + } + +} diff --git a/core/modules/system/system.admin.inc b/core/modules/system/system.admin.inc index 4598ea4160..314bc4950d 100644 --- a/core/modules/system/system.admin.inc +++ b/core/modules/system/system.admin.inc @@ -322,6 +322,9 @@ function template_preprocess_system_themes_page(&$variables) { elseif (!empty($theme->incompatible_module)) { $current_theme['incompatible'] = t('This theme requires the listed modules to operate correctly.'); } + elseif (!empty($theme->insufficient_module_permissions)) { + $current_theme['incompatible'] = t('This theme requires the listed modules to operate correctly. They must first be enabled by a user with permissions to do so.'); + } // Build operation links. $current_theme['operations'] = [ diff --git a/core/modules/system/system.routing.yml b/core/modules/system/system.routing.yml index 9591d84d69..5e90a0b3a8 100644 --- a/core/modules/system/system.routing.yml +++ b/core/modules/system/system.routing.yml @@ -305,6 +305,14 @@ system.theme_install: _permission: 'administer themes' _csrf_token: 'TRUE' +system.theme_install_confirm: + path: '/admin/appearance/install/confirm' + defaults: + _form: '\Drupal\system\Form\ThemeInstallConfirmForm' + requirements: + _permission: 'administer themes,administer modules' + _csrf_token: 'TRUE' + system.status: path: '/admin/reports/status' defaults: diff --git a/core/modules/system/tests/src/Functional/Module/VersionTest.php b/core/modules/system/tests/src/Functional/Module/VersionTest.php index 7dde427260..7b325fef8f 100644 --- a/core/modules/system/tests/src/Functional/Module/VersionTest.php +++ b/core/modules/system/tests/src/Functional/Module/VersionTest.php @@ -49,9 +49,6 @@ public function testModuleVersions() { for ($i = 0; $i < $n; $i++) { $this->drupalGet('admin/modules'); $checkbox = $this->xpath('//input[@id="edit-modules-module-test-enable"]'); - if ($dependencies[$i] === 'common_test (2.x)') { - $stop = 'here'; - } $this->assertEqual(!empty($checkbox[0]->getAttribute('disabled')), $i % 2, $dependencies[$i]); } } diff --git a/core/modules/system/tests/src/Functional/Theme/ThemeUiTest.php b/core/modules/system/tests/src/Functional/Theme/ThemeUiTest.php new file mode 100644 index 0000000000..e7e47c4761 --- /dev/null +++ b/core/modules/system/tests/src/Functional/Theme/ThemeUiTest.php @@ -0,0 +1,158 @@ +adminUser = $this->drupalCreateUser([ + 'access administration pages', + 'view the administration theme', + 'administer themes', + 'bypass node access', + 'administer blocks', + 'administer modules', + ]); + $this->drupalLogin($this->adminUser); + } + + /** + * Tests permissions for enabling themes depending on disabled modules. + */ + public function testModulePermissions() { + // Log in as a user without permission to enable modules. + $user = $this->drupalCreateUser([ + 'access administration pages', + 'view the administration theme', + 'administer themes', + 'bypass node access', + 'administer blocks', + ]); + $this->drupalLogin($user); + $this->drupalGet('admin/appearance'); + + // The links to install a theme that would enable modules should be replaced + // by this message. + $this->assertSession()->pageTextContains('This theme requires the listed modules to operate correctly. They must first be enabled by a user with permissions to do so.'); + + // The confirmation page should not be reachable. + $this->drupalGet('admin/appearance/install/confirm?theme=test_theme_depending_on_modules&modules%5B0%5D=test_module_required_by_theme&modules%5B1%5D=test_another_module_required_by_theme'); + $this->assertSession()->statusCodeEquals(404); + + // The install page should not be reachable. + $this->drupalGet('admin/appearance/install?theme=test_theme_depending_on_modules'); + $this->assertSession()->statusCodeEquals(404); + + $user = $this->drupalCreateUser([ + 'access administration pages', + 'view the administration theme', + 'administer themes', + 'bypass node access', + 'administer blocks', + 'administer modules', + ]); + $this->drupalLogin($user); + $this->drupalGet('admin/appearance'); + $this->assertSession()->pageTextNotContains('This theme requires the listed modules to operate correctly. They must first be enabled by a user with permissions to do so.'); + } + + /** + * Tests installing a theme with existing module dependencies. + */ + public function testInstallModuleWithNotInstalledDependencies() { + $this->drupalGet('admin/appearance'); + $themeXpath = '//h3[contains(text(), "test theme depending on modules")]'; + $elements = $this->xpath($themeXpath); + $this->assertCount(1, $elements); + $this->getSession()->getDriver()->click('//h3[contains(text(), "test theme depending on modules")]/../ul/li[1]/a'); + $this->assertSession()->addressEquals('admin/appearance/install/confirm'); + $this->assertSession()->pageTextContains('Enabling the test theme depending on modules theme will also enable these modules: test module required by theme, test another module required by theme.'); + $this->getSession()->getPage()->clickLink('Confirm'); + $this->assertSession()->addressEquals('admin/appearance'); + $this->assertSession()->pageTextContains('The test theme depending on modules theme and its 2 module dependencies have been installed: test module required by theme, test another module required by theme.'); + $this->drupalGet('admin/modules'); + $this->assertSession()->elementExists('css', '#edit-modules-test-another-module-required-by-theme-enable[checked]'); + $this->assertSession()->elementExists('css', '#edit-modules-test-module-required-by-theme-enable[checked]'); + } + + /** + * Tests installing a theme with existing module dependencies. + */ + public function testInstallModuleWithAlreadyInstalledDependencies() { + $this->drupalGet('admin/appearance'); + $themeXpath = '//h3[contains(text(), "test theme depending on already installed module")]'; + $elements = $this->xpath($themeXpath); + $this->assertCount(1, $elements); + $this->getSession()->getDriver()->click('//h3[contains(text(), "test theme depending on already installed module")]/../ul/li[1]/a'); + $this->assertSession()->addressEquals('admin/appearance'); + $this->assertSession()->pageTextContains('The test theme depending on already installed module theme has been installed.'); + } + + /** + * Tests installing a theme with missing module dependencies. + */ + public function testInstallModuleWithMissingDependencies() { + $this->drupalGet('admin/appearance'); + $themeXpath = '//h3[contains(text(), "test theme depending on nonexisting module")]'; + $elements = $this->xpath($themeXpath); + $this->assertCount(1, $elements); + $parent = $elements[0]->find('xpath', '..'); + $this->assertContains('missing', $parent->getText()); + $this->assertContains('This theme requires the listed modules to operate correctly.', $parent->getText()); + } + + /** + * Tests the module install and uninstall pages with theme dependencies. + */ + public function testModuleInstallUninstall() { + $assert_session = $this->assertSession(); + $this->drupalGet('admin/modules'); + $assert_session->elementTextContains('css', '[data-drupal-selector="edit-modules-node"] .requirements', 'test theme depending on already installed module (Theme) (disabled)'); + $assert_session->elementTextContains('css', '[data-drupal-selector="edit-modules-test-another-module-required-by-theme"] .requirements', 'test theme depending on modules (Theme) (disabled)'); + $assert_session->elementTextContains('css', '[data-drupal-selector="edit-modules-test-module-required-by-theme"] .requirements', 'test theme depending on modules (Theme) (disabled)'); + $this->drupalPostForm('admin/modules', [ + 'modules[test_another_module_required_by_theme][enable]' => 1, + 'modules[test_module_required_by_theme][enable]' => 1, + ], 'Install'); + $assert_session->checkboxChecked('modules[test_another_module_required_by_theme][enable]'); + $assert_session->checkboxChecked('modules[test_module_required_by_theme][enable]'); + + $this->drupalGet('admin/modules/uninstall'); + $assert_session->elementExists('css', '[name="uninstall[test_module_required_by_theme]"]:not([disabled])'); + $assert_session->elementExists('css', '[name="uninstall[test_another_module_required_by_theme]"]:not([disabled])'); + + $this->drupalGet('admin/appearance'); + $this->getSession()->getPage()->clickLink('Install test theme depending on modules theme'); + $assert_session->pageTextContains('The test theme depending on modules theme has been installed.'); + $this->drupalGet('admin/modules/uninstall'); + $assert_session->elementExists('css', '[name="uninstall[test_module_required_by_theme]"][disabled]'); + $assert_session->elementExists('css', '[name="uninstall[test_another_module_required_by_theme]"][disabled]'); + } + +} diff --git a/core/tests/Drupal/Tests/Core/Extension/ThemeHandlerTest.php b/core/tests/Drupal/Tests/Core/Extension/ThemeHandlerTest.php index 99c6769477..35f644cbc6 100644 --- a/core/tests/Drupal/Tests/Core/Extension/ThemeHandlerTest.php +++ b/core/tests/Drupal/Tests/Core/Extension/ThemeHandlerTest.php @@ -11,6 +11,7 @@ use Drupal\Core\Extension\Extension; use Drupal\Core\Extension\ThemeExtensionList; use Drupal\Core\Extension\ThemeHandler; +use Drupal\Core\State\StateInterface; use Drupal\Tests\UnitTestCase; /** @@ -58,7 +59,8 @@ protected function setUp() { $this->themeList = $this->getMockBuilder(ThemeExtensionList::class) ->disableOriginalConstructor() ->getMock(); - $this->themeHandler = new StubThemeHandler($this->root, $this->configFactory, $this->themeList); + $this->state = $this->createMock('Drupal\Core\State\StateInterface'); + $this->themeHandler = new StubThemeHandler($this->root, $this->configFactory, $this->themeList, $this->state); $container = $this->createMock('Symfony\Component\DependencyInjection\ContainerInterface'); $container->expects($this->any()) @@ -74,15 +76,14 @@ protected function setUp() { * @see \Drupal\Core\Extension\ThemeHandler::rebuildThemeData() */ public function testRebuildThemeData() { - $this->themeList->expects($this->at(0)) + $this->themeList->expects($this->any()) ->method('reset') ->willReturnSelf(); - $this->themeList->expects($this->at(1)) + $this->themeList->expects($this->any()) ->method('getList') ->will($this->returnValue([ 'seven' => new Extension($this->root, 'theme', 'core/themes/seven/seven.info.yml', 'seven.theme'), ])); - $theme_data = $this->themeHandler->rebuildThemeData(); $this->assertCount(1, $theme_data); $info = $theme_data['seven'];