diff --git a/core/modules/book/src/BookManager.php b/core/modules/book/src/BookManager.php index 3e14c40..eb5b582 100644 --- a/core/modules/book/src/BookManager.php +++ b/core/modules/book/src/BookManager.php @@ -249,6 +249,13 @@ public function updateOutline(NodeInterface $node) { return FALSE; } + // Prevent changes to the book outline if the node being saved is not the + // default revision. + if (!$node->isDefaultRevision()) { + drupal_set_message($this->t('You can only change the book outline for the published version of this content.'), 'error'); + return FALSE; + } + if (!empty($node->book['bid'])) { if ($node->book['bid'] == 'new') { // New nodes that are their own book. diff --git a/core/modules/book/tests/src/Functional/BookContentModerationTest.php b/core/modules/book/tests/src/Functional/BookContentModerationTest.php new file mode 100644 index 0000000..0b9c831 --- /dev/null +++ b/core/modules/book/tests/src/Functional/BookContentModerationTest.php @@ -0,0 +1,83 @@ +drupalPlaceBlock('system_breadcrumb_block'); + $this->drupalPlaceBlock('page_title_block'); + + $workflow = Workflow::load('editorial'); + $workflow->getTypePlugin()->addEntityTypeAndBundle('node', 'book'); + $workflow->save(); + + // We need a user with additional content moderation permissions. + $this->bookAuthor = $this->drupalCreateUser(['create new books', 'create book content', 'edit own book content', 'add content to books', 'access printer-friendly version', 'view any unpublished content', 'use editorial transition create_new_draft', 'use editorial transition publish']); + } + + /** + * Tests that book drafts can not modify the book outline. + */ + public function testBookWithForwardRevisions() { + // Create two books. + $book_1_nodes = $this->createBook(t('Save and Publish')); + $book_1 = $this->book; + + $this->createBook(t('Save and Publish')); + $book_2 = $this->book; + + $this->drupalLogin($this->bookAuthor); + + // Check that book pages display along with the correct outlines. + $this->book = $book_1; + $this->checkBookNode($book_1, [$book_1_nodes[0], $book_1_nodes[3], $book_1_nodes[4]], FALSE, FALSE, $book_1_nodes[0], []); + $this->checkBookNode($book_1_nodes[0], [$book_1_nodes[1], $book_1_nodes[2]], $book_1, $book_1, $book_1_nodes[1], [$book_1]); + + // Try to move Node 2 to a different parent. + $edit['book[pid]'] = $book_1_nodes[3]->id(); + $this->drupalPostForm('node/' . $book_1_nodes[1]->id() . '/edit', $edit, t('Save and Create New Draft')); + + $this->assertSession()->pageTextContains('You can only change the book outline for the published version of this content.'); + + // Check that the book outline did not change. + $this->book = $book_1; + $this->checkBookNode($book_1, [$book_1_nodes[0], $book_1_nodes[3], $book_1_nodes[4]], FALSE, FALSE, $book_1_nodes[0], []); + $this->checkBookNode($book_1_nodes[0], [$book_1_nodes[1], $book_1_nodes[2]], $book_1, $book_1, $book_1_nodes[1], [$book_1]); + + // Try to move Node 2 to a different book. + $edit['book[bid]'] = $book_2->id(); + $this->drupalPostForm('node/' . $book_1_nodes[1]->id() . '/edit', $edit, t('Save and Create New Draft')); + + $this->assertSession()->pageTextContains('You can only change the book outline for the published version of this content.'); + + // Check that the book outline did not change. + $this->book = $book_1; + $this->checkBookNode($book_1, [$book_1_nodes[0], $book_1_nodes[3], $book_1_nodes[4]], FALSE, FALSE, $book_1_nodes[0], []); + $this->checkBookNode($book_1_nodes[0], [$book_1_nodes[1], $book_1_nodes[2]], $book_1, $book_1, $book_1_nodes[1], [$book_1]); + + } + +} diff --git a/core/modules/book/tests/src/Functional/BookTest.php b/core/modules/book/tests/src/Functional/BookTest.php index 66edec9..640859f 100644 --- a/core/modules/book/tests/src/Functional/BookTest.php +++ b/core/modules/book/tests/src/Functional/BookTest.php @@ -2,9 +2,7 @@ namespace Drupal\Tests\book\Functional; -use Drupal\Component\Utility\SafeMarkup; use Drupal\Core\Cache\Cache; -use Drupal\Core\Entity\EntityInterface; use Drupal\Tests\BrowserTestBase; use Drupal\user\RoleInterface; @@ -15,6 +13,8 @@ */ class BookTest extends BrowserTestBase { + use BookTestTrait; + /** * Modules to install. * @@ -23,20 +23,6 @@ class BookTest extends BrowserTestBase { public static $modules = ['book', 'block', 'node_access_test', 'book_test']; /** - * A book node. - * - * @var \Drupal\node\NodeInterface - */ - protected $book; - - /** - * A user with permission to create and edit books. - * - * @var object - */ - protected $bookAuthor; - - /** * A user with permission to view a book and access printer-friendly version. * * @var object @@ -76,39 +62,6 @@ protected function setUp() { } /** - * Creates a new book with a page hierarchy. - * - * @return \Drupal\node\NodeInterface[] - */ - public function createBook() { - // Create new book. - $this->drupalLogin($this->bookAuthor); - - $this->book = $this->createBookNode('new'); - $book = $this->book; - - /* - * Add page hierarchy to book. - * Book - * |- Node 0 - * |- Node 1 - * |- Node 2 - * |- Node 3 - * |- Node 4 - */ - $nodes = []; - $nodes[] = $this->createBookNode($book->id()); // Node 0. - $nodes[] = $this->createBookNode($book->id(), $nodes[0]->book['nid']); // Node 1. - $nodes[] = $this->createBookNode($book->id(), $nodes[0]->book['nid']); // Node 2. - $nodes[] = $this->createBookNode($book->id()); // Node 3. - $nodes[] = $this->createBookNode($book->id()); // Node 4. - - $this->drupalLogout(); - - return $nodes; - } - - /** * Tests the book navigation cache context. * * @see \Drupal\book\Cache\BookNavigationCacheContext @@ -230,147 +183,6 @@ public function testBook() { } /** - * Checks the outline of sub-pages; previous, up, and next. - * - * Also checks the printer friendly version of the outline. - * - * @param \Drupal\Core\Entity\EntityInterface $node - * Node to check. - * @param $nodes - * Nodes that should be in outline. - * @param $previous - * (optional) Previous link node. Defaults to FALSE. - * @param $up - * (optional) Up link node. Defaults to FALSE. - * @param $next - * (optional) Next link node. Defaults to FALSE. - * @param array $breadcrumb - * The nodes that should be displayed in the breadcrumb. - */ - public function checkBookNode(EntityInterface $node, $nodes, $previous = FALSE, $up = FALSE, $next = FALSE, array $breadcrumb) { - // $number does not use drupal_static as it should not be reset - // since it uniquely identifies each call to checkBookNode(). - static $number = 0; - $this->drupalGet('node/' . $node->id()); - - // Check outline structure. - if ($nodes !== NULL) { - $this->assertPattern($this->generateOutlinePattern($nodes), format_string('Node @number outline confirmed.', ['@number' => $number])); - } - else { - $this->pass(format_string('Node %number does not have outline.', ['%number' => $number])); - } - - // Check previous, up, and next links. - if ($previous) { - /** @var \Drupal\Core\Url $url */ - $url = $previous->urlInfo(); - $url->setOptions(['attributes' => ['rel' => ['prev'], 'title' => t('Go to previous page')]]); - $text = SafeMarkup::format(' @label', ['@label' => $previous->label()]); - $this->assertRaw(\Drupal::l($text, $url), 'Previous page link found.'); - } - - if ($up) { - /** @var \Drupal\Core\Url $url */ - $url = $up->urlInfo(); - $url->setOptions(['attributes' => ['title' => t('Go to parent page')]]); - $this->assertRaw(\Drupal::l('Up', $url), 'Up page link found.'); - } - - if ($next) { - /** @var \Drupal\Core\Url $url */ - $url = $next->urlInfo(); - $url->setOptions(['attributes' => ['rel' => ['next'], 'title' => t('Go to next page')]]); - $text = SafeMarkup::format('@label ', ['@label' => $next->label()]); - $this->assertRaw(\Drupal::l($text, $url), 'Next page link found.'); - } - - // Compute the expected breadcrumb. - $expected_breadcrumb = []; - $expected_breadcrumb[] = \Drupal::url(''); - foreach ($breadcrumb as $a_node) { - $expected_breadcrumb[] = $a_node->url(); - } - - // Fetch links in the current breadcrumb. - $links = $this->xpath('//nav[@class="breadcrumb"]/ol/li/a'); - $got_breadcrumb = []; - foreach ($links as $link) { - $got_breadcrumb[] = $link->getAttribute('href'); - } - - // Compare expected and got breadcrumbs. - $this->assertIdentical($expected_breadcrumb, $got_breadcrumb, 'The breadcrumb is correctly displayed on the page.'); - - // Check printer friendly version. - $this->drupalGet('book/export/html/' . $node->id()); - $this->assertText($node->label(), 'Printer friendly title found.'); - $this->assertRaw($node->body->processed, 'Printer friendly body found.'); - - $number++; - } - - /** - * Creates a regular expression to check for the sub-nodes in the outline. - * - * @param array $nodes - * An array of nodes to check in outline. - * - * @return string - * A regular expression that locates sub-nodes of the outline. - */ - public function generateOutlinePattern($nodes) { - $outline = ''; - foreach ($nodes as $node) { - $outline .= '(node\/' . $node->id() . ')(.*?)(' . $node->label() . ')(.*?)'; - } - - return '/