3 namespace Drupal\Core\Extension;
5 use Drupal\Core\Config\ConfigFactoryInterface;
6 use Drupal\Core\Extension\Exception\UninstalledExtensionException;
7 use Drupal\Core\Extension\Exception\UnknownExtensionException;
8 use Drupal\Core\State\StateInterface;
11 * Default theme handler using the config system to store installation statuses.
13 class ThemeHandler implements ThemeHandlerInterface {
16 * Contains the features enabled for themes by default.
20 * @see _system_default_theme_features()
22 protected $defaultFeatures = [
26 'comment_user_picture',
27 'comment_user_verification',
31 * A list of all currently available themes.
38 * The config factory to get the installed themes.
40 * @var \Drupal\Core\Config\ConfigFactoryInterface
42 protected $configFactory;
45 * The module handler to fire themes_installed/themes_uninstalled hooks.
47 * @var \Drupal\Core\Extension\ModuleHandlerInterface
49 protected $moduleHandler;
54 * @var \Drupal\Core\State\StateInterface
59 * The config installer to install configuration.
61 * @var \Drupal\Core\Config\ConfigInstallerInterface
63 protected $configInstaller;
66 * The info parser to parse the theme.info.yml files.
68 * @var \Drupal\Core\Extension\InfoParserInterface
70 protected $infoParser;
75 * @var \Psr\Log\LoggerInterface
80 * The route builder to rebuild the routes if a theme is installed.
82 * @var \Drupal\Core\Routing\RouteBuilderInterface
84 protected $routeBuilder;
87 * An extension discovery instance.
89 * @var \Drupal\Core\Extension\ExtensionDiscovery
91 protected $extensionDiscovery;
94 * The CSS asset collection optimizer service.
96 * @var \Drupal\Core\Asset\AssetCollectionOptimizerInterface
98 protected $cssCollectionOptimizer;
101 * The config manager used to uninstall a theme.
103 * @var \Drupal\Core\Config\ConfigManagerInterface
105 protected $configManager;
115 * Constructs a new ThemeHandler.
117 * @param string $root
119 * @param \Drupal\Core\Config\ConfigFactoryInterface $config_factory
120 * The config factory to get the installed themes.
121 * @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler
122 * The module handler to fire themes_installed/themes_uninstalled hooks.
123 * @param \Drupal\Core\State\StateInterface $state
125 * @param \Drupal\Core\Extension\InfoParserInterface $info_parser
126 * The info parser to parse the theme.info.yml files.
127 * @param \Drupal\Core\Extension\ExtensionDiscovery $extension_discovery
128 * (optional) A extension discovery instance (for unit tests).
130 public function __construct($root, ConfigFactoryInterface $config_factory, ModuleHandlerInterface $module_handler, StateInterface $state, InfoParserInterface $info_parser, ExtensionDiscovery $extension_discovery = NULL) {
132 $this->configFactory = $config_factory;
133 $this->moduleHandler = $module_handler;
134 $this->state = $state;
135 $this->infoParser = $info_parser;
136 $this->extensionDiscovery = $extension_discovery;
142 public function getDefault() {
143 return $this->configFactory->get('system.theme')->get('default');
149 public function setDefault($name) {
150 $list = $this->listInfo();
151 if (!isset($list[$name])) {
152 throw new UninstalledExtensionException("$name theme is not installed.");
154 $this->configFactory->getEditable('system.theme')
155 ->set('default', $name)
163 public function install(array $theme_list, $install_dependencies = TRUE) {
164 // We keep the old install() method as BC layer but redirect directly to the
166 return \Drupal::service('theme_installer')->install($theme_list, $install_dependencies);
172 public function uninstall(array $theme_list) {
173 // We keep the old uninstall() method as BC layer but redirect directly to
174 // the theme installer.
175 \Drupal::service('theme_installer')->uninstall($theme_list);
181 public function listInfo() {
182 if (!isset($this->list)) {
184 $themes = $this->systemThemeList();
185 // @todo Ensure that systemThemeList() does not contain an empty list
186 // during the batch installer, see https://www.drupal.org/node/2322619.
187 if (empty($themes)) {
188 $this->refreshInfo();
189 $this->list = $this->list ?: [];
190 $themes = \Drupal::state()->get('system.theme.data', []);
192 foreach ($themes as $theme) {
193 $this->addTheme($theme);
202 public function addTheme(Extension $theme) {
203 if (!empty($theme->info['libraries'])) {
204 foreach ($theme->info['libraries'] as $library => $name) {
205 $theme->libraries[$library] = $name;
208 if (isset($theme->info['engine'])) {
209 $theme->engine = $theme->info['engine'];
211 if (isset($theme->info['base theme'])) {
212 $theme->base_theme = $theme->info['base theme'];
214 $this->list[$theme->getName()] = $theme;
220 public function refreshInfo() {
221 $extension_config = $this->configFactory->get('core.extension');
222 $installed = $extension_config->get('theme');
223 // Only refresh the info if a theme has been installed. Modules are
224 // installed before themes by the installer and this method is called during
225 // module installation.
226 if (empty($installed) && empty($this->list)) {
231 // @todo Avoid re-scanning all themes by retaining the original (unaltered)
232 // theme info somewhere.
233 $list = $this->rebuildThemeData();
234 foreach ($list as $name => $theme) {
235 if (isset($installed[$name])) {
236 $this->addTheme($theme);
239 $this->state->set('system.theme.data', $this->list);
245 public function reset() {
246 $this->systemListReset();
253 public function rebuildThemeData() {
254 $listing = $this->getExtensionDiscovery();
255 $themes = $listing->scan('theme');
256 $engines = $listing->scan('theme_engine');
257 $extension_config = $this->configFactory->get('core.extension');
258 $installed = $extension_config->get('theme') ?: [];
260 // Set defaults for theme info.
263 'base theme' => 'stable',
265 'sidebar_first' => 'Left sidebar',
266 'sidebar_second' => 'Right sidebar',
267 'content' => 'Content',
268 'header' => 'Header',
269 'primary_menu' => 'Primary menu',
270 'secondary_menu' => 'Secondary menu',
271 'footer' => 'Footer',
272 'highlighted' => 'Highlighted',
274 'page_top' => 'Page top',
275 'page_bottom' => 'Page bottom',
276 'breadcrumb' => 'Breadcrumb',
279 'features' => $this->defaultFeatures,
280 'screenshot' => 'screenshot.png',
281 'php' => DRUPAL_MINIMUM_PHP,
287 $files_theme_engine = [];
288 // Read info files for each theme.
289 foreach ($themes as $key => $theme) {
290 // @todo Remove all code that relies on the $status property.
291 $theme->status = (int) isset($installed[$key]);
293 $theme->info = $this->infoParser->parse($theme->getPathname()) + $defaults;
294 // Remove the default Stable base theme when 'base theme: false' is set in
295 // a theme .info.yml file.
296 if ($theme->info['base theme'] === FALSE) {
297 unset($theme->info['base theme']);
300 // Add the info file modification time, so it becomes available for
301 // contributed modules to use for ordering theme lists.
302 $theme->info['mtime'] = $theme->getMTime();
304 // Invoke hook_system_info_alter() to give installed modules a chance to
305 // modify the data in the .info.yml files if necessary.
306 // @todo Remove $type argument, obsolete with $theme->getType().
308 $this->moduleHandler->alter('system_info', $theme->info, $theme, $type);
310 if (!empty($theme->info['base theme'])) {
311 $sub_themes[] = $key;
312 // Add the base theme as a proper dependency.
313 $themes[$key]->info['dependencies'][] = $themes[$key]->info['base theme'];
316 // Defaults to 'twig' (see $defaults above).
317 $engine = $theme->info['engine'];
318 if (isset($engines[$engine])) {
319 $theme->owner = $engines[$engine]->getExtensionPathname();
320 $theme->prefix = $engines[$engine]->getName();
321 $files_theme_engine[$engine] = $engines[$engine]->getPathname();
324 // Prefix screenshot with theme path.
325 if (!empty($theme->info['screenshot'])) {
326 $theme->info['screenshot'] = $theme->getPath() . '/' . $theme->info['screenshot'];
329 $files_theme[$key] = $theme->getPathname();
331 // Build dependencies.
332 // @todo Move into a generic ExtensionHandler base class.
333 // @see https://www.drupal.org/node/2208429
334 $themes = $this->moduleHandler->buildModuleDependencies($themes);
336 // Store filenames to allow system_list() and drupal_get_filename() to
337 // retrieve them for themes and theme engines without having to scan the
339 $this->state->set('system.theme.files', $files_theme);
340 $this->state->set('system.theme_engine.files', $files_theme_engine);
342 // After establishing the full list of available themes, fill in data for
344 foreach ($sub_themes as $key) {
345 $sub_theme = $themes[$key];
346 // The $base_themes property is optional; only set for sub themes.
347 // @see ThemeHandlerInterface::listInfo()
348 $sub_theme->base_themes = $this->getBaseThemes($themes, $key);
349 // empty() cannot be used here, since ThemeHandler::doGetBaseThemes() adds
350 // the key of a base theme with a value of NULL in case it is not found,
351 // in order to prevent needless iterations.
352 if (!current($sub_theme->base_themes)) {
355 // Determine the root base theme.
356 $root_key = key($sub_theme->base_themes);
357 // Build the list of sub-themes for each of the theme's base themes.
358 foreach (array_keys($sub_theme->base_themes) as $base_theme) {
359 $themes[$base_theme]->sub_themes[$key] = $sub_theme->info['name'];
361 // Add the theme engine info from the root base theme.
362 if (isset($themes[$root_key]->owner)) {
363 $sub_theme->info['engine'] = $themes[$root_key]->info['engine'];
364 $sub_theme->owner = $themes[$root_key]->owner;
365 $sub_theme->prefix = $themes[$root_key]->prefix;
375 public function getBaseThemes(array $themes, $theme) {
376 return $this->doGetBaseThemes($themes, $theme);
380 * Finds the base themes for the specific theme.
382 * @param array $themes
383 * An array of available themes.
384 * @param string $theme
385 * The name of the theme whose base we are looking for.
386 * @param array $used_themes
387 * (optional) A recursion parameter preventing endless loops. Defaults to
391 * An array of base themes.
393 protected function doGetBaseThemes(array $themes, $theme, $used_themes = []) {
394 if (!isset($themes[$theme]->info['base theme'])) {
398 $base_key = $themes[$theme]->info['base theme'];
399 // Does the base theme exist?
400 if (!isset($themes[$base_key])) {
401 return [$base_key => NULL];
404 $current_base_theme = [$base_key => $themes[$base_key]->info['name']];
406 // Is the base theme itself a child of another theme?
407 if (isset($themes[$base_key]->info['base theme'])) {
408 // Do we already know the base themes of this theme?
409 if (isset($themes[$base_key]->base_themes)) {
410 return $themes[$base_key]->base_themes + $current_base_theme;
413 if (!empty($used_themes[$base_key])) {
414 return [$base_key => NULL];
416 $used_themes[$base_key] = TRUE;
417 return $this->doGetBaseThemes($themes, $base_key, $used_themes) + $current_base_theme;
419 // If we get here, then this is our parent theme.
420 return $current_base_theme;
424 * Returns an extension discovery object.
426 * @return \Drupal\Core\Extension\ExtensionDiscovery
427 * The extension discovery object.
429 protected function getExtensionDiscovery() {
430 if (!isset($this->extensionDiscovery)) {
431 $this->extensionDiscovery = new ExtensionDiscovery($this->root);
433 return $this->extensionDiscovery;
439 public function getName($theme) {
440 $themes = $this->listInfo();
441 if (!isset($themes[$theme])) {
442 throw new UnknownExtensionException("Requested the name of a non-existing theme $theme");
444 return $themes[$theme]->info['name'];
448 * Wraps system_list_reset().
450 protected function systemListReset() {
455 * Wraps system_list().
458 * A list of themes keyed by name.
460 protected function systemThemeList() {
461 return system_list('theme');
467 public function getThemeDirectories() {
469 foreach ($this->listInfo() as $name => $theme) {
470 $dirs[$name] = $this->root . '/' . $theme->getPath();
478 public function themeExists($theme) {
479 $themes = $this->listInfo();
480 return isset($themes[$theme]);
486 public function getTheme($name) {
487 $themes = $this->listInfo();
488 if (isset($themes[$name])) {
489 return $themes[$name];
491 throw new UnknownExtensionException(sprintf('The theme %s does not exist.', $name));
497 public function hasUi($name) {
498 $themes = $this->listInfo();
499 if (isset($themes[$name])) {
500 if (!empty($themes[$name]->info['hidden'])) {
501 $theme_config = $this->configFactory->get('system.theme');
502 return $name == $theme_config->get('default') || $name == $theme_config->get('admin');