book.module 31.2 KB
Newer Older
Dries's avatar
 
Dries committed
1
2
<?php

Dries's avatar
   
Dries committed
3
4
/**
 * @file
5
 * Allows users to create and organize related content in an outline.
Dries's avatar
   
Dries committed
6
7
 */

8
use Drupal\Core\Entity\EntityInterface;
9
use Drupal\node\NodeTypeInterface;
10
use Drupal\Core\Language\Language;
11
use Drupal\entity\Entity\EntityDisplay;
12
use Drupal\Core\Template\Attribute;
13
use Drupal\menu_link\Entity\MenuLink;
14
use Drupal\menu_link\MenuLinkStorageController;
15
use Drupal\node\NodeInterface;
16

17
/**
18
 * Implements hook_help().
19
20
21
22
23
 */
function book_help($path, $arg) {
  switch ($path) {
    case 'admin/help#book':
      $output = '<h3>' . t('About') . '</h3>';
24
      $output .= '<p>' . t('The Book module is used for creating structured, multi-page content, such as site resource guides, manuals, and wikis. It allows you to create content that has chapters, sections, subsections, or any similarly-tiered structure. For more information, see the <a href="!book">online documentation for the Book module</a>.', array('!book' => 'https://drupal.org/documentation/modules/book')) . '</p>';
25
26
27
      $output .= '<h3>' . t('Uses') . '</h3>';
      $output .= '<dl>';
      $output .= '<dt>' . t('Adding and managing book content') . '</dt>';
28
      $output .= '<dd>' . t('You can assign separate permissions for <em>creating new books</em> as well as <em>creating</em>, <em>editing</em> and <em>deleting</em> book content. Users with the <em>Administer book outlines</em> permission can add <em>any</em> type of content to a book by selecting the appropriate book outline while editing the content. They can also view a list of all books, and edit and rearrange section titles on the <a href="!admin-book">Book administration page</a>.', array('!admin-book' => \Drupal::url('book.admin'))) . '</dd>';
29
      $output .= '<dt>' . t('Book navigation') . '</dt>';
30
      $output .= '<dd>' . t("Book pages have a default book-specific navigation block. This navigation block contains links that lead to the previous and next pages in the book, and to the level above the current page in the book's structure. This block can be enabled on the <a href='!admin-block'>Blocks administration page</a>. For book pages to show up in the book navigation, they must be added to a book outline.", array('!admin-block' => \Drupal::url('block.admin_display'))) . '</dd>';
31
32
33
      $output .= '<dt>' . t('Collaboration') . '</dt>';
      $output .= '<dd>' . t('Books can be created collaboratively, as they allow users with appropriate permissions to add pages into existing books, and add those pages to a custom table of contents menu.') . '</dd>';
      $output .= '<dt>' . t('Printing books') . '</dt>';
34
      $output .= '<dd>' . t("Users with the <em>View printer-friendly books</em> permission can select the <em>printer-friendly version</em> link visible at the bottom of a book page's content to generate a printer-friendly display of the page and all of its subsections.") . '</dd>';
35
36
      $output .= '</dl>';
      return $output;
37
    case 'admin/structure/book':
38
39
      return '<p>' . t('The book module offers a means to organize a collection of related content pages, collectively known as a book. When viewed, this content automatically displays links to adjacent book pages, providing a simple navigation system for creating and reviewing structured content.') . '</p>';
    case 'node/%/outline':
40
      return '<p>' . t('The outline feature allows you to include pages in the <a href="!book">Book hierarchy</a>, as well as move them within the hierarchy or to <a href="!book-admin">reorder an entire book</a>.', array('!book' => \Drupal::url('book.render'), '!book-admin' => \Drupal::url('book.admin'))) . '</p>';
41
42
43
  }
}

44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
/**
 * Implements hook_entity_bundle_info().
 */
function book_entity_bundle_info() {
  $bundles['menu_link']['book-toc'] = array(
    'label' => t('Book'),
    'translatable' => FALSE,
  );
  return $bundles;
}

/**
 * Implements hook_TYPE_load().
 */
function book_menu_link_load($entities) {
  foreach ($entities as $entity) {
    // Change the bundle of menu links related to a book.
    if (strpos($entity->menu_name, 'book-toc-') === 0) {
      $entity->bundle = 'book-toc';
    }
  }
}

67
/**
68
 * Implements hook_theme().
69
70
71
72
 */
function book_theme() {
  return array(
    'book_navigation' => array(
73
      'variables' => array('book_link' => NULL),
74
      'template' => 'book-navigation',
75
76
    ),
    'book_export_html' => array(
77
      'variables' => array('title' => NULL, 'contents' => NULL, 'depth' => NULL),
78
      'template' => 'book-export-html',
79
80
    ),
    'book_admin_table' => array(
81
      'render element' => 'form',
82
      'file' => 'book.admin.inc',
83
    ),
Dries's avatar
Dries committed
84
    'book_all_books_block' => array(
85
      'render element' => 'book_menus',
86
87
88
      'template' => 'book-all-books-block',
    ),
    'book_node_export_html' => array(
89
      'variables' => array('node' => NULL, 'children' => NULL),
90
      'template' => 'book-node-export-html',
Dries's avatar
Dries committed
91
    ),
92
93
94
  );
}

95
/**
96
 * Implements hook_permission().
97
 */
98
function book_permission() {
99
  return array(
100
101
102
103
104
105
106
    'administer book outlines' => array(
      'title' => t('Administer book outlines'),
    ),
    'create new books' => array(
      'title' => t('Create new books'),
    ),
    'add content to books' => array(
107
      'title' => t('Add content and child pages to books'),
108
109
    ),
    'access printer-friendly version' => array(
110
      'title' => t('View printer-friendly books'),
111
112
      'description' => t('View a book page and all of its sub-pages as a single document for ease of printing. Can be performance heavy.'),
    ),
113
  );
Dries's avatar
   
Dries committed
114
115
}

116
117
118
119
120
121
122
/**
 * Implements hook_entity_info().
 */
function book_entity_info(&$entity_info) {
  $entity_info['node']['controllers']['form']['book_outline'] = '\Drupal\book\Form\BookOutlineForm';
}

Dries's avatar
   
Dries committed
123
/**
124
125
 * Adds relevant book links to the node's links.
 *
126
 * @param \Drupal\Core\Entity\EntityInterface $node
127
128
129
 *   The book page node to add links to.
 * @param $view_mode
 *   The view mode of the node.
Dries's avatar
   
Dries committed
130
 */
131
function book_node_view_link(NodeInterface $node, $view_mode) {
Dries's avatar
   
Dries committed
132
  $links = array();
133
  $account = \Drupal::currentUser();
Dries's avatar
Dries committed
134

135
  if (isset($node->book['depth'])) {
136
    if ($view_mode == 'full' && node_is_page($node)) {
137
      $child_type = \Drupal::config('book.settings')->get('child_type');
138
      if (($account->hasPermission('add content to books') || $account->hasPermission('administer book outlines')) && node_access('create', $child_type) && $node->isPublished() && $node->book['depth'] < MENU_MAX_DEPTH) {
139
        $links['book_add_child'] = array(
140
          'title' => t('Add child page'),
141
          'href' => 'node/add/' . $child_type,
142
          'query' => array('parent' => $node->book['mlid']),
143
        );
Dries's avatar
   
Dries committed
144
      }
145

146
      if ($account->hasPermission('access printer-friendly version')) {
147
        $links['book_printer'] = array(
148
          'title' => t('Printer-friendly version'),
149
          'href' => 'book/export/html/' . $node->id(),
150
          'attributes' => array('title' => t('Show a printer-friendly version of this book page and its sub-pages.'))
151
        );
152
      }
Dries's avatar
   
Dries committed
153
    }
Dries's avatar
   
Dries committed
154
  }
155

156
  if (!empty($links)) {
157
158
159
160
161
    $node->content['links']['book'] = array(
      '#theme' => 'links__node__book',
      '#links' => $links,
      '#attributes' => array('class' => array('links', 'inline')),
    );
162
  }
Dries's avatar
   
Dries committed
163
164
}

Dries's avatar
   
Dries committed
165
/**
166
 * Implements hook_menu().
Dries's avatar
   
Dries committed
167
 */
168
function book_menu() {
169
  $items['admin/structure/book'] = array(
170
    'title' => 'Books',
171
    'description' => "Manage your site's book outlines.",
172
    'route_name' => 'book.admin',
173
174
  );
  $items['book'] = array(
175
    'title' => 'Books',
176
    'route_name' => 'book.render',
177
178
    'type' => MENU_SUGGESTED_ITEM,
  );
Dries's avatar
Dries committed
179
180
  $items['node/%node/outline/remove'] = array(
    'title' => 'Remove from outline',
181
    'route_name' => 'book.remove',
Dries's avatar
Dries committed
182
  );
183

Dries's avatar
   
Dries committed
184
185
186
  return $items;
}

Dries's avatar
Dries committed
187
/**
188
189
190
 * Access callback: Determines if the outline tab is accessible.
 *
 * Path:
191
 * - admin/structure/book/%node
192
193
 * - node/%node/outline
 *
194
 * @param \Drupal\Core\Entity\EntityInterface $node
195
196
197
 *   The node whose outline tab is to be viewed.
 *
 * @see book_menu()
Dries's avatar
Dries committed
198
 */
199
function _book_outline_access(EntityInterface $node) {
200
  return \Drupal::currentUser()->hasPermission('administer book outlines') && node_access('view', $node);
Dries's avatar
Dries committed
201
202
203
}

/**
204
205
 * Access callback: Determines if the user can remove nodes from the outline.
 *
206
 * @param \Drupal\Core\Entity\EntityInterface $node
207
208
209
 *   The node to remove from the outline.
 *
 * @see book_menu()
Dries's avatar
Dries committed
210
 */
211
function _book_outline_remove_access(EntityInterface $node) {
212
  return \Drupal::service('book.manager')->checkNodeIsRemovable($node)
213
    && _book_outline_access($node);
214
215
}

216
217
218
219
/**
 * Implements hook_admin_paths().
 */
function book_admin_paths() {
220
  if (\Drupal::config('node.settings')->get('use_admin_theme')) {
221
222
223
224
225
226
    $paths = array(
      'node/*/outline' => TRUE,
      'node/*/outline/remove' => TRUE,
    );
    return $paths;
  }
227
228
}

229
/**
Dries's avatar
Dries committed
230
231
 * Returns an array of all books.
 *
232
233
 * @todo Remove in favor of BookManager Service. http://drupal.org/node/1963894
 *
Dries's avatar
Dries committed
234
235
 * This list may be used for generating a list of all the books, or for building
 * the options for a form select.
236
237
238
 *
 * @return
 *   An array of all books.
239
 */
Dries's avatar
Dries committed
240
function book_get_books() {
241
  return \Drupal::service('book.manager')->getAllBooks();
242
}
243

Dries's avatar
Dries committed
244
/**
245
 * Implements hook_form_BASE_FORM_ID_alter() for node_form().
246
 *
247
 * Adds the book form element to the node form.
248
249
 *
 * @see book_pick_book_nojs_submit()
Dries's avatar
Dries committed
250
 */
251
function book_form_node_form_alter(&$form, &$form_state, $form_id) {
252
  $node = $form_state['controller']->getEntity();
253
  $account = \Drupal::currentUser();
254
  $access = $account->hasPermission('administer book outlines');
255
  if (!$access) {
256
    if ($account->hasPermission('add content to books') && ((!empty($node->book['mlid']) && !$node->isNew()) || book_type_is_allowed($node->getType()))) {
257
258
      // Already in the book hierarchy, or this node type is allowed.
      $access = TRUE;
Dries's avatar
Dries committed
259
    }
260
  }
Dries's avatar
Dries committed
261

262
  if ($access) {
263
    $form = \Drupal::service('book.manager')->addFormElements($form, $form_state, $node, $account);
264
    // Since the "Book" dropdown can't trigger a form submission when
265
    // JavaScript is disabled, add a submit button to do that. book.admin.css hides
266
267
268
269
270
271
    // this button when JavaScript is enabled.
    $form['book']['pick-book'] = array(
      '#type' => 'submit',
      '#value' => t('Change book (update list of parents)'),
      '#submit' => array('book_pick_book_nojs_submit'),
      '#weight' => 20,
272
      '#attached' => array(
273
        'css' => array(drupal_get_path('module', 'book') . '/css/book.admin.css'),
274
      ),
275
    );
276
    $form['#entity_builders'][] = 'book_node_builder';
277
  }
Dries's avatar
Dries committed
278
}
279

280
281
282
283
284
285
286
287
288
/**
 * Entity form builder to add the book information to the node.
 *
 * @todo: Remove this in favor of an entity field.
 */
function book_node_builder($entity_type, $entity, &$form, &$form_state) {
  $entity->book = $form_state['values']['book'];
}

289
/**
290
 * Form submission handler for node_form().
291
292
293
294
295
 *
 * This handler is run when JavaScript is disabled. It triggers the form to
 * rebuild so that the "Parent item" options are changed to reflect the newly
 * selected book. When JavaScript is enabled, the submit button that triggers
 * this handler is hidden, and the "Book" dropdown directly triggers the
296
 * book_form_update() Ajax callback instead.
297
298
 *
 * @see book_form_update()
299
 * @see book_form_node_form_alter()
300
301
 */
function book_pick_book_nojs_submit($form, &$form_state) {
302
  $node = $form_state['controller']->getEntity();
303
  $node->book = $form_state['values']['book'];
304
305
306
  $form_state['rebuild'] = TRUE;
}

307
308
309
/**
 * Renders a new parent page select element when the book selection changes.
 *
310
 * This function is called via Ajax when the selected book is changed on a node
311
312
313
314
315
316
317
318
319
 * or book outline form.
 *
 * @return
 *   The rendered parent page select element.
 */
function book_form_update($form, $form_state) {
  return $form['book']['plid'];
}

Dries's avatar
Dries committed
320
/**
321
 * Gets the book menu tree for a page and returns it as a linear array.
322
 *
Dries's avatar
Dries committed
323
324
 * @param $book_link
 *   A fully loaded menu link that is part of the book hierarchy.
325
 *
326
 * @return
Dries's avatar
Dries committed
327
328
 *   A linear array of menu links in the order that the links are shown in the
 *   menu, so the previous and next pages are the elements before and after the
329
330
331
 *   element corresponding to the current node. The children of the current node
 *   (if any) will come immediately after it in the array, and links will only
 *   be fetched as deep as one level deeper than $book_link.
332
 */
Dries's avatar
Dries committed
333
function book_get_flat_menu($book_link) {
334
  $flat = &drupal_static(__FUNCTION__, array());
Dries's avatar
Dries committed
335
336

  if (!isset($flat[$book_link['mlid']])) {
337
    // Call menu_tree_all_data() to take advantage of the menu system's caching.
338
    $tree = menu_tree_all_data($book_link['menu_name'], $book_link, $book_link['depth'] + 1);
Dries's avatar
Dries committed
339
340
    $flat[$book_link['mlid']] = array();
    _book_flatten_menu($tree, $flat[$book_link['mlid']]);
Dries's avatar
   
Dries committed
341
  }
342

Dries's avatar
Dries committed
343
  return $flat[$book_link['mlid']];
Dries's avatar
   
Dries committed
344
345
}

346
/**
347
348
349
350
351
 * Recursively converts a tree of menu links to a flat array.
 *
 * @param $tree
 *   A tree of menu links in an array.
 * @param $flat
352
 *   A flat array of the menu links from $tree, passed by reference.
353
354
 *
 * @see book_get_flat_menu().
355
 */
Dries's avatar
Dries committed
356
357
358
359
360
361
362
363
function _book_flatten_menu($tree, &$flat) {
  foreach ($tree as $data) {
    if (!$data['link']['hidden']) {
      $flat[$data['link']['mlid']] = $data['link'];
      if ($data['below']) {
        _book_flatten_menu($data['below'], $flat);
      }
    }
Dries's avatar
   
Dries committed
364
  }
Dries's avatar
Dries committed
365
}
Dries's avatar
   
Dries committed
366

Dries's avatar
Dries committed
367
368
/**
 * Fetches the menu link for the previous page of the book.
369
370
371
372
373
374
375
 *
 * @param $book_link
 *   A fully loaded menu link that is part of the book hierarchy.
 *
 * @return
 *   A fully loaded menu link for the page before the one represented in
 *   $book_link.
Dries's avatar
Dries committed
376
377
378
379
380
 */
function book_prev($book_link) {
  // If the parent is zero, we are at the start of a book.
  if ($book_link['plid'] == 0) {
    return NULL;
Dries's avatar
   
Dries committed
381
  }
Dries's avatar
Dries committed
382
383
384
385
386
387
388
389
390
391
392
393
  $flat = book_get_flat_menu($book_link);
  // Assigning the array to $flat resets the array pointer for use with each().
  $curr = NULL;
  do {
    $prev = $curr;
    list($key, $curr) = each($flat);
  } while ($key && $key != $book_link['mlid']);

  if ($key == $book_link['mlid']) {
    // The previous page in the book may be a child of the previous visible link.
    if ($prev['depth'] == $book_link['depth'] && $prev['has_children']) {
      // The subtree will have only one link at the top level - get its data.
394
395
      $tree = book_menu_subtree_data($prev);
      $data = array_shift($tree);
Dries's avatar
Dries committed
396
397
398
399
      // The link of interest is the last child - iterate to find the deepest one.
      while ($data['below']) {
        $data = end($data['below']);
      }
400

Dries's avatar
Dries committed
401
402
403
404
405
      return $data['link'];
    }
    else {
      return $prev;
    }
Dries's avatar
   
Dries committed
406
407
408
  }
}

409
/**
Dries's avatar
Dries committed
410
 * Fetches the menu link for the next page of the book.
411
412
413
414
415
416
417
 *
 * @param $book_link
 *   A fully loaded menu link that is part of the book hierarchy.
 *
 * @return
 *   A fully loaded menu link for the page after the one represented in
 *   $book_link.
418
 */
Dries's avatar
Dries committed
419
420
421
422
423
function book_next($book_link) {
  $flat = book_get_flat_menu($book_link);
  // Assigning the array to $flat resets the array pointer for use with each().
  do {
    list($key, $curr) = each($flat);
424
425
426
  }
  while ($key && $key != $book_link['mlid']);

Dries's avatar
Dries committed
427
428
  if ($key == $book_link['mlid']) {
    return current($flat);
Dries's avatar
   
Dries committed
429
  }
Dries's avatar
Dries committed
430
}
Dries's avatar
   
Dries committed
431

Dries's avatar
Dries committed
432
/**
433
434
435
436
437
438
439
 * Formats the menu links for the child pages of the current page.
 *
 * @param $book_link
 *   A fully loaded menu link that is part of the book hierarchy.
 *
 * @return
 *   HTML for the links to the child pages of the current page.
Dries's avatar
Dries committed
440
441
442
 */
function book_children($book_link) {
  $flat = book_get_flat_menu($book_link);
443

Dries's avatar
Dries committed
444
445
446
447
448
449
  $children = array();

  if ($book_link['has_children']) {
    // Walk through the array until we find the current page.
    do {
      $link = array_shift($flat);
450
451
    }
    while ($link && ($link['mlid'] != $book_link['mlid']));
Dries's avatar
Dries committed
452
453
454
455
456
    // Continue though the array and collect the links whose parent is this page.
    while (($link = array_shift($flat)) && $link['plid'] == $book_link['mlid']) {
      $data['link'] = $link;
      $data['below'] = '';
      $children[] = $data;
Dries's avatar
   
Dries committed
457
458
    }
  }
459

460
461
462
463
464
  if ($children) {
    $elements = menu_tree_output($children);
    return drupal_render($elements);
  }
  return '';
Dries's avatar
   
Dries committed
465
466
}

467
/**
468
 * Implements hook_node_load().
469
 */
470
function book_node_load($nodes, $types) {
471
  $result = db_query("SELECT * FROM {book} b INNER JOIN {menu_links} ml ON b.mlid = ml.mlid WHERE b.nid IN (:nids)", array(':nids' =>  array_keys($nodes)), array('fetch' => PDO::FETCH_ASSOC));
472
473
474
475
476
  foreach ($result as $record) {
    $nodes[$record['nid']]->book = $record;
    $nodes[$record['nid']]->book['href'] = $record['link_path'];
    $nodes[$record['nid']]->book['title'] = $record['link_title'];
    $nodes[$record['nid']]->book['options'] = unserialize($record['options']);
477
478
  }
}
479

480
/**
481
 * Implements hook_node_view().
482
 */
483
function book_node_view(EntityInterface $node, EntityDisplay $display, $view_mode) {
484
  if ($view_mode == 'full') {
485
    if (!empty($node->book['bid']) && empty($node->in_preview)) {
486
      $book_navigation = array( '#theme' => 'book_navigation', '#book_link' => $node->book);
487
      $node->content['book_navigation'] = array(
488
        '#markup' => drupal_render($book_navigation),
489
        '#weight' => 100,
490
491
492
493
494
        '#attached' => array(
          'css' => array(
            drupal_get_path('module', 'book') . '/css/book.theme.css',
          ),
        ),
495
496
497
      );
    }
  }
Dries's avatar
Dries committed
498

499
500
  if ($view_mode != 'rss') {
    book_node_view_link($node, $view_mode);
501
  }
502
503
504
}

/**
505
 * Implements hook_page_alter().
506
 *
507
 * Adds the book menu to the list of menus used to build the active trail when
508
509
510
511
512
513
514
515
 * viewing a book page.
 */
function book_page_alter(&$page) {
  if (($node = menu_get_object()) && !empty($node->book['bid'])) {
    $active_menus = menu_get_active_menu_names();
    $active_menus[] = $node->book['menu_name'];
    menu_set_active_menu_names($active_menus);
  }
516
}
517

518
/**
519
 * Implements hook_node_presave().
520
 */
521
function book_node_presave(EntityInterface $node) {
522
  // Always save a revision for non-administrators.
523
  if (!empty($node->book['bid']) && !\Drupal::currentUser()->hasPermission('administer nodes')) {
524
    $node->setNewRevision();
525
526
  }
  // Make sure a new node gets a new menu link.
527
  if ($node->isNew()) {
528
529
530
    $node->book['mlid'] = NULL;
  }
}
531

532
/**
533
 * Implements hook_node_insert().
534
 */
535
function book_node_insert(EntityInterface $node) {
536
  $book_manager = \Drupal::service('book.manager');
537
538
539
  if (!empty($node->book['bid'])) {
    if ($node->book['bid'] == 'new') {
      // New nodes that are their own book.
540
      $node->book['bid'] = $node->id();
541
    }
542
    $node->book['nid'] = $node->id();
543
544
    $node->book['menu_name'] = $book_manager->createMenuName($node->book['bid']);
    $book_manager->updateOutline($node);
545
546
  }
}
547

548
/**
549
 * Implements hook_node_update().
550
 */
551
function book_node_update(EntityInterface $node) {
552
  $book_manager = \Drupal::service('book.manager');
553
554
555
  if (!empty($node->book['bid'])) {
    if ($node->book['bid'] == 'new') {
      // New nodes that are their own book.
556
      $node->book['bid'] = $node->id();
557
    }
558
    $node->book['nid'] = $node->id();
559
560
    $node->book['menu_name'] = $book_manager->createMenuName($node->book['bid']);
    $book_manager->updateOutline($node);
561
562
  }
}
563

564
/**
565
 * Implements hook_node_predelete().
566
 */
567
function book_node_predelete(EntityInterface $node) {
568
  if (!empty($node->book['bid'])) {
569
    if ($node->id() == $node->book['bid']) {
570
      // Handle deletion of a top-level post.
571
572
573
574
      $result = db_query("SELECT b.nid FROM {menu_links} ml INNER JOIN {book} b on b.mlid = ml.mlid WHERE ml.plid = :plid", array(
        ':plid' => $node->book['mlid']
      ));
      foreach ($result as $child) {
575
576
        $child_node = node_load($child->id());
        $child_node->book['bid'] = $child_node->id();
577
        \Drupal::service('book.manager')->updateOutline($child_node);
Dries's avatar
Dries committed
578
      }
579
580
    }
    menu_link_delete($node->book['mlid']);
581
582
583
    db_delete('book')
      ->condition('mlid', $node->book['mlid'])
      ->execute();
584
    drupal_static_reset('book_get_books');
585
586
587
588
  }
}

/**
589
 * Implements hook_node_prepare_form().
590
 */
591
function book_node_prepare_form(NodeInterface $node, $form_display, $operation, array &$form_state) {
592
  // Get BookManager service
593
  $book_manager = \Drupal::service('book.manager');
594

595
  // Prepare defaults for the add/edit form.
596
  $account = \Drupal::currentUser();
597
  if (empty($node->book) && ($account->hasPermission('add content to books') || $account->hasPermission('administer book outlines'))) {
598
599
    $node->book = array();

600
    $query = \Drupal::request()->query;
601
    if ($node->isNew() && !is_null($query->get('parent')) && is_numeric($query->get('parent'))) {
602
      // Handle "Add child page" links:
603
      $parent = book_link_load($query->get('parent'));
604
605
606
607
608

      if ($parent && $parent['access']) {
        $node->book['bid'] = $parent['bid'];
        $node->book['plid'] = $parent['mlid'];
        $node->book['menu_name'] = $parent['menu_name'];
609
      }
610
611
    }
    // Set defaults.
612
613
    $node_ref = !$node->isNew() ? $node->id() : 'new';
    $node->book += $book_manager->getLinkDefaults($node_ref);
614
615
616
617
618
619
620
621
  }
  else {
    if (isset($node->book['bid']) && !isset($node->book['original_bid'])) {
      $node->book['original_bid'] = $node->book['bid'];
    }
  }
  // Find the depth limit for the parent select.
  if (isset($node->book['bid']) && !isset($node->book['parent_depth_limit'])) {
622
    $node->book['parent_depth_limit'] = $book_manager->getParentDepthLimit($node->book);
Dries's avatar
Dries committed
623
624
625
626
  }
}

/**
627
 * Implements hook_form_FORM_ID_alter() for node_delete_confirm().
628
629
630
631
 *
 * Alters the confirm form for a single node deletion.
 *
 * @see node_delete_confirm()
Dries's avatar
Dries committed
632
633
634
635
636
637
 */
function book_form_node_delete_confirm_alter(&$form, $form_state) {
  $node = node_load($form['nid']['#value']);

  if (isset($node->book) && $node->book['has_children']) {
    $form['book_warning'] = array(
638
      '#markup' => '<p>' . 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.', array('%title' => $node->label())) . '</p>',
Dries's avatar
Dries committed
639
640
      '#weight' => -10,
    );
Dries's avatar
   
Dries committed
641
  }
Dries's avatar
   
Dries committed
642
}
Dries's avatar
   
Dries committed
643

644
/**
645
 * Implements hook_preprocess_HOOK() for block templates.
646
647
 */
function book_preprocess_block(&$variables) {
648
  if ($variables['configuration']['module'] == 'book') {
649
    $variables['attributes']['role'] = 'navigation';
650
651
652
  }
}

653
/**
654
655
656
 * Prepares variables for book listing block templates.
 *
 * Default template: book-all-books-block.html.twig.
657
 *
658
659
660
 * All non-renderable elements are removed so that the template has full access
 * to the structured data but can also simply iterate over all elements and
 * render them (as in the default template).
661
 *
662
663
 * @param array $variables
 *   An associative array containing the following key:
664
665
 *   - book_menus: An associative array containing renderable menu links for all
 *     book menus.
666
667
668
669
670
671
672
673
674
675
 */
function template_preprocess_book_all_books_block(&$variables) {
  // Remove all non-renderable elements.
  $elements = $variables['book_menus'];
  $variables['book_menus'] = array();
  foreach (element_children($elements) as $index) {
    $variables['book_menus'][$index] = $elements[$index];
  }
}

Dries's avatar
Dries committed
676
/**
677
678
679
 * Prepares variables for book navigation templates.
 *
 * Default template: book-navigation.html.twig.
Dries's avatar
Dries committed
680
 *
681
682
 * @param array $variables
 *   An associative array containing the following key:
683
684
 *   - book_link: An associative array of book link properties.
 *     Properties used: bid, link_title, depth, plid, mlid.
685
 */
686
687
688
689
690
691
function template_preprocess_book_navigation(&$variables) {
  $book_link = $variables['book_link'];

  // Provide extra variables for themers. Not needed by default.
  $variables['book_id'] = $book_link['bid'];
  $variables['book_title'] = check_plain($book_link['link_title']);
692
  $variables['book_url'] = 'node/' . $book_link['bid'];
693
694
  $variables['current_depth'] = $book_link['depth'];
  $variables['tree'] = '';
695

Dries's avatar
Dries committed
696
  if ($book_link['mlid']) {
697
    $variables['tree'] = book_children($book_link);
Dries's avatar
   
Dries committed
698

699
700
    $build = array();

Dries's avatar
Dries committed
701
    if ($prev = book_prev($book_link)) {
702
      $prev_href = url($prev['href']);
703
704
705
706
      $build['#attached']['drupal_add_html_head_link'][][] = array(
        'rel' => 'prev',
        'href' => $prev_href,
      );
707
708
      $variables['prev_url'] = $prev_href;
      $variables['prev_title'] = check_plain($prev['title']);
Dries's avatar
   
Dries committed
709
    }
710

Dries's avatar
Dries committed
711
    if ($book_link['plid'] && $parent = book_link_load($book_link['plid'])) {
712
      $parent_href = url($parent['link_path']);
713
714
715
716
      $build['#attached']['drupal_add_html_head_link'][][] = array(
        'rel' => 'up',
        'href' => $parent_href,
      );
717
718
      $variables['parent_url'] = $parent_href;
      $variables['parent_title'] = check_plain($parent['title']);
Dries's avatar
   
Dries committed
719
    }
720

Dries's avatar
Dries committed
721
    if ($next = book_next($book_link)) {
722
      $next_href = url($next['href']);
723
724
725
726
      $build['#attached']['drupal_add_html_head_link'][][] = array(
        'rel' => 'next',
        'href' => $next_href,
      );
727
728
      $variables['next_url'] = $next_href;
      $variables['next_title'] = check_plain($next['title']);
Dries's avatar
   
Dries committed
729
    }
730
  }
Dries's avatar
   
Dries committed
731

732
733
734
735
  if (!empty($build)) {
    drupal_render($build);
  }

736
737
738
739
740
741
742
743
744
745
746
  $variables['has_links'] = FALSE;
  // Link variables to filter for values and set state of the flag variable.
  $links = array('prev_url', 'prev_title', 'parent_url', 'parent_title', 'next_url', 'next_title');
  foreach ($links as $link) {
    if (isset($variables[$link])) {
      // Flag when there is a value.
      $variables['has_links'] = TRUE;
    }
    else {
      // Set empty to prevent notices.
      $variables[$link] = '';
747
    }
Dries's avatar
   
Dries committed
748
  }
Dries's avatar
   
Dries committed
749
}
Dries's avatar
 
Dries committed
750

751
/**
752
 * Prepares variables for book export templates.
753
 *
754
 * Default template: book-export-html.html.twig.
755
 *
756
757
758
759
760
 * @param array $variables
 *   An associative array containing:
 *   - title: The title of the book.
 *   - contents: Output of each book page.
 *   - depth: The max depth of the book.
761
762
 */
function template_preprocess_book_export_html(&$variables) {
763
  global $base_url;
764
  $language_interface = language(Language::TYPE_INTERFACE);
765
766
767

  $variables['title'] = check_plain($variables['title']);
  $variables['base_url'] = $base_url;
768
  $variables['language'] = $language_interface;
769
  $variables['language_rtl'] = ($language_interface->direction == Language::DIRECTION_RTL);
770
  $variables['head'] = drupal_get_html_head();
771
772

  // HTML element attributes.
773
  $attributes = array();
774
  $attributes['lang'] = $language_interface->id;
775
776
  $attributes['dir'] = $language_interface->direction ? 'rtl' : 'ltr';
  $variables['html_attributes'] = new Attribute($attributes);
777
778
}

779
/**
780
781
782
 * Prepares variables for single node export templates.
 *
 * Default template: book-node-export-html.html.twig.
Dries's avatar
Dries committed
783
 *
784
785
 * @param array $variables
 *   An associative array containing the following keys:
786
787
788
 *   - node: The node that will be output.
 *   - children: All the rendered child nodes within the current node. Defaults
 *     to an empty string.
789
 */
790
791
function template_preprocess_book_node_export_html(&$variables) {
  $variables['depth'] = $variables['node']->book['depth'];
792
  $variables['title'] = check_plain($variables['node']->label());
793
  $variables['content'] = $variables['node']->rendered;
794
795
}

796
/**
797
 * Determines if a given node type is in the list of types allowed for books.
798
799
800
801
802
803
 *
 * @param string $type
 *   A node type.
 *
 * @return bool
 *   A Boolean TRUE if the node type can be included in books; otherwise, FALSE.
804
 */
Dries's avatar
Dries committed
805
function book_type_is_allowed($type) {
806
  return in_array($type, \Drupal::config('book.settings')->get('allowed_types'));
807
808
}

Dries's avatar
Dries committed
809
/**
810
 * Implements hook_node_type_update().
Dries's avatar
Dries committed
811
 *
812
813
 * Updates book.settings configuration object if the machine-readable name of a
 * node type is changed.
Dries's avatar
Dries committed
814
 */
815
function book_node_type_update(NodeTypeInterface $type) {
816
  if ($type->getOriginalId() != $type->id()) {
817
    $config = \Drupal::config('book.settings');
818
    // Update the list of node types that are allowed to be added to books.
819
    $allowed_types = $config->get('allowed_types');
820
    $old_key = array_search($type->getOriginalId(), $allowed_types);
821

822
    if ($old_key !== FALSE) {
823
      $allowed_types[$old_key] = $type->id();
824
      // Ensure that the allowed_types array is sorted consistently.
825
      // @see BookSettingsForm::submitForm()
826
      sort($allowed_types);
827
      $config->set('allowed_types', $allowed_types);
828
829
830
    }

    // Update the setting for the "Add child page" link.
831
    if ($config->get('child_type') == $type->getOriginalId()) {
832
      $config->set('child_type', $type->id());
833
    }
834
    $config->save();
Dries's avatar
Dries committed
835
  }
Dries's avatar