Commit 163df489 authored by catch's avatar catch
Browse files

Issue #2392815 by alexpott, pooja saraah, bircher, cilefen, pratik_specbee,...

Issue #2392815 by alexpott, pooja saraah, bircher, cilefen, pratik_specbee, catch, fago: Module uninstall validators are not used to validate a config import

(cherry picked from commit 7fbc4c4b)
parent fbd2d2a2
Loading
Loading
Loading
Loading
+1 −0
Original line number Diff line number Diff line
@@ -1286,6 +1286,7 @@ services:
    class: Drupal\Core\EventSubscriber\ConfigImportSubscriber
    tags:
      - { name: event_subscriber }
      - { name: service_collector, tag: 'module_install.uninstall_validator', call: addUninstallValidator }
    arguments: ['@theme_handler', '@extension.list.module']
  config_snapshot_subscriber:
    class: Drupal\Core\EventSubscriber\ConfigSnapshotSubscriber
+7 −2
Original line number Diff line number Diff line
@@ -673,13 +673,18 @@ protected function finish(&$context) {
  /**
   * Gets the next extension operation to perform.
   *
   * Uninstalls are processed first with themes coming before modules. Then
   * installs are processed with modules coming before themes. This order is
   * necessary because themes can depend on modules.
   *
   * @return array|bool
   *   An array containing the next operation and extension name to perform it
   *   on. If there is nothing left to do returns FALSE;
   */
  protected function getNextExtensionOperation() {
    foreach (['module', 'theme'] as $type) {
      foreach (['install', 'uninstall'] as $op) {
    foreach (['uninstall', 'install'] as $op) {
      $types = $op === 'uninstall' ? ['theme', 'module'] : ['module', 'theme'];
      foreach ($types as $type) {
        $unprocessed = $this->getUnprocessedExtensions($type);
        if (!empty($unprocessed[$op])) {
          return [
+46 −1
Original line number Diff line number Diff line
@@ -7,7 +7,9 @@
use Drupal\Core\Config\ConfigImporterEvent;
use Drupal\Core\Config\ConfigImportValidateEventSubscriberBase;
use Drupal\Core\Config\ConfigNameException;
use Drupal\Core\Extension\ConfigImportModuleUninstallValidatorInterface;
use Drupal\Core\Extension\ModuleExtensionList;
use Drupal\Core\Extension\ModuleUninstallValidatorInterface;
use Drupal\Core\Extension\ThemeHandlerInterface;
use Drupal\Core\Installer\InstallerKernel;

@@ -37,6 +39,13 @@ class ConfigImportSubscriber extends ConfigImportValidateEventSubscriberBase {
   */
  protected $themeHandler;

  /**
   * The uninstall validators.
   *
   * @var \Drupal\Core\Extension\ModuleUninstallValidatorInterface[]
   */
  protected $uninstallValidators = [];

  /**
   * Constructs the ConfigImportSubscriber.
   *
@@ -50,6 +59,16 @@ public function __construct(ThemeHandlerInterface $theme_handler, ModuleExtensio
    $this->moduleExtensionList = $extension_list_module;
  }

  /**
   * Adds a module uninstall validator.
   *
   * @param \Drupal\Core\Extension\ModuleUninstallValidatorInterface $uninstall_validator
   *   The uninstall validator to add.
   */
  public function addUninstallValidator(ModuleUninstallValidatorInterface $uninstall_validator): void {
    $this->uninstallValidators[] = $uninstall_validator;
  }

  /**
   * Validates the configuration to be imported.
   *
@@ -150,6 +169,16 @@ protected function validateModules(ConfigImporter $config_importer) {
          $config_importer->logError($this->t('Unable to uninstall the %module module since the %dependent_module module is installed.', ['%module' => $module_name, '%dependent_module' => $dependent_module_name]));
        }
      }
      // Ensure that modules can be uninstalled.
      foreach ($this->uninstallValidators as $validator) {
        $reasons = $validator instanceof ConfigImportModuleUninstallValidatorInterface ?
          $validator->validateConfigImport($module, $config_importer->getStorageComparer()->getSourceStorage()) :
          $validator->validate($module);
        foreach ($reasons as $reason) {
          $config_importer->logError($this->t('Unable to uninstall the %module module because: @reason.',
            ['%module' => $module_data[$module]->info['name'], '@reason' => $reason]));
        }
      }
    }

    // Ensure that the install profile is not being uninstalled.
@@ -169,6 +198,7 @@ protected function validateThemes(ConfigImporter $config_importer) {
    $core_extension = $config_importer->getStorageComparer()->getSourceStorage()->read('core.extension');
    // Get all themes including those that are not installed.
    $theme_data = $this->getThemeData();
    $module_data = $this->moduleExtensionList->getList();
    $installs = $config_importer->getExtensionChangelist('theme', 'install');
    foreach ($installs as $key => $theme) {
      if (!isset($theme_data[$theme])) {
@@ -181,13 +211,28 @@ protected function validateThemes(ConfigImporter $config_importer) {

    // Ensure that all themes being installed have their dependencies met.
    foreach ($installs as $theme) {
      foreach (array_keys($theme_data[$theme]->requires) as $required_theme) {
      $module_dependencies = $theme_data[$theme]->module_dependencies;
      // $theme_data[$theme]->requires contains both theme and module
      // dependencies keyed by the extension machine names.
      // $theme_data[$theme]->module_dependencies contains only the module
      // dependencies 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);
      foreach (array_keys($theme_dependencies) as $required_theme) {
        if (!isset($core_extension['theme'][$required_theme])) {
          $theme_name = $theme_data[$theme]->info['name'];
          $required_theme_name = $theme_data[$required_theme]->info['name'];
          $config_importer->logError($this->t('Unable to install the %theme theme since it requires the %required_theme theme.', ['%theme' => $theme_name, '%required_theme' => $required_theme_name]));
        }
      }
      foreach (array_keys($module_dependencies) as $required_module) {
        if (!isset($core_extension['module'][$required_module])) {
          $theme_name = $theme_data[$theme]->info['name'];
          $required_module_name = $module_data[$required_module]->info['name'];
          $config_importer->logError($this->t('Unable to install the %theme theme since it requires the %required_module module.', ['%theme' => $theme_name, '%required_module' => $required_module_name]));
        }
      }
    }

    // Ensure that all themes being uninstalled are not required by themes that
+34 −0
Original line number Diff line number Diff line
<?php

namespace Drupal\Core\Extension;

use Drupal\Core\Config\StorageInterface;

/**
 * Special interface for module uninstall validators for configuration import.
 *
 * A module uninstall validator that needs different functionality prior to a
 * configuration import should implement this interface and be defined in
 * a Drupal @link container service @endlink that is tagged
 * module_install.uninstall_validator.
 */
interface ConfigImportModuleUninstallValidatorInterface extends ModuleUninstallValidatorInterface {

  /**
   * Determines reasons a module can not be uninstalled prior to config import.
   *
   * @param string $module
   *   A module name.
   * @param \Drupal\Core\Config\StorageInterface $source_storage
   *   Storage object used to read configuration that is about to be imported.
   *
   * @return string[]
   *   An array of reasons the module can not be uninstalled, empty if it can.
   *   Each reason should not end with any punctuation since multiple reasons
   *   can be displayed together.
   *
   * @see \Drupal\Core\EventSubscriber\ConfigImportSubscriber::validateModules()
   */
  public function validateConfigImport(string $module, StorageInterface $source_storage): array;

}
+1 −1
Original line number Diff line number Diff line
@@ -70,7 +70,7 @@ public function install(array $module_list, $enable_dependencies = TRUE);
  public function uninstall(array $module_list, $uninstall_dependents = TRUE);

  /**
   * Adds module a uninstall validator.
   * Adds a module uninstall validator.
   *
   * @param \Drupal\Core\Extension\ModuleUninstallValidatorInterface $uninstall_validator
   *   The uninstall validator to add.
Loading