Issue #474684 by bnjmnm, dawehner, tedbow, pfrenssen, JohnAlbin, ademarco, kalpaitch, vdacosta@voidtek.com, rensingh99, markcarver, jungle, jhedstrom, RobLoach, almaudoh, kevineinarsson, shaal, dpagini, thedavidmeister, sreynen, Snugug, Miguel.kode, kamkejj, alexpott, Pol, sun, Wim Leers, lauriii, tim.plunkett, eaton: Allow themes to declare dependencies on modules
parent
0d78f4c082
commit
a130897f02
|
@ -522,7 +522,7 @@ services:
|
||||||
class: Drupal\Core\Extension\ModuleInstaller
|
class: Drupal\Core\Extension\ModuleInstaller
|
||||||
tags:
|
tags:
|
||||||
- { name: service_collector, tag: 'module_install.uninstall_validator', call: addUninstallValidator }
|
- { name: service_collector, tag: 'module_install.uninstall_validator', call: addUninstallValidator }
|
||||||
arguments: ['%app.root%', '@module_handler', '@kernel']
|
arguments: ['%app.root%', '@module_handler', '@kernel', '@extension.list.theme']
|
||||||
lazy: true
|
lazy: true
|
||||||
extension.list.module:
|
extension.list.module:
|
||||||
class: Drupal\Core\Extension\ModuleExtensionList
|
class: Drupal\Core\Extension\ModuleExtensionList
|
||||||
|
@ -548,12 +548,18 @@ services:
|
||||||
- { name: module_install.uninstall_validator }
|
- { name: module_install.uninstall_validator }
|
||||||
arguments: ['@string_translation', '@extension.list.module']
|
arguments: ['@string_translation', '@extension.list.module']
|
||||||
lazy: true
|
lazy: true
|
||||||
|
module_required_by_themes_uninstall_validator:
|
||||||
|
class: Drupal\Core\Extension\ModuleRequiredByThemesUninstallValidator
|
||||||
|
tags:
|
||||||
|
- { name: module_install.uninstall_validator }
|
||||||
|
arguments: ['@string_translation', '@extension.list.module', '@extension.list.theme']
|
||||||
|
lazy: true
|
||||||
theme_handler:
|
theme_handler:
|
||||||
class: Drupal\Core\Extension\ThemeHandler
|
class: Drupal\Core\Extension\ThemeHandler
|
||||||
arguments: ['%app.root%', '@config.factory', '@extension.list.theme']
|
arguments: ['%app.root%', '@config.factory', '@extension.list.theme']
|
||||||
theme_installer:
|
theme_installer:
|
||||||
class: Drupal\Core\Extension\ThemeInstaller
|
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']
|
arguments: ['@theme_handler', '@config.factory', '@config.installer', '@module_handler', '@config.manager', '@asset.css.collection_optimizer', '@router.builder', '@logger.channel.default', '@state', '@extension.list.module']
|
||||||
entity.memory_cache:
|
entity.memory_cache:
|
||||||
class: Drupal\Core\Cache\MemoryCache\MemoryCache
|
class: Drupal\Core\Cache\MemoryCache\MemoryCache
|
||||||
entity_type.manager:
|
entity_type.manager:
|
||||||
|
|
|
@ -50,6 +50,13 @@ class ModuleInstaller implements ModuleInstallerInterface {
|
||||||
*/
|
*/
|
||||||
protected $uninstallValidators;
|
protected $uninstallValidators;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The theme extension list.
|
||||||
|
*
|
||||||
|
* @var \Drupal\Core\Extension\ThemeExtensionList
|
||||||
|
*/
|
||||||
|
protected $themeExtensionList;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Constructs a new ModuleInstaller instance.
|
* Constructs a new ModuleInstaller instance.
|
||||||
*
|
*
|
||||||
|
@ -59,14 +66,21 @@ class ModuleInstaller implements ModuleInstallerInterface {
|
||||||
* The module handler.
|
* The module handler.
|
||||||
* @param \Drupal\Core\DrupalKernelInterface $kernel
|
* @param \Drupal\Core\DrupalKernelInterface $kernel
|
||||||
* The drupal kernel.
|
* The drupal kernel.
|
||||||
|
* @param \Drupal\Core\Extension\ThemeExtensionList $extension_list_theme
|
||||||
|
* The theme extension list.
|
||||||
*
|
*
|
||||||
* @see \Drupal\Core\DrupalKernel
|
* @see \Drupal\Core\DrupalKernel
|
||||||
* @see \Drupal\Core\CoreServiceProvider
|
* @see \Drupal\Core\CoreServiceProvider
|
||||||
*/
|
*/
|
||||||
public function __construct($root, ModuleHandlerInterface $module_handler, DrupalKernelInterface $kernel) {
|
public function __construct($root, ModuleHandlerInterface $module_handler, DrupalKernelInterface $kernel, ThemeExtensionList $extension_list_theme = NULL) {
|
||||||
$this->root = $root;
|
$this->root = $root;
|
||||||
$this->moduleHandler = $module_handler;
|
$this->moduleHandler = $module_handler;
|
||||||
$this->kernel = $kernel;
|
$this->kernel = $kernel;
|
||||||
|
if (is_null($extension_list_theme)) {
|
||||||
|
@trigger_error('The extension.list.theme service must be passed to ' . __NAMESPACE__ . '\ModuleInstaller::__construct(). It was added in drupal:8.9.0 and will be required before drupal:10.0.0.', E_USER_DEPRECATED);
|
||||||
|
$extension_list_theme = \Drupal::service('extension.list.theme');
|
||||||
|
}
|
||||||
|
$this->themeExtensionList = $extension_list_theme;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -372,12 +386,14 @@ class ModuleInstaller implements ModuleInstallerInterface {
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($uninstall_dependents) {
|
if ($uninstall_dependents) {
|
||||||
|
$theme_list = $this->themeExtensionList->getList();
|
||||||
|
|
||||||
// Add dependent modules to the list. The new modules will be processed as
|
// Add dependent modules to the list. The new modules will be processed as
|
||||||
// the foreach loop continues.
|
// the foreach loop continues.
|
||||||
foreach ($module_list as $module => $value) {
|
foreach ($module_list as $module => $value) {
|
||||||
foreach (array_keys($module_data[$module]->required_by) as $dependent) {
|
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.
|
// The dependent module or theme does not exist.
|
||||||
return FALSE;
|
return FALSE;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -578,6 +594,7 @@ class ModuleInstaller implements ModuleInstallerInterface {
|
||||||
// After rebuilding the container we need to update the injected
|
// After rebuilding the container we need to update the injected
|
||||||
// dependencies.
|
// dependencies.
|
||||||
$container = $this->kernel->getContainer();
|
$container = $this->kernel->getContainer();
|
||||||
|
$this->themeExtensionList = $container->get('extension.list.theme');
|
||||||
$this->moduleHandler = $container->get('module_handler');
|
$this->moduleHandler = $container->get('module_handler');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,84 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Drupal\Core\Extension;
|
||||||
|
|
||||||
|
use Drupal\Core\StringTranslation\StringTranslationTrait;
|
||||||
|
use Drupal\Core\StringTranslation\TranslationInterface;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Ensures modules cannot be uninstalled if enabled themes depend on them.
|
||||||
|
*/
|
||||||
|
class ModuleRequiredByThemesUninstallValidator implements ModuleUninstallValidatorInterface {
|
||||||
|
|
||||||
|
use StringTranslationTrait;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The module extension list.
|
||||||
|
*
|
||||||
|
* @var \Drupal\Core\Extension\ModuleExtensionList
|
||||||
|
*/
|
||||||
|
protected $moduleExtensionList;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The theme extension list.
|
||||||
|
*
|
||||||
|
* @var \Drupal\Core\Extension\ThemeExtensionList
|
||||||
|
*/
|
||||||
|
protected $themeExtensionList;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constructs a new ModuleRequiredByThemesUninstallValidator.
|
||||||
|
*
|
||||||
|
* @param \Drupal\Core\StringTranslation\TranslationInterface $string_translation
|
||||||
|
* The string translation service.
|
||||||
|
* @param \Drupal\Core\Extension\ModuleExtensionList $extension_list_module
|
||||||
|
* The module extension list.
|
||||||
|
* @param \Drupal\Core\Extension\ThemeExtensionList $extension_list_theme
|
||||||
|
* The theme extension list.
|
||||||
|
*/
|
||||||
|
public function __construct(TranslationInterface $string_translation, ModuleExtensionList $extension_list_module, ThemeExtensionList $extension_list_theme) {
|
||||||
|
$this->stringTranslation = $string_translation;
|
||||||
|
$this->moduleExtensionList = $extension_list_module;
|
||||||
|
$this->themeExtensionList = $extension_list_theme;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@inheritdoc}
|
||||||
|
*/
|
||||||
|
public function validate($module) {
|
||||||
|
$reasons = [];
|
||||||
|
|
||||||
|
$themes_depending_on_module = $this->getThemesDependingOnModule($module);
|
||||||
|
if (!empty($themes_depending_on_module)) {
|
||||||
|
$module_name = $this->moduleExtensionList->get($module)->info['name'];
|
||||||
|
$theme_names = implode(', ', $themes_depending_on_module);
|
||||||
|
$reasons[] = $this->formatPlural(count($themes_depending_on_module),
|
||||||
|
'Required by the theme: @theme_names',
|
||||||
|
'Required by the themes: @theme_names',
|
||||||
|
['@module_name' => $module_name, '@theme_names' => $theme_names]);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $reasons;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns themes that depend on a module.
|
||||||
|
*
|
||||||
|
* @param string $module
|
||||||
|
* The module machine name.
|
||||||
|
*
|
||||||
|
* @return string[]
|
||||||
|
* An array of the names of themes that depend on $module.
|
||||||
|
*/
|
||||||
|
protected function getThemesDependingOnModule($module) {
|
||||||
|
$installed_themes = $this->themeExtensionList->getAllInstalledInfo();
|
||||||
|
$themes_depending_on_module = array_map(function ($theme) use ($module) {
|
||||||
|
if (in_array($module, $theme['dependencies'])) {
|
||||||
|
return $theme['name'];
|
||||||
|
}
|
||||||
|
}, $installed_themes);
|
||||||
|
|
||||||
|
return array_filter($themes_depending_on_module);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -51,6 +51,7 @@ class ThemeExtensionList extends ExtensionList {
|
||||||
'libraries' => [],
|
'libraries' => [],
|
||||||
'libraries_extend' => [],
|
'libraries_extend' => [],
|
||||||
'libraries_override' => [],
|
'libraries_override' => [],
|
||||||
|
'dependencies' => [],
|
||||||
];
|
];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -140,6 +141,22 @@ class ThemeExtensionList extends ExtensionList {
|
||||||
// sub-themes.
|
// sub-themes.
|
||||||
$this->fillInSubThemeData($themes, $sub_themes);
|
$this->fillInSubThemeData($themes, $sub_themes);
|
||||||
|
|
||||||
|
foreach ($themes as $key => $theme) {
|
||||||
|
// After $theme is processed by buildModuleDependencies(), there can be a
|
||||||
|
// `$theme->requires` array containing both module and base theme
|
||||||
|
// dependencies. The module dependencies are copied to their own property
|
||||||
|
// so they are available to operations specific to module dependencies.
|
||||||
|
if (isset($theme->requires)) {
|
||||||
|
$theme->module_dependencies = array_diff_key($theme->requires, $themes);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
// Even if no requirements are specified, the theme installation process
|
||||||
|
// expects the presence of the `requires` and `module_dependencies`
|
||||||
|
// properties, so they should be initialized here as empty arrays.
|
||||||
|
$theme->requires = [];
|
||||||
|
$theme->module_dependencies = [];
|
||||||
|
}
|
||||||
|
}
|
||||||
return $themes;
|
return $themes;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -2,6 +2,7 @@
|
||||||
|
|
||||||
namespace Drupal\Core\Extension;
|
namespace Drupal\Core\Extension;
|
||||||
|
|
||||||
|
use Drupal\Component\Utility\Html;
|
||||||
use Drupal\Core\Asset\AssetCollectionOptimizerInterface;
|
use Drupal\Core\Asset\AssetCollectionOptimizerInterface;
|
||||||
use Drupal\Core\Cache\Cache;
|
use Drupal\Core\Cache\Cache;
|
||||||
use Drupal\Core\Config\ConfigFactoryInterface;
|
use Drupal\Core\Config\ConfigFactoryInterface;
|
||||||
|
@ -10,6 +11,8 @@ use Drupal\Core\Config\ConfigManagerInterface;
|
||||||
use Drupal\Core\Extension\Exception\UnknownExtensionException;
|
use Drupal\Core\Extension\Exception\UnknownExtensionException;
|
||||||
use Drupal\Core\Routing\RouteBuilderInterface;
|
use Drupal\Core\Routing\RouteBuilderInterface;
|
||||||
use Drupal\Core\State\StateInterface;
|
use Drupal\Core\State\StateInterface;
|
||||||
|
use Drupal\Core\StringTranslation\StringTranslationTrait;
|
||||||
|
use Drupal\system\ModuleDependencyMessageTrait;
|
||||||
use Psr\Log\LoggerInterface;
|
use Psr\Log\LoggerInterface;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -17,6 +20,9 @@ use Psr\Log\LoggerInterface;
|
||||||
*/
|
*/
|
||||||
class ThemeInstaller implements ThemeInstallerInterface {
|
class ThemeInstaller implements ThemeInstallerInterface {
|
||||||
|
|
||||||
|
use ModuleDependencyMessageTrait;
|
||||||
|
use StringTranslationTrait;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @var \Drupal\Core\Extension\ThemeHandlerInterface
|
* @var \Drupal\Core\Extension\ThemeHandlerInterface
|
||||||
*/
|
*/
|
||||||
|
@ -62,6 +68,13 @@ class ThemeInstaller implements ThemeInstallerInterface {
|
||||||
*/
|
*/
|
||||||
protected $logger;
|
protected $logger;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The module extension list.
|
||||||
|
*
|
||||||
|
* @var \Drupal\Core\Extension\ModuleExtensionList
|
||||||
|
*/
|
||||||
|
protected $moduleExtensionList;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Constructs a new ThemeInstaller.
|
* Constructs a new ThemeInstaller.
|
||||||
*
|
*
|
||||||
|
@ -86,8 +99,10 @@ class ThemeInstaller implements ThemeInstallerInterface {
|
||||||
* A logger instance.
|
* A logger instance.
|
||||||
* @param \Drupal\Core\State\StateInterface $state
|
* @param \Drupal\Core\State\StateInterface $state
|
||||||
* The state store.
|
* The state store.
|
||||||
|
* @param \Drupal\Core\Extension\ModuleExtensionList $module_extension_list
|
||||||
|
* The module extension list.
|
||||||
*/
|
*/
|
||||||
public function __construct(ThemeHandlerInterface $theme_handler, ConfigFactoryInterface $config_factory, ConfigInstallerInterface $config_installer, ModuleHandlerInterface $module_handler, ConfigManagerInterface $config_manager, AssetCollectionOptimizerInterface $css_collection_optimizer, RouteBuilderInterface $route_builder, LoggerInterface $logger, StateInterface $state) {
|
public function __construct(ThemeHandlerInterface $theme_handler, ConfigFactoryInterface $config_factory, ConfigInstallerInterface $config_installer, ModuleHandlerInterface $module_handler, ConfigManagerInterface $config_manager, AssetCollectionOptimizerInterface $css_collection_optimizer, RouteBuilderInterface $route_builder, LoggerInterface $logger, StateInterface $state, ModuleExtensionList $module_extension_list = NULL) {
|
||||||
$this->themeHandler = $theme_handler;
|
$this->themeHandler = $theme_handler;
|
||||||
$this->configFactory = $config_factory;
|
$this->configFactory = $config_factory;
|
||||||
$this->configInstaller = $config_installer;
|
$this->configInstaller = $config_installer;
|
||||||
|
@ -97,6 +112,11 @@ class ThemeInstaller implements ThemeInstallerInterface {
|
||||||
$this->routeBuilder = $route_builder;
|
$this->routeBuilder = $route_builder;
|
||||||
$this->logger = $logger;
|
$this->logger = $logger;
|
||||||
$this->state = $state;
|
$this->state = $state;
|
||||||
|
if ($module_extension_list === NULL) {
|
||||||
|
@trigger_error('The extension.list.module service must be passed to ' . __NAMESPACE__ . '\ThemeInstaller::__construct(). It was added in drupal:8.9.0 and will be required before drupal:10.0.0.', E_USER_DEPRECATED);
|
||||||
|
$module_extension_list = \Drupal::service('extension.list.module');
|
||||||
|
}
|
||||||
|
$this->moduleExtensionList = $module_extension_list;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -106,6 +126,8 @@ class ThemeInstaller implements ThemeInstallerInterface {
|
||||||
$extension_config = $this->configFactory->getEditable('core.extension');
|
$extension_config = $this->configFactory->getEditable('core.extension');
|
||||||
|
|
||||||
$theme_data = $this->themeHandler->rebuildThemeData();
|
$theme_data = $this->themeHandler->rebuildThemeData();
|
||||||
|
$installed_themes = $extension_config->get('theme') ?: [];
|
||||||
|
$installed_modules = $extension_config->get('module') ?: [];
|
||||||
|
|
||||||
if ($install_dependencies) {
|
if ($install_dependencies) {
|
||||||
$theme_list = array_combine($theme_list, $theme_list);
|
$theme_list = array_combine($theme_list, $theme_list);
|
||||||
|
@ -116,16 +138,41 @@ class ThemeInstaller implements ThemeInstallerInterface {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Only process themes that are not installed currently.
|
// Only process themes that are not installed currently.
|
||||||
$installed_themes = $extension_config->get('theme') ?: [];
|
|
||||||
if (!$theme_list = array_diff_key($theme_list, $installed_themes)) {
|
if (!$theme_list = array_diff_key($theme_list, $installed_themes)) {
|
||||||
// Nothing to do. All themes already installed.
|
// Nothing to do. All themes already installed.
|
||||||
return TRUE;
|
return TRUE;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$module_list = $this->moduleExtensionList->getList();
|
||||||
foreach ($theme_list as $theme => $value) {
|
foreach ($theme_list as $theme => $value) {
|
||||||
// Add dependencies to the list. The new themes will be processed as
|
$module_dependencies = $theme_data[$theme]->module_dependencies;
|
||||||
// the parent foreach loop continues.
|
// $theme_data[$theme]->requires contains both theme and module
|
||||||
foreach (array_keys($theme_data[$theme]->requires) as $dependency) {
|
// dependencies keyed by the extension machine names and
|
||||||
|
// $theme_data[$theme]->module_dependencies contains only modules keyed
|
||||||
|
// by the module extension machine name. Therefore we can find the theme
|
||||||
|
// dependencies by finding array keys for 'requires' that are not in
|
||||||
|
// $module_dependencies.
|
||||||
|
$theme_dependencies = array_diff_key($theme_data[$theme]->requires, $module_dependencies);
|
||||||
|
// We can find the unmet module dependencies by finding the module
|
||||||
|
// machine names keys that are not in $installed_modules keys.
|
||||||
|
$unmet_module_dependencies = array_diff_key($module_dependencies, $installed_modules);
|
||||||
|
|
||||||
|
// Prevent themes with unmet module dependencies from being installed.
|
||||||
|
if (!empty($unmet_module_dependencies)) {
|
||||||
|
$unmet_module_dependencies_list = implode(', ', array_keys($unmet_module_dependencies));
|
||||||
|
throw new MissingDependencyException("Unable to install theme: '$theme' due to unmet module dependencies: '$unmet_module_dependencies_list'.");
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($module_dependencies as $dependency => $dependency_object) {
|
||||||
|
if ($incompatible = $this->checkDependencyMessage($module_list, $dependency, $dependency_object)) {
|
||||||
|
$sanitized_message = Html::decodeEntities(strip_tags($incompatible));
|
||||||
|
throw new MissingDependencyException("Unable to install theme: $sanitized_message");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add dependencies to the list of themes to install. The new themes
|
||||||
|
// will be processed as the parent foreach loop continues.
|
||||||
|
foreach (array_keys($theme_dependencies) as $dependency) {
|
||||||
if (!isset($theme_data[$dependency])) {
|
if (!isset($theme_data[$dependency])) {
|
||||||
// The dependency does not exist.
|
// The dependency does not exist.
|
||||||
return FALSE;
|
return FALSE;
|
||||||
|
@ -147,9 +194,6 @@ class ThemeInstaller implements ThemeInstallerInterface {
|
||||||
arsort($theme_list);
|
arsort($theme_list);
|
||||||
$theme_list = array_keys($theme_list);
|
$theme_list = array_keys($theme_list);
|
||||||
}
|
}
|
||||||
else {
|
|
||||||
$installed_themes = $extension_config->get('theme') ?: [];
|
|
||||||
}
|
|
||||||
|
|
||||||
$themes_installed = [];
|
$themes_installed = [];
|
||||||
foreach ($theme_list as $key) {
|
foreach ($theme_list as $key) {
|
||||||
|
|
|
@ -25,6 +25,9 @@ interface ThemeInstallerInterface {
|
||||||
*
|
*
|
||||||
* @throws \Drupal\Core\Extension\Exception\UnknownExtensionException
|
* @throws \Drupal\Core\Extension\Exception\UnknownExtensionException
|
||||||
* Thrown when the theme does not exist.
|
* Thrown when the theme does not exist.
|
||||||
|
*
|
||||||
|
* @throws \Drupal\Core\Extension\MissingDependencyException
|
||||||
|
* Thrown when a requested dependency can't be found.
|
||||||
*/
|
*/
|
||||||
public function install(array $theme_list, $install_dependencies = TRUE);
|
public function install(array $theme_list, $install_dependencies = TRUE);
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,88 @@
|
||||||
|
<?php
|
||||||
|
// @codingStandardsIgnoreFile
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This file was generated via php core/scripts/generate-proxy-class.php 'Drupal\Core\Extension\ModuleRequiredByThemesUninstallValidator' "core/lib/Drupal/Core".
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace Drupal\Core\ProxyClass\Extension {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Provides a proxy class for \Drupal\Core\Extension\ModuleRequiredByThemesUninstallValidator.
|
||||||
|
*
|
||||||
|
* @see \Drupal\Component\ProxyBuilder
|
||||||
|
*/
|
||||||
|
class ModuleRequiredByThemesUninstallValidator implements \Drupal\Core\Extension\ModuleUninstallValidatorInterface
|
||||||
|
{
|
||||||
|
|
||||||
|
use \Drupal\Core\DependencyInjection\DependencySerializationTrait;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The id of the original proxied service.
|
||||||
|
*
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
|
protected $drupalProxyOriginalServiceId;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The real proxied service, after it was lazy loaded.
|
||||||
|
*
|
||||||
|
* @var \Drupal\Core\Extension\ModuleRequiredByThemesUninstallValidator
|
||||||
|
*/
|
||||||
|
protected $service;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The service container.
|
||||||
|
*
|
||||||
|
* @var \Symfony\Component\DependencyInjection\ContainerInterface
|
||||||
|
*/
|
||||||
|
protected $container;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constructs a ProxyClass Drupal proxy object.
|
||||||
|
*
|
||||||
|
* @param \Symfony\Component\DependencyInjection\ContainerInterface $container
|
||||||
|
* The container.
|
||||||
|
* @param string $drupal_proxy_original_service_id
|
||||||
|
* The service ID of the original service.
|
||||||
|
*/
|
||||||
|
public function __construct(\Symfony\Component\DependencyInjection\ContainerInterface $container, $drupal_proxy_original_service_id)
|
||||||
|
{
|
||||||
|
$this->container = $container;
|
||||||
|
$this->drupalProxyOriginalServiceId = $drupal_proxy_original_service_id;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Lazy loads the real service from the container.
|
||||||
|
*
|
||||||
|
* @return object
|
||||||
|
* Returns the constructed real service.
|
||||||
|
*/
|
||||||
|
protected function lazyLoadItself()
|
||||||
|
{
|
||||||
|
if (!isset($this->service)) {
|
||||||
|
$this->service = $this->container->get($this->drupalProxyOriginalServiceId);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->service;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@inheritdoc}
|
||||||
|
*/
|
||||||
|
public function validate($module)
|
||||||
|
{
|
||||||
|
return $this->lazyLoadItself()->validate($module);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@inheritdoc}
|
||||||
|
*/
|
||||||
|
public function setStringTranslation(\Drupal\Core\StringTranslation\TranslationInterface $translation)
|
||||||
|
{
|
||||||
|
return $this->lazyLoadItself()->setStringTranslation($translation);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -4,12 +4,14 @@ namespace Drupal\system\Controller;
|
||||||
|
|
||||||
use Drupal\Core\Cache\CacheableMetadata;
|
use Drupal\Core\Cache\CacheableMetadata;
|
||||||
use Drupal\Core\Controller\ControllerBase;
|
use Drupal\Core\Controller\ControllerBase;
|
||||||
|
use Drupal\Core\Extension\ModuleExtensionList;
|
||||||
use Drupal\Core\Extension\ThemeHandlerInterface;
|
use Drupal\Core\Extension\ThemeHandlerInterface;
|
||||||
use Drupal\Core\Form\FormBuilderInterface;
|
use Drupal\Core\Form\FormBuilderInterface;
|
||||||
use Drupal\Core\Menu\MenuLinkTreeInterface;
|
use Drupal\Core\Menu\MenuLinkTreeInterface;
|
||||||
use Drupal\Core\Menu\MenuTreeParameters;
|
use Drupal\Core\Menu\MenuTreeParameters;
|
||||||
use Drupal\Core\Theme\ThemeAccessCheck;
|
use Drupal\Core\Theme\ThemeAccessCheck;
|
||||||
use Drupal\Core\Url;
|
use Drupal\Core\Url;
|
||||||
|
use Drupal\system\ModuleDependencyMessageTrait;
|
||||||
use Drupal\system\SystemManager;
|
use Drupal\system\SystemManager;
|
||||||
use Symfony\Component\DependencyInjection\ContainerInterface;
|
use Symfony\Component\DependencyInjection\ContainerInterface;
|
||||||
|
|
||||||
|
@ -18,6 +20,8 @@ use Symfony\Component\DependencyInjection\ContainerInterface;
|
||||||
*/
|
*/
|
||||||
class SystemController extends ControllerBase {
|
class SystemController extends ControllerBase {
|
||||||
|
|
||||||
|
use ModuleDependencyMessageTrait;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* System Manager Service.
|
* System Manager Service.
|
||||||
*
|
*
|
||||||
|
@ -53,6 +57,13 @@ class SystemController extends ControllerBase {
|
||||||
*/
|
*/
|
||||||
protected $menuLinkTree;
|
protected $menuLinkTree;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The module extension list.
|
||||||
|
*
|
||||||
|
* @var \Drupal\Core\Extension\ModuleExtensionList
|
||||||
|
*/
|
||||||
|
protected $moduleExtensionList;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Constructs a new SystemController.
|
* Constructs a new SystemController.
|
||||||
*
|
*
|
||||||
|
@ -66,13 +77,20 @@ class SystemController extends ControllerBase {
|
||||||
* The theme handler.
|
* The theme handler.
|
||||||
* @param \Drupal\Core\Menu\MenuLinkTreeInterface $menu_link_tree
|
* @param \Drupal\Core\Menu\MenuLinkTreeInterface $menu_link_tree
|
||||||
* The menu link tree service.
|
* The menu link tree service.
|
||||||
|
* @param \Drupal\Core\Extension\ModuleExtensionList $module_extension_list
|
||||||
|
* The module extension list.
|
||||||
*/
|
*/
|
||||||
public function __construct(SystemManager $systemManager, ThemeAccessCheck $theme_access, FormBuilderInterface $form_builder, ThemeHandlerInterface $theme_handler, MenuLinkTreeInterface $menu_link_tree) {
|
public function __construct(SystemManager $systemManager, ThemeAccessCheck $theme_access, FormBuilderInterface $form_builder, ThemeHandlerInterface $theme_handler, MenuLinkTreeInterface $menu_link_tree, ModuleExtensionList $module_extension_list = NULL) {
|
||||||
$this->systemManager = $systemManager;
|
$this->systemManager = $systemManager;
|
||||||
$this->themeAccess = $theme_access;
|
$this->themeAccess = $theme_access;
|
||||||
$this->formBuilder = $form_builder;
|
$this->formBuilder = $form_builder;
|
||||||
$this->themeHandler = $theme_handler;
|
$this->themeHandler = $theme_handler;
|
||||||
$this->menuLinkTree = $menu_link_tree;
|
$this->menuLinkTree = $menu_link_tree;
|
||||||
|
if ($module_extension_list === NULL) {
|
||||||
|
@trigger_error('The extension.list.module service must be passed to ' . __NAMESPACE__ . '\SystemController::__construct. It was added in Drupal 8.9.0 and will be required before Drupal 10.0.0.', E_USER_DEPRECATED);
|
||||||
|
$module_extension_list = \Drupal::service('extension.list.module');
|
||||||
|
}
|
||||||
|
$this->moduleExtensionList = $module_extension_list;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -84,7 +102,8 @@ class SystemController extends ControllerBase {
|
||||||
$container->get('access_check.theme'),
|
$container->get('access_check.theme'),
|
||||||
$container->get('form_builder'),
|
$container->get('form_builder'),
|
||||||
$container->get('theme_handler'),
|
$container->get('theme_handler'),
|
||||||
$container->get('menu.link_tree')
|
$container->get('menu.link_tree'),
|
||||||
|
$container->get('extension.list.module')
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -231,9 +250,41 @@ class SystemController extends ControllerBase {
|
||||||
$theme->incompatible_base = (isset($theme->info['base theme']) && !($theme->base_themes === array_filter($theme->base_themes)));
|
$theme->incompatible_base = (isset($theme->info['base theme']) && !($theme->base_themes === array_filter($theme->base_themes)));
|
||||||
// Confirm that the theme engine is available.
|
// Confirm that the theme engine is available.
|
||||||
$theme->incompatible_engine = isset($theme->info['engine']) && !isset($theme->owner);
|
$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.
|
||||||
|
if ($theme->module_dependencies) {
|
||||||
|
$modules = $this->moduleExtensionList->getList();
|
||||||
|
foreach ($theme->module_dependencies as $dependency => $dependency_object) {
|
||||||
|
if ($incompatible = $this->checkDependencyMessage($modules, $dependency, $dependency_object)) {
|
||||||
|
$theme->module_dependencies_list[$dependency] = $incompatible;
|
||||||
|
$theme->incompatible_module = TRUE;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// @todo Add logic for not displaying hidden modules in
|
||||||
|
// https://drupal.org/node/3117829.
|
||||||
|
$module_name = $modules[$dependency]->info['name'];
|
||||||
|
$theme->module_dependencies_list[$dependency] = $modules[$dependency]->status ? $this->t('@module_name', ['@module_name' => $module_name]) : $this->t('@module_name (<span class="admin-disabled">disabled</span>)', ['@module_name' => $module_name]);
|
||||||
|
|
||||||
|
// Create an additional property that contains only disabled module
|
||||||
|
// dependencies. This will determine if it is possible to install the
|
||||||
|
// theme, or if modules must first be enabled.
|
||||||
|
if (!$modules[$dependency]->status) {
|
||||||
|
$theme->module_dependencies_disabled[$dependency] = $module_name;
|
||||||
|
if (!$this->currentUser()->hasPermission('administer modules')) {
|
||||||
|
$theme->insufficient_module_permissions = TRUE;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
$theme->operations = [];
|
$theme->operations = [];
|
||||||
if (!empty($theme->status) || !$theme->info['core_incompatible'] && !$theme->incompatible_php && !$theme->incompatible_base && !$theme->incompatible_engine) {
|
if (!empty($theme->status) || !$theme->info['core_incompatible'] && !$theme->incompatible_php && !$theme->incompatible_base && !$theme->incompatible_engine && !$theme->incompatible_module && empty($theme->module_dependencies_disabled)) {
|
||||||
// Create the operations links.
|
// Create the operations links.
|
||||||
$query['theme'] = $theme->getName();
|
$query['theme'] = $theme->getName();
|
||||||
if ($this->themeAccess->checkAccess($theme->getName())) {
|
if ($this->themeAccess->checkAccess($theme->getName())) {
|
||||||
|
|
|
@ -6,6 +6,7 @@ use Drupal\Core\Config\ConfigFactoryInterface;
|
||||||
use Drupal\Core\Config\PreExistingConfigException;
|
use Drupal\Core\Config\PreExistingConfigException;
|
||||||
use Drupal\Core\Config\UnmetDependenciesException;
|
use Drupal\Core\Config\UnmetDependenciesException;
|
||||||
use Drupal\Core\Controller\ControllerBase;
|
use Drupal\Core\Controller\ControllerBase;
|
||||||
|
use Drupal\Core\Extension\MissingDependencyException;
|
||||||
use Drupal\Core\Extension\ThemeExtensionList;
|
use Drupal\Core\Extension\ThemeExtensionList;
|
||||||
use Drupal\Core\Extension\ThemeHandlerInterface;
|
use Drupal\Core\Extension\ThemeHandlerInterface;
|
||||||
use Drupal\Core\Extension\ThemeInstallerInterface;
|
use Drupal\Core\Extension\ThemeInstallerInterface;
|
||||||
|
@ -161,6 +162,9 @@ class ThemeController extends ControllerBase {
|
||||||
catch (UnmetDependenciesException $e) {
|
catch (UnmetDependenciesException $e) {
|
||||||
$this->messenger()->addError($e->getTranslatedMessage($this->getStringTranslation(), $theme));
|
$this->messenger()->addError($e->getTranslatedMessage($this->getStringTranslation(), $theme));
|
||||||
}
|
}
|
||||||
|
catch (MissingDependencyException $e) {
|
||||||
|
$this->messenger()->addError($this->t('Unable to install @theme due to missing module dependencies.', ['@theme' => $theme]));
|
||||||
|
}
|
||||||
|
|
||||||
return $this->redirect('system.themes_page');
|
return $this->redirect('system.themes_page');
|
||||||
}
|
}
|
||||||
|
|
|
@ -17,6 +17,7 @@ use Drupal\Core\Render\Element;
|
||||||
use Drupal\Core\Session\AccountInterface;
|
use Drupal\Core\Session\AccountInterface;
|
||||||
use Drupal\user\PermissionHandlerInterface;
|
use Drupal\user\PermissionHandlerInterface;
|
||||||
use Drupal\Core\Url;
|
use Drupal\Core\Url;
|
||||||
|
use Drupal\system\ModuleDependencyMessageTrait;
|
||||||
use Symfony\Component\DependencyInjection\ContainerInterface;
|
use Symfony\Component\DependencyInjection\ContainerInterface;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -31,6 +32,8 @@ use Symfony\Component\DependencyInjection\ContainerInterface;
|
||||||
*/
|
*/
|
||||||
class ModulesListForm extends FormBase {
|
class ModulesListForm extends FormBase {
|
||||||
|
|
||||||
|
use ModuleDependencyMessageTrait;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The current user.
|
* The current user.
|
||||||
*
|
*
|
||||||
|
@ -326,38 +329,16 @@ class ModulesListForm extends FormBase {
|
||||||
// If this module requires other modules, add them to the array.
|
// If this module requires other modules, add them to the array.
|
||||||
/** @var \Drupal\Core\Extension\Dependency $dependency_object */
|
/** @var \Drupal\Core\Extension\Dependency $dependency_object */
|
||||||
foreach ($module->requires as $dependency => $dependency_object) {
|
foreach ($module->requires as $dependency => $dependency_object) {
|
||||||
if (!isset($modules[$dependency])) {
|
// @todo Add logic for not displaying hidden modules in
|
||||||
$row['#requires'][$dependency] = $this->t('@module (<span class="admin-missing">missing</span>)', ['@module' => $dependency]);
|
// https://drupal.org/node/3117829.
|
||||||
|
if ($incompatible = $this->checkDependencyMessage($modules, $dependency, $dependency_object)) {
|
||||||
|
$row['#requires'][$dependency] = $incompatible;
|
||||||
$row['enable']['#disabled'] = TRUE;
|
$row['enable']['#disabled'] = TRUE;
|
||||||
|
continue;
|
||||||
}
|
}
|
||||||
// Only display visible modules.
|
|
||||||
elseif (empty($modules[$dependency]->hidden)) {
|
$name = $modules[$dependency]->info['name'];
|
||||||
$name = $modules[$dependency]->info['name'];
|
$row['#requires'][$dependency] = $modules[$dependency]->status ? $this->t('@module', ['@module' => $name]) : $this->t('@module (<span class="admin-disabled">disabled</span>)', ['@module' => $name]);
|
||||||
// Disable the module's checkbox if it is incompatible with the
|
|
||||||
// dependency's version.
|
|
||||||
if (!$dependency_object->isCompatible(str_replace(\Drupal::CORE_COMPATIBILITY . '-', '', $modules[$dependency]->info['version']))) {
|
|
||||||
$row['#requires'][$dependency] = $this->t('@module (@constraint) (<span class="admin-missing">incompatible with</span> version @version)', [
|
|
||||||
'@module' => $name,
|
|
||||||
'@constraint' => $dependency_object->getConstraintString(),
|
|
||||||
'@version' => $modules[$dependency]->info['version'],
|
|
||||||
]);
|
|
||||||
$row['enable']['#disabled'] = TRUE;
|
|
||||||
}
|
|
||||||
// Disable the checkbox if the dependency is incompatible with this
|
|
||||||
// version of Drupal core.
|
|
||||||
elseif ($modules[$dependency]->info['core_incompatible']) {
|
|
||||||
$row['#requires'][$dependency] = $this->t('@module (<span class="admin-missing">incompatible with</span> this version of Drupal core)', [
|
|
||||||
'@module' => $name,
|
|
||||||
]);
|
|
||||||
$row['enable']['#disabled'] = TRUE;
|
|
||||||
}
|
|
||||||
elseif ($modules[$dependency]->status) {
|
|
||||||
$row['#requires'][$dependency] = $this->t('@module', ['@module' => $name]);
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
$row['#requires'][$dependency] = $this->t('@module (<span class="admin-disabled">disabled</span>)', ['@module' => $name]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// If this module is required by other modules, list those, and then make it
|
// If this module is required by other modules, list those, and then make it
|
||||||
|
|
|
@ -0,0 +1,55 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Drupal\system;
|
||||||
|
|
||||||
|
use Drupal\Core\Extension\Dependency;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Messages for missing or incompatible dependencies on modules.
|
||||||
|
*
|
||||||
|
* @internal The trait simply helps core classes that display user messages
|
||||||
|
* regarding missing or incompatible module dependencies share exact same
|
||||||
|
* wording and markup.
|
||||||
|
*/
|
||||||
|
trait ModuleDependencyMessageTrait {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Provides messages for missing modules or incompatible dependencies.
|
||||||
|
*
|
||||||
|
* @param array $modules
|
||||||
|
* The list of existing modules.
|
||||||
|
* @param string $dependency
|
||||||
|
* The module dependency to check.
|
||||||
|
* @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 function checkDependencyMessage(array $modules, $dependency, Dependency $dependency_object) {
|
||||||
|
if (!isset($modules[$dependency])) {
|
||||||
|
return $this->t('@module_name (<span class="admin-missing">missing</span>)', ['@module_name' => $dependency]);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
$module_name = $modules[$dependency]->info['name'];
|
||||||
|
|
||||||
|
// Check if the module is compatible with the installed version of core.
|
||||||
|
if ($modules[$dependency]->info['core_incompatible']) {
|
||||||
|
return $this->t('@module_name (<span class="admin-missing">incompatible with</span> this version of Drupal core)', [
|
||||||
|
'@module_name' => $module_name,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if the module is incompatible with the dependency constraints.
|
||||||
|
$version = str_replace(\Drupal::CORE_COMPATIBILITY . '-', '', $modules[$dependency]->info['version']);
|
||||||
|
if (!$dependency_object->isCompatible($version)) {
|
||||||
|
$constraint_string = $dependency_object->getConstraintString();
|
||||||
|
return $this->t('@module_name (<span class="admin-missing">incompatible with</span> version @version)', [
|
||||||
|
'@module_name' => "$module_name ($constraint_string)",
|
||||||
|
'@version' => $modules[$dependency]->info['version'],
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -9,6 +9,7 @@ use Drupal\Component\Utility\Html;
|
||||||
use Drupal\Core\Link;
|
use Drupal\Core\Link;
|
||||||
use Drupal\Core\Render\Element;
|
use Drupal\Core\Render\Element;
|
||||||
use Drupal\Core\Template\Attribute;
|
use Drupal\Core\Template\Attribute;
|
||||||
|
use Drupal\Core\Url;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Prepares variables for administrative content block templates.
|
* Prepares variables for administrative content block templates.
|
||||||
|
@ -123,7 +124,7 @@ function template_preprocess_system_admin_index(&$variables) {
|
||||||
* - version: The version of the module.
|
* - version: The version of the module.
|
||||||
* - links: Administration links provided by the module.
|
* - links: Administration links provided by the module.
|
||||||
* - #requires: A list of modules that the project requires.
|
* - #requires: A list of modules that the project requires.
|
||||||
* - #required_by: A list of modules that require the project.
|
* - #required_by: A list of modules and themes that require the project.
|
||||||
* - #attributes: A list of attributes for the module wrapper.
|
* - #attributes: A list of attributes for the module wrapper.
|
||||||
*
|
*
|
||||||
* @see \Drupal\system\Form\ModulesListForm
|
* @see \Drupal\system\Form\ModulesListForm
|
||||||
|
@ -131,6 +132,18 @@ function template_preprocess_system_admin_index(&$variables) {
|
||||||
function template_preprocess_system_modules_details(&$variables) {
|
function template_preprocess_system_modules_details(&$variables) {
|
||||||
$form = $variables['form'];
|
$form = $variables['form'];
|
||||||
|
|
||||||
|
// Identify modules that are depended on by themes.
|
||||||
|
// Added here instead of ModuleHandler to avoid recursion.
|
||||||
|
$themes = \Drupal::service('extension.list.theme')->getList();
|
||||||
|
foreach ($themes as $theme) {
|
||||||
|
foreach ($theme->info['dependencies'] as $dependency) {
|
||||||
|
if (isset($form[$dependency])) {
|
||||||
|
// Add themes to the module's required by list.
|
||||||
|
$form[$dependency]['#required_by'][] = $theme->status ? t('@theme', ['@theme (theme)' => $theme->info['name']]) : t('@theme (theme) (<span class="admin-disabled">disabled</span>)', ['@theme' => $theme->info['name']]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
$variables['modules'] = [];
|
$variables['modules'] = [];
|
||||||
// Iterate through all the modules, which are children of this element.
|
// Iterate through all the modules, which are children of this element.
|
||||||
foreach (Element::children($form) as $key) {
|
foreach (Element::children($form) as $key) {
|
||||||
|
@ -291,6 +304,12 @@ function template_preprocess_system_themes_page(&$variables) {
|
||||||
$current_theme['is_default'] = $theme->is_default;
|
$current_theme['is_default'] = $theme->is_default;
|
||||||
$current_theme['is_admin'] = $theme->is_admin;
|
$current_theme['is_admin'] = $theme->is_admin;
|
||||||
|
|
||||||
|
$current_theme['module_dependencies'] = !empty($theme->module_dependencies_list) ? [
|
||||||
|
'#theme' => 'item_list',
|
||||||
|
'#items' => $theme->module_dependencies_list,
|
||||||
|
'#context' => ['list_style' => 'comma-list'],
|
||||||
|
] : [];
|
||||||
|
|
||||||
// Make sure to provide feedback on compatibility.
|
// Make sure to provide feedback on compatibility.
|
||||||
$current_theme['incompatible'] = '';
|
$current_theme['incompatible'] = '';
|
||||||
if (!empty($theme->info['core_incompatible'])) {
|
if (!empty($theme->info['core_incompatible'])) {
|
||||||
|
@ -311,6 +330,20 @@ function template_preprocess_system_themes_page(&$variables) {
|
||||||
elseif (!empty($theme->incompatible_engine)) {
|
elseif (!empty($theme->incompatible_engine)) {
|
||||||
$current_theme['incompatible'] = t('This theme requires the theme engine @theme_engine to operate correctly.', ['@theme_engine' => $theme->info['engine']]);
|
$current_theme['incompatible'] = t('This theme requires the theme engine @theme_engine to operate correctly.', ['@theme_engine' => $theme->info['engine']]);
|
||||||
}
|
}
|
||||||
|
elseif (!empty($theme->incompatible_module)) {
|
||||||
|
$current_theme['incompatible'] = t('This theme requires the listed modules to operate correctly.');
|
||||||
|
}
|
||||||
|
elseif (!empty($theme->module_dependencies_disabled)) {
|
||||||
|
if (!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.');
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
$modules_url = (string) Url::fromRoute('system.modules_list')->toString();
|
||||||
|
$current_theme['incompatible'] = t('This theme requires the listed modules to operate correctly. They must first be enabled via the <a href=":modules_url">Extend page</a>.', [
|
||||||
|
':modules_url' => $modules_url,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Build operation links.
|
// Build operation links.
|
||||||
$current_theme['operations'] = [
|
$current_theme['operations'] = [
|
||||||
|
|
|
@ -889,12 +889,6 @@ function system_requirements($phase) {
|
||||||
$php_incompatible_extensions[$file->info['type']][] = $name;
|
$php_incompatible_extensions[$file->info['type']][] = $name;
|
||||||
}
|
}
|
||||||
|
|
||||||
// @todo Remove this 'if' block to allow checking requirements of themes
|
|
||||||
// https://www.drupal.org/project/drupal/issues/474684.
|
|
||||||
if ($file->info['type'] !== 'module') {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check the module's required modules.
|
// Check the module's required modules.
|
||||||
/** @var \Drupal\Core\Extension\Dependency $requirement */
|
/** @var \Drupal\Core\Extension\Dependency $requirement */
|
||||||
foreach ($file->requires as $requirement) {
|
foreach ($file->requires as $requirement) {
|
||||||
|
|
|
@ -22,6 +22,7 @@
|
||||||
* - notes: Identifies what context this theme is being used in, e.g.,
|
* - notes: Identifies what context this theme is being used in, e.g.,
|
||||||
* default theme, admin theme.
|
* default theme, admin theme.
|
||||||
* - incompatible: Text describing any compatibility issues.
|
* - incompatible: Text describing any compatibility issues.
|
||||||
|
* - module_dependencies: A list of modules that this theme requires.
|
||||||
* - operations: A list of operation links, e.g., Settings, Enable, Disable,
|
* - operations: A list of operation links, e.g., Settings, Enable, Disable,
|
||||||
* etc. these links should only be displayed if the theme is compatible.
|
* etc. these links should only be displayed if the theme is compatible.
|
||||||
*
|
*
|
||||||
|
@ -62,6 +63,11 @@
|
||||||
{%- endif -%}
|
{%- endif -%}
|
||||||
</h3>
|
</h3>
|
||||||
<div class="theme-info__description">{{ theme.description }}</div>
|
<div class="theme-info__description">{{ theme.description }}</div>
|
||||||
|
{% if theme.module_dependencies %}
|
||||||
|
<div class="theme-info__requires">
|
||||||
|
{{ 'Requires: @module_dependencies'|t({ '@module_dependencies': theme.module_dependencies|render }) }}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
{# Display operation links if the theme is compatible. #}
|
{# Display operation links if the theme is compatible. #}
|
||||||
{% if theme.incompatible %}
|
{% if theme.incompatible %}
|
||||||
<div class="incompatible">{{ theme.incompatible }}</div>
|
<div class="incompatible">{{ theme.incompatible }}</div>
|
||||||
|
|
|
@ -27,19 +27,14 @@ class ModulesListFormWebTest extends BrowserTestBase {
|
||||||
protected function setUp() {
|
protected function setUp() {
|
||||||
parent::setUp();
|
parent::setUp();
|
||||||
\Drupal::state()->set('system_test.module_hidden', FALSE);
|
\Drupal::state()->set('system_test.module_hidden', FALSE);
|
||||||
|
$this->drupalLogin($this->drupalCreateUser(['administer modules', 'administer permissions']));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Tests the module list form.
|
* Tests the module list form.
|
||||||
*/
|
*/
|
||||||
public function testModuleListForm() {
|
public function testModuleListForm() {
|
||||||
$this->drupalLogin(
|
|
||||||
$this->drupalCreateUser(
|
|
||||||
['administer modules', 'administer permissions']
|
|
||||||
)
|
|
||||||
);
|
|
||||||
$this->drupalGet('admin/modules');
|
$this->drupalGet('admin/modules');
|
||||||
$this->assertResponse('200');
|
|
||||||
|
|
||||||
// Check that system_test's configure link was rendered correctly.
|
// Check that system_test's configure link was rendered correctly.
|
||||||
$this->assertFieldByXPath("//a[contains(@href, '/system-test/configure/bar') and text()='Configure ']/span[contains(@class, 'visually-hidden') and text()='the System test module']");
|
$this->assertFieldByXPath("//a[contains(@href, '/system-test/configure/bar') and text()='Configure ']/span[contains(@class, 'visually-hidden') and text()='the System test module']");
|
||||||
|
@ -92,11 +87,6 @@ BROKEN,
|
||||||
'expected_error' => "'core: 9.x' is not supported. Use 'core_version_requirement' to specify core compatibility. Only 'core: 8.x' is supported to provide backwards compatibility for Drupal 8 when needed in $file_path",
|
'expected_error' => "'core: 9.x' is not supported. Use 'core_version_requirement' to specify core compatibility. Only 'core: 8.x' is supported to provide backwards compatibility for Drupal 8 when needed in $file_path",
|
||||||
],
|
],
|
||||||
];
|
];
|
||||||
$this->drupalLogin(
|
|
||||||
$this->drupalCreateUser(
|
|
||||||
['administer modules', 'administer permissions']
|
|
||||||
)
|
|
||||||
);
|
|
||||||
|
|
||||||
foreach ($broken_infos as $broken_info) {
|
foreach ($broken_infos as $broken_info) {
|
||||||
file_put_contents($file_path, $broken_info['yml']);
|
file_put_contents($file_path, $broken_info['yml']);
|
||||||
|
@ -120,4 +110,17 @@ BROKEN,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Confirm that module 'Required By' descriptions include dependent themes.
|
||||||
|
*/
|
||||||
|
public function testRequiredByThemeMessage() {
|
||||||
|
$this->drupalGet('admin/modules');
|
||||||
|
$module_theme_depends_on_description = $this->getSession()->getPage()->findAll('css', '#edit-modules-test-module-required-by-theme-enable-description .admin-requirements li:contains("Test Theme Depending on Modules (theme) (disabled)")');
|
||||||
|
// Confirm that that 'Test Theme Depending on Modules' is listed as being
|
||||||
|
// required by the module 'Test Module Required by Theme'.
|
||||||
|
$this->assertCount(1, $module_theme_depends_on_description);
|
||||||
|
// Confirm that the required by message does not appear anywhere else.
|
||||||
|
$this->assertSession()->pageTextContains('Test Theme Depending on Modules (Theme) (Disabled)');
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,321 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Drupal\Tests\system\Functional\Theme;
|
||||||
|
|
||||||
|
use Drupal\Tests\BrowserTestBase;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tests the theme UI.
|
||||||
|
*
|
||||||
|
* @group Theme
|
||||||
|
*/
|
||||||
|
class ThemeUiTest extends BrowserTestBase {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@inheritdoc}
|
||||||
|
*/
|
||||||
|
protected $defaultTheme = 'stark';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Modules used for testing.
|
||||||
|
*
|
||||||
|
* @var array
|
||||||
|
*/
|
||||||
|
protected $testModules = [
|
||||||
|
'help' => 'Help',
|
||||||
|
'test_module_required_by_theme' => 'Test Module Required by Theme',
|
||||||
|
'test_another_module_required_by_theme' => 'Test Another Module Required by Theme',
|
||||||
|
];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@inheritdoc}
|
||||||
|
*/
|
||||||
|
protected function setUp() {
|
||||||
|
parent::setUp();
|
||||||
|
|
||||||
|
$this->drupalLogin($this->drupalCreateUser([
|
||||||
|
'administer themes',
|
||||||
|
'administer modules',
|
||||||
|
]));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tests permissions for enabling themes depending on disabled modules.
|
||||||
|
*/
|
||||||
|
public function testModulePermissions() {
|
||||||
|
// Log in as a user without permission to enable modules.
|
||||||
|
$this->drupalLogin($this->drupalCreateUser([
|
||||||
|
'administer themes',
|
||||||
|
]));
|
||||||
|
$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 install page should not be reachable.
|
||||||
|
$this->drupalGet('admin/appearance/install?theme=test_theme_depending_on_modules');
|
||||||
|
$this->assertSession()->statusCodeEquals(404);
|
||||||
|
|
||||||
|
$this->drupalLogin($this->drupalCreateUser([
|
||||||
|
'administer themes',
|
||||||
|
'administer modules',
|
||||||
|
]));
|
||||||
|
$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 module dependencies.
|
||||||
|
*
|
||||||
|
* @param string $theme_name
|
||||||
|
* The name of the theme being tested.
|
||||||
|
* @param string[] $first_modules
|
||||||
|
* Machine names of first modules to enable.
|
||||||
|
* @param string[] $second_modules
|
||||||
|
* Machine names of second modules to enable.
|
||||||
|
* @param string[] $required_by_messages
|
||||||
|
* Expected messages when attempting to uninstall $module_names.
|
||||||
|
* @param string $base_theme_to_uninstall
|
||||||
|
* The name of the theme $theme_name has set as a base theme.
|
||||||
|
* @param string[] $base_theme_module_names
|
||||||
|
* Machine names of the modules required by $base_theme_to_uninstall.
|
||||||
|
*
|
||||||
|
* @dataProvider providerTestThemeInstallWithModuleDependencies
|
||||||
|
*/
|
||||||
|
public function testThemeInstallWithModuleDependencies($theme_name, array $first_modules, array $second_modules, array $required_by_messages, $base_theme_to_uninstall, array $base_theme_module_names) {
|
||||||
|
$assert_session = $this->assertSession();
|
||||||
|
$page = $this->getSession()->getPage();
|
||||||
|
$all_dependent_modules = array_merge($first_modules, $second_modules);
|
||||||
|
$this->drupalGet('admin/appearance');
|
||||||
|
$assert_module_enabled_message = function ($enabled_modules) {
|
||||||
|
$count = count($enabled_modules);
|
||||||
|
$module_enabled_text = $count === 1 ? "{$this->testModules[$enabled_modules[0]]} has been enabled." : $count . " modules have been enabled:";
|
||||||
|
$this->assertSession()->pageTextContains($module_enabled_text);
|
||||||
|
};
|
||||||
|
// All the modules should be listed as disabled.
|
||||||
|
foreach ($all_dependent_modules as $module) {
|
||||||
|
$expected_required_list_items[$module] = $this->testModules[$module] . " (disabled)";
|
||||||
|
}
|
||||||
|
$this->assertUninstallableTheme($expected_required_list_items, $theme_name);
|
||||||
|
|
||||||
|
// Enable the first group of dependee modules.
|
||||||
|
$first_module_form_post = [];
|
||||||
|
foreach ($first_modules as $module) {
|
||||||
|
$first_module_form_post["modules[$module][enable]"] = 1;
|
||||||
|
}
|
||||||
|
$this->drupalPostForm('admin/modules', $first_module_form_post, 'Install');
|
||||||
|
$assert_module_enabled_message($first_modules);
|
||||||
|
|
||||||
|
$this->drupalGet('admin/appearance');
|
||||||
|
|
||||||
|
// Confirm the theme is still uninstallable due to a remaining module
|
||||||
|
// dependency.
|
||||||
|
// The modules that have already been enabled will no longer be listed as
|
||||||
|
// disabled.
|
||||||
|
foreach ($first_modules as $module) {
|
||||||
|
$expected_required_list_items[$module] = $this->testModules[$module];
|
||||||
|
}
|
||||||
|
$this->assertUninstallableTheme($expected_required_list_items, $theme_name);
|
||||||
|
|
||||||
|
// Enable the second group of dependee modules.
|
||||||
|
$second_module_form_post = [];
|
||||||
|
foreach ($second_modules as $module) {
|
||||||
|
$second_module_form_post["modules[$module][enable]"] = 1;
|
||||||
|
}
|
||||||
|
$this->drupalPostForm('admin/modules', $second_module_form_post, 'Install');
|
||||||
|
$assert_module_enabled_message($second_modules);
|
||||||
|
|
||||||
|
// The theme should now be installable, so install it.
|
||||||
|
$this->drupalGet('admin/appearance');
|
||||||
|
$page->clickLink("Install $theme_name theme");
|
||||||
|
$assert_session->addressEquals('admin/appearance');
|
||||||
|
$assert_session->pageTextContains("The $theme_name theme has been installed");
|
||||||
|
|
||||||
|
// Confirm that the dependee modules can't be uninstalled because an enabled
|
||||||
|
// theme depends on them.
|
||||||
|
$this->drupalGet('admin/modules/uninstall');
|
||||||
|
foreach ($all_dependent_modules as $attribute) {
|
||||||
|
$assert_session->elementExists('css', "[name=\"uninstall[$attribute]\"][disabled]");
|
||||||
|
}
|
||||||
|
foreach ($required_by_messages as $selector => $message) {
|
||||||
|
$assert_session->elementTextContains('css', $selector, $message);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Uninstall the theme that depends on the modules, and confirm the modules
|
||||||
|
// can now be uninstalled.
|
||||||
|
$this->uninstallTheme($theme_name);
|
||||||
|
$this->drupalGet('admin/modules/uninstall');
|
||||||
|
|
||||||
|
// Only attempt to uninstall modules not required by the base theme.
|
||||||
|
$modules_to_uninstall = array_diff($all_dependent_modules, $base_theme_module_names);
|
||||||
|
$this->uninstallModules($modules_to_uninstall);
|
||||||
|
|
||||||
|
if (!empty($base_theme_to_uninstall)) {
|
||||||
|
$this->uninstallTheme($base_theme_to_uninstall);
|
||||||
|
$this->drupalGet('admin/modules/uninstall');
|
||||||
|
$this->uninstallModules($base_theme_module_names);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Uninstalls modules via the admin UI.
|
||||||
|
*
|
||||||
|
* @param string[] $module_names
|
||||||
|
* An array of module machine names.
|
||||||
|
*/
|
||||||
|
protected function uninstallModules(array $module_names) {
|
||||||
|
$assert_session = $this->assertSession();
|
||||||
|
$this->drupalGet('admin/modules/uninstall');
|
||||||
|
foreach ($module_names as $attribute) {
|
||||||
|
$assert_session->elementExists('css', "[name=\"uninstall[$attribute]\"]:not([disabled])");
|
||||||
|
}
|
||||||
|
$to_uninstall = [];
|
||||||
|
foreach ($module_names as $attribute) {
|
||||||
|
$to_uninstall["uninstall[$attribute]"] = 1;
|
||||||
|
}
|
||||||
|
if (!empty($to_uninstall)) {
|
||||||
|
$this->drupalPostForm('admin/modules/uninstall', $to_uninstall, 'Uninstall');
|
||||||
|
$assert_session->pageTextContains('The following modules will be completely uninstalled from your site, and all data from these modules will be lost!');
|
||||||
|
$assert_session->pageTextContains('Would you like to continue with uninstalling the above?');
|
||||||
|
foreach ($module_names as $module_name) {
|
||||||
|
$assert_session->pageTextContains($this->testModules[$module_name]);
|
||||||
|
}
|
||||||
|
$this->getSession()->getPage()->pressButton('Uninstall');
|
||||||
|
$assert_session->pageTextContains('The selected modules have been uninstalled.');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Uninstalls a theme via the admin UI.
|
||||||
|
*
|
||||||
|
* @param string $theme_name
|
||||||
|
* The theme name.
|
||||||
|
*/
|
||||||
|
protected function uninstallTheme($theme_name) {
|
||||||
|
$this->drupalGet('admin/appearance');
|
||||||
|
$this->clickLink("Uninstall $theme_name theme");
|
||||||
|
$this->assertSession()->pageTextContains("The $theme_name theme has been uninstalled.");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Data provider for testThemeInstallWithModuleDependencies().
|
||||||
|
*
|
||||||
|
* @return array
|
||||||
|
* An array of arrays. Details on the specific elements can be found in the
|
||||||
|
* function body.
|
||||||
|
*/
|
||||||
|
public function providerTestThemeInstallWithModuleDependencies() {
|
||||||
|
// Data provider values with the following keys:
|
||||||
|
// -'theme_name': The name of the theme being tested.
|
||||||
|
// -'first_modules': Array of module machine names to enable first.
|
||||||
|
// -'second_modules': Array of module machine names to enable second.
|
||||||
|
// -'required_by_messages': Array for checking the messages explaining why a
|
||||||
|
// module can't be uninstalled. The array key is the selector where the
|
||||||
|
// message should appear, the array value is the expected message.
|
||||||
|
// -'base_theme_to_uninstall': The name of a base theme that needs to be
|
||||||
|
// uninstalled before modules it depends on can be uninstalled.
|
||||||
|
// -'base_theme_module_names': Array of machine names of the modules
|
||||||
|
// required by base_theme_to_uninstall.
|
||||||
|
return [
|
||||||
|
'test theme with a module dependency and base theme with a different module dependency' => [
|
||||||
|
'theme_name' => 'Test Theme with a Module Dependency and Base Theme with a Different Module Dependency',
|
||||||
|
'first_modules' => [
|
||||||
|
'test_module_required_by_theme',
|
||||||
|
'test_another_module_required_by_theme',
|
||||||
|
],
|
||||||
|
'second_modules' => [
|
||||||
|
'help',
|
||||||
|
],
|
||||||
|
'required_by_messages' => [
|
||||||
|
'[data-drupal-selector="edit-test-another-module-required-by-theme"] .item-list' => 'Required by the theme: Test Theme Depending on Modules',
|
||||||
|
'[data-drupal-selector="edit-test-module-required-by-theme"] .item-list' => 'Required by the theme: Test Theme Depending on Modules',
|
||||||
|
'[data-drupal-selector="edit-help"] .item-list' => 'Required by the theme: Test Theme with a Module Dependency and Base Theme with a Different Module Dependency',
|
||||||
|
],
|
||||||
|
'base_theme_to_uninstall' => 'Test Theme Depending on Modules',
|
||||||
|
'base_theme_module_names' => [
|
||||||
|
'test_module_required_by_theme',
|
||||||
|
'test_another_module_required_by_theme',
|
||||||
|
],
|
||||||
|
],
|
||||||
|
'Test Theme Depending on Modules' => [
|
||||||
|
'theme_name' => 'Test Theme Depending on Modules',
|
||||||
|
'first_modules' => [
|
||||||
|
'test_module_required_by_theme',
|
||||||
|
],
|
||||||
|
'second_modules' => [
|
||||||
|
'test_another_module_required_by_theme',
|
||||||
|
],
|
||||||
|
'required_by_messages' => [
|
||||||
|
'[data-drupal-selector="edit-test-another-module-required-by-theme"] .item-list' => 'Required by the theme: Test Theme Depending on Modules',
|
||||||
|
'[data-drupal-selector="edit-test-module-required-by-theme"] .item-list' => 'Required by the theme: Test Theme Depending on Modules',
|
||||||
|
],
|
||||||
|
'base_theme_to_uninstall' => '',
|
||||||
|
'base_theme_module_names' => [],
|
||||||
|
],
|
||||||
|
'test theme with a base theme depending on modules' => [
|
||||||
|
'theme_name' => 'Test Theme with a Base Theme Depending on Modules',
|
||||||
|
'first_modules' => [
|
||||||
|
'test_module_required_by_theme',
|
||||||
|
],
|
||||||
|
'second_modules' => [
|
||||||
|
'test_another_module_required_by_theme',
|
||||||
|
],
|
||||||
|
'required_by_messages' => [
|
||||||
|
'[data-drupal-selector="edit-test-another-module-required-by-theme"] .item-list' => 'Required by the theme: Test Theme Depending on Modules',
|
||||||
|
'[data-drupal-selector="edit-test-module-required-by-theme"] .item-list' => 'Required by the theme: Test Theme Depending on Modules',
|
||||||
|
],
|
||||||
|
'base_theme_to_uninstall' => 'Test Theme Depending on Modules',
|
||||||
|
'base_theme_module_names' => [
|
||||||
|
'test_module_required_by_theme',
|
||||||
|
'test_another_module_required_by_theme',
|
||||||
|
],
|
||||||
|
],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks related to uninstallable themes due to module dependencies.
|
||||||
|
*
|
||||||
|
* @param string[] $expected_requires_list_items
|
||||||
|
* The modules listed as being required to install the theme.
|
||||||
|
* @param string $theme_name
|
||||||
|
* The name of the theme.
|
||||||
|
*/
|
||||||
|
protected function assertUninstallableTheme(array $expected_requires_list_items, $theme_name) {
|
||||||
|
$theme_container = $this->getSession()->getPage()->find('css', "h3:contains(\"$theme_name\")")->getParent();
|
||||||
|
$requires_list_items = $theme_container->findAll('css', '.theme-info__requires li');
|
||||||
|
$this->assertCount(count($expected_requires_list_items), $requires_list_items);
|
||||||
|
|
||||||
|
foreach ($requires_list_items as $key => $item) {
|
||||||
|
$this->assertTrue(in_array($item->getText(), $expected_requires_list_items));
|
||||||
|
}
|
||||||
|
|
||||||
|
$incompatible = $theme_container->find('css', '.incompatible');
|
||||||
|
$expected_incompatible_text = 'This theme requires the listed modules to operate correctly. They must first be enabled via the Extend page.';
|
||||||
|
$this->assertSame($expected_incompatible_text, $incompatible->getText());
|
||||||
|
$this->assertFalse($theme_container->hasLink('Install Test Theme Depending on Modules theme'));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tests installing a theme with missing module dependencies.
|
||||||
|
*/
|
||||||
|
public function testInstallModuleWithMissingDependencies() {
|
||||||
|
$this->drupalGet('admin/appearance');
|
||||||
|
$theme_container = $this->getSession()->getPage()->find('css', 'h3:contains("Test Theme Depending on Nonexisting Module")')->getParent();
|
||||||
|
$this->assertContains('Requires: test_module_non_existing (missing)', $theme_container->getText());
|
||||||
|
$this->assertContains('This theme requires the listed modules to operate correctly.', $theme_container->getText());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tests installing a theme with incompatible module dependencies.
|
||||||
|
*/
|
||||||
|
public function testInstallModuleWithIncompatibleDependencies() {
|
||||||
|
$this->container->get('module_installer')->install(['test_module_compatible_constraint', 'test_module_incompatible_constraint']);
|
||||||
|
$this->drupalGet('admin/appearance');
|
||||||
|
$theme_container = $this->getSession()->getPage()->find('css', 'h3:contains("Test Theme Depending on Version Constrained Modules")')->getParent();
|
||||||
|
$this->assertContains('Requires: Test Module Theme Depends on with Compatible ConstraintTest Module Theme Depends on with Incompatible Constraint (>=8.x-2.x) (incompatible with version 8.x-1.8)', $theme_container->getText());
|
||||||
|
$this->assertContains('This theme requires the listed modules to operate correctly.', $theme_container->getText());
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -24,7 +24,13 @@ class UpdateScriptTest extends BrowserTestBase {
|
||||||
*
|
*
|
||||||
* @var array
|
* @var array
|
||||||
*/
|
*/
|
||||||
public static $modules = ['update_script_test', 'dblog', 'language'];
|
protected static $modules = [
|
||||||
|
'update_script_test',
|
||||||
|
'dblog',
|
||||||
|
'language',
|
||||||
|
'test_module_required_by_theme',
|
||||||
|
'test_another_module_required_by_theme',
|
||||||
|
];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* {@inheritdoc}
|
* {@inheritdoc}
|
||||||
|
@ -61,7 +67,11 @@ class UpdateScriptTest extends BrowserTestBase {
|
||||||
parent::setUp();
|
parent::setUp();
|
||||||
$this->updateUrl = Url::fromRoute('system.db_update');
|
$this->updateUrl = Url::fromRoute('system.db_update');
|
||||||
$this->statusReportUrl = Url::fromRoute('system.status');
|
$this->statusReportUrl = Url::fromRoute('system.status');
|
||||||
$this->updateUser = $this->drupalCreateUser(['administer software updates', 'access site in maintenance mode']);
|
$this->updateUser = $this->drupalCreateUser([
|
||||||
|
'administer software updates',
|
||||||
|
'access site in maintenance mode',
|
||||||
|
'administer themes',
|
||||||
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -175,6 +185,31 @@ class UpdateScriptTest extends BrowserTestBase {
|
||||||
$this->drupalGet($this->updateUrl, ['external' => TRUE]);
|
$this->drupalGet($this->updateUrl, ['external' => TRUE]);
|
||||||
$this->assertSession()->assertEscaped('Node (Version <7.x-0.0-dev required)');
|
$this->assertSession()->assertEscaped('Node (Version <7.x-0.0-dev required)');
|
||||||
$this->assertSession()->responseContains('Update script test requires this module and version. Currently using Node version ' . \Drupal::VERSION);
|
$this->assertSession()->responseContains('Update script test requires this module and version. Currently using Node version ' . \Drupal::VERSION);
|
||||||
|
|
||||||
|
// Test that issues with modules that themes depend on are properly
|
||||||
|
// displayed.
|
||||||
|
$this->assertSession()->responseNotContains('Test Module Required by Theme');
|
||||||
|
$this->drupalGet('admin/appearance');
|
||||||
|
$this->getSession()->getPage()->clickLink('Install Test Theme Depending on Modules theme');
|
||||||
|
$this->assertSession()->addressEquals('admin/appearance');
|
||||||
|
$this->assertSession()->pageTextContains('The Test Theme Depending on Modules theme has been installed');
|
||||||
|
|
||||||
|
// Ensure that when a theme depends on a module and that module's
|
||||||
|
// requirements change, errors are displayed in the same manner as modules
|
||||||
|
// depending on other modules.
|
||||||
|
\Drupal::state()->set('test_theme_depending_on_modules.system_info_alter', ['dependencies' => ['test_module_required_by_theme (<7.x-0.0-dev)']]);
|
||||||
|
$this->drupalGet($this->updateUrl, ['external' => TRUE]);
|
||||||
|
$this->assertSession()->assertEscaped('Test Module Required by Theme (Version <7.x-0.0-dev required)');
|
||||||
|
$this->assertSession()->responseContains('Test Theme Depending on Modules requires this module and version. Currently using Test Module Required by Theme version ' . \Drupal::VERSION);
|
||||||
|
|
||||||
|
// Ensure that when a theme is updated to depend on an unavailable module,
|
||||||
|
// errors are displayed in the same manner as modules depending on other
|
||||||
|
// modules.
|
||||||
|
\Drupal::state()->set('test_theme_depending_on_modules.system_info_alter', ['dependencies' => ['a_module_theme_needs_that_does_not_exist']]);
|
||||||
|
$this->drupalGet($this->updateUrl, ['external' => TRUE]);
|
||||||
|
$this->assertSession()->responseContains('a_module_theme_needs_that_does_not_exist (Missing)');
|
||||||
|
$this->assertSession()->responseContains('Test Theme Depending on Modules requires this module.');
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -0,0 +1,4 @@
|
||||||
|
name: Test Module Theme Depends on with Compatible Constraint
|
||||||
|
type: module
|
||||||
|
package: Testing
|
||||||
|
version: '8.x-1.2'
|
|
@ -0,0 +1,4 @@
|
||||||
|
name: Test Module Theme Depends on with Incompatible Constraint
|
||||||
|
type: module
|
||||||
|
package: Testing
|
||||||
|
version: '8.x-1.8'
|
|
@ -0,0 +1,6 @@
|
||||||
|
name: Test Theme Depending on Version Constrained Modules
|
||||||
|
type: theme
|
||||||
|
base theme: stark
|
||||||
|
dependencies:
|
||||||
|
- test_module_compatible_constraint (>=8.x-1.x)
|
||||||
|
- test_module_incompatible_constraint (>=8.x-2.x)
|
|
@ -0,0 +1,4 @@
|
||||||
|
name: Test Another Module Required by Theme
|
||||||
|
type: module
|
||||||
|
package: Testing
|
||||||
|
version: VERSION
|
|
@ -0,0 +1,4 @@
|
||||||
|
name: Test Module Required by Theme
|
||||||
|
type: module
|
||||||
|
package: Testing
|
||||||
|
version: VERSION
|
|
@ -0,0 +1,20 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @file
|
||||||
|
* This file provides testing functionality for update.php.
|
||||||
|
*/
|
||||||
|
|
||||||
|
use Drupal\Core\Extension\Extension;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Implements hook_system_info_alter().
|
||||||
|
*/
|
||||||
|
function test_module_required_by_theme_system_info_alter(array &$info, Extension $file, $type) {
|
||||||
|
if ($file->getName() == 'test_theme_depending_on_modules') {
|
||||||
|
$new_info = \Drupal::state()->get('test_theme_depending_on_modules.system_info_alter');
|
||||||
|
if ($new_info) {
|
||||||
|
$info = $new_info + $info;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,6 @@
|
||||||
|
name: Test Theme Depending on Modules
|
||||||
|
type: theme
|
||||||
|
base theme: stark
|
||||||
|
dependencies:
|
||||||
|
- test_module_required_by_theme
|
||||||
|
- test_another_module_required_by_theme
|
|
@ -0,0 +1,6 @@
|
||||||
|
name: Test Theme Depending on Nonexisting Module
|
||||||
|
type: theme
|
||||||
|
base theme: stark
|
||||||
|
version: VERSION
|
||||||
|
dependencies:
|
||||||
|
- test_module_non_existing
|
|
@ -0,0 +1,5 @@
|
||||||
|
name: Test Theme with a Module Dependency and Base Theme with a Different Module Dependency
|
||||||
|
type: theme
|
||||||
|
base theme: test_theme_depending_on_modules
|
||||||
|
dependencies:
|
||||||
|
- help
|
|
@ -0,0 +1,3 @@
|
||||||
|
name: Test Theme with a Base Theme Depending on Modules
|
||||||
|
type: theme
|
||||||
|
base theme: test_theme_depending_on_modules
|
|
@ -4,6 +4,8 @@ namespace Drupal\KernelTests\Core\Theme;
|
||||||
|
|
||||||
use Drupal\Core\DependencyInjection\ContainerBuilder;
|
use Drupal\Core\DependencyInjection\ContainerBuilder;
|
||||||
use Drupal\Core\Extension\ExtensionNameLengthException;
|
use Drupal\Core\Extension\ExtensionNameLengthException;
|
||||||
|
use Drupal\Core\Extension\MissingDependencyException;
|
||||||
|
use Drupal\Core\Extension\ModuleUninstallValidatorException;
|
||||||
use Drupal\Core\Extension\Exception\UnknownExtensionException;
|
use Drupal\Core\Extension\Exception\UnknownExtensionException;
|
||||||
use Drupal\KernelTests\KernelTestBase;
|
use Drupal\KernelTests\KernelTestBase;
|
||||||
|
|
||||||
|
@ -137,6 +139,79 @@ class ThemeInstallerTest extends KernelTestBase {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tests installing a theme with unmet module dependencies.
|
||||||
|
*
|
||||||
|
* @dataProvider providerTestInstallThemeWithUnmetModuleDependencies
|
||||||
|
*/
|
||||||
|
public function testInstallThemeWithUnmetModuleDependencies($theme_name, $installed_modules, $message) {
|
||||||
|
$this->container->get('module_installer')->install($installed_modules);
|
||||||
|
$themes = $this->themeHandler()->listInfo();
|
||||||
|
$this->assertEmpty($themes);
|
||||||
|
$this->expectException(MissingDependencyException::class);
|
||||||
|
$this->expectExceptionMessage($message);
|
||||||
|
$this->themeInstaller()->install([$theme_name]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Data provider for testInstallThemeWithUnmetModuleDependencies().
|
||||||
|
*/
|
||||||
|
public function providerTestInstallThemeWithUnmetModuleDependencies() {
|
||||||
|
return [
|
||||||
|
'theme with uninstalled module dependencies' => [
|
||||||
|
'test_theme_depending_on_modules',
|
||||||
|
[],
|
||||||
|
"Unable to install theme: 'test_theme_depending_on_modules' due to unmet module dependencies: 'test_module_required_by_theme, test_another_module_required_by_theme'.",
|
||||||
|
],
|
||||||
|
'theme with a base theme with uninstalled module dependencies' => [
|
||||||
|
'test_theme_with_a_base_theme_depending_on_modules',
|
||||||
|
[],
|
||||||
|
"Unable to install theme: 'test_theme_with_a_base_theme_depending_on_modules' due to unmet module dependencies: 'test_module_required_by_theme, test_another_module_required_by_theme'.",
|
||||||
|
],
|
||||||
|
'theme and base theme have uninstalled module dependencies' => [
|
||||||
|
'test_theme_mixed_module_dependencies',
|
||||||
|
[],
|
||||||
|
"Unable to install theme: 'test_theme_mixed_module_dependencies' due to unmet module dependencies: 'help, test_module_required_by_theme, test_another_module_required_by_theme'.",
|
||||||
|
],
|
||||||
|
'theme with already installed module dependencies, base theme module dependencies are not installed' => [
|
||||||
|
'test_theme_mixed_module_dependencies',
|
||||||
|
['help'],
|
||||||
|
"Unable to install theme: 'test_theme_mixed_module_dependencies' due to unmet module dependencies: 'test_module_required_by_theme, test_another_module_required_by_theme'.",
|
||||||
|
],
|
||||||
|
'theme with module dependencies not installed, base theme module dependencies are already installed, ' => [
|
||||||
|
'test_theme_mixed_module_dependencies',
|
||||||
|
['test_module_required_by_theme', 'test_another_module_required_by_theme'],
|
||||||
|
"Unable to install theme: 'test_theme_mixed_module_dependencies' due to unmet module dependencies: 'help'.",
|
||||||
|
],
|
||||||
|
'theme depending on a module that does not exist' => [
|
||||||
|
'test_theme_depending_on_nonexisting_module',
|
||||||
|
[],
|
||||||
|
"Unable to install theme: 'test_theme_depending_on_nonexisting_module' due to unmet module dependencies: 'test_module_non_existing",
|
||||||
|
],
|
||||||
|
'theme depending on an installed but incompatible module' => [
|
||||||
|
'test_theme_depending_on_constrained_modules',
|
||||||
|
['test_module_compatible_constraint', 'test_module_incompatible_constraint'],
|
||||||
|
"Unable to install theme: Test Module Theme Depends on with Incompatible Constraint (>=8.x-2.x) (incompatible with version 8.x-1.8)",
|
||||||
|
],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tests installing a theme with module dependencies that are met.
|
||||||
|
*/
|
||||||
|
public function testInstallThemeWithMetModuleDependencies() {
|
||||||
|
$name = 'test_theme_depending_on_modules';
|
||||||
|
$themes = $this->themeHandler()->listInfo();
|
||||||
|
$this->assertArrayNotHasKey($name, $themes);
|
||||||
|
$this->container->get('module_installer')->install(['test_module_required_by_theme', 'test_another_module_required_by_theme']);
|
||||||
|
$this->themeInstaller()->install([$name]);
|
||||||
|
$themes = $this->themeHandler()->listInfo();
|
||||||
|
$this->assertArrayHasKey($name, $themes);
|
||||||
|
$this->expectException(ModuleUninstallValidatorException::class);
|
||||||
|
$this->expectExceptionMessage('The following reasons prevent the modules from being uninstalled: Required by the theme: Test Theme Depending on Modules');
|
||||||
|
$this->container->get('module_installer')->uninstall(['test_module_required_by_theme']);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Tests uninstalling the default theme.
|
* Tests uninstalling the default theme.
|
||||||
*/
|
*/
|
||||||
|
|
|
@ -0,0 +1,161 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Drupal\Tests\Core\Extension;
|
||||||
|
|
||||||
|
use Drupal\Core\Extension\ModuleExtensionList;
|
||||||
|
use Drupal\Core\Extension\ModuleRequiredByThemesUninstallValidator;
|
||||||
|
use Drupal\Core\Extension\ThemeExtensionList;
|
||||||
|
use Drupal\Tests\AssertHelperTrait;
|
||||||
|
use Drupal\Tests\UnitTestCase;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @coversDefaultClass \Drupal\Core\Extension\ModuleRequiredByThemesUninstallValidator
|
||||||
|
* @group Extension
|
||||||
|
*/
|
||||||
|
class ModuleRequiredByThemesUninstallValidatorTest extends UnitTestCase {
|
||||||
|
|
||||||
|
use AssertHelperTrait;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Instance of ModuleRequiredByThemesUninstallValidator.
|
||||||
|
*
|
||||||
|
* @var \Drupal\Core\Extension\ModuleRequiredByThemesUninstallValidator
|
||||||
|
*/
|
||||||
|
protected $moduleRequiredByThemeUninstallValidator;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Mock of ModuleExtensionList.
|
||||||
|
*
|
||||||
|
* @var \Drupal\Core\Extension\ModuleExtensionList
|
||||||
|
*/
|
||||||
|
protected $moduleExtensionList;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Mock of ThemeExtensionList.
|
||||||
|
*
|
||||||
|
* @var \Drupal\Core\Extension\ThemeExtensionList
|
||||||
|
*/
|
||||||
|
protected $themeExtensionList;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@inheritdoc}
|
||||||
|
*/
|
||||||
|
protected function setUp() {
|
||||||
|
parent::setUp();
|
||||||
|
$this->moduleExtensionList = $this->prophesize(ModuleExtensionList::class);
|
||||||
|
$this->themeExtensionList = $this->prophesize(ThemeExtensionList::class);
|
||||||
|
$this->moduleRequiredByThemeUninstallValidator = new ModuleRequiredByThemesUninstallValidator($this->getStringTranslationStub(), $this->moduleExtensionList->reveal(), $this->themeExtensionList->reveal());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @covers ::validate
|
||||||
|
*/
|
||||||
|
public function testValidateNoThemeDependency() {
|
||||||
|
$this->themeExtensionList->getAllInstalledInfo()->willReturn([
|
||||||
|
'stable' => [
|
||||||
|
'name' => 'Stable',
|
||||||
|
'dependencies' => [],
|
||||||
|
],
|
||||||
|
'claro' => [
|
||||||
|
'name' => 'Claro',
|
||||||
|
'dependencies' => [],
|
||||||
|
],
|
||||||
|
]);
|
||||||
|
|
||||||
|
$module = $this->randomMachineName();
|
||||||
|
$expected = [];
|
||||||
|
$reasons = $this->moduleRequiredByThemeUninstallValidator->validate($module);
|
||||||
|
$this->assertSame($expected, $reasons);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @covers ::validate
|
||||||
|
*/
|
||||||
|
public function testValidateOneThemeDependency() {
|
||||||
|
$module = 'single_module';
|
||||||
|
$module_name = 'Single Module';
|
||||||
|
$theme = 'one_theme';
|
||||||
|
$theme_name = 'One Theme';
|
||||||
|
$this->themeExtensionList->getAllInstalledInfo()->willReturn([
|
||||||
|
'stable' => [
|
||||||
|
'name' => 'Stable',
|
||||||
|
'dependencies' => [],
|
||||||
|
],
|
||||||
|
'claro' => [
|
||||||
|
'name' => 'Claro',
|
||||||
|
'dependencies' => [],
|
||||||
|
],
|
||||||
|
$theme => [
|
||||||
|
'name' => $theme_name,
|
||||||
|
'dependencies' => [
|
||||||
|
$module,
|
||||||
|
],
|
||||||
|
],
|
||||||
|
]);
|
||||||
|
|
||||||
|
$this->moduleExtensionList->get($module)->willReturn((object) [
|
||||||
|
'info' => [
|
||||||
|
'name' => $module_name,
|
||||||
|
],
|
||||||
|
]);
|
||||||
|
|
||||||
|
$expected = [
|
||||||
|
"Required by the theme: $theme_name",
|
||||||
|
];
|
||||||
|
|
||||||
|
$reasons = $this->moduleRequiredByThemeUninstallValidator->validate($module);
|
||||||
|
$this->assertSame($expected, $this->castSafeStrings($reasons));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @covers ::validate
|
||||||
|
*/
|
||||||
|
public function testValidateTwoThemeDependencies() {
|
||||||
|
$module = 'popular_module';
|
||||||
|
$module_name = 'Popular Module';
|
||||||
|
$theme1 = 'first_theme';
|
||||||
|
$theme2 = 'second_theme';
|
||||||
|
$theme_name_1 = 'First Theme';
|
||||||
|
$theme_name_2 = 'Second Theme';
|
||||||
|
$this->themeExtensionList->getAllInstalledInfo()->willReturn([
|
||||||
|
'stable' => [
|
||||||
|
'name' => 'Stable',
|
||||||
|
'dependencies' => [],
|
||||||
|
],
|
||||||
|
'claro' => [
|
||||||
|
'name' => 'Claro',
|
||||||
|
'dependencies' => [],
|
||||||
|
],
|
||||||
|
$theme1 => [
|
||||||
|
'name' => $theme_name_1,
|
||||||
|
'dependencies' => [
|
||||||
|
$module,
|
||||||
|
],
|
||||||
|
],
|
||||||
|
$theme2 => [
|
||||||
|
'name' => $theme_name_2,
|
||||||
|
'dependencies' => [
|
||||||
|
$module,
|
||||||
|
],
|
||||||
|
],
|
||||||
|
]);
|
||||||
|
|
||||||
|
$this->moduleExtensionList->get($module)->willReturn((object) [
|
||||||
|
'info' => [
|
||||||
|
'name' => $module_name,
|
||||||
|
],
|
||||||
|
]);
|
||||||
|
|
||||||
|
$expected = [
|
||||||
|
"Required by the themes: $theme_name_1, $theme_name_2",
|
||||||
|
];
|
||||||
|
|
||||||
|
$reasons = $this->moduleRequiredByThemeUninstallValidator->validate($module);
|
||||||
|
$this->assertSame($expected, $this->castSafeStrings($reasons));
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!defined('DRUPAL_MINIMUM_PHP')) {
|
||||||
|
define('DRUPAL_MINIMUM_PHP', '7.3.0');
|
||||||
|
}
|
|
@ -22,6 +22,7 @@
|
||||||
* - notes: Identifies what context this theme is being used in, e.g.,
|
* - notes: Identifies what context this theme is being used in, e.g.,
|
||||||
* default theme, admin theme.
|
* default theme, admin theme.
|
||||||
* - incompatible: Text describing any compatibility issues.
|
* - incompatible: Text describing any compatibility issues.
|
||||||
|
* - module_dependencies: A list of modules that this theme requires.
|
||||||
* - operations: A list of operation links, e.g., Settings, Enable, Disable,
|
* - operations: A list of operation links, e.g., Settings, Enable, Disable,
|
||||||
* etc. these links should only be displayed if the theme is compatible.
|
* etc. these links should only be displayed if the theme is compatible.
|
||||||
* - title_id: The unique id of the theme label.
|
* - title_id: The unique id of the theme label.
|
||||||
|
@ -97,6 +98,11 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="card__footer">
|
<div class="card__footer">
|
||||||
|
{% if theme.module_dependencies %}
|
||||||
|
<div class="theme-info__requires">
|
||||||
|
{{ 'Requires: @module_dependencies'|t({ '@module_dependencies': theme.module_dependencies|render }) }}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
{# Display operation links only if the theme is compatible. #}
|
{# Display operation links only if the theme is compatible. #}
|
||||||
{% if theme.incompatible %}
|
{% if theme.incompatible %}
|
||||||
<small class="incompatible">{{ theme.incompatible }}</small>
|
<small class="incompatible">{{ theme.incompatible }}</small>
|
||||||
|
|
|
@ -22,6 +22,7 @@
|
||||||
* - notes: Identifies what context this theme is being used in, e.g.,
|
* - notes: Identifies what context this theme is being used in, e.g.,
|
||||||
* default theme, admin theme.
|
* default theme, admin theme.
|
||||||
* - incompatible: Text describing any compatibility issues.
|
* - incompatible: Text describing any compatibility issues.
|
||||||
|
* - module_dependencies: A list of modules that this theme requires.
|
||||||
* - operations: A list of operation links, e.g., Settings, Enable, Disable,
|
* - operations: A list of operation links, e.g., Settings, Enable, Disable,
|
||||||
* etc. these links should only be displayed if the theme is compatible.
|
* etc. these links should only be displayed if the theme is compatible.
|
||||||
*
|
*
|
||||||
|
@ -60,6 +61,11 @@
|
||||||
{%- endif -%}
|
{%- endif -%}
|
||||||
</h3>
|
</h3>
|
||||||
<div class="theme-info__description">{{ theme.description }}</div>
|
<div class="theme-info__description">{{ theme.description }}</div>
|
||||||
|
{% if theme.module_dependencies %}
|
||||||
|
<div class="theme-info__requires">
|
||||||
|
{{ 'Requires: @module_dependencies'|t({ '@module_dependencies': theme.module_dependencies|render }) }}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
{# Display operation links if the theme is compatible. #}
|
{# Display operation links if the theme is compatible. #}
|
||||||
{% if theme.incompatible %}
|
{% if theme.incompatible %}
|
||||||
<div class="incompatible">{{ theme.incompatible }}</div>
|
<div class="incompatible">{{ theme.incompatible }}</div>
|
||||||
|
|
Loading…
Reference in New Issue