ThemeExtensionList.php 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322
  1. <?php
  2. namespace Drupal\Core\Extension;
  3. use Drupal\Core\Cache\CacheBackendInterface;
  4. use Drupal\Core\Config\ConfigFactoryInterface;
  5. use Drupal\Core\State\StateInterface;
  6. /**
  7. * Provides a list of available themes.
  8. *
  9. * @internal
  10. * This class is not yet stable and therefore there are no guarantees that the
  11. * internal implementations including constructor signature and protected
  12. * properties / methods will not change over time. This will be reviewed after
  13. * https://www.drupal.org/project/drupal/issues/2940481
  14. */
  15. class ThemeExtensionList extends ExtensionList {
  16. /**
  17. * {@inheritdoc}
  18. */
  19. protected $defaults = [
  20. 'engine' => 'twig',
  21. 'regions' => [
  22. 'sidebar_first' => 'Left sidebar',
  23. 'sidebar_second' => 'Right sidebar',
  24. 'content' => 'Content',
  25. 'header' => 'Header',
  26. 'primary_menu' => 'Primary menu',
  27. 'secondary_menu' => 'Secondary menu',
  28. 'footer' => 'Footer',
  29. 'highlighted' => 'Highlighted',
  30. 'help' => 'Help',
  31. 'page_top' => 'Page top',
  32. 'page_bottom' => 'Page bottom',
  33. 'breadcrumb' => 'Breadcrumb',
  34. ],
  35. 'description' => '',
  36. // The following array should be kept inline with
  37. // _system_default_theme_features().
  38. 'features' => [
  39. 'favicon',
  40. 'logo',
  41. 'node_user_picture',
  42. 'comment_user_picture',
  43. 'comment_user_verification',
  44. ],
  45. 'screenshot' => 'screenshot.png',
  46. 'version' => NULL,
  47. 'php' => DRUPAL_MINIMUM_PHP,
  48. 'libraries' => [],
  49. 'libraries_extend' => [],
  50. 'libraries_override' => [],
  51. 'dependencies' => [],
  52. ];
  53. /**
  54. * The config factory.
  55. *
  56. * @var \Drupal\Core\Config\ConfigFactoryInterface
  57. */
  58. protected $configFactory;
  59. /**
  60. * The theme engine list needed by this theme list.
  61. *
  62. * @var \Drupal\Core\Extension\ThemeEngineExtensionList
  63. */
  64. protected $engineList;
  65. /**
  66. * The list of installed themes.
  67. *
  68. * @var string[]
  69. */
  70. protected $installedThemes;
  71. /**
  72. * Constructs a new ThemeExtensionList instance.
  73. *
  74. * @param string $root
  75. * The app root.
  76. * @param string $type
  77. * The extension type.
  78. * @param \Drupal\Core\Cache\CacheBackendInterface $cache
  79. * The cache.
  80. * @param \Drupal\Core\Extension\InfoParserInterface $info_parser
  81. * The info parser.
  82. * @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler
  83. * The module handler.
  84. * @param \Drupal\Core\State\StateInterface $state
  85. * The state service.
  86. * @param \Drupal\Core\Config\ConfigFactoryInterface $config_factory
  87. * The config factory.
  88. * @param \Drupal\Core\Extension\ThemeEngineExtensionList $engine_list
  89. * The theme engine extension listing.
  90. * @param string $install_profile
  91. * The install profile used by the site.
  92. */
  93. public function __construct($root, $type, CacheBackendInterface $cache, InfoParserInterface $info_parser, ModuleHandlerInterface $module_handler, StateInterface $state, ConfigFactoryInterface $config_factory, ThemeEngineExtensionList $engine_list, $install_profile) {
  94. parent::__construct($root, $type, $cache, $info_parser, $module_handler, $state, $install_profile);
  95. $this->configFactory = $config_factory;
  96. $this->engineList = $engine_list;
  97. }
  98. /**
  99. * {@inheritdoc}
  100. */
  101. protected function doList() {
  102. // Find themes.
  103. $themes = parent::doList();
  104. $engines = $this->engineList->getList();
  105. // Always get the freshest list of themes (rather than the already cached
  106. // list in $this->installedThemes) when building the theme listing because a
  107. // theme could have just been installed or uninstalled.
  108. $this->installedThemes = $this->configFactory->get('core.extension')->get('theme') ?: [];
  109. $sub_themes = [];
  110. // Read info files for each theme.
  111. foreach ($themes as $name => $theme) {
  112. // Defaults to 'twig' (see self::defaults above).
  113. $engine = $theme->info['engine'];
  114. if (isset($engines[$engine])) {
  115. $theme->owner = $engines[$engine]->getExtensionPathname();
  116. $theme->prefix = $engines[$engine]->getName();
  117. }
  118. // Add this theme as a sub-theme if it has a base theme.
  119. if (!empty($theme->info['base theme'])) {
  120. $sub_themes[] = $name;
  121. }
  122. // Add status.
  123. $theme->status = (int) isset($this->installedThemes[$name]);
  124. }
  125. // Build dependencies.
  126. $themes = $this->moduleHandler->buildModuleDependencies($themes);
  127. // After establishing the full list of available themes, fill in data for
  128. // sub-themes.
  129. $this->fillInSubThemeData($themes, $sub_themes);
  130. foreach ($themes as $key => $theme) {
  131. // After $theme is processed by buildModuleDependencies(), there can be a
  132. // `$theme->requires` array containing both module and base theme
  133. // dependencies. The module dependencies are copied to their own property
  134. // so they are available to operations specific to module dependencies.
  135. if (isset($theme->requires)) {
  136. $theme->module_dependencies = array_diff_key($theme->requires, $themes);
  137. }
  138. else {
  139. // Even if no requirements are specified, the theme installation process
  140. // expects the presence of the `requires` and `module_dependencies`
  141. // properties, so they should be initialized here as empty arrays.
  142. $theme->requires = [];
  143. $theme->module_dependencies = [];
  144. }
  145. }
  146. return $themes;
  147. }
  148. /**
  149. * Fills in data for themes that are also sub-themes.
  150. *
  151. * @param array $themes
  152. * The array of partly processed theme information.
  153. * @param array $sub_themes
  154. * A list of themes from the $theme array that are also sub-themes.
  155. */
  156. protected function fillInSubThemeData(array &$themes, array $sub_themes) {
  157. foreach ($sub_themes as $name) {
  158. $sub_theme = $themes[$name];
  159. // The $base_themes property is optional; only set for sub themes.
  160. // @see ThemeHandlerInterface::listInfo()
  161. $sub_theme->base_themes = $this->doGetBaseThemes($themes, $name);
  162. // empty() cannot be used here, since static::doGetBaseThemes() adds
  163. // the key of a base theme with a value of NULL in case it is not found,
  164. // in order to prevent needless iterations.
  165. if (!current($sub_theme->base_themes)) {
  166. continue;
  167. }
  168. // Determine the root base theme.
  169. $root_key = key($sub_theme->base_themes);
  170. // Build the list of sub-themes for each of the theme's base themes.
  171. foreach (array_keys($sub_theme->base_themes) as $base_theme) {
  172. $themes[$base_theme]->sub_themes[$name] = $sub_theme->info['name'];
  173. }
  174. // Add the theme engine info from the root base theme.
  175. if (isset($themes[$root_key]->owner)) {
  176. $sub_theme->info['engine'] = $themes[$root_key]->info['engine'];
  177. $sub_theme->owner = $themes[$root_key]->owner;
  178. $sub_theme->prefix = $themes[$root_key]->prefix;
  179. }
  180. }
  181. }
  182. /**
  183. * Finds all the base themes for the specified theme.
  184. *
  185. * Themes can inherit templates and function implementations from earlier
  186. * themes.
  187. *
  188. * @param \Drupal\Core\Extension\Extension[] $themes
  189. * An array of available themes.
  190. * @param string $theme
  191. * The name of the theme whose base we are looking for.
  192. *
  193. * @return array
  194. * Returns an array of all of the theme's ancestors; the first element's
  195. * value will be NULL if an error occurred.
  196. */
  197. public function getBaseThemes(array $themes, $theme) {
  198. return $this->doGetBaseThemes($themes, $theme);
  199. }
  200. /**
  201. * Finds the base themes for the specific theme.
  202. *
  203. * @param array $themes
  204. * An array of available themes.
  205. * @param string $theme
  206. * The name of the theme whose base we are looking for.
  207. * @param array $used_themes
  208. * (optional) A recursion parameter preventing endless loops. Defaults to
  209. * an empty array.
  210. *
  211. * @return array
  212. * An array of base themes.
  213. */
  214. protected function doGetBaseThemes(array $themes, $theme, array $used_themes = []) {
  215. if (!isset($themes[$theme]->info['base theme'])) {
  216. return [];
  217. }
  218. $base_key = $themes[$theme]->info['base theme'];
  219. // Does the base theme exist?
  220. if (!isset($themes[$base_key])) {
  221. return [$base_key => NULL];
  222. }
  223. $current_base_theme = [$base_key => $themes[$base_key]->info['name']];
  224. // Is the base theme itself a child of another theme?
  225. if (isset($themes[$base_key]->info['base theme'])) {
  226. // Do we already know the base themes of this theme?
  227. if (isset($themes[$base_key]->base_themes)) {
  228. return $themes[$base_key]->base_themes + $current_base_theme;
  229. }
  230. // Prevent loops.
  231. if (!empty($used_themes[$base_key])) {
  232. return [$base_key => NULL];
  233. }
  234. $used_themes[$base_key] = TRUE;
  235. return $this->doGetBaseThemes($themes, $base_key, $used_themes) + $current_base_theme;
  236. }
  237. // If we get here, then this is our parent theme.
  238. return $current_base_theme;
  239. }
  240. /**
  241. * {@inheritdoc}
  242. */
  243. protected function createExtensionInfo(Extension $extension) {
  244. $info = parent::createExtensionInfo($extension);
  245. // In the past, Drupal used to default to the `stable` theme as the base
  246. // theme. Explicitly opting out by specifying `base theme: false` was (and
  247. // still is) possible. However, defaulting to `base theme: stable` prevents
  248. // automatic updates to the next major version of Drupal, since each major
  249. // version may have a different version of "the stable theme", for example:
  250. // - for Drupal 8: `stable`
  251. // - for Drupal 9: `stable9`
  252. // - for Drupal 10: `stable10`
  253. // - et cetera
  254. // It is impossible to reliably determine which should be used by default,
  255. // hence we now require the base theme to be explicitly specified.
  256. if (!isset($info['base theme'])) {
  257. @trigger_error(sprintf('There is no `base theme` property specified in the %s.info.yml file. The optionality of the `base theme` property is deprecated in drupal:8.8.0 and is removed from drupal:9.0.0. All Drupal 8 themes must add `base theme: stable` to their *.info.yml file for them to continue to work as-is in future versions of Drupal. Drupal 9 requires the `base theme` property to be specified. See https://www.drupal.org/node/3066038', $extension->getName()), E_USER_DEPRECATED);
  258. $info['base theme'] = 'stable';
  259. }
  260. // Remove the default Stable base theme when 'base theme: false' is set in
  261. // a theme .info.yml file.
  262. if ($info['base theme'] === FALSE) {
  263. unset($info['base theme']);
  264. }
  265. if (!empty($info['base theme'])) {
  266. // Add the base theme as a proper dependency.
  267. $info['dependencies'][] = $info['base theme'];
  268. }
  269. // Prefix screenshot with theme path.
  270. if (!empty($info['screenshot'])) {
  271. $info['screenshot'] = $extension->getPath() . '/' . $info['screenshot'];
  272. }
  273. return $info;
  274. }
  275. /**
  276. * {@inheritdoc}
  277. */
  278. protected function getInstalledExtensionNames() {
  279. // Cache the installed themes to avoid multiple calls to the config system.
  280. if (!isset($this->installedThemes)) {
  281. $this->installedThemes = $this->configFactory->get('core.extension')->get('theme') ?: [];
  282. }
  283. return array_keys($this->installedThemes);
  284. }
  285. /**
  286. * {@inheritdoc}
  287. */
  288. public function reset() {
  289. parent::reset();
  290. $this->installedThemes = NULL;
  291. return $this;
  292. }
  293. }