diff --git a/core/config/schema/core.data_types.schema.yml b/core/config/schema/core.data_types.schema.yml index c695934..676b6ba 100644 --- a/core/config/schema/core.data_types.schema.yml +++ b/core/config/schema/core.data_types.schema.yml @@ -208,9 +208,8 @@ route: label: 'Param' # Config dependencies. -config_dependencies: +config_dependencies_calculated: type: mapping - label: 'Configuration dependencies' mapping: entity: type: sequence @@ -228,6 +227,14 @@ config_dependencies: sequence: - type: string +config_dependencies: + type: config_dependencies_calculated + label: 'Configuration dependencies' + mapping: + fixed: + type: config_dependencies_calculated + label: 'Fixed configuration dependencies' + config_entity: type: mapping mapping: diff --git a/core/lib/Drupal/Core/Config/Entity/ConfigEntityBase.php b/core/lib/Drupal/Core/Config/Entity/ConfigEntityBase.php index d226243..44ef9f2 100644 --- a/core/lib/Drupal/Core/Config/Entity/ConfigEntityBase.php +++ b/core/lib/Drupal/Core/Config/Entity/ConfigEntityBase.php @@ -316,7 +316,14 @@ public function preSave(EntityStorageInterface $storage) { public function calculateDependencies() { // Dependencies should be recalculated on every save. This ensures stale // dependencies are never saved. - $this->dependencies = array(); + if (isset($this->dependencies['fixed'])) { + $dependencies = $this->dependencies['fixed']; + $this->dependencies = $dependencies; + $this->dependencies['fixed'] = $dependencies; + } + else { + $this->dependencies = array(); + } if ($this instanceof EntityWithPluginBagsInterface) { // Configuration entities need to depend on the providers of any plugins // that they store the configuration for. diff --git a/core/modules/block/src/Tests/BlockConfigSchemaTest.php b/core/modules/block/src/Tests/BlockConfigSchemaTest.php index 47e2421..ccd0a73 100644 --- a/core/modules/block/src/Tests/BlockConfigSchemaTest.php +++ b/core/modules/block/src/Tests/BlockConfigSchemaTest.php @@ -36,6 +36,7 @@ class BlockConfigSchemaTest extends KernelTestBase { 'system', 'taxonomy', 'user', + 'text', ); /** @@ -61,6 +62,8 @@ protected function setUp() { $this->typedConfig = \Drupal::service('config.typed'); $this->blockManager = \Drupal::service('plugin.manager.block'); $this->installEntitySchema('block_content'); + $this->installEntitySchema('taxonomy_term'); + $this->installEntitySchema('node'); } /** diff --git a/core/modules/comment/src/Tests/CommentFieldsTest.php b/core/modules/comment/src/Tests/CommentFieldsTest.php index a0cb95d..017f3fb 100644 --- a/core/modules/comment/src/Tests/CommentFieldsTest.php +++ b/core/modules/comment/src/Tests/CommentFieldsTest.php @@ -63,6 +63,29 @@ function testCommentDefaultFields() { } /** + * Tests that you can remove a comment field. + */ + public function testCommentFieldDelete() { + $this->drupalCreateContentType(array('type' => 'test_node_type')); + $this->container->get('comment.manager')->addDefaultField('node', 'test_node_type'); + // We want to test the handling of removing the primary comment field, so we + // ensure there is at least one other comment field attached to a node type + // so that comment_entity_load() runs for nodes. + $this->container->get('comment.manager')->addDefaultField('node', 'test_node_type', 'comment2'); + + // Create a sample node. + $node = $this->drupalCreateNode(array( + 'title' => 'Baloney', + 'type' => 'test_node_type', + )); + + // Delete the first comment field. + FieldStorageConfig::loadByName('node', 'comment')->delete(); + $this->drupalGet('node/' . $node->nid->value); + $this->assertResponse(200); + } + + /** * Tests that comment module works when installed after a content module. */ function testCommentInstallAfterContentModule() { diff --git a/core/modules/config/src/Tests/ConfigImportAllTest.php b/core/modules/config/src/Tests/ConfigImportAllTest.php index 7b73631..bc801c3 100644 --- a/core/modules/config/src/Tests/ConfigImportAllTest.php +++ b/core/modules/config/src/Tests/ConfigImportAllTest.php @@ -76,8 +76,16 @@ public function testInstallUninstall() { // Purge the data. field_purge_batch(1000); + // Delete any forum terms so it can be uninstalled. + $vid = \Drupal::config('forum.settings')->get('vocabulary'); + $terms = entity_load_multiple_by_properties('taxonomy_term', ['vid' => $vid]); + foreach ($terms as $term) { + $term->delete(); + } + system_list_reset(); $all_modules = system_rebuild_module_data(); + $modules_to_uninstall = array_filter($all_modules, function ($module) { // Filter required and not enabled modules. if (!empty($module->info['required']) || $module->status == FALSE) { diff --git a/core/modules/forum/config/install/node.type.forum.yml b/core/modules/forum/config/install/node.type.forum.yml index f7833f7..a3dccc4 100644 --- a/core/modules/forum/config/install/node.type.forum.yml +++ b/core/modules/forum/config/install/node.type.forum.yml @@ -1,9 +1,13 @@ -type: forum +langcode: en +status: true +dependencies: + fixed: + module: + - forum name: 'Forum topic' +type: forum description: 'A forum topic starts a new discussion thread within a forum.' help: '' new_revision: false -display_submitted: true preview_mode: 1 -status: true -langcode: en +display_submitted: true diff --git a/core/modules/forum/forum.module b/core/modules/forum/forum.module index b744c0f..6f942c5 100644 --- a/core/modules/forum/forum.module +++ b/core/modules/forum/forum.module @@ -11,8 +11,11 @@ use Drupal\Core\Entity\EntityInterface; use Drupal\Core\Url; use Drupal\Component\Utility\String; +use Drupal\Core\Extension\Extension; use Drupal\Core\Form\FormStateInterface; use Drupal\Core\Routing\RouteMatchInterface; +use Drupal\taxonomy\Entity\Vocabulary; +use Symfony\Component\Routing\Exception\RouteNotFoundException; /** * Implements hook_help(). @@ -690,3 +693,63 @@ function template_preprocess_forum_submitted(&$variables) { } $variables['time'] = isset($variables['topic']->created) ? \Drupal::service('date.formatter')->formatInterval(REQUEST_TIME - $variables['topic']->created) : ''; } + +/** + * Implements hook_system_info_alter(). + * + * Prevents forum module from being uninstalled whilst any forum nodes exist + * or there are any terms in the forum vocabulary. + */ +function forum_system_info_alter(&$info, Extension $file, $type) { + // It is not safe use the entity query service during maintenance mode. + if ($type == 'module' && !defined('MAINTENANCE_MODE') && $file->getName() == 'forum') { + $factory = \Drupal::service('entity.query'); + $nodes = $factory->get('node') + ->condition('type', 'forum') + ->accessCheck(FALSE) + ->range(0, 1) + ->execute(); + if (!empty($nodes)) { + $info['required'] = TRUE; + $info['explanation'] = t('To uninstall Forum first delete all Forum content.'); + } + $vid = \Drupal::config('forum.settings')->get('vocabulary'); + $terms = $factory->get('taxonomy_term') + ->condition('vid', $vid) + ->accessCheck(FALSE) + ->range(0, 1) + ->execute(); + if (!empty($terms)) { + $vocabulary = Vocabulary::load($vid); + $info['required'] = TRUE; + try { + if (!empty($info['explanation'])) { + if ($vocabulary->access('view')) { + $info['explanation'] = t('To uninstall Forum first delete all Forum content and %vocabulary terms.', [ + '%vocabulary' => $vocabulary->label(), + '!url' => $vocabulary->url('overview-form'), + ]); + } + else { + $info['explanation'] = t('To uninstall Forum first delete all Forum content and %vocabulary terms.', [ + '%vocabulary' => $vocabulary->label() + ]); + } + } + else { + $info['explanation'] = t('To uninstall Forum first delete all %vocabulary terms.', [ + '%vocabulary' => $vocabulary->label(), + '!url' => $vocabulary->url('overview-form'), + ]); + } + } + catch (RouteNotFoundException $e) { + // Route rebuilding might not have occurred before this hook is fired. + // Just use an explanation without a link for the time being. + $info['explanation'] = t('To uninstall Forum first delete all Forum content and %vocabulary terms.', [ + '%vocabulary' => $vocabulary->label() + ]); + } + } + } +} diff --git a/core/modules/forum/src/Tests/ForumUninstallTest.php b/core/modules/forum/src/Tests/ForumUninstallTest.php index 388910c..2c2e89e 100644 --- a/core/modules/forum/src/Tests/ForumUninstallTest.php +++ b/core/modules/forum/src/Tests/ForumUninstallTest.php @@ -9,7 +9,12 @@ use Drupal\comment\CommentInterface; use Drupal\comment\Plugin\Field\FieldType\CommentItemInterface; +use Drupal\Component\Utility\String; +use Drupal\Core\DrupalKernel; +use Drupal\Core\Session\UserSession; +use Drupal\Core\Site\Settings; use Drupal\field\Entity\FieldStorageConfig; +use Drupal\node\Entity\NodeType; use Drupal\simpletest\WebTestBase; /** @@ -29,7 +34,8 @@ class ForumUninstallTest extends WebTestBase { /** * Tests if forum module uninstallation properly deletes the field. */ - function testForumUninstallWithField() { + public function testForumUninstallWithField() { + $this->drupalLogin($this->drupalCreateUser(['administer taxonomy', 'administer nodes', 'administer modules', 'delete any forum content', 'administer content types'])); // Ensure that the field exists before uninstallation. $field_storage = FieldStorageConfig::loadByName('node', 'taxonomy_forums'); $this->assertNotNull($field_storage, 'The taxonomy_forums field storage exists.'); @@ -65,27 +71,69 @@ function testForumUninstallWithField() { )); $comment->save(); - // Uninstall the forum module which should trigger field deletion. - $this->container->get('module_handler')->uninstall(array('forum')); - - // We want to test the handling of removing the forum comment field, so we - // ensure there is at least one other comment field attached to a node type - // so that comment_entity_load() runs for nodes. - \Drupal::service('comment.manager')->addDefaultField('node', 'forum', 'another_comment_field', CommentItemInterface::OPEN, 'another_comment_field'); - - $this->drupalGet('node/' . $node->nid->value); - $this->assertResponse(200); + // Attempt to uninstall forum. + $this->drupalGet('admin/modules/uninstall'); + // Assert forum is required. + $this->assertNoFieldByName('uninstall[forum]'); + $this->drupalGet('admin/modules'); + $this->assertText('To uninstall Forum first delete all Forum content'); + + // Delete the node. + $this->drupalPostForm('node/' . $node->id() . '/delete', array(), t('Delete')); + + // Attempt to uninstall forum. + $this->drupalGet('admin/modules/uninstall'); + // Assert forum is still required. + $this->assertNoFieldByName('uninstall[forum]'); + $this->drupalGet('admin/modules'); + $this->assertText('To uninstall Forum first delete all Forums terms'); + + // Delete any forum terms. + $vid = \Drupal::config('forum.settings')->get('vocabulary'); + $terms = entity_load_multiple_by_properties('taxonomy_term', ['vid' => $vid]); + foreach ($terms as $term) { + $term->delete(); + } + + // Ensure that the forum node type can not be deleted. + $this->drupalGet('admin/structure/types/manage/forum'); + $this->assertNoLink(t('Delete')); + + // Now attempt to uninstall forum. + $this->drupalGet('admin/modules/uninstall'); + // Assert forum is no longer required. + $this->assertFieldByName('uninstall[forum]'); + $this->drupalGet('admin/modules'); + $this->assertNoText('To uninstall Forum first delete all Forum content'); + $this->drupalPostForm('admin/modules/uninstall', array( + 'uninstall[forum]' => 1, + ), t('Uninstall')); + $this->drupalPostForm(NULL, [], t('Uninstall')); // Check that the field is now deleted. $field_storage = FieldStorageConfig::loadByName('node', 'taxonomy_forums'); $this->assertNull($field_storage, 'The taxonomy_forums field storage has been deleted.'); - } + // Check that a node type with a machine name of forum can be created after + // uninstalling the forum module and the node type is not locked. + $edit = array( + 'name' => 'Forum', + 'title_label' => 'title for forum', + 'type' => 'forum', + ); + $this->drupalPostForm('admin/structure/types/add', $edit, t('Save content type')); + $this->assertTrue((bool) NodeType::load('forum'), 'Node type with machine forum created.'); + $this->drupalGet('admin/structure/types/manage/forum'); + $this->clickLink(t('Delete')); + $this->drupalPostForm(NULL, array(), t('Delete')); + $this->assertResponse(200); + $this->assertFalse((bool) NodeType::load('forum'), 'Node type with machine forum deleted.'); + } /** * Tests uninstallation if the field storage has been deleted beforehand. */ - function testForumUninstallWithoutFieldStorage() { + public function testForumUninstallWithoutFieldStorage() { // Manually delete the taxonomy_forums field before module uninstallation. $field_storage = FieldStorageConfig::loadByName('node', 'taxonomy_forums'); $this->assertNotNull($field_storage, 'The taxonomy_forums field storage exists.'); diff --git a/core/modules/node/src/Tests/NodeTypePersistenceTest.php b/core/modules/node/src/Tests/NodeTypePersistenceTest.php deleted file mode 100644 index 62c2783..0000000 --- a/core/modules/node/src/Tests/NodeTypePersistenceTest.php +++ /dev/null @@ -1,62 +0,0 @@ -drupalCreateUser(array('bypass node access', 'administer content types', 'administer modules')); - $this->drupalLogin($web_user); - $forum_key = 'modules[Core][forum][enable]'; - $forum_enable = array($forum_key => "1"); - - // Enable forum and verify that the node type exists and is not disabled. - $this->drupalPostForm('admin/modules', $forum_enable, t('Save configuration')); - $forum = entity_load('node_type', 'forum'); - $this->assertTrue($forum->id(), 'Forum node type found.'); - $this->assertTrue($forum->isLocked(), 'Forum node type is locked'); - - // Check that forum node type (uncustomized) shows up. - $this->drupalGet('node/add'); - $this->assertText('forum', 'forum type is found on node/add'); - - // Customize forum description. - $description = $this->randomMachineName(); - $edit = array('description' => $description); - $this->drupalPostForm('admin/structure/types/manage/forum', $edit, t('Save content type')); - - // Check that forum node type customization shows up. - $this->drupalGet('node/add'); - $this->assertText($description, 'Customized description found'); - - // Uninstall forum. - $edit = array('uninstall[forum]' => 'forum'); - $this->drupalPostForm('admin/modules/uninstall', $edit, t('Uninstall')); - $this->drupalPostForm(NULL, array(), t('Uninstall')); - $forum = entity_load('node_type', 'forum'); - $this->assertFalse($forum->isLocked(), 'Forum node type is not locked'); - $this->drupalGet('node/add'); - $this->assertNoText('forum', 'forum type is no longer found on node/add'); - - // Reenable forum and check that the customization survived the module - // uninstall. - $this->drupalPostForm('admin/modules', $forum_enable, t('Save configuration')); - $this->drupalGet('node/add'); - $this->assertText($description, 'Customized description is found even after uninstall and reenable.'); - } - -} diff --git a/core/modules/node/src/Tests/NodeTypeTest.php b/core/modules/node/src/Tests/NodeTypeTest.php index b0bb658..7b7c50a 100644 --- a/core/modules/node/src/Tests/NodeTypeTest.php +++ b/core/modules/node/src/Tests/NodeTypeTest.php @@ -7,6 +7,7 @@ namespace Drupal\node\Tests; use Drupal\field\Entity\FieldConfig; +use Drupal\node\Entity\NodeType; /** * Ensures that node type functions work correctly. @@ -183,20 +184,28 @@ function testNodeTypeDeletion() { 'The content type is available for deletion.' ); $this->assertText(t('This action cannot be undone.'), 'The node type deletion confirmation form is available.'); - // Test that forum node type could not be deleted while forum active. - $this->container->get('module_handler')->install(array('forum')); + + // Test that a locked node type could not be deleted. + $this->container->get('module_handler')->install(array('node_test_config')); + // Lock the default node type. + $locked = \Drupal::state()->get('node.type.locked'); + $locked['default'] = 'default'; + \Drupal::state()->set('node.type.locked', $locked); // Call to flush all caches after installing the forum module in the same // way installing a module through the UI does. $this->resetAll(); - $this->drupalGet('admin/structure/types/manage/forum'); + $this->drupalGet('admin/structure/types/manage/default'); $this->assertNoLink(t('Delete')); - $this->drupalGet('admin/structure/types/manage/forum/delete'); + $this->drupalGet('admin/structure/types/manage/default/delete'); $this->assertResponse(403); - $this->container->get('module_handler')->uninstall(array('forum')); - $this->drupalGet('admin/structure/types/manage/forum'); - $this->assertLink(t('Delete')); - $this->drupalGet('admin/structure/types/manage/forum/delete'); + $this->container->get('module_handler')->uninstall(array('node_test_config')); + unset($locked['default']); + \Drupal::state()->set('node.type.locked', $locked); + $this->drupalGet('admin/structure/types/manage/default'); + $this->clickLink(t('Delete')); $this->assertResponse(200); + $this->drupalPostForm(NULL, array(), t('Delete')); + $this->assertFalse((bool) NodeType::load('default'), 'Node type with machine default deleted.'); } /** diff --git a/core/modules/system/src/Tests/Module/DependencyTest.php b/core/modules/system/src/Tests/Module/DependencyTest.php index 6ef1fb9..710341c 100644 --- a/core/modules/system/src/Tests/Module/DependencyTest.php +++ b/core/modules/system/src/Tests/Module/DependencyTest.php @@ -154,6 +154,15 @@ function testUninstallDependents() { $checkbox = $this->xpath('//input[@type="checkbox" and @name="uninstall[comment]"]'); $this->assert(count($checkbox) == 0, 'Checkbox for uninstalling the comment module not found.'); + // Delete any forum terms. + $vid = \Drupal::config('forum.settings')->get('vocabulary'); + // Ensure taxonomy has been loaded into the test-runner after forum was + // enabled. + \Drupal::moduleHandler()->load('taxonomy'); + $terms = entity_load_multiple_by_properties('taxonomy_term', ['vid' => $vid]); + foreach ($terms as $term) { + $term->delete(); + } // Uninstall the forum module, and check that taxonomy now can also be // uninstalled. $edit = array('uninstall[forum]' => 'forum'); diff --git a/core/modules/system/src/Tests/Module/InstallUninstallTest.php b/core/modules/system/src/Tests/Module/InstallUninstallTest.php index c826d1c..4005ee1 100644 --- a/core/modules/system/src/Tests/Module/InstallUninstallTest.php +++ b/core/modules/system/src/Tests/Module/InstallUninstallTest.php @@ -14,7 +14,7 @@ */ class InstallUninstallTest extends ModuleTestBase { - public static $modules = array('system_test', 'dblog'); + public static $modules = array('system_test', 'dblog', 'taxonomy'); /** * Tests that a fixed set of modules can be installed and uninstalled. @@ -151,6 +151,15 @@ public function testInstallUninstall() { */ protected function assertSuccessfullUninstall($module, $package = 'Core') { $edit = array(); + if ($module == 'forum') { + // Forum cannot be uninstalled until all of the content entities related + // to it have been deleted. + $vid = \Drupal::config('forum.settings')->get('vocabulary'); + $terms = entity_load_multiple_by_properties('taxonomy_term', ['vid' => $vid]); + foreach ($terms as $term) { + $term->delete(); + } + } $edit['uninstall[' . $module . ']'] = TRUE; $this->drupalPostForm('admin/modules/uninstall', $edit, t('Uninstall')); $this->drupalPostForm(NULL, NULL, t('Uninstall'));