diff --git a/core/modules/block/src/Tests/BlockConfigSchemaTest.php b/core/modules/block/src/Tests/BlockConfigSchemaTest.php index ccd0a73..2a70ecf 100644 --- a/core/modules/block/src/Tests/BlockConfigSchemaTest.php +++ b/core/modules/block/src/Tests/BlockConfigSchemaTest.php @@ -64,6 +64,7 @@ protected function setUp() { $this->installEntitySchema('block_content'); $this->installEntitySchema('taxonomy_term'); $this->installEntitySchema('node'); + $this->installSchema('book', array('book')); } /** diff --git a/core/modules/book/book.module b/core/modules/book/book.module index 7f7830c..9d3db19 100644 --- a/core/modules/book/book.module +++ b/core/modules/book/book.module @@ -19,6 +19,7 @@ use Drupal\Core\Language\LanguageInterface; use Drupal\Core\Entity\Display\EntityViewDisplayInterface; use Drupal\Core\Template\Attribute; +use Drupal\Core\Extension\Extension; /** * Implements hook_help(). @@ -572,3 +573,33 @@ function book_node_type_update(NodeTypeInterface $type) { $config->save(); } } + +/** + * Implements hook_system_info_alter(). + * + * Prevents book module from being uninstalled whilst any book nodes exist + * or there are any book outline stored. + */ +function book_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() == 'book') { + if (\Drupal::service('book.outline_storage')->hasBooks()) { + $info['required'] = TRUE; + $info['explanation'] = t('To uninstall Book, delete all content that is part of a book.'); + } + else { + // The book node type is provided by the Book module. Prevent uninstall if + // there are any node's of that type. + $factory = \Drupal::service('entity.query'); + $nodes = $factory->get('node') + ->condition('type', 'book') + ->accessCheck(FALSE) + ->range(0, 1) + ->execute(); + if (!empty($nodes)) { + $info['required'] = TRUE; + $info['explanation'] = t('To uninstall Book, delete all content that has the Book content type.'); + } + } + } +} diff --git a/core/modules/book/config/install/node.type.book.yml b/core/modules/book/config/install/node.type.book.yml index 2fb20c3..1a5cc16 100644 --- a/core/modules/book/config/install/node.type.book.yml +++ b/core/modules/book/config/install/node.type.book.yml @@ -1,11 +1,15 @@ langcode: en status: true -dependencies: { } +dependencies: + enforced: + module: + - book name: 'Book page' type: book description: 'Books have a built-in hierarchical navigation. Use for handbooks or tutorials.' help: '' new_revision: false +display_submitted: true preview_mode: 1 display_submitted: true third_party_settings: { } diff --git a/core/modules/book/src/BookOutlineStorage.php b/core/modules/book/src/BookOutlineStorage.php index cfc281a..8e8271d 100644 --- a/core/modules/book/src/BookOutlineStorage.php +++ b/core/modules/book/src/BookOutlineStorage.php @@ -38,6 +38,15 @@ public function getBooks() { /** * {@inheritdoc} */ + public function hasBooks() { + return (bool) $this->connection + ->query('SELECT count(bid) FROM {book}') + ->fetchField(); + } + + /** + * {@inheritdoc} + */ public function loadMultiple($nids) { $query = $this->connection->select('book', 'b', array('fetch' => \PDO::FETCH_ASSOC)); $query->fields('b'); diff --git a/core/modules/book/src/BookOutlineStorageInterface.php b/core/modules/book/src/BookOutlineStorageInterface.php index 8a27592..e08fab6 100644 --- a/core/modules/book/src/BookOutlineStorageInterface.php +++ b/core/modules/book/src/BookOutlineStorageInterface.php @@ -21,6 +21,14 @@ public function getBooks(); /** + * Checks if there are any books. + * + * @return bool + * TRUE if there are books, FALSE if not. + */ + public function hasBooks(); + + /** * Loads books. * * @param array $nids diff --git a/core/modules/book/src/Tests/BookUninstallTest.php b/core/modules/book/src/Tests/BookUninstallTest.php new file mode 100644 index 0000000..8a7a0c8 --- /dev/null +++ b/core/modules/book/src/Tests/BookUninstallTest.php @@ -0,0 +1,103 @@ +installEntitySchema('user'); + $this->installEntitySchema('node'); + $this->installSchema('book', array('book')); + $this->installSchema('node', array('node_access')); + $this->installConfig(array('node', 'book', 'field')); + // For uninstall to work. + $this->installSchema('user', array('users_data')); + } + + /** + * Tests the book_system_info_alter() method. + */ + public function testBookUninstall() { + + // No nodes + $module_data = _system_rebuild_module_data(); + $this->assertFalse(isset($module_data['book']->info['required']), 'The book module is not required.'); + + $content_type = entity_create('node_type', array( + 'type' => $this->randomMachineName(), + 'name' => $this->randomString(), + )); + $content_type->save(); + $book_config = \Drupal::config('book.settings'); + $allowed_types = $book_config->get('allowed_types'); + $allowed_types[] = $content_type->id(); + $book_config->set('allowed_types', $allowed_types)->save(); + + $node = Node::create(array('type' => $content_type->id())); + $node->book['bid'] = 'new'; + $node->save(); + + // One node in a book but not of type book. + $module_data = _system_rebuild_module_data(); + $this->assertTrue($module_data['book']->info['required'], 'The book module is required.'); + $this->assertEqual($module_data['book']->info['explanation'], t('To uninstall Book, delete all content that is part of a book.')); + + $book_node = Node::create(array('type' => 'book')); + $book_node->book['bid'] = FALSE; + $book_node->save(); + + // Two nodes, one in a book but not of type book and one book node (which is + // not in a book). + $module_data = _system_rebuild_module_data(); + $this->assertTrue($module_data['book']->info['required'], 'The book module is required.'); + $this->assertEqual($module_data['book']->info['explanation'], t('To uninstall Book, delete all content that is part of a book.')); + + $node->delete(); + // One node of type book but not actually part of a book. + $module_data = _system_rebuild_module_data(); + $this->assertTrue($module_data['book']->info['required'], 'The book module is not required.'); + $this->assertEqual($module_data['book']->info['explanation'], t('To uninstall Book, delete all content that has the Book content type.')); + + $book_node->delete(); + // No nodes exist therefore the book module is not required. + $module_data = _system_rebuild_module_data(); + $this->assertFalse(isset($module_data['book']->info['required']), 'The book module is not required.'); + + $node = Node::create(array('type' => $content_type->id())); + $node->save(); + // One node exists but is not part of a book therefore the book module is + // not required. + $module_data = _system_rebuild_module_data(); + $this->assertFalse(isset($module_data['book']->info['required']), 'The book module is not required.'); + + // Uninstall the Book module and check the node type is deleted. + \Drupal::moduleHandler()->uninstall(['book']); + $this->assertNull(NodeType::load('book'), "The book node type does not exist."); + } + +} diff --git a/core/modules/config/src/Tests/ConfigImportAllTest.php b/core/modules/config/src/Tests/ConfigImportAllTest.php index bc801c3..4900f98 100644 --- a/core/modules/config/src/Tests/ConfigImportAllTest.php +++ b/core/modules/config/src/Tests/ConfigImportAllTest.php @@ -110,6 +110,8 @@ public function testInstallUninstall() { // Import the configuration thereby re-installing all the modules. $this->drupalPostForm('admin/config/development/configuration', array(), t('Import all')); + // Modules have been installed that have services. + $this->rebuildContainer(); // Check that there are no errors. $this->assertIdentical($this->configImporter()->getErrors(), array()); diff --git a/core/modules/node/src/Tests/NodeTypeTest.php b/core/modules/node/src/Tests/NodeTypeTest.php index 23b692d..dde6bf9 100644 --- a/core/modules/node/src/Tests/NodeTypeTest.php +++ b/core/modules/node/src/Tests/NodeTypeTest.php @@ -123,35 +123,6 @@ function testNodeTypeEditing() { } /** - * Tests that node types correctly handles their locking. - */ - function testNodeTypeStatus() { - // Enable all core node modules, and all types should be active. - $this->container->get('module_handler')->install(array('book'), FALSE); - $types = node_type_get_types(); - foreach (array('book', 'article', 'page') as $type) { - $this->assertTrue(isset($types[$type]), format_string('%type is found in node types.', array('%type' => $type))); - $this->assertFalse($types[$type]->isLocked(), format_string('%type type is not locked.', array('%type' => $type))); - } - - // Disable book module and the respective type should still be active, since - // it is not provided by shipped configuration entity. - $this->container->get('module_handler')->uninstall(array('book'), FALSE); - $types = node_type_get_types(); - $this->assertFalse($types['book']->isLocked(), "Book module's node type still active."); - $this->assertFalse($types['article']->isLocked(), 'Article node type still active.'); - $this->assertFalse($types['page']->isLocked(), 'Basic page node type still active.'); - - // Re-install the modules and verify that the types are active again. - $this->container->get('module_handler')->install(array('book'), FALSE); - $types = node_type_get_types(); - foreach (array('book', 'article', 'page') as $type) { - $this->assertTrue(isset($types[$type]), format_string('%type is found in node types.', array('%type' => $type))); - $this->assertFalse($types[$type]->isLocked(), format_string('%type type is not locked.', array('%type' => $type))); - } - } - - /** * Tests deleting a content type that still has content. */ function testNodeTypeDeletion() {