/[drupal]/drupal/modules/book/book.module
ViewVC logotype

Contents of /drupal/modules/book/book.module

Parent Directory Parent Directory | Revision Log Revision Log | View Revision Graph Revision Graph


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