Skip to content
Snippets Groups Projects
Select Git revision
  • d89c1f708d5765d2d826fdf353cfbc7c736c2f98
  • 11.x default protected
  • 11.2.x protected
  • 10.6.x protected
  • 10.5.x protected
  • 11.1.x protected
  • 10.4.x protected
  • 11.0.x protected
  • 10.3.x protected
  • 7.x protected
  • 10.2.x protected
  • 10.1.x protected
  • 9.5.x protected
  • 10.0.x protected
  • 9.4.x protected
  • 9.3.x protected
  • 9.2.x protected
  • 9.1.x protected
  • 8.9.x protected
  • 9.0.x protected
  • 8.8.x protected
  • 10.5.1 protected
  • 11.2.2 protected
  • 11.2.1 protected
  • 11.2.0 protected
  • 10.5.0 protected
  • 11.2.0-rc2 protected
  • 10.5.0-rc1 protected
  • 11.2.0-rc1 protected
  • 10.4.8 protected
  • 11.1.8 protected
  • 10.5.0-beta1 protected
  • 11.2.0-beta1 protected
  • 11.2.0-alpha1 protected
  • 10.4.7 protected
  • 11.1.7 protected
  • 10.4.6 protected
  • 11.1.6 protected
  • 10.3.14 protected
  • 10.4.5 protected
  • 11.0.13 protected
41 results

ThemeHandler.php

Blame
  • Alex Pott's avatar
    Issue #2289917 by Wim Leers, mdrummond, lauriii, Manuel Garcia, emma.maria,...
    Alex Pott authored
    Issue #2289917 by Wim Leers, mdrummond, lauriii, Manuel Garcia, emma.maria, Scionar, davidhernandez: Convert "messages" page element into blocks
    d89c1f70
    History
    Code owners
    Assign users and groups as approvers for specific file changes. Learn more.
    ThemeHandler.php 22.25 KiB
    <?php
    
    /**
     * @file
     * Contains \Drupal\Core\Extension\ThemeHandler.
     */
    
    namespace Drupal\Core\Extension;
    
    use Drupal\Component\Utility\String;
    use Drupal\Core\Asset\AssetCollectionOptimizerInterface;
    use Drupal\Core\Cache\Cache;
    use Drupal\Core\Config\ConfigFactoryInterface;
    use Drupal\Core\Config\ConfigInstallerInterface;
    use Drupal\Core\Config\ConfigManagerInterface;
    use Drupal\Core\Config\PreExistingConfigException;
    use Drupal\Core\Routing\RouteBuilderIndicatorInterface;
    use Drupal\Core\State\StateInterface;
    use Psr\Log\LoggerInterface;
    
    /**
     * Default theme handler using the config system to store installation statuses.
     */
    class ThemeHandler implements ThemeHandlerInterface {
    
      /**
       * Contains the features enabled for themes by default.
       *
       * @var array
       */
      protected $defaultFeatures = array(
        'logo',
        'favicon',
        'name',
        'slogan',
        'node_user_picture',
        'comment_user_picture',
        'comment_user_verification',
      );
    
      /**
       * A list of all currently available themes.
       *
       * @var array
       */
      protected $list;
    
      /**
       * The config factory to get the installed themes.
       *
       * @var \Drupal\Core\Config\ConfigFactoryInterface
       */
      protected $configFactory;
    
      /**
       * The module handler to fire themes_installed/themes_uninstalled hooks.
       *
       * @var \Drupal\Core\Extension\ModuleHandlerInterface
       */
      protected $moduleHandler;
    
      /**
       * The state backend.
       *
       * @var \Drupal\Core\State\StateInterface
       */
      protected $state;
    
      /**
       *  The config installer to install configuration.
       *
       * @var \Drupal\Core\Config\ConfigInstallerInterface
       */
      protected $configInstaller;
    
      /**
       * The info parser to parse the theme.info.yml files.
       *
       * @var \Drupal\Core\Extension\InfoParserInterface
       */
      protected $infoParser;
    
      /**
       * A logger instance.
       *
       * @var \Psr\Log\LoggerInterface
       */
      protected $logger;
    
      /**
       * The route builder indicator to rebuild the routes if a theme is installed.
       *
       * @var \Drupal\Core\Routing\RouteBuilderIndicatorInterface
       */
      protected $routeBuilderIndicator;
    
      /**
       * An extension discovery instance.
       *
       * @var \Drupal\Core\Extension\ExtensionDiscovery
       */
      protected $extensionDiscovery;
    
      /**
       * The CSS asset collection optimizer service.
       *
       * @var \Drupal\Core\Asset\AssetCollectionOptimizerInterface
       */
      protected $cssCollectionOptimizer;
    
      /**
       * The config manager used to uninstall a theme.
       *
       * @var \Drupal\Core\Config\ConfigManagerInterface
       */
      protected $configManager;
    
      /**
       * The app root.
       *
       * @var string
       */
      protected $root;
    
      /**
       * Constructs a new ThemeHandler.
       *
       * @param string $root
       *   The app root.
       * @param \Drupal\Core\Config\ConfigFactoryInterface $config_factory
       *   The config factory to get the installed themes.
       * @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler
       *   The module handler to fire themes_installed/themes_uninstalled hooks.
       * @param \Drupal\Core\State\StateInterface $state
       *   The state store.
       * @param \Drupal\Core\Extension\InfoParserInterface $info_parser
       *   The info parser to parse the theme.info.yml files.
       * @param \Psr\Log\LoggerInterface $logger
       *   A logger instance.
       * @param \Drupal\Core\Asset\AssetCollectionOptimizerInterface $css_collection_optimizer
       *   The CSS asset collection optimizer service.
       * @param \Drupal\Core\Config\ConfigInstallerInterface $config_installer
       *   (optional) The config installer to install configuration. This optional
       *   to allow the theme handler to work before Drupal is installed and has a
       *   database.
       * @param \Drupal\Core\Config\ConfigManagerInterface $config_manager
       *   The config manager used to uninstall a theme.
       * @param \Drupal\Core\Routing\RouteBuilderIndicatorInterface $route_builder_indicator
       *   (optional) The route builder indicator service to rebuild the routes if a
       *   theme is installed.
       * @param \Drupal\Core\Extension\ExtensionDiscovery $extension_discovery
       *   (optional) A extension discovery instance (for unit tests).
       */
      public function __construct($root, ConfigFactoryInterface $config_factory, ModuleHandlerInterface $module_handler, StateInterface $state, InfoParserInterface $info_parser,LoggerInterface $logger, AssetCollectionOptimizerInterface $css_collection_optimizer = NULL, ConfigInstallerInterface $config_installer = NULL, ConfigManagerInterface $config_manager = NULL, RouteBuilderIndicatorInterface $route_builder_indicator = NULL, ExtensionDiscovery $extension_discovery = NULL) {
        $this->root = $root;
        $this->configFactory = $config_factory;
        $this->moduleHandler = $module_handler;
        $this->state = $state;
        $this->infoParser = $info_parser;
        $this->logger = $logger;
        $this->cssCollectionOptimizer = $css_collection_optimizer;
        $this->configInstaller = $config_installer;
        $this->configManager = $config_manager;
        $this->routeBuilderIndicator = $route_builder_indicator;
        $this->extensionDiscovery = $extension_discovery;
      }
    
      /**
       * {@inheritdoc}
       */
      public function getDefault() {
        return $this->configFactory->get('system.theme')->get('default');
      }
    
      /**
       * {@inheritdoc}
       */
      public function setDefault($name) {
        $list = $this->listInfo();
        if (!isset($list[$name])) {
          throw new \InvalidArgumentException("$name theme is not installed.");
        }
        $this->configFactory->getEditable('system.theme')
          ->set('default', $name)
          ->save();
        return $this;
      }
    
      /**
       * {@inheritdoc}
       */
      public function install(array $theme_list, $install_dependencies = TRUE) {
        $extension_config = $this->configFactory->getEditable('core.extension');
    
        $theme_data = $this->rebuildThemeData();
    
        if ($install_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 installed currently.
          $installed_themes = $extension_config->get('theme') ?: array();
          if (!$theme_list = array_diff_key($theme_list, $installed_themes)) {
            // Nothing to do. All themes already installed.
            return TRUE;
          }
    
          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();
        }
    
        $themes_installed = array();
        foreach ($theme_list as $key) {
          // Only process themes that are not already installed.
          $installed = $extension_config->get("theme.$key") !== NULL;
          if ($installed) {
            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(
              '%name' => $key,
              '@max' => DRUPAL_EXTENSION_NAME_MAX_LENGTH,
            )));
          }
    
          // Validate default configuration of the theme. If there is existing
          // configuration then stop installing.
          $existing_configuration = $this->configInstaller->findPreExistingConfiguration('theme', $key);
          if (!empty($existing_configuration)) {
            throw PreExistingConfigException::create($key, $existing_configuration);
          }
    
          // The value is not used; the weight is ignored for themes currently.
          $extension_config
            ->set("theme.$key", 0)
            ->save();
    
          // 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 installed 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);
          }
    
          $themes_installed[] = $key;
    
          // Record the fact that it was installed.
          $this->logger->info('%theme theme installed.', array('%theme' => $key));
        }
    
        $this->cssCollectionOptimizer->deleteAll();
        $this->resetSystem();
    
        // Invoke hook_themes_installed() after the themes have been installed.
        $this->moduleHandler->invokeAll('themes_installed', array($themes_installed));
    
        return !empty($themes_installed);
      }
    
      /**
       * {@inheritdoc}
       */
      public function uninstall(array $theme_list) {
        $extension_config = $this->configFactory->getEditable('core.extension');
        $theme_config = $this->configFactory->getEditable('system.theme');
        $list = $this->listInfo();
        foreach ($theme_list as $key) {
          if (!isset($list[$key])) {
            throw new \InvalidArgumentException("Unknown theme: $key.");
          }
          if ($key === $theme_config->get('default')) {
            throw new \InvalidArgumentException("The current default theme $key cannot be uninstalled.");
          }
          if ($key === $theme_config->get('admin')) {
            throw new \InvalidArgumentException("The current admin theme $key cannot be uninstalled.");
          }
          // Base themes cannot be uninstalled if sub themes are installed, and if
          // they are not uninstalled at the same time.
          // @todo https://www.drupal.org/node/474684 and
          //   https://www.drupal.org/node/1297856 themes should leverage the module
          //   dependency system.
          if (!empty($list[$key]->sub_themes)) {
            foreach ($list[$key]->sub_themes as $sub_key => $sub_label) {
              if (isset($list[$sub_key]) && !in_array($sub_key, $theme_list, TRUE)) {
                throw new \InvalidArgumentException("The base theme $key cannot be uninstalled, because theme $sub_key depends on it.");
              }
            }
          }
        }
    
        $this->cssCollectionOptimizer->deleteAll();
        $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");
    
          // 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();
    
          // Remove all configuration belonging to the theme.
          $this->configManager->uninstall('theme', $key);
    
        }
        $extension_config->save();
        $this->state->set('system.theme.data', $current_theme_data);
    
        $this->resetSystem();
    
        $this->moduleHandler->invokeAll('themes_uninstalled', [$theme_list]);
      }
    
      /**
       * {@inheritdoc}
       */
      public function listInfo() {
        if (!isset($this->list)) {
          $this->list = array();
          $themes = $this->systemThemeList();
          // @todo Ensure that systemThemeList() does not contain an empty list
          //   during the batch installer, see https://www.drupal.org/node/2322619.
          if (empty($themes)) {
            $this->refreshInfo();
            $this->list = $this->list ?: array();
            $themes = \Drupal::state()->get('system.theme.data', array());
          }
          foreach ($themes as $theme) {
            $this->addTheme($theme);
          }
        }
        return $this->list;
      }
    
      /**
       * {@inheritdoc}
       */
      public function addTheme(Extension $theme) {
        foreach ($theme->info['libraries'] as $library => $name) {
          $theme->libraries[$library] = $name;
        }
        if (isset($theme->info['engine'])) {
          $theme->engine = $theme->info['engine'];
        }
        if (isset($theme->info['base theme'])) {
          $theme->base_theme = $theme->info['base theme'];
        }
        $this->list[$theme->getName()] = $theme;
      }
    
      /**
       * {@inheritdoc}
       */
      public function refreshInfo() {
        $this->reset();
        $extension_config = $this->configFactory->get('core.extension');
        $installed = $extension_config->get('theme');
    
        // @todo Avoid re-scanning all themes by retaining the original (unaltered)
        //   theme info somewhere.
        $list = $this->rebuildThemeData();
        foreach ($list as $name => $theme) {
          if (isset($installed[$name])) {
            $this->addTheme($theme);
          }
        }
        $this->state->set('system.theme.data', $this->list);
      }
    
      /**
       * {@inheritdoc}
       */
      public function reset() {
        $this->systemListReset();
        $this->list = NULL;
      }
    
      /**
       * {@inheritdoc}
       */
      public function rebuildThemeData() {
        $listing = $this->getExtensionDiscovery();
        $themes = $listing->scan('theme');
        $engines = $listing->scan('theme_engine');
        $extension_config = $this->configFactory->get('core.extension');
        $installed = $extension_config->get('theme') ?: array();
    
        // Set defaults for theme info.
        $defaults = array(
          'engine' => 'twig',
          'regions' => array(
            'sidebar_first' => 'Left sidebar',
            'sidebar_second' => 'Right sidebar',
            'content' => 'Content',
            'header' => 'Header',
            'primary_menu' => 'Primary menu',
            'secondary_menu' => 'Secondary menu',
            'footer' => 'Footer',
            'highlighted' => 'Highlighted',
            'messages' => 'Messages',
            'help' => 'Help',
            'page_top' => 'Page top',
            'page_bottom' => 'Page bottom',
            'breadcrumb' => 'Breadcrumb',
          ),
          'description' => '',
          'features' => $this->defaultFeatures,
          'screenshot' => 'screenshot.png',
          'php' => DRUPAL_MINIMUM_PHP,
          'libraries' => array(),
        );
    
        $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($installed[$key]);
    
          $theme->info = $this->infoParser->parse($theme->getPathname()) + $defaults;
    
          // Add the info file modification time, so it becomes available for
          // contributed modules to use for ordering theme lists.
          $theme->info['mtime'] = $theme->getMTime();
    
          // Invoke hook_system_info_alter() to give installed modules a chance to
          // modify the data in the .info.yml files if necessary.
          // @todo Remove $type argument, obsolete with $theme->getType().
          $type = 'theme';
          $this->moduleHandler->alter('system_info', $theme->info, $theme, $type);
    
          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).
          $engine = $theme->info['engine'];
          if (isset($engines[$engine])) {
            $theme->owner = $engines[$engine]->getExtensionPathname();
            $theme->prefix = $engines[$engine]->getName();
          }
    
          // Prefix screenshot with theme path.
          if (!empty($theme->info['screenshot'])) {
            $theme->info['screenshot'] = $theme->getPath() . '/' . $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.
        foreach ($sub_themes as $key) {
          $sub_theme = $themes[$key];
          // The $base_themes property is optional; only set for sub themes.
          // @see ThemeHandlerInterface::listInfo()
          $sub_theme->base_themes = $this->getBaseThemes($themes, $key);
          // empty() cannot be used here, since ThemeHandler::doGetBaseThemes() adds
          // the key of a base theme with a value of NULL in case it is not found,
          // in order to prevent needless iterations.
          if (!current($sub_theme->base_themes)) {
            continue;
          }
          // Determine the root base theme.
          $root_key = key($sub_theme->base_themes);
          // Build the list of sub-themes for each of the theme's base themes.
          foreach (array_keys($sub_theme->base_themes) as $base_theme) {
            $themes[$base_theme]->sub_themes[$key] = $sub_theme->info['name'];
          }
          // Add the theme engine info from the root base theme.
          if (isset($themes[$root_key]->owner)) {
            $sub_theme->info['engine'] = $themes[$root_key]->info['engine'];
            $sub_theme->owner = $themes[$root_key]->owner;
            $sub_theme->prefix = $themes[$root_key]->prefix;
          }
        }
    
        return $themes;
      }
    
      /**
       * {@inheritdoc}
       */
      public function getBaseThemes(array $themes, $theme) {
        return $this->doGetBaseThemes($themes, $theme);
      }
    
      /**
       * Finds the base themes for the specific theme.
       *
       * @param array $themes
       *   An array of available themes.
       * @param string $theme
       *   The name of the theme whose base we are looking for.
       * @param array $used_themes
       *   (optional) A recursion parameter preventing endless loops. Defaults to
       *   an empty array.
       *
       * @return array
       *   An array of base themes.
       */
      protected function doGetBaseThemes(array $themes, $theme, $used_themes = array()) {
        if (!isset($themes[$theme]->info['base theme'])) {
          return array();
        }
    
        $base_key = $themes[$theme]->info['base theme'];
        // Does the base theme exist?
        if (!isset($themes[$base_key])) {
          return array($base_key => NULL);
        }
    
        $current_base_theme = array($base_key => $themes[$base_key]->info['name']);
    
        // Is the base theme itself a child of another theme?
        if (isset($themes[$base_key]->info['base theme'])) {
          // Do we already know the base themes of this theme?
          if (isset($themes[$base_key]->base_themes)) {
            return $themes[$base_key]->base_themes + $current_base_theme;
          }
          // Prevent loops.
          if (!empty($used_themes[$base_key])) {
            return array($base_key => NULL);
          }
          $used_themes[$base_key] = TRUE;
          return $this->doGetBaseThemes($themes, $base_key, $used_themes) + $current_base_theme;
        }
        // If we get here, then this is our parent theme.
        return $current_base_theme;
      }
    
      /**
       * Returns an extension discovery object.
       *
       * @return \Drupal\Core\Extension\ExtensionDiscovery
       *   The extension discovery object.
       */
      protected function getExtensionDiscovery() {
        if (!isset($this->extensionDiscovery)) {
          $this->extensionDiscovery = new ExtensionDiscovery($this->root);
        }
        return $this->extensionDiscovery;
      }
    
      /**
       * Resets some other systems like rebuilding the route information or caches.
       */
      protected function resetSystem() {
        if ($this->routeBuilderIndicator) {
          $this->routeBuilderIndicator->setRebuildNeeded();
        }
        $this->systemListReset();
    
        // @todo It feels wrong to have the requirement to clear the local tasks
        //   cache here.
        Cache::invalidateTags(array('local_task'));
        $this->themeRegistryRebuild();
      }
    
      /**
       * {@inheritdoc}
       */
      public function getName($theme) {
        $themes = $this->listInfo();
        if (!isset($themes[$theme])) {
          throw new \InvalidArgumentException(String::format('Requested the name of a non-existing theme @theme', array('@theme' => $theme)));
        }
        return String::checkPlain($themes[$theme]->info['name']);
      }
    
      /**
       * Wraps system_list_reset().
       */
      protected function systemListReset() {
        system_list_reset();
      }
    
      /**
       * Wraps drupal_theme_rebuild().
       */
      protected function themeRegistryRebuild() {
        drupal_theme_rebuild();
      }
    
      /**
       * Wraps system_list().
       *
       * @return array
       *   A list of themes keyed by name.
       */
      protected function systemThemeList() {
        return system_list('theme');
      }
    
      /**
       * {@inheritdoc}
       */
      public function getThemeDirectories() {
        $dirs = array();
        foreach ($this->listInfo() as $name => $theme) {
          $dirs[$name] = $this->root . '/' . $theme->getPath();
        }
        return $dirs;
      }
    
      /**
       * {@inheritdoc}
       */
      public function themeExists($theme) {
        $themes = $this->listInfo();
        return isset($themes[$theme]);
      }
    
      /**
       * {@inheritdoc}
       */
      public function getTheme($name) {
        $themes = $this->listInfo();
        if (isset($themes[$name])) {
          return $themes[$name];
        }
        throw new \InvalidArgumentException(sprintf('The theme %s does not exist.', $name));
      }
    
    }