diff --git a/core/modules/book/book.module b/core/modules/book/book.module
index d0aa9b0..4c62274 100644
--- a/core/modules/book/book.module
+++ b/core/modules/book/book.module
@@ -82,7 +82,8 @@ function book_entity_type_build(array &$entity_types) {
$entity_types['node']
->setFormClass('book_outline', 'Drupal\book\Form\BookOutlineForm')
->setLinkTemplate('book-outline-form', '/node/{node}/outline')
- ->setLinkTemplate('book-remove-form', '/node/{node}/outline/remove');
+ ->setLinkTemplate('book-remove-form', '/node/{node}/outline/remove')
+ ->addConstraint('BookOutline', []);
}
/**
diff --git a/core/modules/book/src/BookManager.php b/core/modules/book/src/BookManager.php
index 3e14c40..402776c 100644
--- a/core/modules/book/src/BookManager.php
+++ b/core/modules/book/src/BookManager.php
@@ -275,6 +275,14 @@ public function updateOutline(NodeInterface $node) {
// handle it here if it did not.
$node->book['pid'] = $node->book['bid'];
}
+
+ // Prevent changes to the book outline if the node being saved is not the
+ // default revision.
+ $updated = !$new && (($original = $this->loadBookLink($node->id(), FALSE)) && ($node->book['bid'] != $original['bid'] || $node->book['pid'] != $original['pid']));
+ if (($new || $updated) && !$node->isDefaultRevision()) {
+ return FALSE;
+ }
+
return $this->saveBookLink($node->book, $new);
}
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..24e3d15
--- /dev/null
+++ b/core/modules/book/tests/src/Functional/BookContentModerationTest.php
@@ -0,0 +1,88 @@
+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('This is not the default revision. 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('This is not the default revision. 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]);
+
+ // Save a new draft revision for the node without any changes and check that
+ // the error message is not displayed.
+ $this->drupalPostForm('node/' . $book_1_nodes[1]->id() . '/edit', [], t('Save and Create New Draft'));
+
+ $this->assertSession()->pageTextNotContains('This is not the default revision. You can only change the book outline for the published version of this content.');
+ }
+
+}
diff --git a/core/modules/book/tests/src/Functional/BookTest.php b/core/modules/book/tests/src/Functional/BookTest.php
index e52e484..9aa04f1 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 '/