diff --git a/core/includes/module.inc b/core/includes/module.inc index 7bb0e3e..530d089 100644 --- a/core/includes/module.inc +++ b/core/includes/module.inc @@ -13,7 +13,7 @@ * * @param $type * The type of list to return: - * - module_enabled: All enabled modules. + * - module: All enabled modules. * - bootstrap: All enabled modules required for bootstrap. * - theme: All themes. * @@ -42,21 +42,19 @@ function system_list($type) { ); // Build a list of themes. $enabled_themes = (array) \Drupal::config('system.theme')->get('enabled'); - // @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 + array_walk($enabled_themes, function (&$value, $key) { + $value = 'system.theme.data.' . $key; + }); + $theme_data = (array) \Drupal::state()->getMultiple($enabled_themes); + // @todo Remove me? + if (0 && empty($theme_data)) { + // system_list() may be called from _drupal_bootstrap_code() and // 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(); } 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])) { @@ -84,6 +82,7 @@ function system_list($type) { function system_list_reset() { drupal_static_reset('system_list'); drupal_static_reset('system_rebuild_module_data'); + drupal_static_reset('system_rebuild_theme_data'); drupal_static_reset('list_themes'); \Drupal::cache('bootstrap')->delete('system_list'); \Drupal::cache()->delete('system_info'); @@ -95,14 +94,12 @@ function system_list_reset() { // are recorded and handled as modules. 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'); + // Rebuild theme data to update theme states. + // 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. + system_rebuild_theme_data(); } /** diff --git a/core/includes/theme.maintenance.inc b/core/includes/theme.maintenance.inc index 57183d6..3ebb0d6 100644 --- a/core/includes/theme.maintenance.inc +++ b/core/includes/theme.maintenance.inc @@ -78,8 +78,15 @@ function _drupal_maintenance_theme() { $module_handler->load('system'); } + // Check access to installed themes. $themes = list_themes(); + // If retrieval failed or 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])) { + $themes = _system_rebuild_theme_data(); + } + // 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/Extension/ThemeHandler.php b/core/lib/Drupal/Core/Extension/ThemeHandler.php index db22e5f..9c437de 100644 --- a/core/lib/Drupal/Core/Extension/ThemeHandler.php +++ b/core/lib/Drupal/Core/Extension/ThemeHandler.php @@ -124,12 +124,59 @@ public function __construct(ConfigFactoryInterface $config_factory, ModuleHandle /** * {@inheritdoc} + * + * @todo Rename to install(). */ - public function enable(array $theme_list) { + public function enable(array $theme_list, $enable_dependencies = TRUE) { + if ($enable_dependencies) { + // Get all theme data so we can find dependencies and sort. + $theme_data = $this->rebuildThemeData(); + // Create an associative array with weights as values. + $theme_list = array_flip(array_values($theme_list)); + + while (list($theme) = each($theme_list)) { + if (!isset($theme_data[$theme])) { + // This theme is not found in the filesystem, abort. + return FALSE; + } + if ($theme_data[$theme]->status) { + // Skip already enabled themes. + unset($theme_list[$theme]); + continue; + } + $theme_list[$theme] = $theme_data[$theme]->sort; + + // Add dependencies to the list, with a placeholder weight. + // The new themes will be processed as the while loop continues. + foreach (array_keys($theme_data[$theme]->requires) as $dependency) { + if (!isset($theme_list[$dependency])) { + $theme_list[$dependency] = 0; + } + } + + if (!$theme_list) { + // Nothing to do. All themes already enabled. + return TRUE; + } + } + + // Sort the theme list by pre-calculated weights. + arsort($theme_list); + $theme_list = array_keys($theme_list); + } + $this->clearCssCache(); + $themes_installed = array(); + $themes_enabled = array(); $theme_config = $this->configFactory->get('system.theme'); $disabled_themes = $this->configFactory->get('system.theme.disabled'); foreach ($theme_list as $key) { + // Only process themes that are not already enabled. + $enabled = $theme_config->get("enabled.$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( @@ -145,8 +192,12 @@ public function enable(array $theme_list) { // Refresh the theme list as installation of default configuration needs // an updated list to work. $this->reset(); + // Install default configuration of the theme. $this->configInstaller->installDefaultConfig('theme', $key); + + // Record the fact that it was installed. + watchdog('system', '%theme theme installed.', array('%theme' => $key), WATCHDOG_INFO); } $this->resetSystem(); @@ -157,6 +208,8 @@ public function enable(array $theme_list) { /** * {@inheritdoc} + * + * @todo Rename to uninstall(). */ public function disable(array $theme_list) { // Don't disable the default theme. @@ -285,6 +338,9 @@ public function rebuildThemeData() { if (!empty($theme->info['base theme'])) { $sub_themes[] = $key; + // Add the defined base theme as dependency to the theme, in order to + // re-use the dependency graph resolution of _module_build_dependencies(). + $themes[$key]->info['dependencies'][] = $themes[$key]->info['base theme']; } // Defaults to 'twig' (see $defaults above). diff --git a/core/lib/Drupal/Core/Extension/ThemeHandlerInterface.php b/core/lib/Drupal/Core/Extension/ThemeHandlerInterface.php index 2ba52cd..013f8b9 100644 --- a/core/lib/Drupal/Core/Extension/ThemeHandlerInterface.php +++ b/core/lib/Drupal/Core/Extension/ThemeHandlerInterface.php @@ -17,11 +17,22 @@ * * @param array $theme_list * An array of theme names. + * @param bool $enable_dependencies + * If TRUE, dependencies will automatically be added and enabled 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 + * + * @todo D8: This is almost identical to module_enable() now. Only differences: + * - There is no required default .theme PHP file to load. + * - There is no schema to install. + * - System list, state, theme list, and static resets slightly differ. + * @todo hook_themes_installed(), hook_themes_enabled(), etc are not invoked in + * themes, since they are excluded from the hook system. */ - public function enable(array $theme_list); + public function enable(array $theme_list, $enable_dependencies = TRUE); /** * Disables a given list of themes. diff --git a/core/lib/Drupal/Core/Theme/ThemeAccessCheck.php b/core/lib/Drupal/Core/Theme/ThemeAccessCheck.php index 75afddc..06376fd 100644 --- a/core/lib/Drupal/Core/Theme/ThemeAccessCheck.php +++ b/core/lib/Drupal/Core/Theme/ThemeAccessCheck.php @@ -35,7 +35,7 @@ public function access(Route $route, Request $request, AccountInterface $account */ public function checkAccess($theme) { $themes = list_themes(); - return !empty($themes[$theme]->status); + return isset($themes[$theme]) && !empty($themes[$theme]->status); } } diff --git a/core/modules/block/lib/Drupal/block/Tests/BlockAdminThemeTest.php b/core/modules/block/lib/Drupal/block/Tests/BlockAdminThemeTest.php index d553440..181f5b9 100644 --- a/core/modules/block/lib/Drupal/block/Tests/BlockAdminThemeTest.php +++ b/core/modules/block/lib/Drupal/block/Tests/BlockAdminThemeTest.php @@ -37,9 +37,9 @@ function testAdminTheme() { $admin_user = $this->drupalCreateUser(array('administer blocks', 'administer themes')); $this->drupalLogin($admin_user); - // Ensure that access to block admin page is denied when theme is disabled. + // Ensure the block admin page cannot be accessed for a disabled theme. $this->drupalGet('admin/structure/block/list/bartik'); - $this->assertResponse(403); + $this->assertResponse(404); // Enable admin theme and confirm that tab is accessible. theme_enable(array('bartik')); diff --git a/core/modules/system/lib/Drupal/system/Controller/SystemController.php b/core/modules/system/lib/Drupal/system/Controller/SystemController.php index 361f716..1f6cdb7 100644 --- a/core/modules/system/lib/Drupal/system/Controller/SystemController.php +++ b/core/modules/system/lib/Drupal/system/Controller/SystemController.php @@ -183,6 +183,8 @@ 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'); @@ -191,8 +193,9 @@ public function themesPage() { 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'])) { @@ -321,6 +324,8 @@ public function themesPage() { /** * @todo Remove system_theme_default(). + * + * @todo Move into ThemeController. */ public function themeSetDefault() { module_load_include('admin.inc', 'system'); diff --git a/core/modules/system/lib/Drupal/system/Controller/ThemeController.php b/core/modules/system/lib/Drupal/system/Controller/ThemeController.php index ece04d5..7f1ad47 100644 --- a/core/modules/system/lib/Drupal/system/Controller/ThemeController.php +++ b/core/modules/system/lib/Drupal/system/Controller/ThemeController.php @@ -76,12 +76,9 @@ public function enable(Request $request) { $theme = $request->get('theme'); if (isset($theme)) { - // Get current list of themes. - $themes = list_themes(TRUE); - - // Check if the specified theme is one recognized by the system. - if (!empty($themes[$theme])) { + if (drupal_get_path('theme', $theme)) { theme_enable(array($theme)); + $themes = list_themes(); drupal_set_message(t('The %theme theme has been enabled.', array('%theme' => $themes[$theme]->info['name']))); } else { 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..c0824d6 100644 --- a/core/modules/system/lib/Drupal/system/Tests/Batch/PageTest.php +++ b/core/modules/system/lib/Drupal/system/Tests/Batch/PageTest.php @@ -35,10 +35,10 @@ 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_hander')->enable(array('seven', 'bartik')); \Drupal::config('system.theme') ->set('default', 'bartik') ->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')); diff --git a/core/modules/system/lib/Drupal/system/Tests/System/ThemeTest.php b/core/modules/system/lib/Drupal/system/Tests/System/ThemeTest.php index 520cb11..14ec017 100644 --- a/core/modules/system/lib/Drupal/system/Tests/System/ThemeTest.php +++ b/core/modules/system/lib/Drupal/system/Tests/System/ThemeTest.php @@ -213,7 +213,7 @@ function testAdministrationTheme() { // Reset to the default theme settings. \Drupal::config('system.theme') - ->set('default', 'bartik') + ->set('default', 'stark') ->save(); $edit = array( 'admin_theme' => '0', @@ -222,10 +222,10 @@ function testAdministrationTheme() { $this->drupalPostForm('admin/appearance', $edit, t('Save configuration')); $this->drupalGet('admin'); - $this->assertRaw('core/themes/bartik', 'Site default theme used on administration page.'); + $this->assertRaw('core/themes/stark', 'Site default theme used on administration page.'); $this->drupalGet('node/add'); - $this->assertRaw('core/themes/bartik', 'Site default theme used on the add content page.'); + $this->assertRaw('core/themes/stark', 'Site default theme used on the add content page.'); } /** diff --git a/core/modules/system/lib/Drupal/system/Tests/Theme/ThemeTest.php b/core/modules/system/lib/Drupal/system/Tests/Theme/ThemeTest.php index 638d8e5..0f17091 100644 --- a/core/modules/system/lib/Drupal/system/Tests/Theme/ThemeTest.php +++ b/core/modules/system/lib/Drupal/system/Tests/Theme/ThemeTest.php @@ -202,11 +202,11 @@ function testFunctionOverride() { * Test the list_themes() function. */ function testListThemes() { + \Drupal::service('theme_handler')->enable(array('test_subtheme')); $themes = list_themes(); // Check if drupal_theme_access() retrieves enabled themes properly from list_themes(). $this->assertTrue(drupal_theme_access('test_theme'), 'Enabled theme detected'); - // Check if list_themes() returns disabled themes. - $this->assertTrue(array_key_exists('test_basetheme', $themes), 'Disabled theme detected'); + // Check for base theme and subtheme lists. $base_theme_list = array('test_basetheme' => 'Theme test base theme'); $sub_theme_list = array('test_subtheme' => 'Theme test subtheme'); @@ -223,6 +223,7 @@ function testListThemes() { * Test the theme_get_setting() function. */ function testThemeGetSetting() { + theme_enable(array('test_subtheme')); $GLOBALS['theme_key'] = 'test_theme'; $this->assertIdentical(theme_get_setting('theme_test_setting'), 'default value', 'theme_get_setting() uses the default theme automatically.'); $this->assertNotEqual(theme_get_setting('subtheme_override', 'test_basetheme'), theme_get_setting('subtheme_override', 'test_subtheme'), 'Base theme\'s default settings values can be overridden by subtheme.'); diff --git a/core/modules/system/system.admin.inc b/core/modules/system/system.admin.inc index 75f1c60..2cb14f5 100644 --- a/core/modules/system/system.admin.inc +++ b/core/modules/system/system.admin.inc @@ -18,14 +18,14 @@ function system_theme_default() { $request = \Drupal::request(); $theme = $request->get('theme'); if (!empty($theme)) { - // Get current list of themes. - $themes = list_themes(); // Check if the specified theme is one recognized by the system. - if (!empty($themes[$theme])) { + if (drupal_get_path('theme', $theme)) { + $themes = list_themes(); // Enable the theme if it is currently disabled. - if (empty($themes[$theme]->status)) { - theme_enable(array($theme)); + if (!isset($themes[$theme])) { + \Drupal::service('theme_handler')->enable(array($theme)); + $themes = list_themes(); } // Set the default theme. \Drupal::config('system.theme') diff --git a/core/modules/system/system.module b/core/modules/system/system.module index 081d68d..fce3816 100644 --- a/core/modules/system/system.module +++ b/core/modules/system/system.module @@ -1364,6 +1364,7 @@ function system_get_info($type, $name = NULL) { } } else { + // @todo Migrate theme info to ModuleInfo implementation. $list = system_list($type); foreach ($list as $shortname => $item) { if (!empty($item->status)) { @@ -1538,12 +1539,16 @@ function system_rebuild_theme_data() { // based on the current config. Remove this code when themes have a proper // installation status. // @see http://drupal.org/node/1067408 + // Add human-readable name and status. $enabled_themes = (array) \Drupal::config('system.theme')->get('enabled'); $files = array(); foreach ($themes as $name => $theme) { $theme->status = (int) isset($enabled_themes[$name]); $files[$name] = $theme->getPathname(); } + // Set up dependencies. + $themes = \Drupal::moduleHandler()->buildModuleDependencies($themes); + // Replace last known theme data state. // @todo Obsolete with proper installation status for themes. \Drupal::state()->set('system.theme.data', $themes); @@ -1575,25 +1580,26 @@ function _system_default_theme_features() { /** * Get a list of available regions from a specified theme. * - * @param $theme_key - * The name of a theme. + * @param $theme + * A theme object, or the name of a theme. * @param $show * Possible values: REGIONS_ALL or REGIONS_VISIBLE. Visible excludes hidden * regions. * @return * An array of regions in the form $region['name'] = 'description'. */ -function system_region_list($theme_key, $show = REGIONS_ALL) { - $themes = list_themes(); - if (!isset($themes[$theme_key])) { - return array(); +function system_region_list($theme, $show = REGIONS_ALL) { + if (!is_object($theme)) { + $themes = list_themes(); + if (!isset($themes[$theme])) { + return array(); + } + $theme = $themes[$theme]; } - $list = array(); - $info = $themes[$theme_key]->info; // If requested, suppress hidden regions. See block_admin_display_form(). - foreach ($info['regions'] as $name => $label) { - if ($show == REGIONS_ALL || !isset($info['regions_hidden']) || !in_array($name, $info['regions_hidden'])) { + foreach ($theme->info['regions'] as $name => $label) { + if ($show == REGIONS_ALL || !isset($theme->info['regions_hidden']) || !in_array($name, $theme->info['regions_hidden'])) { $list[$name] = t($label); } }