Commit 20a8ccfd authored by Dries's avatar Dries

Issue #2278017 by cilefen, bircher, effulgentsia: When a content entity type...

Issue #2278017 by cilefen, bircher, effulgentsia: When a content entity type providing module is uninstalled, the entities are not fully deleted, leaving broken reference
parent 5c6438d0
......@@ -289,7 +289,14 @@ services:
arguments: ['@app.root', '%container.modules%', '@cache.bootstrap']
module_installer:
class: Drupal\Core\Extension\ModuleInstaller
tags:
- { name: service_collector, tag: 'module_install.uninstall_validator', call: addUninstallValidator }
arguments: ['@app.root', '@module_handler', '@kernel']
content_uninstall_validator:
class: Drupal\Core\Entity\ContentUninstallValidator
tags:
- { name: module_install.uninstall_validator }
arguments: ['@entity.manager', '@string_translation']
theme_handler:
class: Drupal\Core\Extension\ThemeHandler
arguments: ['@app.root', '@config.factory', '@module_handler', '@state', '@info_parser', '@logger.channel.default', '@asset.css.collection_optimizer', '@config.installer', '@config.manager', '@router.builder_indicator']
......
<?php
/**
* @file
* Contains Drupal\Core\Entity\ContentUninstallValidator.
*/
namespace Drupal\Core\Entity;
use Drupal\Core\Extension\ModuleUninstallValidatorInterface;
use Drupal\Core\StringTranslation\StringTranslationTrait;
use Drupal\Core\StringTranslation\TranslationInterface;
/**
* Validates module uninstall readiness based on existing content entities.
*/
class ContentUninstallValidator implements ModuleUninstallValidatorInterface {
use StringTranslationTrait;
/**
* Constructs a new ContentUninstallValidator.
*
* @param \Drupal\Core\Entity\EntityManagerInterface $entity_manager
* The entity manager.
* @param \Drupal\Core\StringTranslation\TranslationInterface $string_translation
* The string translation service.
*/
public function __construct(EntityManagerInterface $entity_manager, TranslationInterface $string_translation) {
$this->entityManager = $entity_manager;
$this->stringTranslation = $string_translation;
}
/**
* {@inheritdoc}
*/
public function validate($module) {
$entity_types = $this->entityManager->getDefinitions();
$reasons = array();
foreach ($entity_types as $entity_type) {
if ($module == $entity_type->getProvider() && $entity_type instanceof ContentEntityTypeInterface && $this->entityManager->getStorage($entity_type->id())->hasData()) {
$reasons[] = $this->t('There is content for the entity type: @entity_type', array('@entity_type' => $entity_type->getLabel()));
}
}
return $reasons;
}
}
......@@ -41,6 +41,13 @@ class ModuleInstaller implements ModuleInstallerInterface {
*/
protected $root;
/**
* The uninstall validators.
*
* @var \Drupal\Core\Extension\ModuleUninstallValidatorInterface[]
*/
protected $uninstallValidators;
/**
* Constructs a new ModuleInstaller instance.
*
......@@ -60,6 +67,13 @@ public function __construct($root, ModuleHandlerInterface $module_handler, Drupa
$this->kernel = $kernel;
}
/**
* {@inheritdoc}
*/
public function addUninstallValidator(ModuleUninstallValidatorInterface $uninstall_validator) {
$this->uninstallValidators[] = $uninstall_validator;
}
/**
* {@inheritdoc}
*/
......@@ -306,6 +320,15 @@ public function uninstall(array $module_list, $uninstall_dependents = TRUE) {
}
}
// Use the validators and throw an exception with the reasons.
if ($reasons = $this->validateUninstall($module_list)) {
foreach ($reasons as $reason) {
$reason_message[] = implode(', ', $reason);
}
throw new ModuleUninstallValidatorException(format_string('The following reasons prevents the modules from being uninstalled: @reasons', array(
'@reasons' => implode(', ', $reason_message),
)));
}
// Set the actual module weights.
$module_list = array_map(function ($module) use ($module_data) {
return $module_data[$module]->sort;
......@@ -469,4 +492,23 @@ protected function updateKernel($module_filenames) {
$this->moduleHandler = $container->get('module_handler');
}
/**
* {@inheritdoc}
*/
public function validateUninstall(array $module_list) {
$reasons = array();
foreach ($module_list as $module) {
foreach ($this->uninstallValidators as $validator) {
$validation_reasons = $validator->validate($module);
if (!empty($validation_reasons)) {
if (!isset($reasons[$module])) {
$reasons[$module] = array();
}
$reasons[$module] = array_merge($reasons[$module], $validation_reasons);
}
}
}
return $reasons;
}
}
......@@ -23,7 +23,7 @@ interface ModuleInstallerInterface {
* - Invoke hook_install() and add it to the list of installed modules.
* - Invoke hook_modules_installed().
*
* @param array $module_list
* @param string[] $module_list
* An array of module names.
* @param bool $enable_dependencies
* (optional) If TRUE, dependencies will automatically be installed in the
......@@ -42,7 +42,7 @@ public function install(array $module_list, $enable_dependencies = TRUE);
/**
* Uninstalls a given list of modules.
*
* @param array $module_list
* @param string[] $module_list
* The modules to uninstall.
* @param bool $uninstall_dependents
* (optional) If TRUE, dependent modules will automatically be uninstalled
......@@ -58,5 +58,24 @@ public function install(array $module_list, $enable_dependencies = TRUE);
*/
public function uninstall(array $module_list, $uninstall_dependents = TRUE);
/**
* Adds module a uninstall validator.
*
* @param \Drupal\Core\Extension\ModuleUninstallValidatorInterface $uninstall_validator
* The uninstall validator to add.
*/
public function addUninstallValidator(ModuleUninstallValidatorInterface $uninstall_validator);
/**
* Determines whether a list of modules can be uninstalled.
*
* @param string[] $module_list
* An array of module names.
*
* @return string[]
* An array of reasons the module can not be uninstalled, empty if it can.
*/
public function validateUninstall(array $module_list);
}
<?php
/**
* @file
* Contains \Drupal\Core\Extension\ModuleUninstallValidatorException.
*/
namespace Drupal\Core\Extension;
/**
* Defines an exception thrown when uninstalling a module that did not validate.
*/
class ModuleUninstallValidatorException extends \InvalidArgumentException { }
<?php
/**
* @file
* Contains Drupal\Core\Extension\ModuleUninstallValidatorInterface.
*/
namespace Drupal\Core\Extension;
/**
* Common interface for module uninstall validators.
*/
interface ModuleUninstallValidatorInterface {
/**
* Determines the reasons a module can not be uninstalled.
*
* @param string $module
* A module name.
*
* @return string[]
* An array of reasons the module can not be uninstalled, empty if it can.
*/
public function validate($module);
}
......@@ -9,6 +9,7 @@
use Drupal\Core\Config\StorageComparer;
use Drupal\system\Tests\Module\ModuleTestBase;
use Drupal\shortcut\Entity\Shortcut;
/**
* Tests the largest configuration import possible with the modules and profiles
......@@ -83,6 +84,10 @@ public function testInstallUninstall() {
$term->delete();
}
// Delete any shortcuts so the shortcut module can be uninstalled.
$shortcuts = Shortcut::loadMultiple();
entity_delete_multiple('shortcut', array_keys($shortcuts));
system_list_reset();
$all_modules = system_rebuild_module_data();
......
......@@ -8,6 +8,7 @@
namespace Drupal\system\Form;
use Drupal\Core\Extension\ModuleHandlerInterface;
use Drupal\Core\Extension\ModuleInstallerInterface;
use Drupal\Core\Form\FormBase;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\KeyValueStore\KeyValueStoreExpirableInterface;
......@@ -25,6 +26,13 @@ class ModulesUninstallForm extends FormBase {
*/
protected $moduleHandler;
/**
* The module installer service.
*
* @var \Drupal\Core\Extension\ModuleInstallerInterface
*/
protected $moduleInstaller;
/**
* The expirable key value store.
*
......@@ -38,6 +46,7 @@ class ModulesUninstallForm extends FormBase {
public static function create(ContainerInterface $container) {
return new static(
$container->get('module_handler'),
$container->get('module_installer'),
$container->get('keyvalue.expirable')->get('modules_uninstall')
);
}
......@@ -47,11 +56,14 @@ public static function create(ContainerInterface $container) {
*
* @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler
* The module handler.
* @param \Drupal\Core\Extension\ModuleInstallerInterface $module_installer
* The module installer.
* @param \Drupal\Core\KeyValueStore\KeyValueStoreExpirableInterface $key_value_expirable
* The key value expirable factory.
*/
public function __construct(ModuleHandlerInterface $module_handler, KeyValueStoreExpirableInterface $key_value_expirable) {
public function __construct(ModuleHandlerInterface $module_handler, ModuleInstallerInterface $module_installer, KeyValueStoreExpirableInterface $key_value_expirable) {
$this->moduleHandler = $module_handler;
$this->moduleInstaller = $module_installer;
$this->keyValueExpirable = $key_value_expirable;
}
......@@ -69,7 +81,7 @@ public function buildForm(array $form, FormStateInterface $form_state) {
// Make sure the install API is available.
include_once DRUPAL_ROOT . '/core/includes/install.inc';
// Get a list of disabled, installed modules.
// Get a list of all available modules.
$modules = system_rebuild_module_data();
$uninstallable = array_filter($modules, function ($module) use ($modules) {
return empty($modules[$module->getName()]->info['required']) && drupal_get_installed_schema_version($module->getName()) > SCHEMA_UNINSTALLED;
......@@ -110,9 +122,10 @@ public function buildForm(array $form, FormStateInterface $form_state) {
// Sort all modules by their name.
uasort($uninstallable, 'system_sort_modules_by_info_name');
$validation_reasons = $this->moduleInstaller->validateUninstall(array_keys($uninstallable));
$form['uninstall'] = array('#tree' => TRUE);
foreach ($uninstallable as $module) {
foreach ($uninstallable as $module_key => $module) {
$name = $module->info['name'] ?: $module->getName();
$form['modules'][$module->getName()]['#module_name'] = $name;
$form['modules'][$module->getName()]['name']['#markup'] = $name;
......@@ -124,6 +137,12 @@ public function buildForm(array $form, FormStateInterface $form_state) {
'#title_display' => 'invisible',
);
// If a validator returns reasons not to uninstall a module,
// list the reasons and disable the check box.
if (isset($validation_reasons[$module_key])) {
$form['modules'][$module->getName()]['#validation_reasons'] = $validation_reasons[$module_key];
$form['uninstall'][$module->getName()]['#disabled'] = TRUE;
}
// All modules which depend on this one must be uninstalled first, before
// we can allow this module to be uninstalled. (The installation profile
// is excluded from this list.)
......
......@@ -9,7 +9,7 @@
use Drupal\Core\DependencyInjection\ContainerBuilder;
use Drupal\simpletest\KernelTestBase;
use Symfony\Component\HttpFoundation\Response;
use \Drupal\Core\Extension\ModuleUninstallValidatorException;
/**
* Tests ModuleHandler functionality.
......@@ -199,6 +199,60 @@ function testUninstallProfileDependency() {
$this->assertFalse(in_array($profile, $uninstalled_modules), 'The installation profile is not in the list of uninstalled modules.');
}
/**
* Tests uninstalling a module that has content.
*/
function testUninstallContentDependency() {
$this->enableModules(array('module_test', 'entity_test', 'text', 'user', 'help'));
$this->assertTrue($this->moduleHandler()->moduleExists('entity_test'), 'Test module is enabled.');
$this->assertTrue($this->moduleHandler()->moduleExists('module_test'), 'Test module is enabled.');
$this->installSchema('user', 'users_data');
$entity_types = \Drupal::entityManager()->getDefinitions();
foreach ($entity_types as $entity_type) {
if ('entity_test' == $entity_type->getProvider()) {
$this->installEntitySchema($entity_type->id());
}
}
// Create a fake dependency.
// entity_test will depend on help. This way help can not be uninstalled
// when there is test content preventing entity_test from being uninstalled.
\Drupal::state()->set('module_test.dependency', 'dependency');
drupal_static_reset('system_rebuild_module_data');
// Create an entity so that the modules can not be disabled.
$entity = entity_create('entity_test', array('name' => $this->randomString()));
$entity->save();
// Uninstalling entity_test is not possible when there is content.
try {
$message = 'ModuleHandler::uninstall() throws ModuleUninstallValidatorException upon uninstalling a module which does not pass validation.';
$this->moduleInstaller()->uninstall(array('entity_test'));
$this->fail($message);
}
catch (ModuleUninstallValidatorException $e) {
$this->pass(get_class($e) . ': ' . $e->getMessage());
}
// Uninstalling help needs entity_test to be un-installable.
try {
$message = 'ModuleHandler::uninstall() throws ModuleUninstallValidatorException upon uninstalling a module which does not pass validation.';
$this->moduleInstaller()->uninstall(array('help'));
$this->fail($message);
}
catch (ModuleUninstallValidatorException $e) {
$this->pass(get_class($e) . ': ' . $e->getMessage());
}
// Deleting the entity.
$entity->delete();
$result = $this->moduleInstaller()->uninstall(array('help'));
$this->assertTrue($result, 'ModuleHandler::uninstall() returns TRUE.');
$this->assertEqual(drupal_get_installed_schema_version('entity_test'), SCHEMA_UNINSTALLED, "entity_test module was uninstalled.");
}
/**
* Tests whether the correct module metadata is returned.
*/
......
......@@ -43,9 +43,22 @@ function testUserPermsUninstalled() {
function testUninstallPage() {
$account = $this->drupalCreateUser(array('administer modules'));
$this->drupalLogin($account);
// Create a node type.
$node_type = entity_create('node_type', array('type' => 'uninstall_blocker'));
$node_type->save();
// Add a node to prevent node from being uninstalled.
$node = entity_create('node', array('type' => 'uninstall_blocker'));
$node->save();
$this->drupalGet('admin/modules/uninstall');
$this->assertTitle(t('Uninstall') . ' | Drupal');
$this->assertText(\Drupal::translation()->translate('The following reasons prevents Node from being uninstalled: There is content for the entity type: Content'), 'Content prevents uninstalling node module.');
// Delete the node to allow node to be uninstalled.
$node->delete();
$node_type->delete();
// Uninstall module_test.
$edit = array();
$edit['uninstall[module_test]'] = TRUE;
......
......@@ -303,14 +303,16 @@ function theme_system_modules_uninstall($variables) {
// Display table.
$rows = array();
foreach (Element::children($form['modules']) as $module) {
$disabled_message = '';
// Add the modules requiring the module in question as a validation reason.
if (!empty($form['modules'][$module]['#required_by'])) {
$disabled_message = format_plural(count($form['modules'][$module]['#required_by']),
'To uninstall @module, the following module must be uninstalled first: @required_modules',
'To uninstall @module, the following modules must be uninstalled first: @required_modules',
array('@module' => $form['modules'][$module]['#module_name'], '@required_modules' => implode(', ', $form['modules'][$module]['#required_by'])));
$form['modules'][$module]['#validation_reasons'][] = \Drupal::translation()->translate('Required by: @modules', array('@modules' => implode(', ',$form['modules'][$module]['#required_by'])));
}
else {
$disabled_message = '';
if (!empty($form['modules'][$module]['#validation_reasons'])) {
$disabled_message = \Drupal::translation()->formatPlural(count($form['modules'][$module]['#validation_reasons']),
'The following reason prevents @module from being uninstalled: @reasons',
'The following reasons prevents @module from being uninstalled: @reasons',
array('@module' => $form['modules'][$module]['#module_name'], '@reasons' => implode('; ', $form['modules'][$module]['#validation_reasons'])));
}
$rows[] = array(
array('data' => drupal_render($form['uninstall'][$module]), 'align' => 'center'),
......
......@@ -27,6 +27,10 @@ function module_test_system_info_alter(&$info, Extension $file, $type) {
// Make config module depend on help module.
$info['dependencies'][] = 'help';
}
elseif ($file->getName() == 'entity_test') {
// Make entity test module depend on help module.
$info['dependencies'][] = 'help';
}
}
elseif (\Drupal::state()->get('module_test.dependency') == 'version dependency') {
if ($file->getName() == 'color') {
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment