ThemeInstaller.php 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326
  1. <?php
  2. namespace Drupal\Core\Extension;
  3. use Drupal\Component\Utility\Html;
  4. use Drupal\Core\Asset\AssetCollectionOptimizerInterface;
  5. use Drupal\Core\Cache\Cache;
  6. use Drupal\Core\Config\ConfigFactoryInterface;
  7. use Drupal\Core\Config\ConfigInstallerInterface;
  8. use Drupal\Core\Config\ConfigManagerInterface;
  9. use Drupal\Core\Extension\Exception\UnknownExtensionException;
  10. use Drupal\Core\Routing\RouteBuilderInterface;
  11. use Drupal\Core\State\StateInterface;
  12. use Drupal\Core\StringTranslation\StringTranslationTrait;
  13. use Drupal\system\ModuleDependencyMessageTrait;
  14. use Psr\Log\LoggerInterface;
  15. /**
  16. * Manages theme installation/uninstallation.
  17. */
  18. class ThemeInstaller implements ThemeInstallerInterface {
  19. use ModuleDependencyMessageTrait;
  20. use StringTranslationTrait;
  21. /**
  22. * @var \Drupal\Core\Extension\ThemeHandlerInterface
  23. */
  24. protected $themeHandler;
  25. /**
  26. * @var \Drupal\Core\Config\ConfigFactoryInterface
  27. */
  28. protected $configFactory;
  29. /**
  30. * @var \Drupal\Core\Config\ConfigInstallerInterface
  31. */
  32. protected $configInstaller;
  33. /**
  34. * @var \Drupal\Core\Extension\ModuleHandlerInterface
  35. */
  36. protected $moduleHandler;
  37. /**
  38. * @var \Drupal\Core\State\StateInterface
  39. */
  40. protected $state;
  41. /**
  42. * @var \Drupal\Core\Config\ConfigManagerInterface
  43. */
  44. protected $configManager;
  45. /**
  46. * @var \Drupal\Core\Asset\AssetCollectionOptimizerInterface
  47. */
  48. protected $cssCollectionOptimizer;
  49. /**
  50. * @var \Drupal\Core\Routing\RouteBuilderInterface
  51. */
  52. protected $routeBuilder;
  53. /**
  54. * @var \Psr\Log\LoggerInterface
  55. */
  56. protected $logger;
  57. /**
  58. * The module extension list.
  59. *
  60. * @var \Drupal\Core\Extension\ModuleExtensionList
  61. */
  62. protected $moduleExtensionList;
  63. /**
  64. * Constructs a new ThemeInstaller.
  65. *
  66. * @param \Drupal\Core\Extension\ThemeHandlerInterface $theme_handler
  67. * The theme handler.
  68. * @param \Drupal\Core\Config\ConfigFactoryInterface $config_factory
  69. * The config factory to get the installed themes.
  70. * @param \Drupal\Core\Config\ConfigInstallerInterface $config_installer
  71. * (optional) The config installer to install configuration. This optional
  72. * to allow the theme handler to work before Drupal is installed and has a
  73. * database.
  74. * @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler
  75. * The module handler to fire themes_installed/themes_uninstalled hooks.
  76. * @param \Drupal\Core\Config\ConfigManagerInterface $config_manager
  77. * The config manager used to uninstall a theme.
  78. * @param \Drupal\Core\Asset\AssetCollectionOptimizerInterface $css_collection_optimizer
  79. * The CSS asset collection optimizer service.
  80. * @param \Drupal\Core\Routing\RouteBuilderInterface $route_builder
  81. * (optional) The route builder service to rebuild the routes if a theme is
  82. * installed.
  83. * @param \Psr\Log\LoggerInterface $logger
  84. * A logger instance.
  85. * @param \Drupal\Core\State\StateInterface $state
  86. * The state store.
  87. * @param \Drupal\Core\Extension\ModuleExtensionList $module_extension_list
  88. * The module extension list.
  89. */
  90. 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) {
  91. $this->themeHandler = $theme_handler;
  92. $this->configFactory = $config_factory;
  93. $this->configInstaller = $config_installer;
  94. $this->moduleHandler = $module_handler;
  95. $this->configManager = $config_manager;
  96. $this->cssCollectionOptimizer = $css_collection_optimizer;
  97. $this->routeBuilder = $route_builder;
  98. $this->logger = $logger;
  99. $this->state = $state;
  100. if ($module_extension_list === NULL) {
  101. @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);
  102. $module_extension_list = \Drupal::service('extension.list.module');
  103. }
  104. $this->moduleExtensionList = $module_extension_list;
  105. }
  106. /**
  107. * {@inheritdoc}
  108. */
  109. public function install(array $theme_list, $install_dependencies = TRUE) {
  110. $extension_config = $this->configFactory->getEditable('core.extension');
  111. $theme_data = $this->themeHandler->rebuildThemeData();
  112. $installed_themes = $extension_config->get('theme') ?: [];
  113. $installed_modules = $extension_config->get('module') ?: [];
  114. if ($install_dependencies) {
  115. $theme_list = array_combine($theme_list, $theme_list);
  116. if ($missing = array_diff_key($theme_list, $theme_data)) {
  117. // One or more of the given themes doesn't exist.
  118. throw new UnknownExtensionException('Unknown themes: ' . implode(', ', $missing) . '.');
  119. }
  120. // Only process themes that are not installed currently.
  121. if (!$theme_list = array_diff_key($theme_list, $installed_themes)) {
  122. // Nothing to do. All themes already installed.
  123. return TRUE;
  124. }
  125. $module_list = $this->moduleExtensionList->getList();
  126. foreach ($theme_list as $theme => $value) {
  127. $module_dependencies = $theme_data[$theme]->module_dependencies;
  128. // $theme_data[$theme]->requires contains both theme and module
  129. // dependencies keyed by the extension machine names and
  130. // $theme_data[$theme]->module_dependencies contains only modules keyed
  131. // by the module extension machine name. Therefore we can find the theme
  132. // dependencies by finding array keys for 'requires' that are not in
  133. // $module_dependencies.
  134. $theme_dependencies = array_diff_key($theme_data[$theme]->requires, $module_dependencies);
  135. // We can find the unmet module dependencies by finding the module
  136. // machine names keys that are not in $installed_modules keys.
  137. $unmet_module_dependencies = array_diff_key($module_dependencies, $installed_modules);
  138. // Prevent themes with unmet module dependencies from being installed.
  139. if (!empty($unmet_module_dependencies)) {
  140. $unmet_module_dependencies_list = implode(', ', array_keys($unmet_module_dependencies));
  141. throw new MissingDependencyException("Unable to install theme: '$theme' due to unmet module dependencies: '$unmet_module_dependencies_list'.");
  142. }
  143. foreach ($module_dependencies as $dependency => $dependency_object) {
  144. if ($incompatible = $this->checkDependencyMessage($module_list, $dependency, $dependency_object)) {
  145. $sanitized_message = Html::decodeEntities(strip_tags($incompatible));
  146. throw new MissingDependencyException("Unable to install theme: $sanitized_message");
  147. }
  148. }
  149. // Add dependencies to the list of themes to install. The new themes
  150. // will be processed as the parent foreach loop continues.
  151. foreach (array_keys($theme_dependencies) as $dependency) {
  152. if (!isset($theme_data[$dependency])) {
  153. // The dependency does not exist.
  154. return FALSE;
  155. }
  156. // Skip already installed themes.
  157. if (!isset($theme_list[$dependency]) && !isset($installed_themes[$dependency])) {
  158. $theme_list[$dependency] = $dependency;
  159. }
  160. }
  161. }
  162. // Set the actual theme weights.
  163. $theme_list = array_map(function ($theme) use ($theme_data) {
  164. return $theme_data[$theme]->sort;
  165. }, $theme_list);
  166. // Sort the theme list by their weights (reverse).
  167. arsort($theme_list);
  168. $theme_list = array_keys($theme_list);
  169. }
  170. $themes_installed = [];
  171. foreach ($theme_list as $key) {
  172. // Only process themes that are not already installed.
  173. $installed = $extension_config->get("theme.$key") !== NULL;
  174. if ($installed) {
  175. continue;
  176. }
  177. // Throw an exception if the theme name is too long.
  178. if (strlen($key) > DRUPAL_EXTENSION_NAME_MAX_LENGTH) {
  179. throw new ExtensionNameLengthException("Theme name $key is over the maximum allowed length of " . DRUPAL_EXTENSION_NAME_MAX_LENGTH . ' characters.');
  180. }
  181. // Validate default configuration of the theme. If there is existing
  182. // configuration then stop installing.
  183. $this->configInstaller->checkConfigurationToInstall('theme', $key);
  184. // The value is not used; the weight is ignored for themes currently. Do
  185. // not check schema when saving the configuration.
  186. $extension_config
  187. ->set("theme.$key", 0)
  188. ->save(TRUE);
  189. // Reset theme settings.
  190. $theme_settings = &drupal_static('theme_get_setting');
  191. unset($theme_settings[$key]);
  192. // Reset theme listing.
  193. $this->themeHandler->reset();
  194. // Only install default configuration if this theme has not been installed
  195. // already.
  196. if (!isset($installed_themes[$key])) {
  197. // Install default configuration of the theme.
  198. $this->configInstaller->installDefaultConfig('theme', $key);
  199. }
  200. $themes_installed[] = $key;
  201. // Record the fact that it was installed.
  202. $this->logger->info('%theme theme installed.', ['%theme' => $key]);
  203. }
  204. $this->cssCollectionOptimizer->deleteAll();
  205. $this->resetSystem();
  206. // Invoke hook_themes_installed() after the themes have been installed.
  207. $this->moduleHandler->invokeAll('themes_installed', [$themes_installed]);
  208. return !empty($themes_installed);
  209. }
  210. /**
  211. * {@inheritdoc}
  212. */
  213. public function uninstall(array $theme_list) {
  214. $extension_config = $this->configFactory->getEditable('core.extension');
  215. $theme_config = $this->configFactory->getEditable('system.theme');
  216. $list = $this->themeHandler->listInfo();
  217. foreach ($theme_list as $key) {
  218. if (!isset($list[$key])) {
  219. throw new UnknownExtensionException("Unknown theme: $key.");
  220. }
  221. if ($key === $theme_config->get('default')) {
  222. throw new \InvalidArgumentException("The current default theme $key cannot be uninstalled.");
  223. }
  224. if ($key === $theme_config->get('admin')) {
  225. throw new \InvalidArgumentException("The current administration theme $key cannot be uninstalled.");
  226. }
  227. // Base themes cannot be uninstalled if sub themes are installed, and if
  228. // they are not uninstalled at the same time.
  229. // @todo https://www.drupal.org/node/474684 and
  230. // https://www.drupal.org/node/1297856 themes should leverage the module
  231. // dependency system.
  232. if (!empty($list[$key]->sub_themes)) {
  233. foreach ($list[$key]->sub_themes as $sub_key => $sub_label) {
  234. if (isset($list[$sub_key]) && !in_array($sub_key, $theme_list, TRUE)) {
  235. throw new \InvalidArgumentException("The base theme $key cannot be uninstalled, because theme $sub_key depends on it.");
  236. }
  237. }
  238. }
  239. }
  240. $this->cssCollectionOptimizer->deleteAll();
  241. foreach ($theme_list as $key) {
  242. // The value is not used; the weight is ignored for themes currently.
  243. $extension_config->clear("theme.$key");
  244. // Reset theme settings.
  245. $theme_settings = &drupal_static('theme_get_setting');
  246. unset($theme_settings[$key]);
  247. // Remove all configuration belonging to the theme.
  248. $this->configManager->uninstall('theme', $key);
  249. }
  250. // Don't check schema when uninstalling a theme since we are only clearing
  251. // keys.
  252. $extension_config->save(TRUE);
  253. // Refresh theme info.
  254. $this->resetSystem();
  255. $this->themeHandler->reset();
  256. $this->moduleHandler->invokeAll('themes_uninstalled', [$theme_list]);
  257. }
  258. /**
  259. * Resets some other systems like rebuilding the route information or caches.
  260. */
  261. protected function resetSystem() {
  262. if ($this->routeBuilder) {
  263. $this->routeBuilder->setRebuildNeeded();
  264. }
  265. // @todo It feels wrong to have the requirement to clear the local tasks
  266. // cache here.
  267. Cache::invalidateTags(['local_task']);
  268. $this->themeRegistryRebuild();
  269. }
  270. /**
  271. * Wraps drupal_theme_rebuild().
  272. */
  273. protected function themeRegistryRebuild() {
  274. drupal_theme_rebuild();
  275. }
  276. }