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'];