diff --git a/core/config/install/core.extension.yml b/core/config/install/core.extension.yml index eae39ef..1514a9e 100644 --- a/core/config/install/core.extension.yml +++ b/core/config/install/core.extension.yml @@ -1,5 +1,4 @@ module: {} -theme: - stark: 0 +theme: {} disabled: theme: {} diff --git a/core/core.services.yml b/core/core.services.yml index 1e724a9..afbd007 100644 --- a/core/core.services.yml +++ b/core/core.services.yml @@ -195,7 +195,7 @@ services: arguments: ['%container.modules%', '@state', '@cache.bootstrap'] theme_handler: class: Drupal\Core\Extension\ThemeHandler - arguments: ['@config.factory', '@module_handler', '@cache.default', '@info_parser', '@config.installer', '@router.builder'] + arguments: ['@config.factory', '@module_handler', '@state', '@info_parser', '@config.installer', '@router.builder'] entity.manager: class: Drupal\Core\Entity\EntityManager arguments: ['@container.namespaces', '@module_handler', '@cache.discovery', '@language_manager', '@string_translation'] diff --git a/core/includes/install.core.inc b/core/includes/install.core.inc index d66bb30..994757a 100644 --- a/core/includes/install.core.inc +++ b/core/includes/install.core.inc @@ -685,6 +685,8 @@ function install_tasks($install_state) { 'display_name' => t('Install site'), 'type' => 'batch', ), + 'install_profile_themes' => array( + ), 'install_import_translations' => array( 'display_name' => t('Set up translations'), 'display' => $needs_translations, @@ -1836,6 +1838,34 @@ function install_profile_modules(&$install_state) { } /** + * Installs themes. + * + * This does not use a batch, since installing themes is faster than modules and + * because an installation profile typically enables 1-3 themes only (default + * theme, base theme, admin theme). + * + * @param $install_state + * An array of information about the current installation state. + */ +function install_profile_themes(&$install_state) { + $theme_handler = \Drupal::service('theme_handler'); + + // ThemeHandler::enable() resets the current list of themes. The theme used in + // the installer is not necessarily in the list of themes to install, so + // retain the current list. + // @see _drupal_maintenance_theme() + $current_themes = $theme_handler->listInfo(); + + // Install the themes specified by the installation profile. + $themes = $install_state['profile_info']['themes']; + $theme_handler->enable($themes); + + foreach ($current_themes as $theme) { + $theme_handler->addTheme($theme); + } +} + +/** * Imports languages via a batch process during installation. * * @param $install_state diff --git a/core/includes/install.inc b/core/includes/install.inc index 3789ff3..ac6e00e 100644 --- a/core/includes/install.inc +++ b/core/includes/install.inc @@ -1077,6 +1077,7 @@ function install_profile_info($profile, $langcode = 'en') { // Set defaults for module info. $defaults = array( 'dependencies' => array(), + 'themes' => array('stark'), 'description' => '', 'version' => NULL, 'hidden' => FALSE, diff --git a/core/includes/module.inc b/core/includes/module.inc index 18de4fc..cbbe811 100644 --- a/core/includes/module.inc +++ b/core/includes/module.inc @@ -9,26 +9,18 @@ use Drupal\Core\Extension\ExtensionDiscovery; /** - * Builds a list of bootstrap modules and enabled modules and themes. + * Builds a list of enabled themes. * * @param $type * The type of list to return: - * - module_enabled: All enabled modules. - * - bootstrap: All enabled modules required for bootstrap. - * - theme: All themes. + * - theme: All enabled themes. * * @return - * An associative array of modules or themes, keyed by name. For $type - * 'bootstrap' and 'module_enabled', the array values equal the keys. + * An associative array of themes, keyed by name. * For $type 'theme', the array values are objects representing the * respective database row, with the 'info' property already unserialized. * * @see list_themes() - * - * @todo There are too many layers/levels of caching involved for system_list() - * data. Consider to add a \Drupal::config($name, $cache = TRUE) argument to allow - * callers like system_list() to force-disable a possible configuration - * storage cache or some other way to circumvent it/take it over. */ function system_list($type) { $lists = &drupal_static(__FUNCTION__); @@ -40,32 +32,15 @@ function system_list($type) { 'theme' => array(), 'filepaths' => array(), ); - // Build a list of themes. - $enabled_themes = \Drupal::config('core.extension')->get('theme') ?: array(); - // @todo Themes include all themes, including disabled/uninstalled. This - // system.theme.data state will go away entirely as soon as themes have - // a proper installation status. - // @see http://drupal.org/node/1067408 - $theme_data = \Drupal::state()->get('system.theme.data'); - if (empty($theme_data)) { - // @todo: system_list() may be called from _drupal_bootstrap_code(), in - // which case system.module is not loaded yet. - // Prevent a filesystem scan in drupal_load() and include it directly. - // @see http://drupal.org/node/1067408 - require_once DRUPAL_ROOT . '/core/modules/system/system.module'; - $theme_data = system_rebuild_theme_data(); - } + // ThemeHandler maintains the 'system.theme.data' state record. + $theme_data = \Drupal::state()->get('system.theme.data', array()); foreach ($theme_data as $name => $theme) { - $theme->status = (int) isset($enabled_themes[$name]); $lists['theme'][$name] = $theme; - // Build a list of filenames so drupal_get_filename can use it. - if (isset($enabled_themes[$name])) { - $lists['filepaths'][] = array( - 'type' => 'theme', - 'name' => $name, - 'filepath' => $theme->getPathname(), - ); - } + $lists['filepaths'][] = array( + 'type' => 'theme', + 'name' => $name, + 'filepath' => $theme->getPathname(), + ); } \Drupal::cache('bootstrap')->set('system_list', $lists); } @@ -84,25 +59,17 @@ function system_list($type) { function system_list_reset() { drupal_static_reset('system_list'); drupal_static_reset('system_rebuild_module_data'); - drupal_static_reset('list_themes'); \Drupal::cache('bootstrap')->delete('system_list'); - \Drupal::cache()->delete('system_info'); // Clear the library info cache. // Libraries may be provided by all extension types, and may be altered by any // other extensions (types) due to the nature of // \Drupal\Core\Extension\ModuleHandler::alter() and the fact that profiles // are recorded and handled as modules. + // @todo Trigger an event upon module install/uninstall and theme + // enable/disable, and move this into an event subscriber. + // @see https://drupal.org/node/2206347 Cache::invalidateTags(array('extension' => TRUE)); - - // Remove last known theme data state. - // This causes system_list() to call system_rebuild_theme_data() on its next - // invocation. When enabling a module that implements hook_system_info_alter() - // to inject a new (testing) theme or manipulate an existing theme, then that - // will cause system_list_reset() to be called, but theme data is not - // necessarily rebuilt afterwards. - // @todo Obsolete with proper installation status for themes. - \Drupal::state()->delete('system.theme.data'); } /** diff --git a/core/includes/theme.inc b/core/includes/theme.inc index 6909403..5ef827c 100644 --- a/core/includes/theme.inc +++ b/core/includes/theme.inc @@ -103,7 +103,19 @@ function drupal_theme_initialize() { // Determine the active theme for the theme negotiator service. This includes // the default theme as well as really specific ones like the ajax base theme. $request = \Drupal::request(); - $theme = \Drupal::service('theme.negotiator')->determineActiveTheme($request) ?: 'stark'; + $theme = \Drupal::service('theme.negotiator')->determineActiveTheme($request); + + // If no theme could be negotiated, or if the negotiated theme is not within + // the list of enabled themes, fall back to the default theme output of core + // and modules (similar to Stark, but without a theme extension at all). This + // is possible, because _drupal_theme_initialize() always loads the Twig theme + // engine. + if (!$theme || !isset($themes[$theme])) { + $theme = 'core'; + $theme_key = $theme; + _drupal_theme_initialize(new Extension('theme', 'core/core.info.yml')); + return; + } // Store the identifier for retrieving theme settings with. $theme_key = $theme; @@ -401,6 +413,8 @@ function _theme($hook, $variables = array()) { if (!$module_handler->isLoaded() && !defined('MAINTENANCE_MODE')) { throw new Exception(t('_theme() may not be called until all modules are loaded.')); } + // Ensure the theme is initialized. + drupal_theme_initialize(); /** @var \Drupal\Core\Utility\ThemeRegistry $theme_registry */ $theme_registry = \Drupal::service('theme.registry')->getRuntime(); @@ -851,8 +865,8 @@ function theme_get_setting($setting_name, $theme = NULL) { // Get the values for the theme-specific settings from the .info.yml files // of the theme and all its base themes. - if ($theme) { - $themes = list_themes(); + $themes = list_themes(); + if ($theme && isset($themes[$theme])) { $theme_object = $themes[$theme]; // Create a list which includes the current theme and all its base themes. @@ -874,7 +888,7 @@ function theme_get_setting($setting_name, $theme = NULL) { // Get the global settings from configuration. $cache[$theme]->merge(\Drupal::config('system.theme.global')->get()); - if ($theme) { + if ($theme && isset($themes[$theme])) { // Retrieve configured theme-specific settings, if any. try { if ($theme_settings = \Drupal::config($theme . '.settings')->get()) { @@ -981,7 +995,7 @@ function theme_settings_convert_to_config(array $theme_settings, Config $config) * @see \Drupal\Core\Extension\ThemeHandler::enable(). */ function theme_enable($theme_list) { - \Drupal::service('theme_handler')->enable($theme_list); + return \Drupal::service('theme_handler')->enable($theme_list); } /** @@ -996,7 +1010,7 @@ function theme_enable($theme_list) { * @see \Drupal\Core\Extension\ThemeHandler::disable(). */ function theme_disable($theme_list) { - \Drupal::service('theme_handler')->disable($theme_list); + return \Drupal::service('theme_handler')->disable($theme_list); } /** diff --git a/core/includes/theme.maintenance.inc b/core/includes/theme.maintenance.inc index 57183d6..171afa0 100644 --- a/core/includes/theme.maintenance.inc +++ b/core/includes/theme.maintenance.inc @@ -72,7 +72,7 @@ function _drupal_maintenance_theme() { } // Ensure that system.module is loaded. - if (!function_exists('_system_rebuild_theme_data')) { + if (!function_exists('system_rebuild_theme_data')) { $module_handler = \Drupal::moduleHandler(); $module_handler->addModule('system', 'core/modules/system'); $module_handler->load('system'); @@ -80,6 +80,14 @@ function _drupal_maintenance_theme() { $themes = list_themes(); + // If no themes are installed yet, or if the requested custom theme is not + // installed, retrieve all available themes. + if (empty($themes) || !isset($themes[$custom_theme])) { + $theme_handler = \Drupal::service('theme_handler'); + $themes = $theme_handler->rebuildThemeData(); + $theme_handler->addTheme($themes[$custom_theme]); + } + // list_themes() triggers a \Drupal\Core\Extension\ModuleHandler::alter() in // maintenance mode, but we can't let themes alter the .info.yml data until // we know a theme's base themes. So don't set global $theme until after diff --git a/core/lib/Drupal/Core/DependencyInjection/UpdateServiceProvider.php b/core/lib/Drupal/Core/DependencyInjection/UpdateServiceProvider.php index 60dfb53..32ad9b7 100644 --- a/core/lib/Drupal/Core/DependencyInjection/UpdateServiceProvider.php +++ b/core/lib/Drupal/Core/DependencyInjection/UpdateServiceProvider.php @@ -41,7 +41,7 @@ public function register(ContainerBuilder $container) { $container->register('theme_handler', 'Drupal\Core\Extension\ThemeHandler') ->addArgument(new Reference('config.factory')) ->addArgument(new Reference('module_handler')) - ->addArgument(new Reference('cache.default')) + ->addArgument(new Reference('state')) ->addArgument(new Reference('info_parser')); } } diff --git a/core/lib/Drupal/Core/Extension/ModuleHandler.php b/core/lib/Drupal/Core/Extension/ModuleHandler.php index ac48c17..e70399d 100644 --- a/core/lib/Drupal/Core/Extension/ModuleHandler.php +++ b/core/lib/Drupal/Core/Extension/ModuleHandler.php @@ -656,8 +656,6 @@ public function install(array $module_list, $enable_dependencies = TRUE) { // Refresh the schema to include it. drupal_get_schema(NULL, TRUE); - // Update the theme registry to include it. - drupal_theme_rebuild(); // Allow modules to react prior to the installation of a module. $this->invokeAll('module_preinstall', array($module)); @@ -702,8 +700,18 @@ public function install(array $module_list, $enable_dependencies = TRUE) { // Record the fact that it was installed. $modules_installed[] = $module; + // Update the theme registry to include it. + drupal_theme_rebuild(); + + // Modules can alter theme info, so refresh theme data. + // @todo ThemeHandler cannot be injected into ModuleHandler, since that + // causes a circular service dependency. + // @see https://drupal.org/node/2208429 + \Drupal::service('theme_handler')->refreshInfo(); + // Allow the module to perform install tasks. $this->invoke($module, 'install'); + // Record the fact that it was installed. watchdog('system', '%module module installed.', array('%module' => $module), WATCHDOG_INFO); } @@ -810,6 +818,12 @@ public function uninstall(array $module_list, $uninstall_dependents = TRUE) { // Update the theme registry to remove the newly uninstalled module. drupal_theme_rebuild(); + // Modules can alter theme info, so refresh theme data. + // @todo ThemeHandler cannot be injected into ModuleHandler, since that + // causes a circular service dependency. + // @see https://drupal.org/node/2208429 + \Drupal::service('theme_handler')->refreshInfo(); + watchdog('system', '%module module uninstalled.', array('%module' => $module), WATCHDOG_INFO); $schema_store->delete($module); diff --git a/core/lib/Drupal/Core/Extension/ThemeHandler.php b/core/lib/Drupal/Core/Extension/ThemeHandler.php index b43d3e4..b47fea1 100644 --- a/core/lib/Drupal/Core/Extension/ThemeHandler.php +++ b/core/lib/Drupal/Core/Extension/ThemeHandler.php @@ -9,9 +9,9 @@ use Drupal\Component\Utility\String; use Drupal\Core\Cache\Cache; -use Drupal\Core\Cache\CacheBackendInterface; use Drupal\Core\Config\ConfigFactoryInterface; use Drupal\Core\Config\ConfigInstallerInterface; +use Drupal\Core\KeyValueStore\StateInterface; use Drupal\Core\Routing\RouteBuilder; /** @@ -41,7 +41,7 @@ class ThemeHandler implements ThemeHandlerInterface { * * @var array */ - protected $list = array(); + protected $list; /** * The config factory to get the enabled themes. @@ -58,11 +58,11 @@ class ThemeHandler implements ThemeHandlerInterface { protected $moduleHandler; /** - * The cache backend to clear the local tasks cache. + * The state backend. * - * @var \Drupal\Core\Cache\CacheBackendInterface + * @var \Drupal\Core\KeyValueStore\StateInterface */ - protected $cacheBackend; + protected $state; /** * The config installer to install configuration. @@ -99,7 +99,7 @@ class ThemeHandler implements ThemeHandlerInterface { * The config factory to get the enabled themes. * @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler * The module handler to fire themes_enabled/themes_disabled hooks. - * @param \Drupal\Core\Cache\CacheBackendInterface $cache_backend + * @param \Drupal\Core\KeyValueStore\StateInterface $state * The cache backend to clear the local tasks cache. * @param \Drupal\Core\Extension\InfoParserInterface $info_parser * The info parser to parse the theme.info.yml files. @@ -112,10 +112,10 @@ class ThemeHandler implements ThemeHandlerInterface { * @param \Drupal\Core\Extension\ExtensionDiscovery $extension_discovery * (optional) A extension discovery instance (for unit tests). */ - public function __construct(ConfigFactoryInterface $config_factory, ModuleHandlerInterface $module_handler, CacheBackendInterface $cache_backend, InfoParserInterface $info_parser, ConfigInstallerInterface $config_installer = NULL, RouteBuilder $route_builder = NULL, ExtensionDiscovery $extension_discovery = NULL) { + public function __construct(ConfigFactoryInterface $config_factory, ModuleHandlerInterface $module_handler, StateInterface $state, InfoParserInterface $info_parser, ConfigInstallerInterface $config_installer = NULL, RouteBuilder $route_builder = NULL, ExtensionDiscovery $extension_discovery = NULL) { $this->configFactory = $config_factory; $this->moduleHandler = $module_handler; - $this->cacheBackend = $cache_backend; + $this->state = $state; $this->infoParser = $info_parser; $this->configInstaller = $config_installer; $this->routeBuilder = $route_builder; @@ -125,10 +125,90 @@ public function __construct(ConfigFactoryInterface $config_factory, ModuleHandle /** * {@inheritdoc} */ - public function enable(array $theme_list) { - $this->clearCssCache(); + public function getDefault() { + return $this->configFactory->get('system.theme')->get('default'); + } + + /** + * {@inheritdoc} + */ + public function setDefault($name) { + if (!isset($this->list)) { + $this->listInfo(); + } + if (!isset($this->list[$name])) { + throw new \InvalidArgumentException("$name theme is not enabled."); + } + $this->configFactory->get('system.theme') + ->set('default', $name) + ->save(); + return $this; + } + + /** + * {@inheritdoc} + */ + public function enable(array $theme_list, $enable_dependencies = TRUE) { $extension_config = $this->configFactory->get('core.extension'); + + $theme_data = $this->rebuildThemeData(); + + if ($enable_dependencies) { + $theme_list = array_combine($theme_list, $theme_list); + + if ($missing = array_diff_key($theme_list, $theme_data)) { + // One or more of the given themes doesn't exist. + throw new \InvalidArgumentException(String::format('Unknown themes: !themes.', array( + '!themes' => implode(', ', $missing), + ))); + } + + // Only process themes that are not enabled currently. + $installed_themes = $extension_config->get('theme') ?: array(); + if (!$theme_list = array_diff_key($theme_list, $installed_themes)) { + // Nothing to do. All themes already enabled. + return TRUE; + } + $installed_themes += $extension_config->get('disabled.theme') ?: array(); + + while (list($theme) = each($theme_list)) { + // Add dependencies to the list. The new themes will be processed as + // the while loop continues. + foreach (array_keys($theme_data[$theme]->requires) as $dependency) { + if (!isset($theme_data[$dependency])) { + // The dependency does not exist. + return FALSE; + } + + // Skip already installed themes. + if (!isset($theme_list[$dependency]) && !isset($installed_themes[$dependency])) { + $theme_list[$dependency] = $dependency; + } + } + } + + // Set the actual theme weights. + $theme_list = array_map(function ($theme) use ($theme_data) { + return $theme_data[$theme]->sort; + }, $theme_list); + + // Sort the theme list by their weights (reverse). + arsort($theme_list); + $theme_list = array_keys($theme_list); + } + else { + $installed_themes = $extension_config->get('theme') ?: array(); + $installed_themes += $extension_config->get('disabled.theme') ?: array(); + } + + $themes_enabled = array(); foreach ($theme_list as $key) { + // Only process themes that are not already enabled. + $enabled = $extension_config->get("theme.$key") !== NULL; + if ($enabled) { + continue; + } + // Throw an exception if the theme name is too long. if (strlen($key) > DRUPAL_EXTENSION_NAME_MAX_LENGTH) { throw new ExtensionNameLengthException(String::format('Theme name %name is over the maximum allowed length of @max characters.', array( @@ -143,53 +223,94 @@ public function enable(array $theme_list) { ->clear("disabled.theme.$key") ->save(); - // Refresh the theme list as installation of default configuration needs - // an updated list to work. - $this->reset(); - - // The default config installation storage only knows about the currently - // enabled list of themes, so it has to be reset in order to pick up the - // default config of the newly installed theme. However, do not reset the - // source storage when synchronizing configuration, since that would - // needlessly trigger a reload of the whole configuration to be imported. - if (!$this->configInstaller->isSyncing()) { - $this->configInstaller->resetSourceStorage(); + // Add the theme to the current list. + // @todo Remove all code that relies on $status property. + $theme_data[$key]->status = 1; + $this->addTheme($theme_data[$key]); + + // Update the current theme data accordingly. + $current_theme_data = $this->state->get('system.theme.data', array()); + $current_theme_data[$key] = $theme_data[$key]; + $this->state->set('system.theme.data', $current_theme_data); + + // Reset theme settings. + $theme_settings = &drupal_static('theme_get_setting'); + unset($theme_settings[$key]); + + // @todo Remove system_list(). + $this->systemListReset(); + + // Only install default configuration if this theme has not been installed + // already. + if (!isset($installed_themes[$key])) { + // The default config installation storage only knows about the currently + // enabled list of themes, so it has to be reset in order to pick up the + // default config of the newly installed theme. However, do not reset the + // source storage when synchronizing configuration, since that would + // needlessly trigger a reload of the whole configuration to be imported. + if (!$this->configInstaller->isSyncing()) { + $this->configInstaller->resetSourceStorage(); + } + + // Install default configuration of the theme. + $this->configInstaller->installDefaultConfig('theme', $key); } - // Install default configuration of the theme. - $this->configInstaller->installDefaultConfig('theme', $key); + + $themes_enabled[] = $key; + + // Record the fact that it was enabled. + watchdog('system', '%theme theme enabled.', array('%theme' => $key), WATCHDOG_INFO); } + $this->clearCssCache(); $this->resetSystem(); // Invoke hook_themes_enabled() after the themes have been enabled. - $this->moduleHandler->invokeAll('themes_enabled', array($theme_list)); + $this->moduleHandler->invokeAll('themes_enabled', array($themes_enabled)); + + return !empty($themes_enabled); } /** * {@inheritdoc} */ public function disable(array $theme_list) { - // Don't disable the default or admin themes. $theme_config = $this->configFactory->get('system.theme'); - $default_theme = $theme_config->get('default'); - $admin_theme = $theme_config->get('admin'); - $theme_list = array_diff($theme_list, array($default_theme, $admin_theme)); - if (empty($theme_list)) { - return; + // Do not disable the default theme. + if (in_array($name = $theme_config->get('default'), $theme_list, TRUE)) { + throw new \InvalidArgumentException("The current default theme $name cannot be disabled."); + } + // Do not disable the admin theme. + if (in_array($name = $theme_config->get('admin'), $theme_list, TRUE)) { + throw new \InvalidArgumentException("The current admin theme $name cannot be disabled."); } $this->clearCssCache(); $extension_config = $this->configFactory->get('core.extension'); + $current_theme_data = $this->state->get('system.theme.data', array()); foreach ($theme_list as $key) { // The value is not used; the weight is ignored for themes currently. $extension_config ->clear("theme.$key") ->set("disabled.theme.$key", 0); + + // Remove the theme from the current list. + unset($this->list[$key]); + + // Update the current theme data accordingly. + unset($current_theme_data[$key]); + + // Reset theme settings. + $theme_settings = &drupal_static('theme_get_setting'); + unset($theme_settings[$key]); + + // @todo Remove system_list(). + $this->systemListReset(); } $extension_config->save(); + $this->state->set('system.theme.data', $current_theme_data); - $this->reset(); $this->resetSystem(); // Invoke hook_themes_disabled after the themes have been disabled. @@ -200,52 +321,63 @@ public function disable(array $theme_list) { * {@inheritdoc} */ public function listInfo() { - if (empty($this->list)) { + if (!isset($this->list)) { $this->list = array(); - try { - $themes = $this->systemThemeList(); + $themes = $this->systemThemeList(); + foreach ($themes as $theme) { + $this->addTheme($theme); } - catch (\Exception $e) { - // If the database is not available, rebuild the theme data. - $themes = $this->rebuildThemeData(); + } + return $this->list; + } + + /** + * {@inheritdoc} + */ + public function addTheme(Extension $theme) { + // @todo Remove this 100% unnecessary duplication of properties. + foreach ($theme->info['stylesheets'] as $media => $stylesheets) { + foreach ($stylesheets as $stylesheet => $path) { + $theme->stylesheets[$media][$stylesheet] = $path; } + } + 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; + } - foreach ($themes as $theme) { - foreach ($theme->info['stylesheets'] as $media => $stylesheets) { - foreach ($stylesheets as $stylesheet => $path) { - $theme->stylesheets[$media][$stylesheet] = $path; - } - } - 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']; - } - // Status is normally retrieved from the database. Add zero values when - // read from the installation directory to prevent notices. - if (!isset($theme->status)) { - $theme->status = 0; - } - $this->list[$theme->getName()] = $theme; + /** + * {@inheritdoc} + */ + public function refreshInfo() { + $this->reset(); + $extension_config = $this->configFactory->get('core.extension'); + $enabled = $extension_config->get('theme') ?: array(); + + // @todo Avoid re-scanning all themes by retaining the original (unaltered) + // theme info somewhere. + $list = $this->rebuildThemeData(); + foreach ($list as $name => $theme) { + if (isset($enabled[$name])) { + $this->list[$name] = $theme; } } - return $this->list; + $this->state->set('system.theme.data', $this->list); } /** * {@inheritdoc} */ public function reset() { - // listInfo() calls system_info() which has a lot of side effects that have - // to be triggered like the classloading of theme classes. - $this->list = array(); $this->systemListReset(); - $this->listInfo(); - $this->list = array(); + $this->list = NULL; } /** @@ -255,6 +387,8 @@ public function rebuildThemeData() { $listing = $this->getExtensionDiscovery(); $themes = $listing->scan('theme'); $engines = $listing->scan('theme_engine'); + $extension_config = $this->configFactory->get('core.extension'); + $enabled = $extension_config->get('theme') ?: array(); // Set defaults for theme info. $defaults = array( @@ -279,8 +413,12 @@ public function rebuildThemeData() { ); $sub_themes = array(); + $files = array(); // Read info files for each theme. foreach ($themes as $key => $theme) { + // @todo Remove all code that relies on the $status property. + $theme->status = (int) isset($enabled[$key]); + $theme->info = $this->infoParser->parse($theme->getPathname()) + $defaults; // Add the info file modification time, so it becomes available for @@ -295,6 +433,8 @@ public function rebuildThemeData() { 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']; } // Defaults to 'twig' (see $defaults above). @@ -310,7 +450,17 @@ public function rebuildThemeData() { if (!empty($theme->info['screenshot'])) { $theme->info['screenshot'] = $path . '/' . $theme->info['screenshot']; } + + $files[$key] = $theme->getPathname(); } + // Build dependencies. + // @todo Move into a generic ExtensionHandler base class. + // @see https://drupal.org/node/2208429 + $themes = $this->moduleHandler->buildModuleDependencies($themes); + + // Store filenames to allow system_list() and drupal_get_filename() to + // retrieve them without having to scan the filesystem. + $this->state->set('system.theme.files', $files); // After establishing the full list of available themes, fill in data for // sub-themes. diff --git a/core/lib/Drupal/Core/Extension/ThemeHandlerInterface.php b/core/lib/Drupal/Core/Extension/ThemeHandlerInterface.php index 2ba52cd..fbf57fd 100644 --- a/core/lib/Drupal/Core/Extension/ThemeHandlerInterface.php +++ b/core/lib/Drupal/Core/Extension/ThemeHandlerInterface.php @@ -17,11 +17,15 @@ * * @param array $theme_list * An array of theme names. + * @param bool $enable_dependencies + * (optional) If TRUE, dependencies will automatically be installed in the + * correct order. This incurs a significant performance cost, so use FALSE + * if you know $theme_list is already complete and in the correct order. * * @throws \Drupal\Core\Extension\ExtensionNameLengthException * Thrown when the theme name is to long */ - public function enable(array $theme_list); + public function enable(array $theme_list, $enable_dependencies = TRUE); /** * Disables a given list of themes. @@ -32,13 +36,10 @@ public function enable(array $theme_list); public function disable(array $theme_list); /** - * Returns a list of all currently available themes. - * - * Retrieved from the database, if available and the site is not in - * maintenance mode; otherwise compiled freshly from the filesystem. + * Returns a list of currently enabled themes. * * @return \Drupal\Core\Extension\Extension[] - * An associative array of the currently available themes. The keys are the + * An associative array of the currently enabled themes. The keys are the * themes' machine names and the values are objects having the following * properties: * - filename: The filepath and name of the .info.yml file. @@ -76,6 +77,14 @@ public function disable(array $theme_list); public function listInfo(); /** + * Refreshes the theme info data of currently enabled themes. + * + * Modules can alter theme info, so this is typically called after a module + * has been installed or uninstalled. + */ + public function refreshInfo(); + + /** * Resets the internal state of the theme handler. */ public function reset(); diff --git a/core/lib/Drupal/Core/Theme/Registry.php b/core/lib/Drupal/Core/Theme/Registry.php index e84b33b..7cc66f7 100644 --- a/core/lib/Drupal/Core/Theme/Registry.php +++ b/core/lib/Drupal/Core/Theme/Registry.php @@ -10,6 +10,7 @@ use Drupal\Core\Cache\Cache; use Drupal\Core\Cache\CacheBackendInterface; use Drupal\Core\DestructableInterface; +use Drupal\Core\Extension\Extension; use Drupal\Core\Extension\ModuleHandlerInterface; use Drupal\Core\Lock\LockBackendInterface; use Drupal\Core\Utility\ThemeRegistry; @@ -145,23 +146,16 @@ public function __construct(CacheBackendInterface $cache, LockBackendInterface $ protected function init($theme_name = NULL) { // Unless instantiated for a specific theme, use globals. if (!isset($theme_name)) { - // #1: The theme registry might get instantiated before the theme was - // initialized. Cope with that. - if (!isset($GLOBALS['theme_info']) || !isset($GLOBALS['theme'])) { - unset($this->runtimeRegistry); - unset($this->registry); - drupal_theme_initialize(); + if (isset($GLOBALS['theme']) && isset($GLOBALS['theme_info'])) { + $this->theme = $GLOBALS['theme_info']; + $this->baseThemes = $GLOBALS['base_theme_info']; + $this->engine = $GLOBALS['theme_engine']; } - // #2: The testing framework only cares for the global $theme variable at - // this point. Cope with that. - if ($GLOBALS['theme'] != $GLOBALS['theme_info']->getName()) { - unset($this->runtimeRegistry); - unset($this->registry); - $this->initializeTheme(); + else { + $this->theme = new Extension('theme', 'core/modules/system/system.info.yml'); + $this->baseThemes = array(); + $this->engine = 'twig'; } - $this->theme = $GLOBALS['theme_info']; - $this->baseThemes = $GLOBALS['base_theme_info']; - $this->engine = $GLOBALS['theme_engine']; } // Instead of the global theme, a specific theme was requested. else { diff --git a/core/modules/config/lib/Drupal/config/Tests/ConfigImportUITest.php b/core/modules/config/lib/Drupal/config/Tests/ConfigImportUITest.php index 92b7656..b95a666 100644 --- a/core/modules/config/lib/Drupal/config/Tests/ConfigImportUITest.php +++ b/core/modules/config/lib/Drupal/config/Tests/ConfigImportUITest.php @@ -211,7 +211,7 @@ function testImport() { $this->assertTrue(empty($installed), 'No modules installed during import'); $theme_info = \Drupal::service('theme_handler')->listInfo(); - $this->assertTrue(isset($theme_info['bartik']) && !$theme_info['bartik']->status, 'Bartik theme disabled during import.'); + $this->assertFalse(isset($theme_info['bartik']), 'Bartik theme disabled during import.'); // Verify that the action.settings configuration object was only deleted // once during the import process. diff --git a/core/modules/config_translation/config_translation.module b/core/modules/config_translation/config_translation.module index 74047f7..e30f09a 100644 --- a/core/modules/config_translation/config_translation.module +++ b/core/modules/config_translation/config_translation.module @@ -56,6 +56,28 @@ function config_translation_theme() { } /** + * Implements hook_themes_enabled(). + */ +function config_translation_themes_enabled() { + // Themes can provide *.config_translation.yml declarations. + // @todo Make ThemeHandler trigger an event instead and make + // ConfigMapperManager plugin manager subscribe to it. + // @see https://drupal.org/node/2206347 + \Drupal::service('plugin.manager.config_translation.mapper')->clearCachedDefinitions(); +} + +/** + * Implements hook_themes_disabled(). + */ +function config_translation_themes_disabled() { + // Themes can provide *.config_translation.yml declarations. + // @todo Make ThemeHandler trigger an event instead and make + // ConfigMapperManager plugin manager subscribe to it. + // @see https://drupal.org/node/2206347 + \Drupal::service('plugin.manager.config_translation.mapper')->clearCachedDefinitions(); +} + +/** * Implements hook_entity_type_alter(). */ function config_translation_entity_type_alter(array &$entity_types) { diff --git a/core/modules/config_translation/lib/Drupal/config_translation/ConfigMapperManager.php b/core/modules/config_translation/lib/Drupal/config_translation/ConfigMapperManager.php index bdfbe55..e90c5bf 100644 --- a/core/modules/config_translation/lib/Drupal/config_translation/ConfigMapperManager.php +++ b/core/modules/config_translation/lib/Drupal/config_translation/ConfigMapperManager.php @@ -67,6 +67,14 @@ public function __construct(CacheBackendInterface $cache_backend, LanguageManage $this->typedConfigManager = $typed_config_manager; // Look at all themes and modules. + // @todo If the list of enabled modules and themes is changed, new + // definitions are not picked up immediately and obsolete definitions are + // not removed, because the list of search directories is only compiled + // once in this constructor. The current code only works due to + // coincidence: The request that enables e.g. a new theme does not + // instantiate this plugin manager at the beginning of the request; when + // routes are being rebuilt at the end of the request, this service only + // happens to get instantiated with the updated list of enabled themes. $directories = array(); foreach ($module_handler->getModuleList() as $name => $module) { $directories[$name] = $module->getPath(); diff --git a/core/modules/config_translation/lib/Drupal/config_translation/Tests/ConfigTranslationUiTest.php b/core/modules/config_translation/lib/Drupal/config_translation/Tests/ConfigTranslationUiTest.php index cb07cd4..b9028a8 100644 --- a/core/modules/config_translation/lib/Drupal/config_translation/Tests/ConfigTranslationUiTest.php +++ b/core/modules/config_translation/lib/Drupal/config_translation/Tests/ConfigTranslationUiTest.php @@ -655,25 +655,6 @@ public function testAlterInfo() { } /** - * Tests that theme provided *.config_translation.yml files are found. - */ - public function testThemeDiscovery() { - // Enable the test theme and rebuild routes. - $theme = 'config_translation_test_theme'; - theme_enable(array($theme)); - // Enabling a theme will cause the kernel terminate event to rebuild the - // router. Simulate that here. - \Drupal::service('router.builder')->rebuildIfNeeded(); - - $this->drupalLogin($this->admin_user); - - $translation_base_url = 'admin/config/development/performance/translate'; - $this->drupalGet($translation_base_url); - $this->assertResponse(200); - $this->assertLinkByHref("$translation_base_url/fr/add"); - } - - /** * Gets translation from locale storage. * * @param $config_name diff --git a/core/modules/config_translation/lib/Drupal/config_translation/Tests/ConfigTranslationUiThemeTest.php b/core/modules/config_translation/lib/Drupal/config_translation/Tests/ConfigTranslationUiThemeTest.php new file mode 100644 index 0000000..b51cee3 --- /dev/null +++ b/core/modules/config_translation/lib/Drupal/config_translation/Tests/ConfigTranslationUiThemeTest.php @@ -0,0 +1,88 @@ + 'Theme Configuration Translation', + 'description' => 'Verifies theme configuration translation settings.', + 'group' => 'Configuration Translation', + ); + } + + public function setUp() { + parent::setUp(); + + $admin_permissions = array( + 'administer themes', + 'administer languages', + 'administer site configuration', + 'translate configuration', + ); + // Create and login user. + $this->admin_user = $this->drupalCreateUser($admin_permissions); + + // Add languages. + foreach ($this->langcodes as $langcode) { + $language = new Language(array('id' => $langcode)); + language_save($language); + } + } + + /** + * Tests that theme provided *.config_translation.yml files are found. + */ + public function testThemeDiscovery() { + // Enable the test theme and rebuild routes. + $theme = 'config_translation_test_theme'; + + $this->drupalLogin($this->admin_user); + + $this->drupalGet('admin/appearance'); + $elements = $this->xpath('//a[normalize-space()=:label and contains(@href, :theme)]', array( + ':label' => 'Enable and set as default', + ':theme' => $theme, + )); + $this->drupalGet($GLOBALS['base_root'] . $elements[0]['href'], array('external' => TRUE)); + + $translation_base_url = 'admin/config/development/performance/translate'; + $this->drupalGet($translation_base_url); + $this->assertResponse(200); + $this->assertLinkByHref("$translation_base_url/fr/add"); + } + +} diff --git a/core/modules/config_translation/tests/modules/config_translation_test/config_translation_test.module b/core/modules/config_translation/tests/modules/config_translation_test/config_translation_test.module index c71166f..d38be97 100644 --- a/core/modules/config_translation/tests/modules/config_translation_test/config_translation_test.module +++ b/core/modules/config_translation/tests/modules/config_translation_test/config_translation_test.module @@ -5,6 +5,18 @@ * Configuration Translation Test module. */ +use Drupal\Core\Extension\Extension; + +/** + * Implements hook_system_info_alter(). + */ +function config_translation_test_system_info_alter(array &$info, Extension $file, $type) { + // @see \Drupal\config_translation\Tests\ConfigTranslationUiThemeTest + if ($file->getType() == 'theme' && $file->getName() == 'config_translation_test_theme') { + $info['hidden'] = FALSE; + } +} + /** * Implements hook_entity_type_alter(). */ diff --git a/core/modules/system/lib/Drupal/system/Controller/SystemController.php b/core/modules/system/lib/Drupal/system/Controller/SystemController.php index 308701c..4d16d05 100644 --- a/core/modules/system/lib/Drupal/system/Controller/SystemController.php +++ b/core/modules/system/lib/Drupal/system/Controller/SystemController.php @@ -183,16 +183,19 @@ public function systemAdminMenuBlockPage() { * * @return string * An HTML string of the theme listing page. + * + * @todo Move into ThemeController. */ public function themesPage() { $config = $this->config('system.theme'); // Get current list of themes. - $themes = $this->themeHandler->listInfo(); + $themes = system_rebuild_theme_data(); uasort($themes, 'system_sort_modules_by_info_name'); $theme_default = $config->get('default'); - $theme_groups = array(); + $theme_groups = array('enabled' => array(), 'disabled' => array()); $admin_theme = $config->get('admin'); + $admin_theme_options = array(); foreach ($themes as &$theme) { if (!empty($theme->info['hidden'])) { diff --git a/core/modules/system/lib/Drupal/system/Controller/ThemeController.php b/core/modules/system/lib/Drupal/system/Controller/ThemeController.php index 86f6a4d..51d91f9 100644 --- a/core/modules/system/lib/Drupal/system/Controller/ThemeController.php +++ b/core/modules/system/lib/Drupal/system/Controller/ThemeController.php @@ -115,12 +115,8 @@ public function enable(Request $request) { $theme = $request->get('theme'); if (isset($theme)) { - // Get current list of themes. - $themes = $this->themeHandler->listInfo(); - - // Check if the specified theme is one recognized by the system. - if (!empty($themes[$theme])) { - $this->themeHandler->enable(array($theme)); + if ($this->themeHandler->enable(array($theme))) { + $themes = $this->themeHandler->listInfo(); drupal_set_message($this->t('The %theme theme has been enabled.', array('%theme' => $themes[$theme]->info['name']))); } else { @@ -154,11 +150,9 @@ public function setDefaultTheme(Request $request) { $themes = $this->themeHandler->listInfo(); // Check if the specified theme is one recognized by the system. - if (!empty($themes[$theme])) { - // Enable the theme if it is currently disabled. - if (empty($themes[$theme]->status)) { - $this->themeHandler->enable(array($theme)); - } + // Or try to enable the theme. + if (isset($themes[$theme]) || $this->themeHandler->enable(array($theme))) { + $themes = $this->themeHandler->listInfo(); // Set the default theme. $config->set('default', $theme)->save(); diff --git a/core/modules/system/lib/Drupal/system/Tests/Batch/PageTest.php b/core/modules/system/lib/Drupal/system/Tests/Batch/PageTest.php index f268e02..06521a7 100644 --- a/core/modules/system/lib/Drupal/system/Tests/Batch/PageTest.php +++ b/core/modules/system/lib/Drupal/system/Tests/Batch/PageTest.php @@ -35,11 +35,12 @@ public static function getInfo() { function testBatchProgressPageTheme() { // Make sure that the page which starts the batch (an administrative page) // is using a different theme than would normally be used by the batch API. + \Drupal::service('theme_handler')->enable(array('seven', 'bartik')); \Drupal::config('system.theme') ->set('default', 'bartik') + ->set('admin', 'seven') ->save(); - theme_enable(array('seven')); - \Drupal::config('system.theme')->set('admin', 'seven')->save(); + // Log in as an administrator who can see the administrative theme. $admin_user = $this->drupalCreateUser(array('view the administration theme')); $this->drupalLogin($admin_user); diff --git a/core/modules/system/lib/Drupal/system/Tests/Common/RenderElementTypesTest.php b/core/modules/system/lib/Drupal/system/Tests/Common/RenderElementTypesTest.php index 001b807..41a9ac5 100644 --- a/core/modules/system/lib/Drupal/system/Tests/Common/RenderElementTypesTest.php +++ b/core/modules/system/lib/Drupal/system/Tests/Common/RenderElementTypesTest.php @@ -37,7 +37,6 @@ public static function getInfo() { protected function setUp() { parent::setUp(); $this->installConfig(array('system')); - $this->container->get('theme_handler')->enable(array('stark')); } /** @@ -68,6 +67,17 @@ function assertElements($elements) { $expected->loadXML($element['expected']); $message = isset($element['name']) ? '"' . $element['name'] . '" input rendered correctly by drupal_render().' : NULL; + $this->verbose('
Expected | Actual |
---|---|
' + . String::checkPlain(strtr($expected->saveXML(), array('<' => "\n<"))) + . ' | '
+ . '' + . String::checkPlain(strtr($value->saveXML(), array('<' => "\n<"))) + . ' | '
+ . '