Simpletest Coverage - modules/book/book.module

1 <?php
2 // $Id: book.module,v 1.503 2009/08/11 15:50:55 webchick Exp $
3
4 /**
5 * @file
6 * Allows users to create and organize related content in an outline.
7 */
8
9 /**
10 * Implement hook_theme().
11 */
12 function book_theme() {
13 return array(
14 'book_navigation' => array(
15 'arguments' => array('book_link' => NULL),
16 'template' => 'book-navigation',
17 ),
18 'book_export_html' => array(
19 'arguments' => array('title' => NULL, 'contents' => NULL, 'depth' => NULL),
20 'template' => 'book-export-html',
21 ),
22 'book_admin_table' => array(
23 'arguments' => array('form' => NULL),
24 ),
25 'book_title_link' => array(
26 'arguments' => array('link' => NULL),
27 ),
28 'book_all_books_block' => array(
29 'arguments' => array('book_menus' => array()),
30 'template' => 'book-all-books-block',
31 ),
32 'book_node_export_html' => array(
33 'arguments' => array('node' => NULL, 'children' => NULL),
34 'template' => 'book-node-export-html',
35 ),
36 );
37 }
38
39 /**
40 * Implement hook_permission().
41 */
42 function book_permission() {
43 return array(
44 'administer book outlines' => array(
45 'title' => t('Administer book outlines'),
46 'description' => t('Manage books through the administration panel.'),
47 ),
48 'create new books' => array(
49 'title' => t('Create new books'),
50 'description' => t('Add new top-level books.'),
51 ),
52 'add content to books' => array(
53 'title' => t('Add content to books'),
54 'description' => t('Add new content and child pages to books.'),
55 ),
56 'access printer-friendly version' => array(
57 'title' => t('Access printer-friendly version'),
58 'description' => t('View a book page and all of its sub-pages as a single document for ease of printing. Can be performance heavy.'),
59 ),
60 );
61 }
62
63 /**
64 * Inject links into $node as needed.
65 */
66 function book_node_view_link($node, $build_mode) {
67 $links = array();
68
69 if (isset($node->book['depth'])) {
70 if ($build_mode == 'full') {
71 $child_type = variable_get('book_child_type', 'book');
72 if ((user_access('add content to books') || user_access('administer book outlines')) && node_access('create', $child_type) && $node->status == 1 && $node->book['depth'] < MENU_MAX_DEPTH) {
73 $links['book_add_child'] = array(
74 'title' => t('Add child page'),
75 'href' => 'node/add/' . str_replace('_', '-', $child_type),
76 'query' => 'parent=' . $node->book['mlid'],
77 );
78 }
79
80 if (user_access('access printer-friendly version')) {
81 $links['book_printer'] = array(
82 'title' => t('Printer-friendly version'),
83 'href' => 'book/export/html/' . $node->nid,
84 'attributes' => array('title' => t('Show a printer-friendly version of this book page and its sub-pages.'))
85 );
86 }
87 }
88 }
89
90 if (!empty($links)) {
91 $node->content['links']['book'] = array(
92 '#theme' => 'links',
93 '#links' => $links,
94 '#attributes' => array('class' => 'links inline'),
95 );
96 }
97 }
98
99 /**
100 * Implement hook_menu().
101 */
102 function book_menu() {
103 $items['admin/content/book'] = array(
104 'title' => 'Books',
105 'page callback' => 'book_admin_overview',
106 'access arguments' => array('administer book outlines'),
107 'type' => MENU_LOCAL_TASK,
108 );
109 $items['admin/content/book/list'] = array(
110 'title' => 'List',
111 'type' => MENU_DEFAULT_LOCAL_TASK,
112 );
113 $items['admin/content/book/settings'] = array(
114 'title' => 'Settings',
115 'page callback' => 'drupal_get_form',
116 'page arguments' => array('book_admin_settings'),
117 'access arguments' => array('administer site configuration'),
118 'type' => MENU_LOCAL_TASK,
119 'weight' => 8,
120 );
121 $items['admin/content/book/%node'] = array(
122 'title' => 'Re-order book pages and change titles',
123 'page callback' => 'drupal_get_form',
124 'page arguments' => array('book_admin_edit', 3),
125 'access callback' => '_book_outline_access',
126 'access arguments' => array(3),
127 'type' => MENU_CALLBACK,
128 );
129 $items['book'] = array(
130 'title' => 'Books',
131 'page callback' => 'book_render',
132 'access arguments' => array('access content'),
133 'type' => MENU_SUGGESTED_ITEM,
134 );
135 $items['book/export/%/%'] = array(
136 'page callback' => 'book_export',
137 'page arguments' => array(2, 3),
138 'access arguments' => array('access printer-friendly version'),
139 'type' => MENU_CALLBACK,
140 );
141 $items['node/%node/outline'] = array(
142 'title' => 'Outline',
143 'page callback' => 'book_outline',
144 'page arguments' => array(1),
145 'access callback' => '_book_outline_access',
146 'access arguments' => array(1),
147 'type' => MENU_LOCAL_TASK,
148 'weight' => 2,
149 );
150 $items['node/%node/outline/remove'] = array(
151 'title' => 'Remove from outline',
152 'page callback' => 'drupal_get_form',
153 'page arguments' => array('book_remove_form', 1),
154 'access callback' => '_book_outline_remove_access',
155 'access arguments' => array(1),
156 'type' => MENU_CALLBACK,
157 );
158 $items['book/js/form'] = array(
159 'page callback' => 'book_form_update',
160 'access arguments' => array('access content'),
161 'type' => MENU_CALLBACK,
162 );
163
164 return $items;
165 }
166
167 /**
168 * Menu item access callback - determine if the outline tab is accessible.
169 */
170 function _book_outline_access($node) {
171 return user_access('administer book outlines') && node_access('view', $node);
172 }
173
174 /**
175 * Menu item access callback - determine if the user can remove nodes from the outline.
176 */
177 function _book_outline_remove_access($node) {
178 return isset($node->book) && ($node->book['bid'] != $node->nid) && _book_outline_access($node);
179 }
180
181 /**
182 * Implement hook_init().
183 */
184 function book_init() {
185 drupal_add_css(drupal_get_path('module', 'book') . '/book.css');
186 }
187
188 /**
189 * Implement hook_field_build_modes().
190 */
191 function book_field_build_modes($obj_type) {
192 $modes = array();
193 if ($obj_type == 'node') {
194 $modes = array(
195 'print' => t('Print'),
196 );
197 }
198 return $modes;
199 }
200
201 /**
202 * Implement hook_block_list().
203 */
204 function book_block_list() {
205 $block = array();
206 $block['navigation']['info'] = t('Book navigation');
207 $block['navigation']['cache'] = BLOCK_CACHE_PER_PAGE | BLOCK_CACHE_PER_ROLE;
208
209 return $block;
210 }
211
212 /**
213 * Implement hook_block_view().
214 *
215 * Displays the book table of contents in a block when the current page is a
216 * single-node view of a book node.
217 */
218 function book_block_view($delta = '') {
219 $block = array();
220 $current_bid = 0;
221 if ($node = menu_get_object()) {
222 $current_bid = empty($node->book['bid']) ? 0 : $node->book['bid'];
223 }
224
225 if (variable_get('book_block_mode', 'all pages') == 'all pages') {
226 $block['subject'] = t('Book navigation');
227 $book_menus = array();
228 $pseudo_tree = array(0 => array('below' => FALSE));
229 foreach (book_get_books() as $book_id => $book) {
230 if ($book['bid'] == $current_bid) {
231 // If the current page is a node associated with a book, the menu
232 // needs to be retrieved.
233 $book_menus[$book_id] = menu_tree_output(menu_tree_all_data($node->book['menu_name'], $node->book));
234 }
235 else {
236 // Since we know we will only display a link to the top node, there
237 // is no reason to run an additional menu tree query for each book.
238 $book['in_active_trail'] = FALSE;
239 $pseudo_tree[0]['link'] = $book;
240 $book_menus[$book_id] = menu_tree_output($pseudo_tree);
241 }
242 }
243 $block['content'] = theme('book_all_books_block', $book_menus);
244 }
245 elseif ($current_bid) {
246 // Only display this block when the user is browsing a book.
247 $select = db_select('node');
248 $select->addField('node', 'title');
249 $select->condition('nid', $node->book['bid']);
250 $select->addTag('node_access');
251 $title = $select->execute()->fetchField();
252 // Only show the block if the user has view access for the top-level node.
253 if ($title) {
254 $tree = menu_tree_all_data($node->book['menu_name'], $node->book);
255 // There should only be one element at the top level.
256 $data = array_shift($tree);
257 $block['subject'] = theme('book_title_link', $data['link']);
258 $block['content'] = ($data['below']) ? menu_tree_output($data['below']) : '';
259 }
260 }
261
262 return $block;
263 }
264
265 /**
266 * Implement hook_block_configure().
267 */
268 function book_block_configure($delta = '') {
269 $block = array();
270 $options = array(
271 'all pages' => t('Show block on all pages'),
272 'book pages' => t('Show block only on book pages'),
273 );
274 $form['book_block_mode'] = array(
275 '#type' => 'radios',
276 '#title' => t('Book navigation block display'),
277 '#options' => $options,
278 '#default_value' => variable_get('book_block_mode', 'all pages'),
279 '#description' => t("If <em>Show block on all pages</em> is selected, the block will contain the automatically generated menus for all of the site's books. If <em>Show block only on book pages</em> is selected, the block will contain only the one menu corresponding to the current page's book. In this case, if the current page is not in a book, no block will be displayed. The <em>Page specific visibility settings</em> or other visibility settings can be used in addition to selectively display this block."),
280 );
281
282 return $form;
283 }
284
285 /**
286 * Implement hook_block_save().
287 */
288 function book_block_save($delta = '', $edit = array()) {
289 $block = array();
290 variable_set('book_block_mode', $edit['book_block_mode']);
291 }
292
293 /**
294 * Generate the HTML output for a link to a book title when used as a block title.
295 *
296 * @ingroup themeable
297 */
298 function theme_book_title_link($link) {
299 $link['options']['attributes']['class'] = 'book-title';
300
301 return l($link['title'], $link['href'], $link['options']);
302 }
303
304 /**
305 * Returns an array of all books.
306 *
307 * This list may be used for generating a list of all the books, or for building
308 * the options for a form select.
309 */
310 function book_get_books() {
311 $all_books = &drupal_static(__FUNCTION__);
312
313 if (!isset($all_books)) {
314 $all_books = array();
315 $nids = db_query("SELECT DISTINCT(bid) FROM {book}")->fetchCol();
316
317 if ($nids) {
318 $query = db_select('book', 'b', array('fetch' => PDO::FETCH_ASSOC));
319 $node_alias = $query->join('node', 'n', 'b.nid = n.nid');
320 $menu_links_alias = $query->join('menu_links', 'ml', 'b.mlid = ml.mlid');
321 $query->addField('n', 'type', 'type');
322 $query->addField('n', 'title', 'title');
323 $query->fields('b');
324 $query->fields($menu_links_alias);
325 $query->condition('n.nid', $nids, 'IN');
326 $query->condition('n.status', 1);
327 $query->orderBy('ml.weight');
328 $query->orderBy('ml.link_title');
329 $query->addTag('node_access');
330 $result2 = $query->execute();
331 foreach ($result2 as $link) {
332 $link['href'] = $link['link_path'];
333 $link['options'] = unserialize($link['options']);
334 $all_books[$link['bid']] = $link;
335 }
336 }
337 }
338
339 return $all_books;
340 }
341
342 /**
343 * Implement hook_form_alter().
344 *
345 * Adds the book fieldset to the node form.
346 *
347 * @see book_pick_book_submit()
348 * @see book_submit()
349 */
350 function book_form_alter(&$form, $form_state, $form_id) {
351
352 if (!empty($form['#node_edit_form'])) {
353 // Add elements to the node form.
354 $node = $form['#node'];
355
356 $access = user_access('administer book outlines');
357 if (!$access) {
358 if (user_access('add content to books') && ((!empty($node->book['mlid']) && !empty($node->nid)) || book_type_is_allowed($node->type))) {
359 // Already in the book hierarchy, or this node type is allowed.
360 $access = TRUE;
361 }
362 }
363
364 if ($access) {
365 _book_add_form_elements($form, $node);
366 $form['book']['pick-book'] = array(
367 '#type' => 'submit',
368 '#value' => t('Change book (update list of parents)'),
369 // Submit the node form so the parent select options get updated.
370 // This is typically only used when JS is disabled. Since the parent options
371 // won't be changed via AJAX, a button is provided in the node form to submit
372 // the form and generate options in the parent select corresponding to the
373 // selected book. This is similar to what happens during a node preview.
374 '#submit' => array('node_form_submit_build_node'),
375 '#weight' => 20,
376 );
377 }
378 }
379 }
380
381 /**
382 * Build the parent selection form element for the node form or outline tab.
383 *
384 * This function is also called when generating a new set of options during the
385 * AJAX callback, so an array is returned that can be used to replace an existing
386 * form element.
387 */
388 function _book_parent_select($book_link) {
389 if (variable_get('menu_override_parent_selector', FALSE)) {
390 return array();
391 }
392 // Offer a message or a drop-down to choose a different parent page.
393 $form = array(
394 '#type' => 'hidden',
395 '#value' => -1,
396 '#prefix' => '<div id="edit-book-plid-wrapper">',
397 '#suffix' => '</div>',
398 );
399
400 if ($book_link['nid'] === $book_link['bid']) {
401 // This is a book - at the top level.
402 if ($book_link['original_bid'] === $book_link['bid']) {
403 $form['#prefix'] .= '<em>' . t('This is the top-level page in this book.') . '</em>';
404 }
405 else {
406 $form['#prefix'] .= '<em>' . t('This will be the top-level page in this book.') . '</em>';
407 }
408 }
409 elseif (!$book_link['bid']) {
410 $form['#prefix'] .= '<em>' . t('No book selected.') . '</em>';
411 }
412 else {
413 $form = array(
414 '#type' => 'select',
415 '#title' => t('Parent item'),
416 '#default_value' => $book_link['plid'],
417 '#description' => t('The parent page in the book. The maximum depth for a book and all child pages is !maxdepth. Some pages in the selected book may not be available as parents if selecting them would exceed this limit.', array('!maxdepth' => MENU_MAX_DEPTH)),
418 '#options' => book_toc($book_link['bid'], $book_link['parent_depth_limit'], array($book_link['mlid'])),
419 '#attributes' => array('class' => 'book-title-select'),
420 );
421 }
422
423 return $form;
424 }
425
426 /**
427 * Build the common elements of the book form for the node and outline forms.
428 */
429 function _book_add_form_elements(&$form, $node) {
430 // Need this for AJAX.
431 $form['#cache'] = TRUE;
432
433 $form['book'] = array(
434 '#type' => 'fieldset',
435 '#title' => t('Book outline'),
436 '#weight' => 10,
437 '#collapsible' => TRUE,
438 '#collapsed' => TRUE,
439 '#group' => 'additional_settings',
440 '#attached_js' => array(drupal_get_path('module', 'book') . '/book.js'),
441 '#tree' => TRUE,
442 '#attributes' => array('class' => 'book-outline-form'),
443 );
444 foreach (array('menu_name', 'mlid', 'nid', 'router_path', 'has_children', 'options', 'module', 'original_bid', 'parent_depth_limit') as $key) {
445 $form['book'][$key] = array(
446 '#type' => 'value',
447 '#value' => $node->book[$key],
448 );
449 }
450
451 $form['book']['plid'] = _book_parent_select($node->book);
452
453 $form['book']['weight'] = array(
454 '#type' => 'weight',
455 '#title' => t('Weight'),
456 '#default_value' => $node->book['weight'],
457 '#delta' => 15,
458 '#weight' => 5,
459 '#description' => t('Pages at a given level are ordered first by weight and then by title.'),
460 );
461 $options = array();
462 $nid = isset($node->nid) ? $node->nid : 'new';
463
464 if (isset($node->nid) && ($nid == $node->book['original_bid']) && ($node->book['parent_depth_limit'] == 0)) {
465 // This is the top level node in a maximum depth book and thus cannot be moved.
466 $options[$node->nid] = $node->title;
467 }
468 else {
469 foreach (book_get_books() as $book) {
470 $options[$book['nid']] = $book['title'];
471 }
472 }
473
474 if (user_access('create new books') && ($nid == 'new' || ($nid != $node->book['original_bid']))) {
475 // The node can become a new book, if it is not one already.
476 $options = array($nid => '<' . t('create a new book') . '>') + $options;
477 }
478 if (!$node->book['mlid']) {
479 // The node is not currently in the hierarchy.
480 $options = array(0 => '<' . t('none') . '>') + $options;
481 }
482
483 // Add a drop-down to select the destination book.
484 $form['book']['bid'] = array(
485 '#type' => 'select',
486 '#title' => t('Book'),
487 '#default_value' => $node->book['bid'],
488 '#options' => $options,
489 '#access' => (bool)$options,
490 '#description' => t('Your page will be a part of the selected book.'),
491 '#weight' => -5,
492 '#attributes' => array('class' => 'book-title-select'),
493 '#ahah' => array(
494 'path' => 'book/js/form',
495 'wrapper' => 'edit-book-plid-wrapper',
496 'effect' => 'slide',
497 ),
498 );
499 }
500
501 /**
502 * Common helper function to handles additions and updates to the book outline.
503 *
504 * Performs all additions and updates to the book outline through node addition,
505 * node editing, node deletion, or the outline tab.
506 */
507 function _book_update_outline($node) {
508 if (empty($node->book['bid'])) {
509 return FALSE;
510 }
511 $new = empty($node->book['mlid']);
512
513 $node->book['link_path'] = 'node/' . $node->nid;
514 $node->book['link_title'] = $node->title;
515 $node->book['parent_mismatch'] = FALSE; // The normal case.
516
517 if ($node->book['bid'] == $node->nid) {
518 $node->book['plid'] = 0;
519 $node->book['menu_name'] = book_menu_name($node->nid);
520 }
521 else {
522 // Check in case the parent is not is this book; the book takes precedence.
523 if (!empty($node->book['plid'])) {
524 $parent = db_query("SELECT * FROM {book} WHERE mlid = :mlid", array(
525 ':mlid' => $node->book['plid'],
526 ))->fetchAssoc();
527 }
528 if (empty($node->book['plid']) || !$parent || $parent['bid'] != $node->book['bid']) {
529 $node->book['plid'] = db_query("SELECT mlid FROM {book} WHERE nid = :nid", array(
530 ':nid' => $node->book['bid'],
531 ))->fetchField();
532 $node->book['parent_mismatch'] = TRUE; // Likely when JS is disabled.
533 }
534 }
535
536 if (menu_link_save($node->book)) {
537 if ($new) {
538 // Insert new.
539 db_insert('book')
540 ->fields(array(
541 'nid' => $node->nid,
542 'mlid' => $node->book['mlid'],
543 'bid' => $node->book['bid'],
544 ))
545 ->execute();
546 }
547 else {
548 if ($node->book['bid'] != db_query("SELECT bid FROM {book} WHERE nid = :nid", array(
549 ':nid' => $node->nid,
550 ))->fetchField()) {
551 // Update the bid for this page and all children.
552 book_update_bid($node->book);
553 }
554 }
555
556 return TRUE;
557 }
558
559 // Failed to save the menu link.
560 return FALSE;
561 }
562
563 /**
564 * Update the bid for a page and its children when it is moved to a new book.
565 *
566 * @param $book_link
567 * A fully loaded menu link that is part of the book hierarchy.
568 */
569 function book_update_bid($book_link) {
570 $query = db_select('menu_links');
571 $query->addField('menu_links', 'mlid');
572 for ($i = 1; $i <= MENU_MAX_DEPTH && $book_link["p$i"]; $i++) {
573 $query->condition("p$i", $book_link["p$i"]);
574 }
575 $mlids = $query->execute()->fetchCol();
576
577 if ($mlids) {
578 db_update('book')
579 ->fields(array('bid' => $book_link['bid']))
580 ->condition('mlid', $mlids, 'IN')
581 ->execute();
582 }
583 }
584
585 /**
586 * Get the book menu tree for a page, and return it as a linear array.
587 *
588 * @param $book_link
589 * A fully loaded menu link that is part of the book hierarchy.
590 * @return
591 * A linear array of menu links in the order that the links are shown in the
592 * menu, so the previous and next pages are the elements before and after the
593 * element corresponding to $node. The children of $node (if any) will come
594 * immediately after it in the array.
595 */
596 function book_get_flat_menu($book_link) {
597 $flat = &drupal_static(__FUNCTION__, array());
598
599 if (!isset($flat[$book_link['mlid']])) {
600 // Call menu_tree_all_data() to take advantage of the menu system's caching.
601 $tree = menu_tree_all_data($book_link['menu_name'], $book_link);
602 $flat[$book_link['mlid']] = array();
603 _book_flatten_menu($tree, $flat[$book_link['mlid']]);
604 }
605
606 return $flat[$book_link['mlid']];
607 }
608
609 /**
610 * Recursive helper function for book_get_flat_menu().
611 */
612 function _book_flatten_menu($tree, &$flat) {
613 foreach ($tree as $data) {
614 if (!$data['link']['hidden']) {
615 $flat[$data['link']['mlid']] = $data['link'];
616 if ($data['below']) {
617 _book_flatten_menu($data['below'], $flat);
618 }
619 }
620 }
621 }
622
623 /**
624 * Fetches the menu link for the previous page of the book.
625 */
626 function book_prev($book_link) {
627 // If the parent is zero, we are at the start of a book.
628 if ($book_link['plid'] == 0) {
629 return NULL;
630 }
631 $flat = book_get_flat_menu($book_link);
632 // Assigning the array to $flat resets the array pointer for use with each().
633 $curr = NULL;
634 do {
635 $prev = $curr;
636 list($key, $curr) = each($flat);
637 } while ($key && $key != $book_link['mlid']);
638
639 if ($key == $book_link['mlid']) {
640 // The previous page in the book may be a child of the previous visible link.
641 if ($prev['depth'] == $book_link['depth'] && $prev['has_children']) {
642 // The subtree will have only one link at the top level - get its data.
643 $tree = book_menu_subtree_data($prev);
644 $data = array_shift($tree);
645 // The link of interest is the last child - iterate to find the deepest one.
646 while ($data['below']) {
647 $data = end($data['below']);
648 }
649
650 return $data['link'];
651 }
652 else {
653 return $prev;
654 }
655 }
656 }
657
658 /**
659 * Fetches the menu link for the next page of the book.
660 */
661 function book_next($book_link) {
662 $flat = book_get_flat_menu($book_link);
663 // Assigning the array to $flat resets the array pointer for use with each().
664 do {
665 list($key, $curr) = each($flat);
666 }
667 while ($key && $key != $book_link['mlid']);
668
669 if ($key == $book_link['mlid']) {
670 return current($flat);
671 }
672 }
673
674 /**
675 * Format the menu links for the child pages of the current page.
676 */
677 function book_children($book_link) {
678 $flat = book_get_flat_menu($book_link);
679
680 $children = array();
681
682 if ($book_link['has_children']) {
683 // Walk through the array until we find the current page.
684 do {
685 $link = array_shift($flat);
686 }
687 while ($link && ($link['mlid'] != $book_link['mlid']));
688 // Continue though the array and collect the links whose parent is this page.
689 while (($link = array_shift($flat)) && $link['plid'] == $book_link['mlid']) {
690 $data['link'] = $link;
691 $data['below'] = '';
692 $children[] = $data;
693 }
694 }
695
696 return $children ? menu_tree_output($children) : '';
697 }
698
699 /**
700 * Generate the corresponding menu name from a book ID.
701 */
702 function book_menu_name($bid) {
703 return 'book-toc-' . $bid;
704 }
705
706 /**
707 * Implement hook_node_load().
708 */
709 function book_node_load($nodes, $types) {
710 $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));
711 foreach ($result as $record) {
712 $nodes[$record['nid']]->book = $record;
713 $nodes[$record['nid']]->book['href'] = $record['link_path'];
714 $nodes[$record['nid']]->book['title'] = $record['link_title'];
715 $nodes[$record['nid']]->book['options'] = unserialize($record['options']);
716 }
717 }
718
719 /**
720 * Implement hook_node_view().
721 */
722 function book_node_view($node, $build_mode) {
723 if ($build_mode == 'full') {
724 if (!empty($node->book['bid']) && empty($node->in_preview)) {
725 $node->content['book_navigation'] = array(
726 '#markup' => theme('book_navigation', $node->book),
727 '#weight' => 100,
728 );
729 }
730 }
731
732 if ($build_mode != 'rss') {
733 book_node_view_link($node, $build_mode);
734 }
735 }
736
737 /**
738 * Implement hook_page_alter().
739 *
740 * Add the book menu to the list of menus used to build the active trail when
741 * viewing a book page.
742 */
743 function book_page_alter(&$page) {
744 if (($node = menu_get_object()) && !empty($node->book['bid'])) {
745 $active_menus = menu_get_active_menu_names();
746 $active_menus[] = $node->book['menu_name'];
747 menu_set_active_menu_names($active_menus);
748 }
749 }
750
751 /**
752 * Implement hook_node_presave().
753 */
754 function book_node_presave($node) {
755 // Always save a revision for non-administrators.
756 if (!empty($node->book['bid']) && !user_access('administer nodes')) {
757 $node->revision = 1;
758 // The database schema requires a log message for every revision.
759 if (!isset($node->log)) {
760 $node->log = '';
761 }
762 }
763 // Make sure a new node gets a new menu link.
764 if (empty($node->nid)) {
765 $node->book['mlid'] = NULL;
766 }
767 }
768
769 /**
770 * Implement hook_node_insert().
771 */
772 function book_node_insert($node) {
773 if (!empty($node->book['bid'])) {
774 if ($node->book['bid'] == 'new') {
775 // New nodes that are their own book.
776 $node->book['bid'] = $node->nid;
777 }
778 $node->book['nid'] = $node->nid;
779 $node->book['menu_name'] = book_menu_name($node->book['bid']);
780 _book_update_outline($node);
781 }
782 }
783
784 /**
785 * Implement hook_node_update().
786 */
787 function book_node_update($node) {
788 if (!empty($node->book['bid'])) {
789 if ($node->book['bid'] == 'new') {
790 // New nodes that are their own book.
791 $node->book['bid'] = $node->nid;
792 }
793 $node->book['nid'] = $node->nid;
794 $node->book['menu_name'] = book_menu_name($node->book['bid']);
795 _book_update_outline($node);
796 }
797 }
798
799 /**
800 * Implement hook_node_delete().
801 */
802 function book_node_delete($node) {
803 if (!empty($node->book['bid'])) {
804 if ($node->nid == $node->book['bid']) {
805 // Handle deletion of a top-level post.
806 $result = db_query("SELECT b.nid FROM {menu_links} ml INNER JOIN {book} b on b.mlid = ml.mlid WHERE ml.plid = :plid", array(
807 ':plid' => $node->book['mlid']
808 ));
809 foreach ($result as $child) {
810 $child_node = node_load($child->nid);
811 $child_node->book['bid'] = $child_node->nid;
812 _book_update_outline($child_node);
813 }
814 }
815 menu_link_delete($node->book['mlid']);
816 db_delete('book')
817 ->condition('mlid', $node->book['mlid'])
818 ->execute();
819 }
820 }
821
822 /**
823 * Implement hook_node_prepare().
824 */
825 function book_node_prepare($node) {
826 // Prepare defaults for the add/edit form.
827 if (empty($node->book) && (user_access('add content to books') || user_access('administer book outlines'))) {
828 $node->book = array();
829
830 if (empty($node->nid) && isset($_GET['parent']) && is_numeric($_GET['parent'])) {
831 // Handle "Add child page" links:
832 $parent = book_link_load($_GET['parent']);
833
834 if ($parent && $parent['access']) {
835 $node->book['bid'] = $parent['bid'];
836 $node->book['plid'] = $parent['mlid'];
837 $node->book['menu_name'] = $parent['menu_name'];
838 }
839 }
840 // Set defaults.
841 $node->book += _book_link_defaults(!empty($node->nid) ? $node->nid : 'new');
842 }
843 else {
844 if (isset($node->book['bid']) && !isset($node->book['original_bid'])) {
845 $node->book['original_bid'] = $node->book['bid'];
846 }
847 }
848 // Find the depth limit for the parent select.
849 if (isset($node->book['bid']) && !isset($node->book['parent_depth_limit'])) {
850 $node->book['parent_depth_limit'] = _book_parent_depth_limit($node->book);
851 }
852 }
853
854 /**
855 * Find the depth limit for items in the parent select.
856 */
857 function _book_parent_depth_limit($book_link) {
858 return MENU_MAX_DEPTH - 1 - (($book_link['mlid'] && $book_link['has_children']) ? menu_link_children_relative_depth($book_link) : 0);
859 }
860
861 /**
862 * Form altering function for the confirm form for a single node deletion.
863 */
864 function book_form_node_delete_confirm_alter(&$form, $form_state) {
865 $node = node_load($form['nid']['#value']);
866
867 if (isset($node->book) && $node->book['has_children']) {
868 $form['book_warning'] = array(
869 '#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->title)) . '</p>',
870 '#weight' => -10,
871 );
872 }
873 }
874
875 /**
876 * Return an array with default values for a book link.
877 */
878 function _book_link_defaults($nid) {
879 return array('original_bid' => 0, 'menu_name' => '', 'nid' => $nid, 'bid' => 0, 'router_path' => 'node/%', 'plid' => 0, 'mlid' => 0, 'has_children' => 0, 'weight' => 0, 'module' => 'book', 'options' => array());
880 }
881
882 /**
883 * Process variables for book-navigation.tpl.php.
884 *
885 * The $variables array contains the following arguments:
886 * - $book_link
887 *
888 * @see book-navigation.tpl.php
889 */
890 function template_preprocess_book_navigation(&$variables) {
891 $book_link = $variables['book_link'];
892
893 // Provide extra variables for themers. Not needed by default.
894 $variables['book_id'] = $book_link['bid'];
895 $variables['book_title'] = check_plain($book_link['link_title']);
896 $variables['book_url'] = 'node/' . $book_link['bid'];
897 $variables['current_depth'] = $book_link['depth'];
898 $variables['tree'] = '';
899
900 if ($book_link['mlid']) {
901 $variables['tree'] = book_children($book_link);
902
903 if ($prev = book_prev($book_link)) {
904 $prev_href = url($prev['href']);
905 drupal_add_link(array('rel' => 'prev', 'href' => $prev_href));
906 $variables['prev_url'] = $prev_href;
907 $variables['prev_title'] = check_plain($prev['title']);
908 }
909
910 if ($book_link['plid'] && $parent = book_link_load($book_link['plid'])) {
911 $parent_href = url($parent['href']);
912 drupal_add_link(array('rel' => 'up', 'href' => $parent_href));
913 $variables['parent_url'] = $parent_href;
914 $variables['parent_title'] = check_plain($parent['title']);
915 }
916
917 if ($next = book_next($book_link)) {
918 $next_href = url($next['href']);
919 drupal_add_link(array('rel' => 'next', 'href' => $next_href));
920 $variables['next_url'] = $next_href;
921 $variables['next_title'] = check_plain($next['title']);
922 }
923 }
924
925 $variables['has_links'] = FALSE;
926 // Link variables to filter for values and set state of the flag variable.
927 $links = array('prev_url', 'prev_title', 'parent_url', 'parent_title', 'next_url', 'next_title');
928 foreach ($links as $link) {
929 if (isset($variables[$link])) {
930 // Flag when there is a value.
931 $variables['has_links'] = TRUE;
932 }
933 else {
934 // Set empty to prevent notices.
935 $variables[$link] = '';
936 }
937 }
938 }
939
940 /**
941 * A recursive helper function for book_toc().
942 */
943 function _book_toc_recurse($tree, $indent, &$toc, $exclude, $depth_limit) {
944 foreach ($tree as $data) {
945 if ($data['link']['depth'] > $depth_limit) {
946 // Don't iterate through any links on this level.
947 break;
948 }
949
950 if (!in_array($data['link']['mlid'], $exclude)) {
951 $toc[$data['link']['mlid']] = $indent . ' ' . truncate_utf8($data['link']['title'], 30, TRUE, TRUE);
952 if ($data['below']) {
953 _book_toc_recurse($data['below'], $indent . '--', $toc, $exclude, $depth_limit);
954 }
955 }
956 }
957 }
958
959 /**
960 * Returns an array of book pages in table of contents order.
961 *
962 * @param $bid
963 * The ID of the book whose pages are to be listed.
964 * @param $depth_limit
965 * Any link deeper than this value will be excluded (along with its children).
966 * @param $exclude
967 * Optional array of mlid values. Any link whose mlid is in this array
968 * will be excluded (along with its children).
969 * @return
970 * An array of mlid, title pairs for use as options for selecting a book page.
971 */
972 function book_toc($bid, $depth_limit, $exclude = array()) {
973 $tree = menu_tree_all_data(book_menu_name($bid));
974 $toc = array();
975 _book_toc_recurse($tree, '', $toc, $exclude, $depth_limit);
976
977 return $toc;
978 }
979
980 /**
981 * Process variables for book-export-html.tpl.php.
982 *
983 * The $variables array contains the following arguments:
984 * - $title
985 * - $contents
986 * - $depth
987 *
988 * @see book-export-html.tpl.php
989 */
990 function template_preprocess_book_export_html(&$variables) {
991 global $base_url, $language;
992
993 $variables['title'] = check_plain($variables['title']);
994 $variables['base_url'] = $base_url;
995 $variables['language'] = $language;
996 $variables['language_rtl'] = ($language->direction == LANGUAGE_RTL);
997 $variables['head'] = drupal_get_html_head();
998 }
999
1000 /**
1001 * Traverse the book tree to build printable or exportable output.
1002 *
1003 * During the traversal, the $visit_func() callback is applied to each
1004 * node, and is called recursively for each child of the node (in weight,
1005 * title order).
1006 *
1007 * @param $tree
1008 * A subtree of the book menu hierarchy, rooted at the current page.
1009 * @param $visit_func
1010 * A function callback to be called upon visiting a node in the tree.
1011 * @return
1012 * The output generated in visiting each node.
1013 */
1014 function book_export_traverse($tree, $visit_func) {
1015 $output = '';
1016
1017 foreach ($tree as $data) {
1018 // Note- access checking is already performed when building the tree.
1019 if ($node = node_load($data['link']['nid'], FALSE)) {
1020 $children = '';
1021
1022 if ($data['below']) {
1023 $children = book_export_traverse($data['below'], $visit_func);
1024 }
1025
1026 if (function_exists($visit_func)) {
1027 $output .= call_user_func($visit_func, $node, $children);
1028 }
1029 else {
1030 // Use the default function.
1031 $output .= book_node_export($node, $children);
1032 }
1033 }
1034 }
1035
1036 return $output;
1037 }
1038
1039 /**
1040 * Generates printer-friendly HTML for a node.
1041 *
1042 * @see book_export_traverse()
1043 *
1044 * @param $node
1045 * The node that will be output.
1046 * @param $children
1047 * All the rendered child nodes within the current node.
1048 * @return
1049 * The HTML generated for the given node.
1050 */
1051 function book_node_export($node, $children = '') {
1052 $node = node_build_content($node, 'print');
1053 $node->rendered = drupal_render($node->content);
1054
1055 return theme('book_node_export_html', $node, $children);
1056 }
1057
1058 /**
1059 * Process variables for book-node-export-html.tpl.php.
1060 *
1061 * The $variables array contains the following arguments:
1062 * - $node
1063 * - $children
1064 *
1065 * @see book-node-export-html.tpl.php
1066 */
1067 function template_preprocess_book_node_export_html(&$variables) {
1068 $variables['depth'] = $variables['node']->book['depth'];
1069 $variables['title'] = check_plain($variables['node']->title);
1070 $variables['content'] = $variables['node']->rendered;
1071 }
1072
1073 /**
1074 * Determine if a given node type is in the list of types allowed for books.
1075 */
1076 function book_type_is_allowed($type) {
1077 return in_array($type, variable_get('book_allowed_types', array('book')));
1078 }
1079
1080 /**
1081 * Implement hook_node_type_update().
1082 *
1083 * Update book module's persistent variables if the machine-readable name of a
1084 * node type is changed.
1085 */
1086 function book_node_type_update($type) {
1087 if (!empty($type->old_type) && $type->old_type != $type->type) {
1088 // Update the list of node types that are allowed to be added to books.
1089 $allowed_types = variable_get('book_allowed_types', array('book'));
1090 $key = array_search($type->old_type, $allowed_types);
1091
1092 if ($key !== FALSE) {
1093 $allowed_types[$type->type] = $allowed_types[$key] ? $type->type : 0;
1094 unset($allowed_types[$key]);
1095 variable_set('book_allowed_types', $allowed_types);
1096 }
1097
1098 // Update the setting for the "Add child page" link.
1099 if (variable_get('book_child_type', 'book') == $type->old_type) {
1100 variable_set('book_child_type', $type->type);
1101 }
1102 }
1103 }
1104
1105 /**
1106 * Implement hook_help().
1107 */
1108 function book_help($path, $arg) {
1109 switch ($path) {
1110 case 'admin/help#book':
1111 $output = '<p>' . t('The book module is suited for creating structured, multi-page hypertexts such as site resource guides, manuals, and Frequently Asked Questions (FAQs). It permits a document to have chapters, sections, subsections, etc. Authors with suitable permissions can add pages to a collaborative book, placing them into the existing document by adding them to a table of contents menu.') . '</p>';
1112 $output .= '<p>' . t('Pages in the book hierarchy have navigation elements at the bottom of the page for moving through the text. These links lead to the previous and next pages in the book, and to the level above the current page in the book\'s structure. More comprehensive navigation may be provided by enabling the <em>book navigation block</em> on the <a href="@admin-block">blocks administration page</a>.', array('@admin-block' => url('admin/structure/block'))) . '</p>';
1113 $output .= '<p>' . t('Users can select the <em>printer-friendly version</em> link visible at the bottom of a book page to generate a printer-friendly display of the page and all of its subsections. ') . '</p>';
1114 $output .= '<p>' . t("Users with the <em>administer book outlines</em> permission can add a post of any content type to a book, by selecting the appropriate book while editing the post or by using the interface available on the post's <em>outline</em> tab.") . '</p>';
1115 $output .= '<p>' . t('Administrators can view a list of all books on the <a href="@admin-node-book">book administration page</a>. The <em>Outline</em> page for each book allows section titles to be edited or rearranged.', array('@admin-node-book' => url('admin/content/book'))) . '</p>';
1116 $output .= '<p>' . t('For more information, see the online handbook entry for <a href="@book">Book module</a>.', array('@book' => 'http://drupal.org/handbook/modules/book/')) . '</p>';
1117
1118 return $output;
1119
1120 case 'admin/content/book':
1121 return '<p>' . t('The book module offers a means to organize a collection of related posts, collectively known as a book. When viewed, these posts automatically display links to adjacent book pages, providing a simple navigation system for creating and reviewing structured content.') . '</p>';
1122
1123 case 'node/%/outline':
1124 return '<p>' . t('The outline feature allows you to include posts 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' => url('book'), '@book-admin' => url('admin/content/book'))) . '</p>';
1125 }
1126 }
1127
1128 /**
1129 * Like menu_link_load(), but adds additional data from the {book} table.
1130 *
1131 * Do not call when loading a node, since this function may call node_load().
1132 */
1133 function book_link_load($mlid) {
1134 if ($item = db_query("SELECT * FROM {menu_links} ml INNER JOIN {book} b ON b.mlid = ml.mlid LEFT JOIN {menu_router} m ON m.path = ml.router_path WHERE ml.mlid = :mlid", array(
1135 ':mlid' => $mlid,
1136 ))->fetchAssoc()) {
1137 _menu_link_translate($item);
1138 return $item;
1139 }
1140
1141 return FALSE;
1142 }
1143
1144 /**
1145 * Get the data representing a subtree of the book hierarchy.
1146 *
1147 * The root of the subtree will be the link passed as a parameter, so the
1148 * returned tree will contain this item and all its descendents in the menu tree.
1149 *
1150 * @param $item
1151 * A fully loaded menu link.
1152 * @return
1153 * An subtree of menu links in an array, in the order they should be rendered.
1154 */
1155 function book_menu_subtree_data($item) {
1156 $tree = &drupal_static(__FUNCTION__, array());
1157
1158 // Generate a cache ID (cid) specific for this $menu_name and $item.
1159 $cid = 'links:' . $item['menu_name'] . ':subtree-cid:' . $item['mlid'];
1160
1161 if (!isset($tree[$cid])) {
1162 $cache = cache_get($cid, 'cache_menu');
1163
1164 if ($cache && isset($cache->data)) {
1165 // If the cache entry exists, it will just be the cid for the actual data.
1166 // This avoids duplication of large amounts of data.
1167 $cache = cache_get($cache->data, 'cache_menu');
1168
1169 if ($cache && isset($cache->data)) {
1170 $data = $cache->data;
1171 }
1172 }
1173
1174 // If the subtree data was not in the cache, $data will be NULL.
1175 if (!isset($data)) {
1176 $query = db_select('menu_links', 'ml', array('fetch' => PDO::FETCH_ASSOC));
1177 $menu_router_alias = $query->join('menu_router', 'm', 'm.path = ml.router_path');
1178 $book_alias = $query->join('book', 'b', 'ml.mlid = b.mlid');
1179 $query->fields($book_alias);
1180 $query->fields($menu_router_alias, array('load_functions', 'to_arg_functions', 'access_callback', 'access_arguments', 'page_callback', 'page_arguments', 'title', 'title_callback', 'title_arguments', 'type'));
1181 $query->fields('ml');
1182 $query->condition('menu_name', $item['menu_name']);
1183 for ($i = 1; $i <= MENU_MAX_DEPTH && $item["p$i"]; ++$i) {
1184 $query->condition("p$i", $item["p$i"]);
1185 }
1186 for ($i = 1; $i <= MENU_MAX_DEPTH; ++$i) {
1187 $query->orderBy("p$i");
1188 }
1189
1190 $data['tree'] = menu_tree_data($query->execute(), array(), $item['depth']);
1191 $data['node_links'] = array();
1192 menu_tree_collect_node_links($data['tree'], $data['node_links']);
1193 // Compute the real cid for book subtree data.
1194 $tree_cid = 'links:' . $item['menu_name'] . ':subtree-data:' . md5(serialize($data));
1195 // Cache the data, if it is not already in the cache.
1196
1197 if (!cache_get($tree_cid, 'cache_menu')) {
1198 cache_set($tree_cid, $data, 'cache_menu');
1199 }
1200 // Cache the cid of the (shared) data using the menu and item-specific cid.
1201 cache_set($cid, $tree_cid, 'cache_menu');
1202 }
1203 // Check access for the current user to each item in the tree.
1204 menu_tree_check_access($data['tree'], $data['node_links']);
1205 $tree[$cid] = $data['tree'];
1206 }
1207
1208 return $tree[$cid];
1209 }
1210

Legend

Missed
lines code that were not excersized during program execution.
Covered
lines code were excersized during program execution.
Comment/non executable
Comment or non-executable line of code.
Dead
lines of code that according to xdebug could not be executed. This is counted as coverage code because in almost all cases it is code that runnable.