diff --git a/core/core.services.yml b/core/core.services.yml index d341606..c63419f 100644 --- a/core/core.services.yml +++ b/core/core.services.yml @@ -170,6 +170,9 @@ services: arguments: [ '%container.namespaces%' ] tags: - { name: persist } + theme_handler: + class: Drupal\Core\Extension\ThemeHandler + arguments: ['@config.factory', '@module_handler', '@cache.cache', '@info_parser', '@router.builder'] entity.manager: class: Drupal\Core\Entity\EntityManager arguments: ['@container.namespaces', '@service_container', '@module_handler', '@cache.cache', '@language_manager', '@string_translation'] diff --git a/core/includes/install.core.inc b/core/includes/install.core.inc index 36b6b14..301c574 100644 --- a/core/includes/install.core.inc +++ b/core/includes/install.core.inc @@ -484,7 +484,14 @@ function install_begin_request(&$install_state) { // Register the info parser. $container->register('info_parser', 'Drupal\Core\Extension\InfoParser'); + + $container->register('theme_handler', 'Drupal\Core\Extension\ThemeHandler') + ->addArgument(new Reference('config.factory')) + ->addArgument(new Reference('module_handler')) + ->addArgument(new Reference('cache.cache')) + ->addArgument(new Reference('info_parser')); } + // Set the request in the kernel to the new created Request above // so it is available to the rest of the installation process. $container->set('request', $request); diff --git a/core/includes/theme.inc b/core/includes/theme.inc index 069b497..41f1768 100644 --- a/core/includes/theme.inc +++ b/core/includes/theme.inc @@ -355,58 +355,19 @@ function drupal_theme_rebuild() { * the themes' machine names, and the values are the themes' human-readable * names. This element is not set if there are no themes on the system that * declare this theme as their base theme. + * + * @deprecated as of Drupal 8.0. Use \Drupal::service('theme_handler')->listInfo(). */ function list_themes($refresh = FALSE) { - $list = &drupal_static(__FUNCTION__, array()); + /** @var \Drupal\Core\Extension\ThemeHandler $theme_handler */ + $theme_handler = \Drupal::service('theme_handler'); if ($refresh) { - $list = array(); + $theme_handler->reset(); system_list_reset(); } - if (empty($list)) { - $list = array(); - // Extract from the database only when it is available. - // Also check that the site is not in the middle of an install or update. - if (!defined('MAINTENANCE_MODE')) { - try { - $themes = system_list('theme'); - } - catch (Exception $e) { - // If the database is not available, rebuild the theme data. - $themes = _system_rebuild_theme_data(); - } - } - else { - // Scan the installation when the database should not be read. - $themes = _system_rebuild_theme_data(); - } - - foreach ($themes as $theme) { - foreach ($theme->info['stylesheets'] as $media => $stylesheets) { - foreach ($stylesheets as $stylesheet => $path) { - $theme->stylesheets[$media][$stylesheet] = $path; - } - } - foreach ($theme->info['scripts'] as $script => $path) { - $theme->scripts[$script] = $path; - } - 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; - } - $list[$theme->name] = $theme; - } - } - - return $list; + return $theme_handler->listInfo(); } /** @@ -419,38 +380,15 @@ function list_themes($refresh = FALSE) { * An array of available themes. * @param $key * The name of the theme whose base we are looking for. - * @param $used_keys - * (optional) A recursion parameter preventing endless loops. Defaults to - * NULL. * * @return * Returns an array of all of the theme's ancestors; the first element's value * will be NULL if an error occurred. + * + * @deprecated as of Drupal 8.0. Use \Drupal::service('theme_handler')->getBaseThemes(). */ -function drupal_find_base_themes($themes, $key, $used_keys = array()) { - $base_key = $themes[$key]->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_keys[$base_key])) { - return array($base_key => NULL); - } - $used_keys[$base_key] = TRUE; - return drupal_find_base_themes($themes, $base_key, $used_keys) + $current_base_theme; - } - // If we get here, then this is our parent theme. - return $current_base_theme; +function drupal_find_base_themes($themes, $key) { + return \Drupal::service('theme_handler')->getBaseThemes($themes, $key); } /** @@ -1139,38 +1077,11 @@ function theme_settings_convert_to_config(array $theme_settings, Config $config) * * @param $theme_list * An array of theme names. + * + * @deprecated as of Drupal 8.0. Use \Drupal::service('theme_handler')->enable(). */ function theme_enable($theme_list) { - drupal_clear_css_cache(); - $theme_config = \Drupal::config('system.theme'); - $disabled_themes = \Drupal::config('system.theme.disabled'); - foreach ($theme_list as $key) { - // Throw an exception if the theme name is too long. - if (strlen($key) > DRUPAL_EXTENSION_NAME_MAX_LENGTH) { - throw new ExtensionNameLengthException(format_string('Theme name %name is over the maximum allowed length of @max characters.', array( - '%name' => $key, - '@max' => DRUPAL_EXTENSION_NAME_MAX_LENGTH, - ))); - } - - // The value is not used; the weight is ignored for themes currently. - $theme_config->set("enabled.$key", 0)->save(); - $disabled_themes->clear($key)->save(); - - // Refresh the theme list as config_install_default_config() needs an - // updated list to work. - list_themes(TRUE); - // Install default configuration of the theme. - config_install_default_config('theme', $key); - } - - \Drupal::service('router.builder')->rebuild(); - menu_router_rebuild(); - \Drupal::cache('cache')->deleteTags(array('local_task' => 1)); - drupal_theme_rebuild(); - - // Invoke hook_themes_enabled() after the themes have been enabled. - \Drupal::moduleHandler()->invokeAll('themes_enabled', array($theme_list)); + \Drupal::service('theme_handler')->enable($theme_list); } /** @@ -1178,36 +1089,11 @@ function theme_enable($theme_list) { * * @param $theme_list * An array of theme names. + * + * @deprecated as of Drupal 8.0. Use \Drupal::service('theme_handler')->disable(). */ function theme_disable($theme_list) { - // Don't disable the default theme. - if ($pos = array_search(\Drupal::config('system.theme')->get('default'), $theme_list) !== FALSE) { - unset($theme_list[$pos]); - if (empty($theme_list)) { - return; - } - } - - drupal_clear_css_cache(); - - $theme_config = \Drupal::config('system.theme'); - $disabled_themes = \Drupal::config('system.theme.disabled'); - foreach ($theme_list as $key) { - // The value is not used; the weight is ignored for themes currently. - $theme_config->clear("enabled.$key"); - $disabled_themes->set($key, 0); - } - $theme_config->save(); - $disabled_themes->save(); - - list_themes(TRUE); - \Drupal::service('router.builder')->rebuild(); - menu_router_rebuild(); - \Drupal::cache('cache')->deleteTags(array('local_task' => 1)); - drupal_theme_rebuild(); - - // Invoke hook_themes_disabled after the themes have been disabled. - \Drupal::moduleHandler()->invokeAll('themes_disabled', array($theme_list)); + \Drupal::service('theme_handler')->disable($theme_list); } /** diff --git a/core/lib/Drupal/Core/DependencyInjection/UpdateServiceProvider.php b/core/lib/Drupal/Core/DependencyInjection/UpdateServiceProvider.php index d458665..ee637ea 100644 --- a/core/lib/Drupal/Core/DependencyInjection/UpdateServiceProvider.php +++ b/core/lib/Drupal/Core/DependencyInjection/UpdateServiceProvider.php @@ -9,6 +9,7 @@ use Drupal\Core\DependencyInjection\ContainerBuilder; use Drupal\Core\DependencyInjection\ServiceProviderInterface; +use Symfony\Component\DependencyInjection\Reference; /** * ServiceProvider class for update.php service overrides. @@ -38,6 +39,12 @@ public function register(ContainerBuilder $container) { ->register("cache_factory", 'Drupal\Core\Cache\MemoryBackendFactory'); $container ->register('router.builder', 'Drupal\Core\Routing\RouteBuilderStatic'); + + $container->register('theme_handler', 'Drupal\Core\Extension\ThemeHandler') + ->addArgument(new Reference('config.factory')) + ->addArgument(new Reference('module_handler')) + ->addArgument(new Reference('cache.cache')) + ->addArgument(new Reference('info_parser')); } } diff --git a/core/lib/Drupal/Core/Extension/ThemeHandler.php b/core/lib/Drupal/Core/Extension/ThemeHandler.php new file mode 100644 index 0000000..9398072 --- /dev/null +++ b/core/lib/Drupal/Core/Extension/ThemeHandler.php @@ -0,0 +1,497 @@ +configFactory = $config_factory; + $this->moduleHandler = $module_handler; + $this->cacheBackend = $cache_backend; + $this->infoParser = $info_parser; + $this->routeBuilder = $route_builder; + $this->systemListingInfo = $system_list_info; + } + + /** + * {@inheritdoc} + */ + public function enable(array $theme_list) { + $this->clearCssCache(); + $theme_config = $this->configFactory->get('system.theme'); + $disabled_themes = $this->configFactory->get('system.theme.disabled'); + foreach ($theme_list as $key) { + // 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( + '%name' => $key, + '@max' => DRUPAL_EXTENSION_NAME_MAX_LENGTH, + ))); + } + + // The value is not used; the weight is ignored for themes currently. + $theme_config->set("enabled.$key", 0)->save(); + $disabled_themes->clear($key)->save(); + + // Refresh the theme list as config_install_default_config() needs an + // updated list to work. + $this->reset(); + // Install default configuration of the theme. + $this->configInstallDefaultConfig($key); + } + + $this->resetSystem(); + + // Invoke hook_themes_enabled() after the themes have been enabled. + $this->moduleHandler->invokeAll('themes_enabled', array($theme_list)); + } + + /** + * {@inheritdoc} + */ + public function disable(array $theme_list) { + // Don't disable the default theme. + if ($pos = array_search($this->configFactory->get('system.theme')->get('default'), $theme_list) !== FALSE) { + unset($theme_list[$pos]); + if (empty($theme_list)) { + return; + } + } + + $this->clearCssCache(); + + $theme_config = $this->configFactory->get('system.theme'); + $disabled_themes = $this->configFactory->get('system.theme.disabled'); + foreach ($theme_list as $key) { + // The value is not used; the weight is ignored for themes currently. + $theme_config->clear("enabled.$key"); + $disabled_themes->set($key, 0); + } + $theme_config->save(); + $disabled_themes->save(); + + $this->reset(); + $this->resetSystem(); + + // Invoke hook_themes_disabled after the themes have been disabled. + $this->moduleHandler->invokeAll('themes_disabled', array($theme_list)); + } + + /** + * {@inheritdoc} + */ + public function listInfo() { + if (empty($this->list)) { + $this->list = array(); + // Check that the site is not in the middle of an install or update. + if (!defined('MAINTENANCE_MODE')) { + try { + $themes = $this->systemThemeList(); + } + catch (\Exception $e) { + // If the database is not available, rebuild the theme data. + $themes = $this->rebuildThemeData(); + } + } + else { + // Scan the installation when the database should not be read. + $themes = $this->rebuildThemeData(); + } + + foreach ($themes as $theme) { + foreach ($theme->info['stylesheets'] as $media => $stylesheets) { + foreach ($stylesheets as $stylesheet => $path) { + $theme->stylesheets[$media][$stylesheet] = $path; + } + } + foreach ($theme->info['scripts'] as $script => $path) { + $theme->scripts[$script] = $path; + } + 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->name] = $theme; + } + } + return $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(); + } + + /** + * {@inheritdoc} + */ + public function rebuildThemeData() { + // Find themes. + $listing = $this->getSystemListingInfo(); + $themes = $listing->scan('/^' . DRUPAL_PHP_FUNCTION_PATTERN . '\.info.yml$/', 'themes', 'name', 1); + // Allow modules to add further themes. + if ($module_themes = $this->moduleHandler->invokeAll('system_theme_info')) { + foreach ($module_themes as $name => $uri) { + // @see file_scan_directory() + $themes[$name] = (object) array( + 'uri' => $uri, + 'filename' => pathinfo($uri, PATHINFO_FILENAME), + 'name' => $name, + ); + } + } + + // Find theme engines + $listing = $this->getSystemListingInfo(); + $engines = $listing->scan('/^' . DRUPAL_PHP_FUNCTION_PATTERN . '\.engine$/', 'themes/engines', 'name', 1); + + // Set defaults for theme info. + $defaults = array( + 'engine' => 'twig', + 'regions' => array( + 'sidebar_first' => 'Left sidebar', + 'sidebar_second' => 'Right sidebar', + 'content' => 'Content', + 'header' => 'Header', + 'footer' => 'Footer', + 'highlighted' => 'Highlighted', + 'help' => 'Help', + 'page_top' => 'Page top', + 'page_bottom' => 'Page bottom', + ), + 'description' => '', + 'features' => $this->defaultFeatures, + 'screenshot' => 'screenshot.png', + 'php' => DRUPAL_MINIMUM_PHP, + 'stylesheets' => array(), + 'scripts' => array(), + ); + + $sub_themes = array(); + // Read info files for each theme + foreach ($themes as $key => $theme) { + $themes[$key]->filename = $theme->uri; + $themes[$key]->info = $this->infoParser->parse($theme->uri) + $defaults; + + // Skip this extension if its type is not theme. + if (!isset($themes[$key]->info['type']) || $themes[$key]->info['type'] != 'theme') { + unset($themes[$key]); + continue; + } + + // Add the info file modification time, so it becomes available for + // contributed modules to use for ordering theme lists. + $themes[$key]->info['mtime'] = filemtime($theme->uri); + + // Invoke hook_system_info_alter() to give installed modules a chance to + // modify the data in the .info.yml files if necessary. + $type = 'theme'; + $this->moduleHandler->alter('system_info', $themes[$key]->info, $themes[$key], $type); + + if (!empty($themes[$key]->info['base theme'])) { + $sub_themes[] = $key; + } + + $engine = $themes[$key]->info['engine']; + if (isset($engines[$engine])) { + $themes[$key]->owner = $engines[$engine]->uri; + $themes[$key]->prefix = $engines[$engine]->name; + $themes[$key]->template = TRUE; + } + + // Prefix stylesheets and scripts with module path. + $path = dirname($theme->uri); + $theme->info['stylesheets'] = $this->themeInfoPrefixPath($theme->info['stylesheets'], $path); + $theme->info['scripts'] = $this->themeInfoPrefixPath($theme->info['scripts'], $path); + + // Give the screenshot proper path information. + if (!empty($themes[$key]->info['screenshot'])) { + $themes[$key]->info['screenshot'] = $path . '/' . $themes[$key]->info['screenshot']; + } + } + + // Now that we've established all our master themes, go back and fill in data + // for subthemes. + foreach ($sub_themes as $key) { + $themes[$key]->base_themes = $this->doGetBaseThemes($themes, $key); + // Don't proceed if there was a problem with the root base theme. + if (!current($themes[$key]->base_themes)) { + continue; + } + $base_key = key($themes[$key]->base_themes); + foreach (array_keys($themes[$key]->base_themes) as $base_theme) { + $themes[$base_theme]->sub_themes[$key] = $themes[$key]->info['name']; + } + // Copy the 'owner' and 'engine' over if the top level theme uses a theme + // engine. + if (isset($themes[$base_key]->owner)) { + if (isset($themes[$base_key]->info['engine'])) { + $themes[$key]->info['engine'] = $themes[$base_key]->info['engine']; + $themes[$key]->owner = $themes[$base_key]->owner; + $themes[$key]->prefix = $themes[$base_key]->prefix; + } + else { + $themes[$key]->prefix = $key; + } + } + } + + return $themes; + } + + /** + * Prefixes all values in an .info.yml file array with a given path. + * + * This helper function is mainly used to prefix all array values of an + * .info.yml file property with a single given path (to the module or theme); + * e.g., to prefix all values of the 'stylesheets' or 'scripts' properties with + * the file path to the defining module/theme. + * + * @param array $info + * A nested array of data of an .info.yml file to be processed. + * @param string $path + * A file path to prepend to each value in $info. + * + * @return array + * The $info array with prefixed values. + * + * @see _system_rebuild_module_data() + * @see self::rebuildThemeData() + */ + protected function themeInfoPrefixPath(array $info, $path) { + foreach ($info as $key => $value) { + // Recurse into nested values until we reach the deepest level. + if (is_array($value)) { + $info[$key] = $this->themeInfoPrefixPath($info[$key], $path); + } + // Unset the original value's key and set the new value with prefix, using + // the original value as key, so original values can still be looked up. + else { + unset($info[$key]); + $info[$value] = $path . '/' . $value; + } + } + return $info; + } + + /** + * {@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 + */ + 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; + return $this->getBaseThemes($themes, $base_key, $used_themes) + $current_base_theme; + } + // If we get here, then this is our parent theme. + return $current_base_theme; + } + + protected function getSystemListingInfo() { + if (!isset($this->systemListingInfo)) { + $this->systemListingInfo = new SystemListingInfo(); + } + return $this->systemListingInfo; + } + + /** + * Installs the default theme config. + * + * @param string $theme + */ + protected function configInstallDefaultConfig($theme) { + config_install_default_config('theme', $theme); + } + + /** + * Resets some other systems like rebuilding the route information or caches. + */ + protected function resetSystem() { + if ($this->routeBuilder) { + $this->routeBuilder->rebuild(); + } + $this->systemListReset(); + + // @todo It feels wrong to have the requirement to clear the local tasks + // cache here. + $this->cacheBackend->deleteTags(array('local_task' => 1)); + $this->themeRegistryRebuild(); + } + + /** + * Wraps system_list_reset(). + */ + protected function systemListReset() { + system_list_reset(); + } + + /** + * Wraps drupal_clear_css_cache(). + */ + protected function clearCssCache() { + drupal_clear_css_cache(); + } + + /** + * Wraps drupal_theme_rebuild(). + */ + protected function themeRegistryRebuild() { + drupal_theme_rebuild(); + } + + /** + * Wraps system_list(). + * + * @return array + */ + protected function systemThemeList() { + return system_list('theme'); + } + +} diff --git a/core/lib/Drupal/Core/Extension/ThemeHandlerInterface.php b/core/lib/Drupal/Core/Extension/ThemeHandlerInterface.php new file mode 100644 index 0000000..eef3381 --- /dev/null +++ b/core/lib/Drupal/Core/Extension/ThemeHandlerInterface.php @@ -0,0 +1,108 @@ +rebuildThemeData(). */ function _system_rebuild_theme_data() { - // Find themes - $themes = drupal_system_listing('/^' . DRUPAL_PHP_FUNCTION_PATTERN . '\.info.yml$/', 'themes'); - // Allow modules to add further themes. - if ($module_themes = \Drupal::moduleHandler()->invokeAll('system_theme_info')) { - foreach ($module_themes as $name => $uri) { - // @see file_scan_directory() - $themes[$name] = (object) array( - 'uri' => $uri, - 'filename' => pathinfo($uri, PATHINFO_FILENAME), - 'name' => $name, - ); - } - } - - // Find theme engines - $engines = drupal_system_listing('/^' . DRUPAL_PHP_FUNCTION_PATTERN . '\.engine$/', 'themes/engines'); - - // Set defaults for theme info. - $defaults = array( - 'engine' => 'twig', - 'regions' => array( - 'sidebar_first' => 'Left sidebar', - 'sidebar_second' => 'Right sidebar', - 'content' => 'Content', - 'header' => 'Header', - 'footer' => 'Footer', - 'highlighted' => 'Highlighted', - 'help' => 'Help', - 'page_top' => 'Page top', - 'page_bottom' => 'Page bottom', - ), - 'description' => '', - 'features' => _system_default_theme_features(), - 'screenshot' => 'screenshot.png', - 'php' => DRUPAL_MINIMUM_PHP, - 'stylesheets' => array(), - 'scripts' => array(), - ); - - $sub_themes = array(); - // Read info files for each theme - foreach ($themes as $key => $theme) { - $themes[$key]->filename = $theme->uri; - $themes[$key]->info = \Drupal::service('info_parser')->parse($theme->uri) + $defaults; - - // Skip this extension if its type is not theme. - if (!isset($themes[$key]->info['type']) || $themes[$key]->info['type'] != 'theme') { - unset($themes[$key]); - continue; - } - - // Add the info file modification time, so it becomes available for - // contributed modules to use for ordering theme lists. - $themes[$key]->info['mtime'] = filemtime($theme->uri); - - // Invoke hook_system_info_alter() to give installed modules a chance to - // modify the data in the .info.yml files if necessary. - $type = 'theme'; - drupal_alter('system_info', $themes[$key]->info, $themes[$key], $type); - - if (!empty($themes[$key]->info['base theme'])) { - $sub_themes[] = $key; - } - - $engine = $themes[$key]->info['engine']; - if (isset($engines[$engine])) { - $themes[$key]->owner = $engines[$engine]->uri; - $themes[$key]->prefix = $engines[$engine]->name; - $themes[$key]->template = TRUE; - } - - // Prefix stylesheets and scripts with module path. - $path = dirname($theme->uri); - $theme->info['stylesheets'] = _system_info_add_path($theme->info['stylesheets'], $path); - $theme->info['scripts'] = _system_info_add_path($theme->info['scripts'], $path); - - // Give the screenshot proper path information. - if (!empty($themes[$key]->info['screenshot'])) { - $themes[$key]->info['screenshot'] = $path . '/' . $themes[$key]->info['screenshot']; - } - } - - // Now that we've established all our master themes, go back and fill in data - // for subthemes. - foreach ($sub_themes as $key) { - $themes[$key]->base_themes = drupal_find_base_themes($themes, $key); - // Don't proceed if there was a problem with the root base theme. - if (!current($themes[$key]->base_themes)) { - continue; - } - $base_key = key($themes[$key]->base_themes); - foreach (array_keys($themes[$key]->base_themes) as $base_theme) { - $themes[$base_theme]->sub_themes[$key] = $themes[$key]->info['name']; - } - // Copy the 'owner' and 'engine' over if the top level theme uses a theme - // engine. - if (isset($themes[$base_key]->owner)) { - if (isset($themes[$base_key]->info['engine'])) { - $themes[$key]->info['engine'] = $themes[$base_key]->info['engine']; - $themes[$key]->owner = $themes[$base_key]->owner; - $themes[$key]->prefix = $themes[$base_key]->prefix; - } - else { - $themes[$key]->prefix = $key; - } - } - } - - return $themes; + return \Drupal::service('theme_handler')->rebuildThemeData(); } /** @@ -2599,41 +2493,6 @@ function system_rebuild_theme_data() { } /** - * Prefixes all values in an .info.yml file array with a given path. - * - * This helper function is mainly used to prefix all array values of an - * .info.yml file property with a single given path (to the module or theme); - * e.g., to prefix all values of the 'stylesheets' or 'scripts' properties with - * the file path to the defining module/theme. - * - * @param $info - * A nested array of data of an .info.yml file to be processed. - * @param $path - * A file path to prepend to each value in $info. - * - * @return - * The $info array with prefixed values. - * - * @see _system_rebuild_module_data() - * @see _system_rebuild_theme_data() - */ -function _system_info_add_path($info, $path) { - foreach ($info as $key => $value) { - // Recurse into nested values until we reach the deepest level. - if (is_array($value)) { - $info[$key] = _system_info_add_path($info[$key], $path); - } - // Unset the original value's key and set the new value with prefix, using - // the original value as key, so original values can still be looked up. - else { - unset($info[$key]); - $info[$value] = $path . '/' . $value; - } - } - return $info; -} - -/** * Returns an array of default theme features. */ function _system_default_theme_features() { diff --git a/core/tests/Drupal/Tests/Core/Extension/ThemeHandlerTest.php b/core/tests/Drupal/Tests/Core/Extension/ThemeHandlerTest.php new file mode 100644 index 0000000..d686cec --- /dev/null +++ b/core/tests/Drupal/Tests/Core/Extension/ThemeHandlerTest.php @@ -0,0 +1,421 @@ + 'Theme handler', + 'description' => 'Tests the theme handler.', + 'group' => 'Theme', + ); + } + + /** + * {@inheritdoc} + */ + protected function setUp() { + $this->configFactory = $this->getConfigFactoryStub(array('system.theme' => array(), 'system.theme.disabled' => array())); + $this->moduleHandler = $this->getMock('Drupal\Core\Extension\ModuleHandlerInterface'); + $this->cacheBackend = $this->getMock('Drupal\Core\Cache\CacheBackendInterface'); + $this->infoParser = $this->getMock('Drupal\Core\Extension\InfoParserInterface'); + $this->routeBuilder = $this->getMockBuilder('Drupal\Core\Routing\RouteBuilder') + ->disableOriginalConstructor() + ->getMock(); + $this->systemListingInfo = $this->getMockBuilder('Drupal\Core\SystemListingInfo') + ->disableOriginalConstructor() + ->getMock(); + + $this->themeHandler = new TestThemeHandler($this->configFactory, $this->moduleHandler, $this->cacheBackend, $this->infoParser, $this->routeBuilder, $this->systemListingInfo); + } + + /** + * Tests enabling a theme with a name longer than 50 chars. + * + * @expectedException \Drupal\Core\Extension\ExtensionNameLengthException + * @expectedExceptionMessage Theme name thisNameIsFarTooLong0000000000000000000000000000051 is over the maximum allowed length of 50 characters. + */ + public function testThemeEnableWithTooLongName() { + $this->themeHandler->enable(array('thisNameIsFarTooLong0000000000000000000000000000051')); + } + + /** + * Tests enabling a single theme. + * + * @see \Drupal\Core\Extension\ThemeHandler::enable() + */ + public function testEnableSingleTheme() { + $theme_list = array('theme_test'); + + $this->configFactory->get('system.theme') + ->expects($this->once()) + ->method('set') + ->with('enabled.theme_test', 0) + ->will($this->returnSelf()); + $this->configFactory->get('system.theme') + ->expects($this->once()) + ->method('save'); + + $this->configFactory->get('system.theme.disabled') + ->expects($this->once()) + ->method('clear') + ->with('theme_test') + ->will($this->returnSelf()); + $this->configFactory->get('system.theme.disabled') + ->expects($this->once()) + ->method('save'); + + $this->systemListingInfo->expects($this->any()) + ->method('scan') + ->will($this->returnValue(array())); + + // Ensure that the themes_enabled hook is fired. + $this->moduleHandler->expects($this->at(0)) + ->method('invokeAll') + ->with('system_theme_info') + ->will($this->returnValue(array())); + + $this->moduleHandler->expects($this->at(1)) + ->method('invokeAll') + ->with('themes_enabled', array($theme_list)); + + $this->themeHandler->enable($theme_list); + + $this->assertTrue($this->themeHandler->clearedCssCache); + $this->assertTrue($this->themeHandler->registryRebuild); + $this->assertTrue($this->themeHandler->installedDefaultConfig['theme_test']); + } + + /** + * Ensures that enabling a theme does clear the theme info listing. + * + * @see \Drupal\Core\Extension\ThemeHandler::listInfo() + */ + public function testEnableAndListInfo() { + $this->configFactory->get('system.theme') + ->expects($this->exactly(2)) + ->method('set') + ->will($this->returnSelf()); + + $this->configFactory->get('system.theme.disabled') + ->expects($this->exactly(2)) + ->method('clear') + ->will($this->returnSelf()); + + $this->systemListingInfo->expects($this->any()) + ->method('scan') + ->will($this->returnValue(array())); + + $this->themeHandler->enable(array('bartik')); + $this->themeHandler->systemList['bartik'] = (object) array( + 'name' => 'bartik', + 'info' => array( + 'stylesheets' => array( + 'all' => array( + 'css/layout.css', + 'css/style.css', + 'css/colors.css', + ), + ), + 'scripts' => array( + 'example' => 'theme.js', + ), + 'engine' => 'twig', + 'base theme' => 'stark', + ), + ); + + $list_info = $this->themeHandler->listInfo(); + $this->assertCount(1, $list_info); + + $this->assertEquals($this->themeHandler->systemList['bartik']->info['stylesheets'], $list_info['bartik']->stylesheets); + $this->assertEquals($this->themeHandler->systemList['bartik']->scripts, $list_info['bartik']->scripts); + $this->assertEquals('twig', $list_info['bartik']->engine); + $this->assertEquals('stark', $list_info['bartik']->base_theme); + $this->assertEquals(0, $list_info['bartik']->status); + + $this->themeHandler->systemList['seven'] = (object) array( + 'name' => 'seven', + 'info' => array( + 'stylesheets' => array( + 'screen' => array( + 'style.css', + ), + ), + 'scripts' => array(), + ), + 'status' => 1, + ); + + $this->themeHandler->enable(array('seven')); + + $list_info = $this->themeHandler->listInfo(); + $this->assertCount(2, $list_info); + + $this->assertEquals($this->themeHandler->systemList['seven']->info['stylesheets'], $list_info['seven']->stylesheets); + $this->assertEquals(1, $list_info['seven']->status); + } + + /** + * Tests rebuilding the theme data. + * + * @see \Drupal\Core\Extension\ThemeHandler::rebuildThemeData() + */ + public function testRebuildThemeData() { + $this->systemListingInfo->expects($this->at(0)) + ->method('scan') + ->with($this->anything(), 'themes', 'name', 1) + ->will($this->returnValue(array( + 'seven' => (object) array( + 'name' => 'seven', + 'uri' => DRUPAL_ROOT . '/core/themes/seven/seven.info.yml', + ) + ))); + $this->infoParser->expects($this->once()) + ->method('parse') + ->with(DRUPAL_ROOT . '/core/themes/seven/seven.info.yml') + ->will($this->returnCallback(function ($file) { + $info_parser = new InfoParser(); + return $info_parser->parse($file); + })); + + $this->moduleHandler->expects($this->once()) + ->method('alter'); + + $theme_data = $this->themeHandler->rebuildThemeData(); + $this->assertCount(1, $theme_data); + $info = $theme_data['seven']; + + // Ensure some basic properties. + $this->assertInstanceOf('stdClass', $info); + $this->assertEquals('seven', $info->name); + $this->assertEquals(DRUPAL_ROOT . '/core/themes/seven/seven.info.yml', $info->uri); + $this->assertEquals(DRUPAL_ROOT . '/core/themes/seven/seven.info.yml', $info->filename); + + $this->assertEquals('twig', $info->info['engine']); + $this->assertEquals(array(), $info->info['scripts']); + + // Ensure that the css paths are set with the proper prefix. + $this->assertEquals(array( + 'screen' => array( + 'style.css' => DRUPAL_ROOT . '/core/themes/seven/style.css', + ), + ), $info->info['stylesheets']); + $this->assertEquals(DRUPAL_ROOT . '/core/themes/seven/screenshot.png', $info->info['screenshot']); + } + + /** + * Tests getting the base themes for a set a defines themes. + * + * @param array $themes + * An array of available themes, keyed by the theme name. + * @param string $theme + * The theme name to find all its base themes. + * @param array $expected + * The expected base themes. + * + * @dataProvider providerTestGetBaseThemes + */ + public function testGetBaseThemes(array $themes, $theme, array $expected) { + $base_themes = $this->themeHandler->getBaseThemes($themes, $theme); + $this->assertEquals($expected, $base_themes); + } + + /** + * Provides test data for testGetBaseThemes + * + * @return array + */ + public function providerTestGetBaseThemes() { + $data = array(); + + // Tests a theme without any base theme. + $themes = array(); + $themes['test_1'] = (object) array( + 'name' => 'test_1', + 'info' => array( + 'name' => 'test_1', + ), + ); + $data[] = array($themes, 'test_1', array()); + + // Tests a theme with a non existing base theme. + $themes = array(); + $themes['test_1'] = (object) array( + 'name' => 'test_1', + 'info' => array( + 'name' => 'test_1', + 'base theme' => 'test_2', + ), + ); + $data[] = array($themes, 'test_1', array('test_2' => NULL)); + + // Tests a theme with a single existing base theme. + $themes = array(); + $themes['test_1'] = (object) array( + 'name' => 'test_1', + 'info' => array( + 'name' => 'test_1', + 'base theme' => 'test_2', + ), + ); + $themes['test_2'] = (object) array( + 'name' => 'test_2', + 'info' => array( + 'name' => 'test_2', + ), + ); + $data[] = array($themes, 'test_1', array('test_2' => 'test_2')); + + // Tests a theme with multiple base themes. + $themes = array(); + $themes['test_1'] = (object) array( + 'name' => 'test_1', + 'info' => array( + 'name' => 'test_1', + 'base theme' => 'test_2', + ), + ); + $themes['test_2'] = (object) array( + 'name' => 'test_2', + 'info' => array( + 'name' => 'test_2', + 'base theme' => 'test_3', + ), + ); + $themes['test_3'] = (object) array( + 'name' => 'test_3', + 'info' => array( + 'name' => 'test_3', + ), + ); + $data[] = array($themes, 'test_1', array('test_2' => 'test_2', 'test_3' => 'test_3')); + + return $data; + } + +} + +/** + * Extends the default theme handler to mock some drupal_ methods. + */ +class TestThemeHandler extends ThemeHandler { + + /** + * {@inheritdoc} + */ + protected function clearCssCache() { + $this->clearedCssCache = TRUE; + } + + /** + * {@inheritdoc} + */ + protected function themeRegistryRebuild() { + $this->registryRebuild = TRUE; + } + + /** + * {@inheritdoc} + */ + protected function configInstallDefaultConfig($theme) { + $this->installedDefaultConfig[$theme] = TRUE; + } + + /** + * {@inheritdoc} + */ + protected function systemThemeList() { + return $this->systemList; + } + + /** + * {@inheritdoc} + */ + protected function systemListReset() { + } + +} + +if (!defined('DRUPAL_EXTENSION_NAME_MAX_LENGTH')) { + define('DRUPAL_EXTENSION_NAME_MAX_LENGTH', 50); +} +if (!defined('DRUPAL_PHP_FUNCTION_PATTERN')) { + define('DRUPAL_PHP_FUNCTION_PATTERN', '[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*'); +} +if (!defined('DRUPAL_ROOT')) { + define('DRUPAL_ROOT', dirname(dirname(substr(__DIR__, 0, -strlen(__NAMESPACE__))))); +} +if (!defined('DRUPAL_MINIMUM_PHP')) { + define('DRUPAL_MINIMUM_PHP', '5.3.10'); +}