d02dfd50d9c3f1fddd4deca01beda884b5726141
[yaffs-website] / web / core / modules / book / tests / src / Functional / BookTest.php
1 <?php
2
3 namespace Drupal\Tests\book\Functional;
4
5 use Drupal\Core\Cache\Cache;
6 use Drupal\Tests\BrowserTestBase;
7 use Drupal\user\RoleInterface;
8
9 /**
10  * Create a book, add pages, and test book interface.
11  *
12  * @group book
13  */
14 class BookTest extends BrowserTestBase {
15
16   use BookTestTrait;
17
18   /**
19    * Modules to install.
20    *
21    * @var array
22    */
23   public static $modules = ['book', 'block', 'node_access_test', 'book_test'];
24
25   /**
26    * A user with permission to view a book and access printer-friendly version.
27    *
28    * @var object
29    */
30   protected $webUser;
31
32   /**
33    * A user with permission to create and edit books and to administer blocks.
34    *
35    * @var object
36    */
37   protected $adminUser;
38
39   /**
40    * A user without the 'node test view' permission.
41    *
42    * @var \Drupal\user\UserInterface
43    */
44   protected $webUserWithoutNodeAccess;
45
46   /**
47    * {@inheritdoc}
48    */
49   protected function setUp() {
50     parent::setUp();
51     $this->drupalPlaceBlock('system_breadcrumb_block');
52     $this->drupalPlaceBlock('page_title_block');
53
54     // node_access_test requires a node_access_rebuild().
55     node_access_rebuild();
56
57     // Create users.
58     $this->bookAuthor = $this->drupalCreateUser(['create new books', 'create book content', 'edit own book content', 'add content to books']);
59     $this->webUser = $this->drupalCreateUser(['access printer-friendly version', 'node test view']);
60     $this->webUserWithoutNodeAccess = $this->drupalCreateUser(['access printer-friendly version']);
61     $this->adminUser = $this->drupalCreateUser(['create new books', 'create book content', 'edit any book content', 'delete any book content', 'add content to books', 'administer blocks', 'administer permissions', 'administer book outlines', 'node test view', 'administer content types', 'administer site configuration']);
62   }
63
64   /**
65    * Tests the book navigation cache context.
66    *
67    * @see \Drupal\book\Cache\BookNavigationCacheContext
68    */
69   public function testBookNavigationCacheContext() {
70     // Create a page node.
71     $this->drupalCreateContentType(['type' => 'page']);
72     $page = $this->drupalCreateNode();
73
74     // Create a book, consisting of book nodes.
75     $book_nodes = $this->createBook();
76
77     // Enable the debug output.
78     \Drupal::state()->set('book_test.debug_book_navigation_cache_context', TRUE);
79     Cache::invalidateTags(['book_test.debug_book_navigation_cache_context']);
80
81     $this->drupalLogin($this->bookAuthor);
82
83     // On non-node route.
84     $this->drupalGet($this->adminUser->urlInfo());
85     $this->assertRaw('[route.book_navigation]=book.none');
86
87     // On non-book node route.
88     $this->drupalGet($page->urlInfo());
89     $this->assertRaw('[route.book_navigation]=book.none');
90
91     // On book node route.
92     $this->drupalGet($book_nodes[0]->urlInfo());
93     $this->assertRaw('[route.book_navigation]=0|2|3');
94     $this->drupalGet($book_nodes[1]->urlInfo());
95     $this->assertRaw('[route.book_navigation]=0|2|3|4');
96     $this->drupalGet($book_nodes[2]->urlInfo());
97     $this->assertRaw('[route.book_navigation]=0|2|3|5');
98     $this->drupalGet($book_nodes[3]->urlInfo());
99     $this->assertRaw('[route.book_navigation]=0|2|6');
100     $this->drupalGet($book_nodes[4]->urlInfo());
101     $this->assertRaw('[route.book_navigation]=0|2|7');
102   }
103
104   /**
105    * Tests saving the book outline on an empty book.
106    */
107   public function testEmptyBook() {
108     // Create a new empty book.
109     $this->drupalLogin($this->bookAuthor);
110     $book = $this->createBookNode('new');
111     $this->drupalLogout();
112
113     // Log in as a user with access to the book outline and save the form.
114     $this->drupalLogin($this->adminUser);
115     $this->drupalPostForm('admin/structure/book/' . $book->id(), [], t('Save book pages'));
116     $this->assertText(t('Updated book @book.', ['@book' => $book->label()]));
117   }
118
119   /**
120    * Tests book functionality through node interfaces.
121    */
122   public function testBook() {
123     // Create new book.
124     $nodes = $this->createBook();
125     $book = $this->book;
126
127     $this->drupalLogin($this->webUser);
128
129     // Check that book pages display along with the correct outlines and
130     // previous/next links.
131     $this->checkBookNode($book, [$nodes[0], $nodes[3], $nodes[4]], FALSE, FALSE, $nodes[0], []);
132     $this->checkBookNode($nodes[0], [$nodes[1], $nodes[2]], $book, $book, $nodes[1], [$book]);
133     $this->checkBookNode($nodes[1], NULL, $nodes[0], $nodes[0], $nodes[2], [$book, $nodes[0]]);
134     $this->checkBookNode($nodes[2], NULL, $nodes[1], $nodes[0], $nodes[3], [$book, $nodes[0]]);
135     $this->checkBookNode($nodes[3], NULL, $nodes[2], $book, $nodes[4], [$book]);
136     $this->checkBookNode($nodes[4], NULL, $nodes[3], $book, FALSE, [$book]);
137
138     $this->drupalLogout();
139     $this->drupalLogin($this->bookAuthor);
140
141     // Check the presence of expected cache tags.
142     $this->drupalGet('node/add/book');
143     $this->assertCacheTag('config:book.settings');
144
145     /*
146      * Add Node 5 under Node 3.
147      * Book
148      *  |- Node 0
149      *   |- Node 1
150      *   |- Node 2
151      *  |- Node 3
152      *   |- Node 5
153      *  |- Node 4
154      */
155     // Node 5.
156     $nodes[] = $this->createBookNode($book->id(), $nodes[3]->book['nid']);
157     $this->drupalLogout();
158     $this->drupalLogin($this->webUser);
159     // Verify the new outline - make sure we don't get stale cached data.
160     $this->checkBookNode($nodes[3], [$nodes[5]], $nodes[2], $book, $nodes[5], [$book]);
161     $this->checkBookNode($nodes[4], NULL, $nodes[5], $book, FALSE, [$book]);
162     $this->drupalLogout();
163     // Create a second book, and move an existing book page into it.
164     $this->drupalLogin($this->bookAuthor);
165     $other_book = $this->createBookNode('new');
166     $node = $this->createBookNode($book->id());
167     $edit = ['book[bid]' => $other_book->id()];
168     $this->drupalPostForm('node/' . $node->id() . '/edit', $edit, t('Save'));
169
170     $this->drupalLogout();
171     $this->drupalLogin($this->webUser);
172
173     // Check that the nodes in the second book are displayed correctly.
174     // First we must set $this->book to the second book, so that the
175     // correct regex will be generated for testing the outline.
176     $this->book = $other_book;
177     $this->checkBookNode($other_book, [$node], FALSE, FALSE, $node, []);
178     $this->checkBookNode($node, NULL, $other_book, $other_book, FALSE, [$other_book]);
179
180     // Test that we can save a book programatically.
181     $this->drupalLogin($this->bookAuthor);
182     $book = $this->createBookNode('new');
183     $book->save();
184   }
185
186   /**
187    * Tests book export ("printer-friendly version") functionality.
188    */
189   public function testBookExport() {
190     // Create a book.
191     $nodes = $this->createBook();
192
193     // Log in as web user and view printer-friendly version.
194     $this->drupalLogin($this->webUser);
195     $this->drupalGet('node/' . $this->book->id());
196     $this->clickLink(t('Printer-friendly version'));
197
198     // Make sure each part of the book is there.
199     foreach ($nodes as $node) {
200       $this->assertText($node->label(), 'Node title found in printer friendly version.');
201       $this->assertRaw($node->body->processed, 'Node body found in printer friendly version.');
202     }
203
204     // Make sure we can't export an unsupported format.
205     $this->drupalGet('book/export/foobar/' . $this->book->id());
206     $this->assertResponse('404', 'Unsupported export format returned "not found".');
207
208     // Make sure we get a 404 on a not existing book node.
209     $this->drupalGet('book/export/html/123');
210     $this->assertResponse('404', 'Not existing book node returned "not found".');
211
212     // Make sure an anonymous user cannot view printer-friendly version.
213     $this->drupalLogout();
214
215     // Load the book and verify there is no printer-friendly version link.
216     $this->drupalGet('node/' . $this->book->id());
217     $this->assertNoLink(t('Printer-friendly version'), 'Anonymous user is not shown link to printer-friendly version.');
218
219     // Try getting the URL directly, and verify it fails.
220     $this->drupalGet('book/export/html/' . $this->book->id());
221     $this->assertResponse('403', 'Anonymous user properly forbidden.');
222
223     // Now grant anonymous users permission to view the printer-friendly
224     // version and verify that node access restrictions still prevent them from
225     // seeing it.
226     user_role_grant_permissions(RoleInterface::ANONYMOUS_ID, ['access printer-friendly version']);
227     $this->drupalGet('book/export/html/' . $this->book->id());
228     $this->assertResponse('403', 'Anonymous user properly forbidden from seeing the printer-friendly version when denied by node access.');
229   }
230
231   /**
232    * Tests the functionality of the book navigation block.
233    */
234   public function testBookNavigationBlock() {
235     $this->drupalLogin($this->adminUser);
236
237     // Enable the block.
238     $block = $this->drupalPlaceBlock('book_navigation');
239
240     // Give anonymous users the permission 'node test view'.
241     $edit = [];
242     $edit[RoleInterface::ANONYMOUS_ID . '[node test view]'] = TRUE;
243     $this->drupalPostForm('admin/people/permissions/' . RoleInterface::ANONYMOUS_ID, $edit, t('Save permissions'));
244     $this->assertText(t('The changes have been saved.'), "Permission 'node test view' successfully assigned to anonymous users.");
245
246     // Test correct display of the block.
247     $nodes = $this->createBook();
248     $this->drupalGet('<front>');
249     $this->assertText($block->label(), 'Book navigation block is displayed.');
250     $this->assertText($this->book->label(), format_string('Link to book root (@title) is displayed.', ['@title' => $nodes[0]->label()]));
251     $this->assertNoText($nodes[0]->label(), 'No links to individual book pages are displayed.');
252   }
253
254   /**
255    * Tests BookManager::getTableOfContents().
256    */
257   public function testGetTableOfContents() {
258     // Create new book.
259     $nodes = $this->createBook();
260     $book = $this->book;
261
262     $this->drupalLogin($this->bookAuthor);
263
264     /*
265      * Add Node 5 under Node 2.
266      * Add Node 6, 7, 8, 9, 10, 11 under Node 3.
267      * Book
268      *  |- Node 0
269      *   |- Node 1
270      *   |- Node 2
271      *    |- Node 5
272      *  |- Node 3
273      *   |- Node 6
274      *    |- Node 7
275      *     |- Node 8
276      *      |- Node 9
277      *       |- Node 10
278      *        |- Node 11
279      *  |- Node 4
280      */
281     foreach ([5 => 2, 6 => 3, 7 => 6, 8 => 7, 9 => 8, 10 => 9, 11 => 10] as $child => $parent) {
282       $nodes[$child] = $this->createBookNode($book->id(), $nodes[$parent]->id());
283     }
284     $this->drupalGet($nodes[0]->toUrl('edit-form'));
285     // Snice Node 0 has children 2 levels deep, nodes 10 and 11 should not
286     // appear in the selector.
287     $this->assertNoOption('edit-book-pid', $nodes[10]->id());
288     $this->assertNoOption('edit-book-pid', $nodes[11]->id());
289     // Node 9 should be available as an option.
290     $this->assertOption('edit-book-pid', $nodes[9]->id());
291
292     // Get a shallow set of options.
293     /** @var \Drupal\book\BookManagerInterface $manager */
294     $manager = $this->container->get('book.manager');
295     $options = $manager->getTableOfContents($book->id(), 3);
296     $expected_nids = [$book->id(), $nodes[0]->id(), $nodes[1]->id(), $nodes[2]->id(), $nodes[3]->id(), $nodes[6]->id(), $nodes[4]->id()];
297     $this->assertEqual(count($options), count($expected_nids));
298     $diff = array_diff($expected_nids, array_keys($options));
299     $this->assertTrue(empty($diff), 'Found all expected option keys');
300     // Exclude Node 3.
301     $options = $manager->getTableOfContents($book->id(), 3, [$nodes[3]->id()]);
302     $expected_nids = [$book->id(), $nodes[0]->id(), $nodes[1]->id(), $nodes[2]->id(), $nodes[4]->id()];
303     $this->assertEqual(count($options), count($expected_nids));
304     $diff = array_diff($expected_nids, array_keys($options));
305     $this->assertTrue(empty($diff), 'Found all expected option keys after excluding Node 3');
306   }
307
308   /**
309    * Tests the book navigation block when an access module is installed.
310    */
311   public function testNavigationBlockOnAccessModuleInstalled() {
312     $this->drupalLogin($this->adminUser);
313     $block = $this->drupalPlaceBlock('book_navigation', ['block_mode' => 'book pages']);
314
315     // Give anonymous users the permission 'node test view'.
316     $edit = [];
317     $edit[RoleInterface::ANONYMOUS_ID . '[node test view]'] = TRUE;
318     $this->drupalPostForm('admin/people/permissions/' . RoleInterface::ANONYMOUS_ID, $edit, t('Save permissions'));
319     $this->assertText(t('The changes have been saved.'), "Permission 'node test view' successfully assigned to anonymous users.");
320
321     // Create a book.
322     $this->createBook();
323
324     // Test correct display of the block to registered users.
325     $this->drupalLogin($this->webUser);
326     $this->drupalGet('node/' . $this->book->id());
327     $this->assertText($block->label(), 'Book navigation block is displayed to registered users.');
328     $this->drupalLogout();
329
330     // Test correct display of the block to anonymous users.
331     $this->drupalGet('node/' . $this->book->id());
332     $this->assertText($block->label(), 'Book navigation block is displayed to anonymous users.');
333
334     // Test the 'book pages' block_mode setting.
335     $this->drupalGet('<front>');
336     $this->assertNoText($block->label(), 'Book navigation block is not shown on non-book pages.');
337   }
338
339   /**
340    * Tests the access for deleting top-level book nodes.
341    */
342   public function testBookDelete() {
343     $node_storage = $this->container->get('entity.manager')->getStorage('node');
344     $nodes = $this->createBook();
345     $this->drupalLogin($this->adminUser);
346     $edit = [];
347
348     // Test access to delete top-level and child book nodes.
349     $this->drupalGet('node/' . $this->book->id() . '/outline/remove');
350     $this->assertResponse('403', 'Deleting top-level book node properly forbidden.');
351     $this->drupalPostForm('node/' . $nodes[4]->id() . '/outline/remove', $edit, t('Remove'));
352     $node_storage->resetCache([$nodes[4]->id()]);
353     $node4 = $node_storage->load($nodes[4]->id());
354     $this->assertTrue(empty($node4->book), 'Deleting child book node properly allowed.');
355
356     // Delete all child book nodes and retest top-level node deletion.
357     foreach ($nodes as $node) {
358       $nids[] = $node->id();
359     }
360     entity_delete_multiple('node', $nids);
361     $this->drupalPostForm('node/' . $this->book->id() . '/outline/remove', $edit, t('Remove'));
362     $node_storage->resetCache([$this->book->id()]);
363     $node = $node_storage->load($this->book->id());
364     $this->assertTrue(empty($node->book), 'Deleting childless top-level book node properly allowed.');
365
366     // Tests directly deleting a book parent.
367     $nodes = $this->createBook();
368     $this->drupalLogin($this->adminUser);
369     $this->drupalGet($this->book->urlInfo('delete-form'));
370     $this->assertRaw(t('%title is part of a book outline, and has associated child pages. If you proceed with deletion, the child pages will be relocated automatically.', ['%title' => $this->book->label()]));
371     // Delete parent, and visit a child page.
372     $this->drupalPostForm($this->book->urlInfo('delete-form'), [], t('Delete'));
373     $this->drupalGet($nodes[0]->urlInfo());
374     $this->assertResponse(200);
375     $this->assertText($nodes[0]->label());
376     // The book parents should be updated.
377     $node_storage = \Drupal::entityTypeManager()->getStorage('node');
378     $node_storage->resetCache();
379     $child = $node_storage->load($nodes[0]->id());
380     $this->assertEqual($child->id(), $child->book['bid'], 'Child node book ID updated when parent is deleted.');
381     // 3rd-level children should now be 2nd-level.
382     $second = $node_storage->load($nodes[1]->id());
383     $this->assertEqual($child->id(), $second->book['bid'], '3rd-level child node is now second level when top-level node is deleted.');
384   }
385
386   /**
387    * Tests outline of a book.
388    */
389   public function testBookOutline() {
390     $this->drupalLogin($this->bookAuthor);
391
392     // Create new node not yet a book.
393     $empty_book = $this->drupalCreateNode(['type' => 'book']);
394     $this->drupalGet('node/' . $empty_book->id() . '/outline');
395     $this->assertNoLink(t('Book outline'), 'Book Author is not allowed to outline');
396
397     $this->drupalLogin($this->adminUser);
398     $this->drupalGet('node/' . $empty_book->id() . '/outline');
399     $this->assertRaw(t('Book outline'));
400     $this->assertOptionSelected('edit-book-bid', 0, 'Node does not belong to a book');
401     $this->assertNoLink(t('Remove from book outline'));
402
403     $edit = [];
404     $edit['book[bid]'] = '1';
405     $this->drupalPostForm('node/' . $empty_book->id() . '/outline', $edit, t('Add to book outline'));
406     $node = \Drupal::entityManager()->getStorage('node')->load($empty_book->id());
407     // Test the book array.
408     $this->assertEqual($node->book['nid'], $empty_book->id());
409     $this->assertEqual($node->book['bid'], $empty_book->id());
410     $this->assertEqual($node->book['depth'], 1);
411     $this->assertEqual($node->book['p1'], $empty_book->id());
412     $this->assertEqual($node->book['pid'], '0');
413
414     // Create new book.
415     $this->drupalLogin($this->bookAuthor);
416     $book = $this->createBookNode('new');
417
418     $this->drupalLogin($this->adminUser);
419     $this->drupalGet('node/' . $book->id() . '/outline');
420     $this->assertRaw(t('Book outline'));
421     $this->clickLink(t('Remove from book outline'));
422     $this->assertRaw(t('Are you sure you want to remove %title from the book hierarchy?', ['%title' => $book->label()]));
423
424     // Create a new node and set the book after the node was created.
425     $node = $this->drupalCreateNode(['type' => 'book']);
426     $edit = [];
427     $edit['book[bid]'] = $node->id();
428     $this->drupalPostForm('node/' . $node->id() . '/edit', $edit, t('Save'));
429     $node = \Drupal::entityManager()->getStorage('node')->load($node->id());
430
431     // Test the book array.
432     $this->assertEqual($node->book['nid'], $node->id());
433     $this->assertEqual($node->book['bid'], $node->id());
434     $this->assertEqual($node->book['depth'], 1);
435     $this->assertEqual($node->book['p1'], $node->id());
436     $this->assertEqual($node->book['pid'], '0');
437
438     // Test the form itself.
439     $this->drupalGet('node/' . $node->id() . '/edit');
440     $this->assertOptionSelected('edit-book-bid', $node->id());
441   }
442
443   /**
444    * Tests that saveBookLink() returns something.
445    */
446   public function testSaveBookLink() {
447     $book_manager = \Drupal::service('book.manager');
448
449     // Mock a link for a new book.
450     $link = ['nid' => 1, 'has_children' => 0, 'original_bid' => 0, 'parent_depth_limit' => 8, 'pid' => 0, 'weight' => 0, 'bid' => 1];
451     $new = TRUE;
452
453     // Save the link.
454     $return = $book_manager->saveBookLink($link, $new);
455
456     // Add the link defaults to $link so we have something to compare to the return from saveBookLink().
457     $link += $book_manager->getLinkDefaults($link['nid']);
458
459     // Test the return from saveBookLink.
460     $this->assertEqual($return, $link);
461   }
462
463   /**
464    * Tests the listing of all books.
465    */
466   public function testBookListing() {
467     // Create a new book.
468     $this->createBook();
469
470     // Must be a user with 'node test view' permission since node_access_test is installed.
471     $this->drupalLogin($this->webUser);
472
473     // Load the book page and assert the created book title is displayed.
474     $this->drupalGet('book');
475
476     $this->assertText($this->book->label(), 'The book title is displayed on the book listing page.');
477   }
478
479   /**
480    * Tests the administrative listing of all books.
481    */
482   public function testAdminBookListing() {
483     // Create a new book.
484     $this->createBook();
485
486     // Load the book page and assert the created book title is displayed.
487     $this->drupalLogin($this->adminUser);
488     $this->drupalGet('admin/structure/book');
489     $this->assertText($this->book->label(), 'The book title is displayed on the administrative book listing page.');
490   }
491
492   /**
493    * Tests the administrative listing of all book pages in a book.
494    */
495   public function testAdminBookNodeListing() {
496     // Create a new book.
497     $this->createBook();
498     $this->drupalLogin($this->adminUser);
499
500     // Load the book page list and assert the created book title is displayed
501     // and action links are shown on list items.
502     $this->drupalGet('admin/structure/book/' . $this->book->id());
503     $this->assertText($this->book->label(), 'The book title is displayed on the administrative book listing page.');
504
505     $elements = $this->xpath('//table//ul[@class="dropbutton"]/li/a');
506     $this->assertEqual($elements[0]->getText(), 'View', 'View link is found from the list.');
507   }
508
509   /**
510    * Ensure the loaded book in hook_node_load() does not depend on the user.
511    */
512   public function testHookNodeLoadAccess() {
513     \Drupal::service('module_installer')->install(['node_access_test']);
514
515     // Ensure that the loaded book in hook_node_load() does NOT depend on the
516     // current user.
517     $this->drupalLogin($this->bookAuthor);
518     $this->book = $this->createBookNode('new');
519     // Reset any internal static caching.
520     $node_storage = \Drupal::entityManager()->getStorage('node');
521     $node_storage->resetCache();
522
523     // Log in as user without access to the book node, so no 'node test view'
524     // permission.
525     // @see node_access_test_node_grants().
526     $this->drupalLogin($this->webUserWithoutNodeAccess);
527     $book_node = $node_storage->load($this->book->id());
528     $this->assertTrue(!empty($book_node->book));
529     $this->assertEqual($book_node->book['bid'], $this->book->id());
530
531     // Reset the internal cache to retrigger the hook_node_load() call.
532     $node_storage->resetCache();
533
534     $this->drupalLogin($this->webUser);
535     $book_node = $node_storage->load($this->book->id());
536     $this->assertTrue(!empty($book_node->book));
537     $this->assertEqual($book_node->book['bid'], $this->book->id());
538   }
539
540   /**
541    * Tests the book navigation block when book is unpublished.
542    *
543    * There was a fatal error with "Show block only on book pages" block mode.
544    */
545   public function testBookNavigationBlockOnUnpublishedBook() {
546     // Create a new book.
547     $this->createBook();
548
549     // Create administrator user.
550     $administratorUser = $this->drupalCreateUser(['administer blocks', 'administer nodes', 'bypass node access']);
551     $this->drupalLogin($administratorUser);
552
553     // Enable the block with "Show block only on book pages" mode.
554     $this->drupalPlaceBlock('book_navigation', ['block_mode' => 'book pages']);
555
556     // Unpublish book node.
557     $edit = ['status[value]' => FALSE];
558     $this->drupalPostForm('node/' . $this->book->id() . '/edit', $edit, t('Save'));
559
560     // Test node page.
561     $this->drupalGet('node/' . $this->book->id());
562     $this->assertText($this->book->label(), 'Unpublished book with "Show block only on book pages" book navigation settings.');
563   }
564
565 }