root = $root; $this->themeHandler = $theme_handler; $this->cache = $cache; $this->moduleHandler = $module_handler; } /** * {@inheritdoc} */ public function initTheme($theme_name) { $active_theme = $this->getActiveThemeByName($theme_name); $this->loadActiveTheme($active_theme); return $active_theme; } /** * {@inheritdoc} */ public function getActiveThemeByName($theme_name) { if ($cached = $this->cache->get('theme.active_theme.' . $theme_name)) { return $cached->data; } $themes = $this->themeHandler->listInfo(); // If no theme could be negotiated, or if the negotiated theme is not within // the list of installed themes, fall back to the default theme output of // core and modules (like Stark, but without a theme extension at all). This // is possible, because loadActiveTheme() always loads the Twig theme // engine. This is desired, because missing or malformed theme configuration // should not leave the application in a broken state. By falling back to // default output, the user is able to reconfigure the theme through the UI. // Lastly, tests are expected to operate with no theme by default, so as to // only assert the original theme output of modules (unless a test manually // installs a specific theme). if (empty($themes) || !$theme_name || !isset($themes[$theme_name])) { $theme_name = 'core'; // /core/core.info.yml does not actually exist, but is required because // Extension expects a pathname. $active_theme = $this->getActiveTheme(new Extension($this->root, 'theme', 'core/core.info.yml')); // Early-return and do not set state, because the initialized $theme_name // differs from the original $theme_name. return $active_theme; } // Find all our ancestor themes and put them in an array. $base_themes = []; $ancestor = $theme_name; while ($ancestor && isset($themes[$ancestor]->base_theme)) { $ancestor = $themes[$ancestor]->base_theme; if (!$this->themeHandler->themeExists($ancestor)) { if ($ancestor == 'stable') { // Themes that depend on Stable will be fixed by system_update_8014(). // There is no harm in not adding it as an ancestor since at worst // some people might experience slight visual regressions on // update.php. continue; } throw new MissingThemeDependencyException(sprintf('Base theme %s has not been installed.', $ancestor), $ancestor); } $base_themes[] = $themes[$ancestor]; } $active_theme = $this->getActiveTheme($themes[$theme_name], $base_themes); $this->cache->set('theme.active_theme.' . $theme_name, $active_theme); return $active_theme; } /** * {@inheritdoc} */ public function loadActiveTheme(ActiveTheme $active_theme) { // Initialize the theme. if ($theme_engine = $active_theme->getEngine()) { // Include the engine. include_once $this->root . '/' . $active_theme->getOwner(); if (function_exists($theme_engine . '_init')) { foreach ($active_theme->getBaseThemes() as $base) { call_user_func($theme_engine . '_init', $base->getExtension()); } call_user_func($theme_engine . '_init', $active_theme->getExtension()); } } else { // include non-engine theme files foreach ($active_theme->getBaseThemes() as $base) { // Include the theme file or the engine. if ($base->getOwner()) { include_once $this->root . '/' . $base->getOwner(); } } // and our theme gets one too. if ($active_theme->getOwner()) { include_once $this->root . '/' . $active_theme->getOwner(); } } // Always include Twig as the default theme engine. include_once $this->root . '/core/themes/engines/twig/twig.engine'; } /** * {@inheritdoc} */ public function getActiveTheme(Extension $theme, array $base_themes = []) { $theme_path = $theme->getPath(); $values['path'] = $theme_path; $values['name'] = $theme->getName(); // @todo Remove in Drupal 9.0.x. $values['stylesheets_remove'] = $this->prepareStylesheetsRemove($theme, $base_themes); // Prepare libraries overrides from this theme and ancestor themes. This // allows child themes to easily remove CSS files from base themes and // modules. $values['libraries_override'] = []; // Get libraries overrides declared by base themes. foreach ($base_themes as $base) { if (!empty($base->info['libraries-override'])) { foreach ($base->info['libraries-override'] as $library => $override) { $values['libraries_override'][$base->getPath()][$library] = $override; } } } // Add libraries overrides declared by this theme. if (!empty($theme->info['libraries-override'])) { foreach ($theme->info['libraries-override'] as $library => $override) { $values['libraries_override'][$theme->getPath()][$library] = $override; } } // Get libraries extensions declared by base themes. foreach ($base_themes as $base) { if (!empty($base->info['libraries-extend'])) { foreach ($base->info['libraries-extend'] as $library => $extend) { if (isset($values['libraries_extend'][$library])) { // Merge if libraries-extend has already been defined for this // library. $values['libraries_extend'][$library] = array_merge($values['libraries_extend'][$library], $extend); } else { $values['libraries_extend'][$library] = $extend; } } } } // Add libraries extensions declared by this theme. if (!empty($theme->info['libraries-extend'])) { foreach ($theme->info['libraries-extend'] as $library => $extend) { if (isset($values['libraries_extend'][$library])) { // Merge if libraries-extend has already been defined for this // library. $values['libraries_extend'][$library] = array_merge($values['libraries_extend'][$library], $extend); } else { $values['libraries_extend'][$library] = $extend; } } } // Do basically the same as the above for libraries $values['libraries'] = []; // Grab libraries from base theme foreach ($base_themes as $base) { if (!empty($base->libraries)) { foreach ($base->libraries as $library) { $values['libraries'][] = $library; } } } // Add libraries used by this theme. if (!empty($theme->libraries)) { foreach ($theme->libraries as $library) { $values['libraries'][] = $library; } } $values['engine'] = isset($theme->engine) ? $theme->engine : NULL; $values['owner'] = isset($theme->owner) ? $theme->owner : NULL; $values['extension'] = $theme; $base_active_themes = []; foreach ($base_themes as $base_theme) { $base_active_themes[$base_theme->getName()] = $this->getActiveTheme($base_theme, array_slice($base_themes, 1)); } $values['base_themes'] = $base_active_themes; if (!empty($theme->info['regions'])) { $values['regions'] = $theme->info['regions']; } return new ActiveTheme($values); } /** * Gets all extensions. * * @return array */ protected function getExtensions() { if (!isset($this->extensions)) { $this->extensions = array_merge($this->moduleHandler->getModuleList(), $this->themeHandler->listInfo()); } return $this->extensions; } /** * Gets CSS file where tokens have been resolved. * * @param string $css_file * CSS file which may contain tokens. * * @return string * CSS file where placeholders are replaced. * * @todo Remove in Drupal 9.0.x. */ protected function resolveStyleSheetPlaceholders($css_file) { $token_candidate = explode('/', $css_file)[0]; if (!preg_match('/@[A-z0-9_-]+/', $token_candidate)) { return $css_file; } $token = substr($token_candidate, 1); // Prime extensions. $extensions = $this->getExtensions(); if (isset($extensions[$token])) { return str_replace($token_candidate, $extensions[$token]->getPath(), $css_file); } } /** * Prepares stylesheets-remove specified in the *.info.yml file. * * @param \Drupal\Core\Extension\Extension $theme * The theme extension object. * @param \Drupal\Core\Extension\Extension[] $base_themes * An array of base themes. * * @return string[] * The list of stylesheets-remove specified in the *.info.yml file. * * @todo Remove in Drupal 9.0.x. */ protected function prepareStylesheetsRemove(Extension $theme, $base_themes) { // Prepare stylesheets from this theme as well as all ancestor themes. // We work it this way so that we can have child themes remove CSS files // easily from parent. $stylesheets_remove = []; // Grab stylesheets from base theme. foreach ($base_themes as $base) { if (!empty($base->info['stylesheets-remove'])) { foreach ($base->info['stylesheets-remove'] as $css_file) { $css_file = $this->resolveStyleSheetPlaceholders($css_file); $stylesheets_remove[$css_file] = $css_file; } } } // Add stylesheets used by this theme. if (!empty($theme->info['stylesheets-remove'])) { foreach ($theme->info['stylesheets-remove'] as $css_file) { $css_file = $this->resolveStyleSheetPlaceholders($css_file); $stylesheets_remove[$css_file] = $css_file; } } return $stylesheets_remove; } }