Commit a130897f authored by catch's avatar catch
Browse files

Issue #474684 by bnjmnm, dawehner, tedbow, pfrenssen, JohnAlbin, ademarco,...

Issue #474684 by bnjmnm, dawehner, tedbow, pfrenssen, JohnAlbin, ademarco, kalpaitch, vdacosta@voidtek.com, rensingh99, markcarver, jungle, jhedstrom, RobLoach, almaudoh, kevineinarsson, shaal, dpagini, thedavidmeister, sreynen, Snugug, Miguel.kode, kamkejj, alexpott, Pol, sun, Wim Leers, lauriii, tim.plunkett, eaton: Allow themes to declare dependencies on modules
parent 0d78f4c0
Loading
Loading
Loading
Loading
+8 −2
Original line number Diff line number Diff line
@@ -522,7 +522,7 @@ services:
    class: Drupal\Core\Extension\ModuleInstaller
    tags:
      - { name: service_collector, tag: 'module_install.uninstall_validator', call: addUninstallValidator }
    arguments: ['%app.root%', '@module_handler', '@kernel']
    arguments: ['%app.root%', '@module_handler', '@kernel', '@extension.list.theme']
    lazy: true
  extension.list.module:
    class: Drupal\Core\Extension\ModuleExtensionList
@@ -548,12 +548,18 @@ services:
      - { name: module_install.uninstall_validator }
    arguments: ['@string_translation', '@extension.list.module']
    lazy: true
  module_required_by_themes_uninstall_validator:
    class: Drupal\Core\Extension\ModuleRequiredByThemesUninstallValidator
    tags:
      - { name: module_install.uninstall_validator }
    arguments: ['@string_translation', '@extension.list.module', '@extension.list.theme']
    lazy: true
  theme_handler:
    class: Drupal\Core\Extension\ThemeHandler
    arguments: ['%app.root%', '@config.factory', '@extension.list.theme']
  theme_installer:
    class: Drupal\Core\Extension\ThemeInstaller
    arguments: ['@theme_handler', '@config.factory', '@config.installer', '@module_handler', '@config.manager', '@asset.css.collection_optimizer', '@router.builder', '@logger.channel.default', '@state']
    arguments: ['@theme_handler', '@config.factory', '@config.installer', '@module_handler', '@config.manager', '@asset.css.collection_optimizer', '@router.builder', '@logger.channel.default', '@state', '@extension.list.module']
  entity.memory_cache:
    class: Drupal\Core\Cache\MemoryCache\MemoryCache
  entity_type.manager:
+20 −3
Original line number Diff line number Diff line
@@ -50,6 +50,13 @@ class ModuleInstaller implements ModuleInstallerInterface {
   */
  protected $uninstallValidators;

  /**
   * The theme extension list.
   *
   * @var \Drupal\Core\Extension\ThemeExtensionList
   */
  protected $themeExtensionList;

  /**
   * Constructs a new ModuleInstaller instance.
   *
@@ -59,14 +66,21 @@ class ModuleInstaller implements ModuleInstallerInterface {
   *   The module handler.
   * @param \Drupal\Core\DrupalKernelInterface $kernel
   *   The drupal kernel.
   * @param \Drupal\Core\Extension\ThemeExtensionList $extension_list_theme
   *   The theme extension list.
   *
   * @see \Drupal\Core\DrupalKernel
   * @see \Drupal\Core\CoreServiceProvider
   */
  public function __construct($root, ModuleHandlerInterface $module_handler, DrupalKernelInterface $kernel) {
  public function __construct($root, ModuleHandlerInterface $module_handler, DrupalKernelInterface $kernel, ThemeExtensionList $extension_list_theme = NULL) {
    $this->root = $root;
    $this->moduleHandler = $module_handler;
    $this->kernel = $kernel;
    if (is_null($extension_list_theme)) {
      @trigger_error('The extension.list.theme service must be passed to ' . __NAMESPACE__ . '\ModuleInstaller::__construct(). It was added in drupal:8.9.0 and will be required before drupal:10.0.0.', E_USER_DEPRECATED);
      $extension_list_theme = \Drupal::service('extension.list.theme');
    }
    $this->themeExtensionList = $extension_list_theme;
  }

  /**
@@ -372,12 +386,14 @@ public function uninstall(array $module_list, $uninstall_dependents = TRUE) {
    }

    if ($uninstall_dependents) {
      $theme_list = $this->themeExtensionList->getList();

      // Add dependent modules to the list. The new modules will be processed as
      // the foreach loop continues.
      foreach ($module_list as $module => $value) {
        foreach (array_keys($module_data[$module]->required_by) as $dependent) {
          if (!isset($module_data[$dependent])) {
            // The dependent module does not exist.
          if (!isset($module_data[$dependent]) && !isset($theme_list[$dependent])) {
            // The dependent module or theme does not exist.
            return FALSE;
          }

@@ -578,6 +594,7 @@ protected function updateKernel($module_filenames) {
    // After rebuilding the container we need to update the injected
    // dependencies.
    $container = $this->kernel->getContainer();
    $this->themeExtensionList = $container->get('extension.list.theme');
    $this->moduleHandler = $container->get('module_handler');
  }

+84 −0
Original line number Diff line number Diff line
<?php

namespace Drupal\Core\Extension;

use Drupal\Core\StringTranslation\StringTranslationTrait;
use Drupal\Core\StringTranslation\TranslationInterface;

/**
 * Ensures modules cannot be uninstalled if enabled themes depend on them.
 */
class ModuleRequiredByThemesUninstallValidator implements ModuleUninstallValidatorInterface {

  use StringTranslationTrait;

  /**
   * The module extension list.
   *
   * @var \Drupal\Core\Extension\ModuleExtensionList
   */
  protected $moduleExtensionList;

  /**
   * The theme extension list.
   *
   * @var \Drupal\Core\Extension\ThemeExtensionList
   */
  protected $themeExtensionList;

  /**
   * Constructs a new ModuleRequiredByThemesUninstallValidator.
   *
   * @param \Drupal\Core\StringTranslation\TranslationInterface $string_translation
   *   The string translation service.
   * @param \Drupal\Core\Extension\ModuleExtensionList $extension_list_module
   *   The module extension list.
   * @param \Drupal\Core\Extension\ThemeExtensionList $extension_list_theme
   *   The theme extension list.
   */
  public function __construct(TranslationInterface $string_translation, ModuleExtensionList $extension_list_module, ThemeExtensionList $extension_list_theme) {
    $this->stringTranslation = $string_translation;
    $this->moduleExtensionList = $extension_list_module;
    $this->themeExtensionList = $extension_list_theme;
  }

  /**
   * {@inheritdoc}
   */
  public function validate($module) {
    $reasons = [];

    $themes_depending_on_module = $this->getThemesDependingOnModule($module);
    if (!empty($themes_depending_on_module)) {
      $module_name = $this->moduleExtensionList->get($module)->info['name'];
      $theme_names = implode(', ', $themes_depending_on_module);
      $reasons[] = $this->formatPlural(count($themes_depending_on_module),
        'Required by the theme: @theme_names',
        'Required by the themes: @theme_names',
        ['@module_name' => $module_name, '@theme_names' => $theme_names]);
    }

    return $reasons;
  }

  /**
   * Returns themes that depend on a module.
   *
   * @param string $module
   *   The module machine name.
   *
   * @return string[]
   *   An array of the names of themes that depend on $module.
   */
  protected function getThemesDependingOnModule($module) {
    $installed_themes = $this->themeExtensionList->getAllInstalledInfo();
    $themes_depending_on_module = array_map(function ($theme) use ($module) {
      if (in_array($module, $theme['dependencies'])) {
        return $theme['name'];
      }
    }, $installed_themes);

    return array_filter($themes_depending_on_module);
  }

}
+17 −0
Original line number Diff line number Diff line
@@ -51,6 +51,7 @@ class ThemeExtensionList extends ExtensionList {
    'libraries' => [],
    'libraries_extend' => [],
    'libraries_override' => [],
    'dependencies' => [],
  ];

  /**
@@ -140,6 +141,22 @@ protected function doList() {
    // sub-themes.
    $this->fillInSubThemeData($themes, $sub_themes);

    foreach ($themes as $key => $theme) {
      // After $theme is processed by buildModuleDependencies(), there can be a
      // `$theme->requires` array containing both module and base theme
      // dependencies. The module dependencies are copied to their own property
      // so they are available to operations specific to module dependencies.
      if (isset($theme->requires)) {
        $theme->module_dependencies = array_diff_key($theme->requires, $themes);
      }
      else {
        // Even if no requirements are specified, the theme installation process
        // expects the presence of the `requires` and `module_dependencies`
        // properties, so they should be initialized here as empty arrays.
        $theme->requires = [];
        $theme->module_dependencies = [];
      }
    }
    return $themes;
  }

+52 −8
Original line number Diff line number Diff line
@@ -2,6 +2,7 @@

namespace Drupal\Core\Extension;

use Drupal\Component\Utility\Html;
use Drupal\Core\Asset\AssetCollectionOptimizerInterface;
use Drupal\Core\Cache\Cache;
use Drupal\Core\Config\ConfigFactoryInterface;
@@ -10,6 +11,8 @@
use Drupal\Core\Extension\Exception\UnknownExtensionException;
use Drupal\Core\Routing\RouteBuilderInterface;
use Drupal\Core\State\StateInterface;
use Drupal\Core\StringTranslation\StringTranslationTrait;
use Drupal\system\ModuleDependencyMessageTrait;
use Psr\Log\LoggerInterface;

/**
@@ -17,6 +20,9 @@
 */
class ThemeInstaller implements ThemeInstallerInterface {

  use ModuleDependencyMessageTrait;
  use StringTranslationTrait;

  /**
   * @var \Drupal\Core\Extension\ThemeHandlerInterface
   */
@@ -62,6 +68,13 @@ class ThemeInstaller implements ThemeInstallerInterface {
   */
  protected $logger;

  /**
   * The module extension list.
   *
   * @var \Drupal\Core\Extension\ModuleExtensionList
   */
  protected $moduleExtensionList;

  /**
   * Constructs a new ThemeInstaller.
   *
@@ -86,8 +99,10 @@ class ThemeInstaller implements ThemeInstallerInterface {
   *   A logger instance.
   * @param \Drupal\Core\State\StateInterface $state
   *   The state store.
   * @param \Drupal\Core\Extension\ModuleExtensionList $module_extension_list
   *   The module extension list.
   */
  public function __construct(ThemeHandlerInterface $theme_handler, ConfigFactoryInterface $config_factory, ConfigInstallerInterface $config_installer, ModuleHandlerInterface $module_handler, ConfigManagerInterface $config_manager, AssetCollectionOptimizerInterface $css_collection_optimizer, RouteBuilderInterface $route_builder, LoggerInterface $logger, StateInterface $state) {
  public function __construct(ThemeHandlerInterface $theme_handler, ConfigFactoryInterface $config_factory, ConfigInstallerInterface $config_installer, ModuleHandlerInterface $module_handler, ConfigManagerInterface $config_manager, AssetCollectionOptimizerInterface $css_collection_optimizer, RouteBuilderInterface $route_builder, LoggerInterface $logger, StateInterface $state, ModuleExtensionList $module_extension_list = NULL) {
    $this->themeHandler = $theme_handler;
    $this->configFactory = $config_factory;
    $this->configInstaller = $config_installer;
@@ -97,6 +112,11 @@ public function __construct(ThemeHandlerInterface $theme_handler, ConfigFactoryI
    $this->routeBuilder = $route_builder;
    $this->logger = $logger;
    $this->state = $state;
    if ($module_extension_list === NULL) {
      @trigger_error('The extension.list.module service must be passed to ' . __NAMESPACE__ . '\ThemeInstaller::__construct(). It was added in drupal:8.9.0 and will be required before drupal:10.0.0.', E_USER_DEPRECATED);
      $module_extension_list = \Drupal::service('extension.list.module');
    }
    $this->moduleExtensionList = $module_extension_list;
  }

  /**
@@ -106,6 +126,8 @@ public function install(array $theme_list, $install_dependencies = TRUE) {
    $extension_config = $this->configFactory->getEditable('core.extension');

    $theme_data = $this->themeHandler->rebuildThemeData();
    $installed_themes = $extension_config->get('theme') ?: [];
    $installed_modules = $extension_config->get('module') ?: [];

    if ($install_dependencies) {
      $theme_list = array_combine($theme_list, $theme_list);
@@ -116,16 +138,41 @@ public function install(array $theme_list, $install_dependencies = TRUE) {
      }

      // Only process themes that are not installed currently.
      $installed_themes = $extension_config->get('theme') ?: [];
      if (!$theme_list = array_diff_key($theme_list, $installed_themes)) {
        // Nothing to do. All themes already installed.
        return TRUE;
      }

      $module_list = $this->moduleExtensionList->getList();
      foreach ($theme_list as $theme => $value) {
        // Add dependencies to the list. The new themes will be processed as
        // the parent foreach loop continues.
        foreach (array_keys($theme_data[$theme]->requires) as $dependency) {
        $module_dependencies = $theme_data[$theme]->module_dependencies;
        // $theme_data[$theme]->requires contains both theme and module
        // dependencies keyed by the extension machine names and
        // $theme_data[$theme]->module_dependencies contains only modules keyed
        // by the module extension machine name. Therefore we can find the theme
        // dependencies by finding array keys for 'requires' that are not in
        // $module_dependencies.
        $theme_dependencies = array_diff_key($theme_data[$theme]->requires, $module_dependencies);
        // We can find the unmet module dependencies by finding the module
        // machine names keys that are not in $installed_modules keys.
        $unmet_module_dependencies = array_diff_key($module_dependencies, $installed_modules);

        // Prevent themes with unmet module dependencies from being installed.
        if (!empty($unmet_module_dependencies)) {
          $unmet_module_dependencies_list = implode(', ', array_keys($unmet_module_dependencies));
          throw new MissingDependencyException("Unable to install theme: '$theme' due to unmet module dependencies: '$unmet_module_dependencies_list'.");
        }

        foreach ($module_dependencies as $dependency => $dependency_object) {
          if ($incompatible = $this->checkDependencyMessage($module_list, $dependency, $dependency_object)) {
            $sanitized_message = Html::decodeEntities(strip_tags($incompatible));
            throw new MissingDependencyException("Unable to install theme: $sanitized_message");
          }
        }

        // Add dependencies to the list of themes to install. The new themes
        // will be processed as the parent foreach loop continues.
        foreach (array_keys($theme_dependencies) as $dependency) {
          if (!isset($theme_data[$dependency])) {
            // The dependency does not exist.
            return FALSE;
@@ -147,9 +194,6 @@ public function install(array $theme_list, $install_dependencies = TRUE) {
      arsort($theme_list);
      $theme_list = array_keys($theme_list);
    }
    else {
      $installed_themes = $extension_config->get('theme') ?: [];
    }

    $themes_installed = [];
    foreach ($theme_list as $key) {
Loading