Commit 2c90c67c authored by webchick's avatar webchick

Issue #1067408 by alexpott, Jalandhar, jessebeach, Désiré, neetu morwani,...

Issue #1067408 by alexpott, Jalandhar, jessebeach, Désiré, neetu morwani, dawehner, sun: Themes do not have an installation status.
parent 45e59c9b
module: {}
theme:
stark: 0
theme: {}
disabled:
theme: {}
......@@ -203,7 +203,7 @@ services:
arguments: ['%container.modules%', '@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']
......
......@@ -686,6 +686,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,
......@@ -1827,6 +1829,34 @@ function install_profile_modules(&$install_state) {
return $batch;
}
/**
* 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.
*
......
......@@ -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,
......
......@@ -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');
}
/**
......
......@@ -103,7 +103,21 @@ 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;
// /core/core.info.yml does not actually exist, but is required because
// Extension expects a pathname.
_drupal_theme_initialize(new Extension('theme', 'core/core.info.yml'));
return;
}
// Store the identifier for retrieving theme settings with.
$theme_key = $theme;
......@@ -401,6 +415,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 +867,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 +890,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()) {
......@@ -975,13 +991,16 @@ function theme_settings_convert_to_config(array $theme_settings, Config $config)
* @param $theme_list
* An array of theme names.
*
* @return bool
* Whether any of the given themes have been enabled.
*
* @deprecated in Drupal 8.x-dev, will be removed before Drupal 8.0.
* Use \Drupal::service('theme_handler')->enable().
*
* @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);
}
/**
......@@ -990,13 +1009,16 @@ function theme_enable($theme_list) {
* @param $theme_list
* An array of theme names.
*
* @return bool
* Whether any of the given themes have been disabled.
*
* @deprecated in Drupal 8.x-dev, will be removed before Drupal 8.0.
* Use \Drupal::service('theme_handler')->disable().
*
* @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);
}
/**
......
......@@ -66,7 +66,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');
......@@ -74,6 +74,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
......
......@@ -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'));
}
}
......
......@@ -758,8 +758,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));
......@@ -804,8 +802,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);
}
......@@ -912,6 +920,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);
......
......@@ -17,28 +17,35 @@ interface ThemeHandlerInterface {
*
* @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.
*
* @return bool
* Whether any of the given themes have been enabled.
*
* @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.
*
* @param array $theme_list
* An array of theme names.
*
* @return bool
* Whether any of the given themes have been disabled.
*/
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.
......@@ -75,6 +82,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.
*/
......
......@@ -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,17 @@ 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 {
// @see drupal_theme_initialize()
$this->theme = new Extension('theme', 'core/core.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 {
......
......@@ -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.
......
......@@ -55,6 +55,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().
*/
......
......@@ -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();
......
......@@ -654,25 +654,6 @@ public function testAlterInfo() {
$this->assertNoText(t('Password recovery'));
}
/**
* 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.
*
......
<?php
/**
* @file
* Contains \Drupal\config_translation\Tests\ConfigTranslationUiThemeTest.
*/
namespace Drupal\config_translation\Tests;
use Drupal\Core\Language\Language;
use Drupal\simpletest\WebTestBase;
/**
* Functional tests for the Language list configuration forms.
*/
class ConfigTranslationUiThemeTest extends WebTestBase {
/**
* Modules to enable.
*
* @var array
*/
public static $modules = array('config_translation', 'config_translation_test');
/**
* Languages to enable.
*
* @var array
*/
protected $langcodes = array('fr', 'ta');
/**
* Administrator user for tests.
*
* @var \Drupal\user\UserInterface
*/
protected $admin_user;
public static function getInfo() {
return array(
'name' => '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");
}
}
......@@ -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().
*/
......
......@@ -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();
// Get all available themes.
$themes = $this->themeHandler->rebuildThemeData();
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'])) {
......
......@@ -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();
......
......@@ -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::config('system.theme')
$this->container->get('theme_handler')->enable(array('seven', 'bartik'));
$this->container->get('config.factory')->get('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);
......
......@@ -33,7 +33,6 @@ public static function getInfo() {
protected function setUp() {
parent::setUp();
$this->installConfig(array('system'));
$this->container->get('theme_handler')->enable(array('stark'));
}
/**
......