123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326 |
- <?php
- namespace Drupal\Core\Extension;
- use Drupal\Component\Utility\Html;
- use Drupal\Core\Asset\AssetCollectionOptimizerInterface;
- use Drupal\Core\Cache\Cache;
- use Drupal\Core\Config\ConfigFactoryInterface;
- use Drupal\Core\Config\ConfigInstallerInterface;
- use Drupal\Core\Config\ConfigManagerInterface;
- use Drupal\Core\Extension\Exception\UnknownExtensionException;
- use Drupal\Core\Routing\RouteBuilderInterface;
- use Drupal\Core\State\StateInterface;
- use Drupal\Core\StringTranslation\StringTranslationTrait;
- use Drupal\system\ModuleDependencyMessageTrait;
- use Psr\Log\LoggerInterface;
- /**
- * Manages theme installation/uninstallation.
- */
- class ThemeInstaller implements ThemeInstallerInterface {
- use ModuleDependencyMessageTrait;
- use StringTranslationTrait;
- /**
- * @var \Drupal\Core\Extension\ThemeHandlerInterface
- */
- protected $themeHandler;
- /**
- * @var \Drupal\Core\Config\ConfigFactoryInterface
- */
- protected $configFactory;
- /**
- * @var \Drupal\Core\Config\ConfigInstallerInterface
- */
- protected $configInstaller;
- /**
- * @var \Drupal\Core\Extension\ModuleHandlerInterface
- */
- protected $moduleHandler;
- /**
- * @var \Drupal\Core\State\StateInterface
- */
- protected $state;
- /**
- * @var \Drupal\Core\Config\ConfigManagerInterface
- */
- protected $configManager;
- /**
- * @var \Drupal\Core\Asset\AssetCollectionOptimizerInterface
- */
- protected $cssCollectionOptimizer;
- /**
- * @var \Drupal\Core\Routing\RouteBuilderInterface
- */
- protected $routeBuilder;
- /**
- * @var \Psr\Log\LoggerInterface
- */
- protected $logger;
- /**
- * The module extension list.
- *
- * @var \Drupal\Core\Extension\ModuleExtensionList
- */
- protected $moduleExtensionList;
- /**
- * Constructs a new ThemeInstaller.
- *
- * @param \Drupal\Core\Extension\ThemeHandlerInterface $theme_handler
- * The theme handler.
- * @param \Drupal\Core\Config\ConfigFactoryInterface $config_factory
- * The config factory to get the installed themes.
- * @param \Drupal\Core\Config\ConfigInstallerInterface $config_installer
- * (optional) The config installer to install configuration. This optional
- * to allow the theme handler to work before Drupal is installed and has a
- * database.
- * @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler
- * The module handler to fire themes_installed/themes_uninstalled hooks.
- * @param \Drupal\Core\Config\ConfigManagerInterface $config_manager
- * The config manager used to uninstall a theme.
- * @param \Drupal\Core\Asset\AssetCollectionOptimizerInterface $css_collection_optimizer
- * The CSS asset collection optimizer service.
- * @param \Drupal\Core\Routing\RouteBuilderInterface $route_builder
- * (optional) The route builder service to rebuild the routes if a theme is
- * installed.
- * @param \Psr\Log\LoggerInterface $logger
- * A logger instance.
- * @param \Drupal\Core\State\StateInterface $state
- * 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, ModuleExtensionList $module_extension_list = NULL) {
- $this->themeHandler = $theme_handler;
- $this->configFactory = $config_factory;
- $this->configInstaller = $config_installer;
- $this->moduleHandler = $module_handler;
- $this->configManager = $config_manager;
- $this->cssCollectionOptimizer = $css_collection_optimizer;
- $this->routeBuilder = $route_builder;
- $this->logger = $logger;
- $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;
- }
- /**
- * {@inheritdoc}
- */
- public function install(array $theme_list, $install_dependencies = TRUE) {
- $extension_config = $this->configFactory->getEditable('core.extension');
- $theme_data = $this->themeHandler->rebuildThemeData();
- $installed_themes = $extension_config->get('theme') ?: [];
- $installed_modules = $extension_config->get('module') ?: [];
- if ($install_dependencies) {
- $theme_list = array_combine($theme_list, $theme_list);
- if ($missing = array_diff_key($theme_list, $theme_data)) {
- // One or more of the given themes doesn't exist.
- throw new UnknownExtensionException('Unknown themes: ' . implode(', ', $missing) . '.');
- }
- // Only process themes that are not installed currently.
- if (!$theme_list = array_diff_key($theme_list, $installed_themes)) {
- // Nothing to do. All themes already installed.
- return TRUE;
- }
- $module_list = $this->moduleExtensionList->getList();
- foreach ($theme_list as $theme => $value) {
- $module_dependencies = $theme_data[$theme]->module_dependencies;
- // $theme_data[$theme]->requires contains both theme and module
- // 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])) {
- // The dependency does not exist.
- return FALSE;
- }
- // Skip already installed themes.
- if (!isset($theme_list[$dependency]) && !isset($installed_themes[$dependency])) {
- $theme_list[$dependency] = $dependency;
- }
- }
- }
- // Set the actual theme weights.
- $theme_list = array_map(function ($theme) use ($theme_data) {
- return $theme_data[$theme]->sort;
- }, $theme_list);
- // Sort the theme list by their weights (reverse).
- arsort($theme_list);
- $theme_list = array_keys($theme_list);
- }
- $themes_installed = [];
- foreach ($theme_list as $key) {
- // Only process themes that are not already installed.
- $installed = $extension_config->get("theme.$key") !== NULL;
- if ($installed) {
- continue;
- }
- // Throw an exception if the theme name is too long.
- if (strlen($key) > DRUPAL_EXTENSION_NAME_MAX_LENGTH) {
- throw new ExtensionNameLengthException("Theme name $key is over the maximum allowed length of " . DRUPAL_EXTENSION_NAME_MAX_LENGTH . ' characters.');
- }
- // Validate default configuration of the theme. If there is existing
- // configuration then stop installing.
- $this->configInstaller->checkConfigurationToInstall('theme', $key);
- // The value is not used; the weight is ignored for themes currently. Do
- // not check schema when saving the configuration.
- $extension_config
- ->set("theme.$key", 0)
- ->save(TRUE);
- // Reset theme settings.
- $theme_settings = &drupal_static('theme_get_setting');
- unset($theme_settings[$key]);
- // Reset theme listing.
- $this->themeHandler->reset();
- // Only install default configuration if this theme has not been installed
- // already.
- if (!isset($installed_themes[$key])) {
- // Install default configuration of the theme.
- $this->configInstaller->installDefaultConfig('theme', $key);
- }
- $themes_installed[] = $key;
- // Record the fact that it was installed.
- $this->logger->info('%theme theme installed.', ['%theme' => $key]);
- }
- $this->cssCollectionOptimizer->deleteAll();
- $this->resetSystem();
- // Invoke hook_themes_installed() after the themes have been installed.
- $this->moduleHandler->invokeAll('themes_installed', [$themes_installed]);
- return !empty($themes_installed);
- }
- /**
- * {@inheritdoc}
- */
- public function uninstall(array $theme_list) {
- $extension_config = $this->configFactory->getEditable('core.extension');
- $theme_config = $this->configFactory->getEditable('system.theme');
- $list = $this->themeHandler->listInfo();
- foreach ($theme_list as $key) {
- if (!isset($list[$key])) {
- throw new UnknownExtensionException("Unknown theme: $key.");
- }
- if ($key === $theme_config->get('default')) {
- throw new \InvalidArgumentException("The current default theme $key cannot be uninstalled.");
- }
- if ($key === $theme_config->get('admin')) {
- throw new \InvalidArgumentException("The current administration theme $key cannot be uninstalled.");
- }
- // Base themes cannot be uninstalled if sub themes are installed, and if
- // they are not uninstalled at the same time.
- // @todo https://www.drupal.org/node/474684 and
- // https://www.drupal.org/node/1297856 themes should leverage the module
- // dependency system.
- if (!empty($list[$key]->sub_themes)) {
- foreach ($list[$key]->sub_themes as $sub_key => $sub_label) {
- if (isset($list[$sub_key]) && !in_array($sub_key, $theme_list, TRUE)) {
- throw new \InvalidArgumentException("The base theme $key cannot be uninstalled, because theme $sub_key depends on it.");
- }
- }
- }
- }
- $this->cssCollectionOptimizer->deleteAll();
- foreach ($theme_list as $key) {
- // The value is not used; the weight is ignored for themes currently.
- $extension_config->clear("theme.$key");
- // Reset theme settings.
- $theme_settings = &drupal_static('theme_get_setting');
- unset($theme_settings[$key]);
- // Remove all configuration belonging to the theme.
- $this->configManager->uninstall('theme', $key);
- }
- // Don't check schema when uninstalling a theme since we are only clearing
- // keys.
- $extension_config->save(TRUE);
- // Refresh theme info.
- $this->resetSystem();
- $this->themeHandler->reset();
- $this->moduleHandler->invokeAll('themes_uninstalled', [$theme_list]);
- }
- /**
- * Resets some other systems like rebuilding the route information or caches.
- */
- protected function resetSystem() {
- if ($this->routeBuilder) {
- $this->routeBuilder->setRebuildNeeded();
- }
- // @todo It feels wrong to have the requirement to clear the local tasks
- // cache here.
- Cache::invalidateTags(['local_task']);
- $this->themeRegistryRebuild();
- }
- /**
- * Wraps drupal_theme_rebuild().
- */
- protected function themeRegistryRebuild() {
- drupal_theme_rebuild();
- }
- }
|