Newer
Older
<?php
/**
* @file
* Contains \Drupal\Core\Extension\ThemeHandler.
*/
namespace Drupal\Core\Extension;
use Drupal\Component\Utility\SafeMarkup;
Alex Pott
committed
use Drupal\Core\Config\ConfigFactoryInterface;
use Drupal\Core\State\StateInterface;
catch
committed
* Default theme handler using the config system to store installation statuses.
*/
class ThemeHandler implements ThemeHandlerInterface {
/**
* Contains the features enabled for themes by default.
*
* @var array
*/
protected $defaultFeatures = array(
'favicon',
'node_user_picture',
'comment_user_picture',
'comment_user_verification',
);
/**
* A list of all currently available themes.
*
* @var array
*/
protected $list;
catch
committed
* The config factory to get the installed themes.
Alex Pott
committed
* @var \Drupal\Core\Config\ConfigFactoryInterface
*/
protected $configFactory;
/**
catch
committed
* The module handler to fire themes_installed/themes_uninstalled hooks.
*
* @var \Drupal\Core\Extension\ModuleHandlerInterface
*/
protected $moduleHandler;
/**
* The state backend.
* @var \Drupal\Core\State\StateInterface
protected $state;
/**
* The config installer to install configuration.
*
* @var \Drupal\Core\Config\ConfigInstallerInterface
*/
protected $configInstaller;
/**
* The info parser to parse the theme.info.yml files.
*
* @var \Drupal\Core\Extension\InfoParserInterface
*/
protected $infoParser;
/**
* A logger instance.
*
* @var \Psr\Log\LoggerInterface
*/
protected $logger;
Alex Pott
committed
* The route builder to rebuild the routes if a theme is installed.
Alex Pott
committed
* @var \Drupal\Core\Routing\RouteBuilderInterface
Alex Pott
committed
protected $routeBuilder;
catch
committed
* An extension discovery instance.
catch
committed
* @var \Drupal\Core\Extension\ExtensionDiscovery
catch
committed
protected $extensionDiscovery;
/**
* The CSS asset collection optimizer service.
*
* @var \Drupal\Core\Asset\AssetCollectionOptimizerInterface
*/
protected $cssCollectionOptimizer;
catch
committed
/**
* The config manager used to uninstall a theme.
*
* @var \Drupal\Core\Config\ConfigManagerInterface
*/
protected $configManager;
/**
* The app root.
*
* @var string
*/
protected $root;
/**
* Constructs a new ThemeHandler.
*
* @param string $root
* The app root.
Alex Pott
committed
* @param \Drupal\Core\Config\ConfigFactoryInterface $config_factory
catch
committed
* The config factory to get the installed themes.
* @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler
catch
committed
* The module handler to fire themes_installed/themes_uninstalled hooks.
* @param \Drupal\Core\State\StateInterface $state
* The state store.
* @param \Drupal\Core\Extension\InfoParserInterface $info_parser
* The info parser to parse the theme.info.yml files.
catch
committed
* @param \Drupal\Core\Extension\ExtensionDiscovery $extension_discovery
* (optional) A extension discovery instance (for unit tests).
public function __construct($root, ConfigFactoryInterface $config_factory, ModuleHandlerInterface $module_handler, StateInterface $state, InfoParserInterface $info_parser, ExtensionDiscovery $extension_discovery = NULL) {
$this->root = $root;
$this->configFactory = $config_factory;
$this->moduleHandler = $module_handler;
$this->state = $state;
$this->infoParser = $info_parser;
catch
committed
$this->extensionDiscovery = $extension_discovery;
}
/**
* {@inheritdoc}
*/
public function getDefault() {
return $this->configFactory->get('system.theme')->get('default');
}
/**
* {@inheritdoc}
*/
public function setDefault($name) {
$list = $this->listInfo();
if (!isset($list[$name])) {
catch
committed
throw new \InvalidArgumentException("$name theme is not installed.");
}
$this->configFactory->getEditable('system.theme')
->set('default', $name)
->save();
return $this;
}
/**
* {@inheritdoc}
*/
catch
committed
public function install(array $theme_list, $install_dependencies = TRUE) {
// We keep the old install() method as BC layer but redirect directly to the
// theme installer.
Alex Pott
committed
return \Drupal::service('theme_installer')->install($theme_list, $install_dependencies);
}
/**
* {@inheritdoc}
*/
catch
committed
public function uninstall(array $theme_list) {
// We keep the old uninstall() method as BC layer but redirect directly to
// the theme installer.
\Drupal::service('theme_installer')->uninstall($theme_list);
}
/**
* {@inheritdoc}
*/
public function listInfo() {
if (!isset($this->list)) {
$this->list = array();
$themes = $this->systemThemeList();
// @todo Ensure that systemThemeList() does not contain an empty list
// during the batch installer, see https://www.drupal.org/node/2322619.
if (empty($themes)) {
$this->refreshInfo();
$this->list = $this->list ?: array();
$themes = \Drupal::state()->get('system.theme.data', array());
}
foreach ($themes as $theme) {
$this->addTheme($theme);
}
return $this->list;
}
/**
* {@inheritdoc}
*/
public function addTheme(Extension $theme) {
foreach ($theme->info['libraries'] as $library => $name) {
$theme->libraries[$library] = $name;
}
if (isset($theme->info['engine'])) {
$theme->engine = $theme->info['engine'];
}
if (isset($theme->info['base theme'])) {
$theme->base_theme = $theme->info['base theme'];
}
$this->list[$theme->getName()] = $theme;
}
/**
* {@inheritdoc}
*/
public function refreshInfo() {
$this->reset();
$extension_config = $this->configFactory->get('core.extension');
catch
committed
$installed = $extension_config->get('theme');
// @todo Avoid re-scanning all themes by retaining the original (unaltered)
// theme info somewhere.
$list = $this->rebuildThemeData();
foreach ($list as $name => $theme) {
catch
committed
if (isset($installed[$name])) {
$this->addTheme($theme);
}
}
$this->state->set('system.theme.data', $this->list);
}
/**
* {@inheritdoc}
*/
public function reset() {
$this->systemListReset();
$this->list = NULL;
}
/**
* {@inheritdoc}
*/
public function rebuildThemeData() {
catch
committed
$listing = $this->getExtensionDiscovery();
$themes = $listing->scan('theme');
$engines = $listing->scan('theme_engine');
$extension_config = $this->configFactory->get('core.extension');
catch
committed
$installed = $extension_config->get('theme') ?: array();
// Set defaults for theme info.
$defaults = array(
'engine' => 'twig',
'regions' => array(
'sidebar_first' => 'Left sidebar',
'sidebar_second' => 'Right sidebar',
'content' => 'Content',
'header' => 'Header',
'primary_menu' => 'Primary menu',
'secondary_menu' => 'Secondary menu',
'footer' => 'Footer',
'highlighted' => 'Highlighted',
'help' => 'Help',
'page_top' => 'Page top',
'page_bottom' => 'Page bottom',
'breadcrumb' => 'Breadcrumb',
),
'description' => '',
'features' => $this->defaultFeatures,
'screenshot' => 'screenshot.png',
'php' => DRUPAL_MINIMUM_PHP,
catch
committed
'libraries' => array(),
);
$sub_themes = array();
$files_theme = array();
$files_theme_engine = array();
// Read info files for each theme.
foreach ($themes as $key => $theme) {
// @todo Remove all code that relies on the $status property.
catch
committed
$theme->status = (int) isset($installed[$key]);
Angie Byron
committed
$theme->info = $this->infoParser->parse($theme->getPathname()) + $defaults;
// Add the info file modification time, so it becomes available for
// contributed modules to use for ordering theme lists.
Angie Byron
committed
$theme->info['mtime'] = $theme->getMTime();
// Invoke hook_system_info_alter() to give installed modules a chance to
// modify the data in the .info.yml files if necessary.
catch
committed
// @todo Remove $type argument, obsolete with $theme->getType().
$type = 'theme';
Angie Byron
committed
$this->moduleHandler->alter('system_info', $theme->info, $theme, $type);
Angie Byron
committed
if (!empty($theme->info['base theme'])) {
$sub_themes[] = $key;
// Add the base theme as a proper dependency.
$themes[$key]->info['dependencies'][] = $themes[$key]->info['base theme'];
Angie Byron
committed
// Defaults to 'twig' (see $defaults above).
$engine = $theme->info['engine'];
if (isset($engines[$engine])) {
$theme->owner = $engines[$engine]->getExtensionPathname();
$theme->prefix = $engines[$engine]->getName();
$files_theme_engine[$engine] = $engines[$engine]->getPathname();
// Prefix screenshot with theme path.
Angie Byron
committed
if (!empty($theme->info['screenshot'])) {
$theme->info['screenshot'] = $theme->getPath() . '/' . $theme->info['screenshot'];
$files_theme[$key] = $theme->getPathname();
// Build dependencies.
// @todo Move into a generic ExtensionHandler base class.
// @see https://www.drupal.org/node/2208429
$themes = $this->moduleHandler->buildModuleDependencies($themes);
// Store filenames to allow system_list() and drupal_get_filename() to
// retrieve them for themes and theme engines without having to scan the
// filesystem.
$this->state->set('system.theme.files', $files_theme);
$this->state->set('system.theme_engine.files', $files_theme_engine);
Angie Byron
committed
// After establishing the full list of available themes, fill in data for
// sub-themes.
foreach ($sub_themes as $key) {
Angie Byron
committed
$sub_theme = $themes[$key];
// The $base_themes property is optional; only set for sub themes.
// @see ThemeHandlerInterface::listInfo()
$sub_theme->base_themes = $this->getBaseThemes($themes, $key);
// empty() cannot be used here, since ThemeHandler::doGetBaseThemes() adds
// the key of a base theme with a value of NULL in case it is not found,
// in order to prevent needless iterations.
if (!current($sub_theme->base_themes)) {
continue;
}
Angie Byron
committed
// Determine the root base theme.
$root_key = key($sub_theme->base_themes);
// Build the list of sub-themes for each of the theme's base themes.
foreach (array_keys($sub_theme->base_themes) as $base_theme) {
$themes[$base_theme]->sub_themes[$key] = $sub_theme->info['name'];
Angie Byron
committed
// Add the theme engine info from the root base theme.
if (isset($themes[$root_key]->owner)) {
$sub_theme->info['engine'] = $themes[$root_key]->info['engine'];
$sub_theme->owner = $themes[$root_key]->owner;
$sub_theme->prefix = $themes[$root_key]->prefix;
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
}
}
return $themes;
}
/**
* {@inheritdoc}
*/
public function getBaseThemes(array $themes, $theme) {
return $this->doGetBaseThemes($themes, $theme);
}
/**
* Finds the base themes for the specific theme.
*
* @param array $themes
* An array of available themes.
* @param string $theme
* The name of the theme whose base we are looking for.
* @param array $used_themes
* (optional) A recursion parameter preventing endless loops. Defaults to
* an empty array.
*
* @return array
* An array of base themes.
*/
protected function doGetBaseThemes(array $themes, $theme, $used_themes = array()) {
if (!isset($themes[$theme]->info['base theme'])) {
return array();
}
$base_key = $themes[$theme]->info['base theme'];
// Does the base theme exist?
if (!isset($themes[$base_key])) {
return array($base_key => NULL);
}
$current_base_theme = array($base_key => $themes[$base_key]->info['name']);
// Is the base theme itself a child of another theme?
if (isset($themes[$base_key]->info['base theme'])) {
// Do we already know the base themes of this theme?
if (isset($themes[$base_key]->base_themes)) {
return $themes[$base_key]->base_themes + $current_base_theme;
}
// Prevent loops.
if (!empty($used_themes[$base_key])) {
return array($base_key => NULL);
}
$used_themes[$base_key] = TRUE;
catch
committed
return $this->doGetBaseThemes($themes, $base_key, $used_themes) + $current_base_theme;
}
// If we get here, then this is our parent theme.
return $current_base_theme;
}
/**
catch
committed
* Returns an extension discovery object.
catch
committed
* @return \Drupal\Core\Extension\ExtensionDiscovery
* The extension discovery object.
catch
committed
protected function getExtensionDiscovery() {
if (!isset($this->extensionDiscovery)) {
$this->extensionDiscovery = new ExtensionDiscovery($this->root);
catch
committed
return $this->extensionDiscovery;
Angie Byron
committed
/**
* {@inheritdoc}
*/
public function getName($theme) {
$themes = $this->listInfo();
if (!isset($themes[$theme])) {
catch
committed
throw new \InvalidArgumentException("Requested the name of a non-existing theme $theme");
Angie Byron
committed
}
return SafeMarkup::checkPlain($themes[$theme]->info['name']);
Angie Byron
committed
}
/**
* Wraps system_list_reset().
*/
protected function systemListReset() {
system_list_reset();
}
/**
* Wraps system_list().
*
* @return array
* A list of themes keyed by name.
*/
protected function systemThemeList() {
return system_list('theme');
}
Angie Byron
committed
/**
* {@inheritdoc}
*/
public function getThemeDirectories() {
$dirs = array();
foreach ($this->listInfo() as $name => $theme) {
$dirs[$name] = $this->root . '/' . $theme->getPath();
Angie Byron
committed
}
return $dirs;
}
/**
* {@inheritdoc}
*/
public function themeExists($theme) {
$themes = $this->listInfo();
return isset($themes[$theme]);
}
/**
* {@inheritdoc}
*/
public function getTheme($name) {
$themes = $this->listInfo();
if (isset($themes[$name])) {
return $themes[$name];
}
throw new \InvalidArgumentException(sprintf('The theme %s does not exist.', $name));
}