Code coverage for /20080809/modules/book/book.module

Line #Times calledCode
1
<?php
2
// $Id: book.module,v 1.467 2008/07/16 21:59:26 dries Exp $
3
4
/**
5
 * @file
6
 * Allows users to structure the pages of a site in a hierarchy or
outline.
7
 */
8
9
/**
10
 * Implementation of hook_theme().
11
 */
1243
function book_theme() {
13
  return array(
14
    'book_navigation' => array(
151
      'arguments' => array('book_link' => NULL),
161
      'template' => 'book-navigation',
171
    ),
18
    'book_export_html' => array(
191
      'arguments' => array('title' => NULL, 'contents' => NULL, 'depth' =>
NULL),
201
      'template' => 'book-export-html',
211
    ),
22
    'book_admin_table' => array(
231
      'arguments' => array('form' => NULL),
241
    ),
25
    'book_title_link' => array(
261
      'arguments' => array('link' => NULL),
271
    ),
28
    'book_all_books_block' => array(
291
      'arguments' => array('book_menus' => array()),
301
      'template' => 'book-all-books-block',
311
    ),
32
    'book_node_export_html' => array(
331
      'arguments' => array('node' => NULL, 'children' => NULL),
341
      'template' => 'book-node-export-html',
351
    ),
361
  );
370
}
38
39
/**
40
 * Implementation of hook_perm().
41
 */
4243
function book_perm() {
43
  return array(
441
    'add content to books' => t('Add new content and child pages to
books.'),
451
    'administer book outlines' => t('Manage books through the
administration panel.'),
461
    'create new books' => t('Add new top-level books.'),
471
    'access printer-friendly version' => t('View a book page and all of its
sub-pages as a single document for ease of printing. Can be performance
heavy.'),
481
  );
490
}
50
51
/**
52
 * Implementation of hook_link().
53
 */
5443
function book_link($type, $node = NULL, $teaser = FALSE) {
5512
  $links = array();
56
5712
  if ($type == 'node' && isset($node->book)) {
5812
    if (!$teaser) {
5912
      $child_type = variable_get('book_child_type', 'book');
6012
      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) {
616
        $links['book_add_child'] = array(
626
          'title' => t('Add child page'),
636
          'href' => "node/add/" . str_replace('_', '-', $child_type),
646
          'query' => "parent=" . $node->book['mlid'],
65
        );
666
      }
67
6812
      if (user_access('access printer-friendly version')) {
696
        $links['book_printer'] = array(
706
          'title' => t('Printer-friendly version'),
716
          'href' => 'book/export/html/' . $node->nid,
726
          'attributes' => array('title' => t('Show a printer-friendly
version of this book page and its sub-pages.'))
736
        );
746
      }
7512
    }
7612
  }
77
7812
  return $links;
790
}
80
81
/**
82
 * Implementation of hook_menu().
83
 */
8443
function book_menu() {
851
  $items['admin/content/book'] = array(
861
    'title' => 'Books',
871
    'description' => "Manage your site's book outlines.",
881
    'page callback' => 'book_admin_overview',
891
    'access arguments' => array('administer book outlines'),
90
  );
911
  $items['admin/content/book/list'] = array(
921
    'title' => 'List',
931
    'type' => MENU_DEFAULT_LOCAL_TASK,
94
  );
951
  $items['admin/content/book/settings'] = array(
961
    'title' => 'Settings',
971
    'page callback' => 'drupal_get_form',
981
    'page arguments' => array('book_admin_settings'),
991
    'access arguments' => array('administer site configuration'),
1001
    'type' => MENU_LOCAL_TASK,
1011
    'weight' => 8,
102
  );
1031
  $items['admin/content/book/%node'] = array(
1041
    'title' => 'Re-order book pages and change titles',
1051
    'page callback' => 'drupal_get_form',
1061
    'page arguments' => array('book_admin_edit', 3),
1071
    'access callback' => '_book_outline_access',
1081
    'access arguments' => array(3),
1091
    'type' => MENU_CALLBACK,
110
  );
1111
  $items['book'] = array(
1121
    'title' => 'Books',
1131
    'page callback' => 'book_render',
1141
    'access arguments' => array('access content'),
1151
    'type' => MENU_SUGGESTED_ITEM,
116
  );
1171
  $items['book/export/%/%'] = array(
1181
    'page callback' => 'book_export',
1191
    'page arguments' => array(2, 3),
1201
    'access arguments' => array('access printer-friendly version'),
1211
    'type' => MENU_CALLBACK,
122
  );
1231
  $items['node/%node/outline'] = array(
1241
    'title' => 'Outline',
1251
    'page callback' => 'book_outline',
1261
    'page arguments' => array(1),
1271
    'access callback' => '_book_outline_access',
1281
    'access arguments' => array(1),
1291
    'type' => MENU_LOCAL_TASK,
1301
    'weight' => 2,
131
  );
1321
  $items['node/%node/outline/remove'] = array(
1331
    'title' => 'Remove from outline',
1341
    'page callback' => 'drupal_get_form',
1351
    'page arguments' => array('book_remove_form', 1),
1361
    'access callback' => '_book_outline_remove_access',
1371
    'access arguments' => array(1),
1381
    'type' => MENU_CALLBACK,
139
  );
1401
  $items['book/js/form'] = array(
1411
    'page callback' => 'book_form_update',
1421
    'access arguments' => array('access content'),
1431
    'type' => MENU_CALLBACK,
144
  );
145
1461
  return $items;
1470
}
148
149
/**
150
 * Menu item access callback - determine if the outline tab is accessible.
151
 */
15243
function _book_outline_access($node) {
15312
  return user_access('administer book outlines') && node_access('view',
$node);
1540
}
155
156
/**
157
 * Menu item access callback - determine if the user can remove nodes from
the outline.
158
 */
15943
function _book_outline_remove_access($node) {
1600
  return isset($node->book) && ($node->book['bid'] != $node->nid) &&
_book_outline_access($node);
1610
}
162
163
/**
164
 * Implementation of hook_init().
165
 */
16643
function book_init() {
16742
  drupal_add_css(drupal_get_path('module', 'book') . '/book.css');
16842
}
169
170
/**
171
 * Implementation of hook_block().
172
 *
173
 * Displays the book table of contents in a block when the current page is
a
174
 * single-node view of a book node.
175
 */
17643
function book_block($op = 'list', $delta = '', $edit = array()) {
1770
  $block = array();
178
  switch ($op) {
1790
    case 'list':
1800
      $block['navigation']['info'] = t('Book navigation');
1810
      $block['navigation']['cache'] = BLOCK_CACHE_PER_PAGE |
BLOCK_CACHE_PER_ROLE;
182
1830
      return $block;
184
1850
    case 'view':
1860
      $current_bid = 0;
1870
      if ($node = menu_get_object()) {
1880
        $current_bid = empty($node->book['bid']) ? 0 : $node->book['bid'];
1890
      }
190
1910
      if (variable_get('book_block_mode', 'all pages') == 'all pages') {
1920
        $block['subject'] = t('Book navigation');
1930
        $book_menus = array();
1940
        $pseudo_tree = array(0 => array('below' => FALSE));
1950
        foreach (book_get_books() as $book_id => $book) {
1960
          if ($book['bid'] == $current_bid) {
197
            // If the current page is a node associated with a book, the
menu
198
            // needs to be retrieved.
1990
            $book_menus[$book_id] =
menu_tree_output(menu_tree_all_data($node->book['menu_name'],
$node->book));
2000
          }
201
          else {
202
            // Since we know we will only display a link to the top node,
there
203
            // is no reason to run an additional menu tree query for each
book.
2040
            $book['in_active_trail'] = FALSE;
2050
            $pseudo_tree[0]['link'] = $book;
2060
            $book_menus[$book_id] = menu_tree_output($pseudo_tree);
207
          }
2080
        }
2090
        $block['content'] = theme('book_all_books_block', $book_menus);
2100
      }
2110
      elseif ($current_bid) {
212
        // Only display this block when the user is browsing a book.
2130
        $title = db_result(db_query(db_rewrite_sql('SELECT n.title FROM
{node} n WHERE n.nid = %d'), $node->book['bid']));
214
        // Only show the block if the user has view access for the
top-level node.
2150
        if ($title) {
2160
          $tree = menu_tree_all_data($node->book['menu_name'],
$node->book);
217
          // There should only be one element at the top level.
2180
          $data = array_shift($tree);
2190
          $block['subject'] = theme('book_title_link', $data['link']);
2200
          $block['content'] = ($data['below']) ?
menu_tree_output($data['below']) : '';
2210
        }
2220
      }
223
2240
      return $block;
225
2260
    case 'configure':
227
      $options = array(
2280
        'all pages' => t('Show block on all pages'),
2290
        'book pages' => t('Show block only on book pages'),
2300
      );
2310
      $form['book_block_mode'] = array(
2320
        '#type' => 'radios',
2330
        '#title' => t('Book navigation block display'),
2340
        '#options' => $options,
2350
        '#default_value' => variable_get('book_block_mode', 'all pages'),
2360
        '#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."),
237
        );
238
2390
      return $form;
240
2410
    case 'save':
2420
      variable_set('book_block_mode', $edit['book_block_mode']);
2430
      break;
2440
  }
2450
}
246
247
/**
248
 * Generate the HTML output for a link to a book title when used as a block
title.
249
 *
250
 * @ingroup themeable
251
 */
25243
function theme_book_title_link($link) {
2530
  $link['options']['attributes']['class'] =  'book-title';
254
2550
  return l($link['title'], $link['href'], $link['options']);
2560
}
257
258
/**
259
 * Returns an array of all books.
260
 *
261
 * This list may be used for generating a list of all the books, or for
building
262
 * the options for a form select.
263
 */
26443
function book_get_books() {
2658
  static $all_books;
266
2678
  if (!isset($all_books)) {
2688
    $all_books = array();
2698
    $result = db_query("SELECT DISTINCT(bid) FROM {book}");
2708
    $nids = array();
2718
    while ($book = db_fetch_array($result)) {
2727
      $nids[] = $book['bid'];
2737
    }
274
2758
    if ($nids) {
2767
      $result2 = db_query(db_rewrite_sql("SELECT n.type, n.title, b.*, ml.*
FROM {book} b INNER JOIN {node} n on b.nid = n.nid INNER JOIN {menu_links}
ml ON b.mlid = ml.mlid WHERE n.nid IN (" . implode(',', $nids) . ") AND
n.status = 1 ORDER BY ml.weight, ml.link_title"));
2777
      while ($link = db_fetch_array($result2)) {
2787
        $link['href'] = $link['link_path'];
2797
        $link['options'] = unserialize($link['options']);
2807
        $all_books[$link['bid']] = $link;
2817
      }
2827
    }
2838
  }
284
2858
  return $all_books;
2860
}
287
288
/**
289
 * Implementation of hook_form_alter().
290
 *
291
 * Adds the book fieldset to the node form.
292
 *
293
 * @see book_pick_book_submit()
294
 * @see book_submit()
295
 */
29643
function book_form_alter(&$form, $form_state, $form_id) {
297
29815
  if (isset($form['type']) && isset($form['#node']) &&
$form['type']['#value'] . '_node_form' == $form_id) {
299
    // Add elements to the node form.
3008
    $node = $form['#node'];
301
3028
    $access = user_access('administer book outlines');
3038
    if (!$access) {
3048
      if (user_access('add content to books') &&
((!empty($node->book['mlid']) && !empty($node->nid)) ||
book_type_is_allowed($node->type))) {
305
        // Already in the book hierarchy, or this node type is allowed.
3068
        $access = TRUE;
3078
      }
3088
    }
309
3108
    if ($access) {
3118
      _book_add_form_elements($form, $node);
3128
      $form['book']['pick-book'] = array(
3138
        '#type' => 'submit',
3148
        '#value' => t('Change book (update list of parents)'),
315
         // Submit the node form so the parent select options get updated.
316
         // This is typically only used when JS is disabled. Since the
parent options
317
         // won't be changed via AJAX, a button is provided in the node
form to submit
318
         // the form and generate options in the parent select
corresponding to the
319
         // selected book. This is similar to what happens during a node
preview.
3208
        '#submit' => array('node_form_submit_build_node'),
3218
        '#weight' => 20,
322
      );
3238
    }
3248
  }
32515
}
326
327
/**
328
 * Build the parent selection form element for the node form or outline
tab.
329
 *
330
 * This function is also called when generating a new set of options during
the
331
 * AJAX callback, so an array is returned that can be used to replace an
existing
332
 * form element.
333
 */
33443
function _book_parent_select($book_link) {
3358
  if (variable_get('menu_override_parent_selector', FALSE)) {
3360
    return array();
3370
  }
338
  // Offer a message or a drop-down to choose a different parent page.
339
  $form = array(
3408
    '#type' => 'hidden',
3418
    '#value' => -1,
3428
    '#prefix' => '<div id="edit-book-plid-wrapper">',
3438
    '#suffix' => '</div>',
3448
  );
345
3468
  if ($book_link['nid'] === $book_link['bid']) {
347
    // This is a book - at the top level.
3480
    if ($book_link['original_bid'] === $book_link['bid']) {
3490
      $form['#prefix'] .= '<em>' . t('This is the top-level page in this
book.') . '</em>';
3500
    }
351
    else {
3520
      $form['#prefix'] .= '<em>' . t('This will be the top-level page in
this book.') . '</em>';
353
    }
3540
  }
3558
  elseif (!$book_link['bid']) {
3566
    $form['#prefix'] .= '<em>' . t('No book selected.') . '</em>';
3576
  }
358
  else {
359
    $form = array(
3602
      '#type' => 'select',
3612
      '#title' => t('Parent item'),
3622
      '#default_value' => $book_link['plid'],
3632
      '#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)),
3642
      '#options' => book_toc($book_link['bid'], array($book_link['mlid']),
$book_link['parent_depth_limit']),
3652
      '#attributes' => array('class' => 'book-title-select'),
3662
    );
367
  }
368
3698
  return $form;
3700
}
371
372
/**
373
 * Build the common elements of the book form for the node and outline
forms.
374
 */
37543
function _book_add_form_elements(&$form, $node) {
376
  // Need this for AJAX.
3778
  $form['#cache'] = TRUE;
3788
  drupal_add_js("if (Drupal.jsEnabled) { $(document).ready(function() {
$('#edit-book-pick-book').css('display', 'none'); }); }", 'inline');
379
3808
  $form['book'] = array(
3818
    '#type' => 'fieldset',
3828
    '#title' => t('Book outline'),
3838
    '#weight' => 10,
3848
    '#collapsible' => TRUE,
3858
    '#collapsed' => TRUE,
3868
    '#tree' => TRUE,
3878
    '#attributes' => array('class' => 'book-outline-form'),
388
  );
3898
  foreach (array('menu_name', 'mlid', 'nid', 'router_path', 'has_children',
'options', 'module', 'original_bid', 'parent_depth_limit') as $key) {
3908
    $form['book'][$key] = array(
3918
      '#type' => 'value',
3928
      '#value' => $node->book[$key],
393
    );
3948
  }
395
3968
  $form['book']['plid'] = _book_parent_select($node->book);
397
3988
  $form['book']['weight'] = array(
3998
    '#type' => 'weight',
4008
    '#title' => t('Weight'),
4018
    '#default_value' => $node->book['weight'],
4028
    '#delta' => 15,
4038
    '#weight' => 5,
4048
    '#description' => t('Pages at a given level are ordered first by weight
and then by title.'),
405
  );
4068
  $options = array();
4078
  $nid = isset($node->nid) ? $node->nid : 'new';
408
4098
  if (isset($node->nid) && ($nid == $node->book['original_bid']) &&
($node->book['parent_depth_limit'] == 0)) {
410
    // This is the top level node in a maximum depth book and thus cannot
be moved.
4110
    $options[$node->nid] = $node->title;
4120
  }
413
  else {
4148
    foreach (book_get_books() as $book) {
4157
      $options[$book['nid']] = $book['title'];
4167
    }
417
  }
418
4198
  if (user_access('create new books') && ($nid == 'new' || ($nid !=
$node->book['original_bid']))) {
420
    // The node can become a new book, if it is not one already.
4218
    $options = array($nid => '<' . t('create a new book') . '>') +
$options;
4228
  }
4238
  if (!$node->book['mlid']) {
424
    // The node is not currently in the hierarchy.
4258
    $options = array(0 => '<' . t('none') . '>') + $options;
4268
  }
427
428
  // Add a drop-down to select the destination book.
4298
  $form['book']['bid'] = array(
4308
    '#type' => 'select',
4318
    '#title' => t('Book'),
4328
    '#default_value' => $node->book['bid'],
4338
    '#options' => $options,
4348
    '#access' => (bool)$options,
4358
    '#description' => t('Your page will be a part of the selected book.'),
4368
    '#weight' => -5,
4378
    '#attributes' => array('class' => 'book-title-select'),
438
    '#ahah' => array(
4398
      'path' => 'book/js/form',
4408
      'wrapper' => 'edit-book-plid-wrapper',
4418
      'effect' => 'slide',
4428
    ),
443
  );
4448
}
445
446
/**
447
 * Common helper function to handles additions and updates to the book
outline.
448
 *
449
 * Performs all additions and updates to the book outline through node
addition,
450
 * node editing, node deletion, or the outline tab.
451
 */
45243
function _book_update_outline(&$node) {
4536
  if (empty($node->book['bid'])) {
4540
    return FALSE;
4550
  }
4566
  $new = empty($node->book['mlid']);
457
4586
  $node->book['link_path'] = 'node/' . $node->nid;
4596
  $node->book['link_title'] = $node->title;
4606
  $node->book['parent_mismatch'] = FALSE; // The normal case.
461
4626
  if ($node->book['bid'] == $node->nid) {
4631
    $node->book['plid'] = 0;
4641
    $node->book['menu_name'] = book_menu_name($node->nid);
4651
  }
466
  else {
467
    // Check in case the parent is not is this book; the book takes
precedence.
4685
    if (!empty($node->book['plid'])) {
4695
      $parent = db_fetch_array(db_query("SELECT * FROM {book} WHERE mlid =
%d", $node->book['plid']));
4705
    }
4715
    if (empty($node->book['plid']) || !$parent || $parent['bid'] !=
$node->book['bid']) {
4723
      $node->book['plid'] = db_result(db_query("SELECT mlid FROM {book}
WHERE nid = %d", $node->book['bid']));
4733
      $node->book['parent_mismatch'] = TRUE; // Likely when JS is
disabled.
4743
    }
475
  }
476
4776
  if (menu_link_save($node->book)) {
4786
    if ($new) {
479
      // Insert new.
4806
      db_query("INSERT INTO {book} (nid, mlid, bid) VALUES (%d, %d, %d)",
$node->nid, $node->book['mlid'], $node->book['bid']);
4816
    }
482
    else {
4830
      if ($node->book['bid'] != db_result(db_query("SELECT bid FROM {book}
WHERE nid = %d", $node->nid))) {
484
        // Update the bid for this page and all children.
4850
        book_update_bid($node->book);
4860
      }
487
    }
488
4896
    return TRUE;
4900
  }
491
492
  // Failed to save the menu link.
4930
  return FALSE;
4940
}
495
496
/**
497
 * Update the bid for a page and its children when it is moved to a new
book.
498
 *
499
 * @param $book_link
500
 *   A fully loaded menu link that is part of the book hierarchy.
501
 */
50243
function book_update_bid($book_link) {
5030
  for ($i = 1; $i <= MENU_MAX_DEPTH && $book_link["p$i"]; $i++) {
5040
    $match[] = "p$i = %d";
5050
    $args[] = $book_link["p$i"];
5060
  }
5070
  $result = db_query("SELECT mlid FROM {menu_links} WHERE " . implode(' AND
', $match), $args);
508
5090
  $mlids = array();
5100
  while ($a = db_fetch_array($result)) {
5110
    $mlids[] = $a['mlid'];
5120
  }
513
5140
  if ($mlids) {
5150
    db_query("UPDATE {book} SET bid = %d WHERE mlid IN (" . implode(',',
$mlids) . ")", $book_link['bid']);
5160
  }
5170
}
518
519
/**
520
 * Get the book menu tree for a page, and return it as a linear array.
521
 *
522
 * @param $book_link
523
 *   A fully loaded menu link that is part of the book hierarchy.
524
 * @return
525
 *   A linear array of menu links in the order that the links are shown in
the
526
 *   menu, so the previous and next pages are the elements before and after
the
527
 *   element corresponding to $node.  The children of $node (if any) will
come
528
 *   immediately after it in the array.
529
 */
53043
function book_get_flat_menu($book_link) {
53112
  static $flat = array();
532
53312
  if (!isset($flat[$book_link['mlid']])) {
534
    // Call menu_tree_all_data() to take advantage of the menu system's
caching.
53512
    $tree = menu_tree_all_data($book_link['menu_name'], $book_link);
53612
    $flat[$book_link['mlid']] = array();
53712
    _book_flatten_menu($tree, $flat[$book_link['mlid']]);
53812
  }
539
54012
  return $flat[$book_link['mlid']];
5410
}
542
543
/**
544
 * Recursive helper function for book_get_flat_menu().
545
 */
54643
function _book_flatten_menu($tree, &$flat) {
54712
  foreach ($tree as $data) {
54812
    if (!$data['link']['hidden']) {
54912
      $flat[$data['link']['mlid']] = $data['link'];
55012
      if ($data['below']) {
55111
        _book_flatten_menu($data['below'], $flat);
55211
      }
55312
    }
55412
  }
55512
}
556
557
/**
558
 * Fetches the menu link for the previous page of the book.
559
 */
56043
function book_prev($book_link) {
561
  // If the parent is zero, we are at the start of a book.
56212
  if ($book_link['plid'] == 0) {
5632
    return NULL;
5640
  }
56510
  $flat = book_get_flat_menu($book_link);
566
  // Assigning the array to $flat resets the array pointer for use with
each().
56710
  $curr = NULL;
568
  do {
56910
    $prev = $curr;
57010
    list($key, $curr) = each($flat);
57110
  } while ($key && $key != $book_link['mlid']);
572
57310
  if ($key == $book_link['mlid']) {
574
    // The previous page in the book may be a child of the previous visible
link.
57510
    if ($prev['depth'] == $book_link['depth'] && $prev['has_children']) {
576
      // The subtree will have only one link at the top level - get its
data.
5772
      $data = array_shift(book_menu_subtree_data($prev));
578
      // The link of interest is the last child - iterate to find the
deepest one.
5792
      while ($data['below']) {
5802
        $data = end($data['below']);
5812
      }
582
5832
      return $data['link'];
5840
    }
585
    else {
5868
      return $prev;
587
    }
5880
  }
5890
}
590
591
/**
592
 * Fetches the menu link for the next page of the book.
593
 */
59443
function book_next($book_link) {
59512
  $flat = book_get_flat_menu($book_link);
596
  // Assigning the array to $flat resets the array pointer for use with
each().
597
  do {
59812
    list($key, $curr) = each($flat);
599
  }
60012
  while ($key && $key != $book_link['mlid']);
601
60212
  if ($key == $book_link['mlid']) {
60312
    return current($flat);
6040
  }
6050
}
606
607
/**
608
 * Format the menu links for the child pages of the current page.
609
 */
61043
function book_children($book_link) {
61112
  $flat = book_get_flat_menu($book_link);
612
61312
  $children = array();
614
61512
  if ($book_link['has_children']) {
616
    // Walk through the array until we find the current page.
617
    do {
6182
      $link = array_shift($flat);
619
    }
6202
    while ($link && ($link['mlid'] != $book_link['mlid']));
621
    // Continue though the array and collect the links whose parent is this
page.
6222
    while (($link = array_shift($flat)) && $link['plid'] ==
$book_link['mlid']) {
6232
      $data['link'] = $link;
6242
      $data['below'] = '';
6252
      $children[] = $data;
6262
    }
6272
  }
628
62912
  return $children ? menu_tree_output($children) : '';
6300
}
631
632
/**
633
 * Generate the corresponding menu name from a book ID.
634
 */
63543
function book_menu_name($bid) {
6368
  return 'book-toc-' . $bid;
6370
}
638
639
/**
640
 * Build an active trail to show in the breadcrumb.
641
 */
64243
function book_build_active_trail($book_link) {
64312
  static $trail;
644
64512
  if (!isset($trail)) {
64612
    $trail = array();
64712
    $trail[] = array('title' => t('Home'), 'href' => '<front>',
'localized_options' => array());
648
64912
    $tree = menu_tree_all_data($book_link['menu_name'], $book_link);
65012
    $curr = array_shift($tree);
651
65212
    while ($curr) {
65312
      if ($curr['link']['href'] == $book_link['href']) {
65412
        $trail[] = $curr['link'];
65512
        $curr = FALSE;
65612
      }
657
      else {
65810
        if ($curr['below'] && $curr['link']['in_active_trail']) {
65910
          $trail[] = $curr['link'];
66010
          $tree = $curr['below'];
66110
        }
66210
        $curr = array_shift($tree);
663
      }
66412
    }
66512
  }
666
66712
  return $trail;
6680
}
669
670
/**
671
 * Implementation of hook_nodeapi().
672
 *
673
 * Appends book navigation to all nodes in the book, and handles book
outline
674
 * insertions and updates via the node form.
675
 */
67643
function book_nodeapi(&$node, $op, $teaser, $page) {
677
  switch ($op) {
67833
    case 'load':
679
      // Note - we cannot use book_link_load() because it will call
node_load().
68019
      $info['book'] = db_fetch_array(db_query('SELECT * FROM {book} b INNER
JOIN {menu_links} ml ON b.mlid = ml.mlid WHERE b.nid = %d', $node->nid));
681
68219
      if ($info['book']) {
68319
        $info['book']['href'] = $info['book']['link_path'];
68419
        $info['book']['title'] = $info['book']['link_title'];
68519
        $info['book']['options'] = unserialize($info['book']['options']);
686
68719
        return $info;
6880
      }
6890
      break;
690
69132
    case 'view':
69218
    if (!$teaser) {
69318
        if (!empty($node->book['bid']) && $node->build_mode ==
NODE_BUILD_NORMAL) {
69412
          $node->content['book_navigation'] = array(
69512
            '#markup' => theme('book_navigation', $node->book),
69612
            '#weight' => 100,
697
          );
698
69912
          if ($page) {
70012
            menu_set_active_trail(book_build_active_trail($node->book));
70112
            menu_set_active_menu_name($node->book['menu_name']);
70212
          }
70312
        }
70418
      }
70518
      break;
706
70726
    case 'presave':
708
      // Always save a revision for non-administrators.
7096
      if (!empty($node->book['bid']) && !user_access('administer nodes'))
{
7106
        $node->revision = 1;
7116
      }
712
      // Make sure a new node gets a new menu link.
7136
      if (empty($node->nid)) {
7146
        $node->book['mlid'] = NULL;
7156
      }
7166
      break;
717
71826
    case 'insert':
71926
    case 'update':
7206
      if (!empty($node->book['bid'])) {
7216
        if ($node->book['bid'] == 'new') {
722
          // New nodes that are their own book.
7231
          $node->book['bid'] = $node->nid;
7241
        }
7256
        $node->book['nid'] = $node->nid;
7266
        $node->book['menu_name'] = book_menu_name($node->book['bid']);
7276
        _book_update_outline($node);
7286
      }
7296
      break;
730
73126
    case 'delete':
7320
      if (!empty($node->book['bid'])) {
7330
        if ($node->nid == $node->book['bid']) {
734
          // Handle deletion of a top-level post.
7350
          $result = db_query("SELECT b.nid FROM {menu_links} ml INNER JOIN
{book} b on b.mlid = ml.mlid WHERE ml.plid = %d", $node->book['mlid']);
7360
          while ($child = db_fetch_array($result)) {
7370
            $child_node = node_load($child['nid']);
7380
            $child_node->book['bid'] = $child_node->nid;
7390
            _book_update_outline($child_node);
7400
          }
7410
        }
7420
        menu_link_delete($node->book['mlid']);
7430
        db_query('DELETE FROM {book} WHERE mlid = %d',
$node->book['mlid']);
7440
      }
7450
      break;
746
74726
    case 'prepare':
748
      // Prepare defaults for the add/edit form.
7498
      if (empty($node->book) && (user_access('add content to books') ||
user_access('administer book outlines'))) {
7506
        $node->book = array();
751
7526
        if (empty($node->nid) && isset($_GET['parent']) &&
is_numeric($_GET['parent'])) {
753
          // Handle "Add child page" links:
7540
          $parent = book_link_load($_GET['parent']);
755
7560
          if ($parent && $parent['access']) {
7570
            $node->book['bid'] = $parent['bid'];
7580
            $node->book['plid'] = $parent['mlid'];
7590
            $node->book['menu_name'] = $parent['menu_name'];
7600
          }
7610
        }
762
        // Set defaults.
7636
        $node->book += _book_link_defaults(!empty($node->nid) ? $node->nid
: 'new');
7646
      }
765
      else {
7662
        if (isset($node->book['bid']) &&
!isset($node->book['original_bid'])) {
7670
          $node->book['original_bid'] = $node->book['bid'];
7680
        }
769
      }
770
      // Find the depth limit for the parent select.
7718
      if (isset($node->book['bid']) &&
!isset($node->book['parent_depth_limit'])) {
7726
        $node->book['parent_depth_limit'] =
_book_parent_depth_limit($node->book);
7736
      }
7748
      break;
7750
  }
77632
}
777
778
/**
779
 * Find the depth limit for items in the parent select.
780
 */
78143
function _book_parent_depth_limit($book_link) {
7826
  return MENU_MAX_DEPTH - 1 - (($book_link['mlid'] &&
$book_link['has_children']) ? menu_link_children_relative_depth($book_link)
: 0);
7830
}
784
785
/**
786
 * Form altering function for the confirm form for a single node deletion.
787
 */
78843
function book_form_node_delete_confirm_alter(&$form, $form_state) {
7890
  $node = node_load($form['nid']['#value']);
790
7910
  if (isset($node->book) && $node->book['has_children']) {
7920
    $form['book_warning'] = array(
7930
      '#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>',
7940
      '#weight' => -10,
795
    );
7960
  }
7970
}
798
799
/**
800
 * Return an array with default values for a book link.
801
 */
80243
function _book_link_defaults($nid) {
8036
  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());
8040
}
805
806
/**
807
 * Process variables for book-navigation.tpl.php.
808
 *
809
 * The $variables array contains the following arguments:
810
 * - $book_link
811
 *
812
 * @see book-navigation.tpl.php
813
 */
81443
function template_preprocess_book_navigation(&$variables) {
81512
  $book_link = $variables['book_link'];
816
817
  // Provide extra variables for themers. Not needed by default.
81812
  $variables['book_id'] = $book_link['bid'];
81912
  $variables['book_title'] = check_plain($book_link['link_title']);
82012
  $variables['book_url'] = 'node/' . $book_link['bid'];
82112
  $variables['current_depth'] = $book_link['depth'];
82212
  $variables['tree'] = '';
823
82412
  if ($book_link['mlid']) {
82512
    $variables['tree'] = book_children($book_link);
826
82712
    if ($prev = book_prev($book_link)) {
82810
      $prev_href = url($prev['href']);
82910
      drupal_add_link(array('rel' => 'prev', 'href' => $prev_href));
83010
      $variables['prev_url'] = $prev_href;
83110
      $variables['prev_title'] = check_plain($prev['title']);
83210
    }
833
83412
    if ($book_link['plid'] && $parent = book_link_load($book_link['plid']))
{
83510
      $parent_href = url($parent['href']);
83610
      drupal_add_link(array('rel' => 'up', 'href' => $parent_href));
83710
      $variables['parent_url'] = $parent_href;
83810
      $variables['parent_title'] = check_plain($parent['title']);
83910
    }
840
84112
    if ($next = book_next($book_link)) {
8425
      $next_href = url($next['href']);
8435
      drupal_add_link(array('rel' => 'next', 'href' => $next_href));
8445
      $variables['next_url'] = $next_href;
8455
      $variables['next_title'] = check_plain($next['title']);
8465
    }
84712
  }
848
84912
  $variables['has_links'] = FALSE;
850
  // Link variables to filter for values and set state of the flag
variable.
85112
  $links = array('prev_url', 'prev_title', 'parent_url', 'parent_title',
'next_url', 'next_title');
85212
  foreach ($links as $link) {
85312
    if (isset($variables[$link])) {
854
      // Flag when there is a value.
85511
      $variables['has_links'] = TRUE;
85611
    }
857
    else {
858
      // Set empty to prevent notices.
8598
      $variables[$link] = '';
860
    }
86112
  }
86212
}
863
864
/**
865
 * A recursive helper function for book_toc().
866
 */
86743
function _book_toc_recurse($tree, $indent, &$toc, $exclude, $depth_limit)
{
8682
  foreach ($tree as $data) {
8692
    if ($data['link']['depth'] > $depth_limit) {
870
      // Don't iterate through any links on this level.
8710
      break;
8720
    }
873
8742
    if (!in_array($data['link']['mlid'], $exclude)) {
8752
      $toc[$data['link']['mlid']] = $indent . ' ' .
truncate_utf8($data['link']['title'], 30, TRUE, TRUE);
8762
      if ($data['below']) {
8772
        _book_toc_recurse($data['below'], $indent . '--', $toc, $exclude,
$depth_limit);
8782
      }
8792
    }
8802
  }
8812
}
882
883
/**
884
 * Returns an array of book pages in table of contents order.
885
 *
886
 * @param $bid
887
 *   The ID of the book whose pages are to be listed.
888
 * @param $exclude
889
 *   Optional array of mlid values.  Any link whose mlid is in this array
890
 *   will be excluded (along with its children).
891
 * @param $depth_limit
892
 *   Any link deeper than this value will be excluded (along with its
children).
893
 * @return
894
 *   An array of mlid, title pairs for use as options for selecting a book
page.
895
 */
89643
function book_toc($bid, $exclude = array(), $depth_limit) {
8972
  $tree = menu_tree_all_data(book_menu_name($bid));
8982
  $toc = array();
8992
  _book_toc_recurse($tree, '', $toc, $exclude, $depth_limit);
900
9012
  return $toc;
9020
}
903
904
/**
905
 * Process variables for book-export-html.tpl.php.
906
 *
907
 * The $variables array contains the following arguments:
908
 * - $title
909
 * - $contents
910
 * - $depth
911
 *
912
 * @see book-export-html.tpl.php
913
 */
91443
function template_preprocess_book_export_html(&$variables) {
9156
  global $base_url, $language;
916
9176
  $variables['title'] = check_plain($variables['title']);
9186
  $variables['base_url'] = $base_url;
9196
  $variables['language'] = $language;
9206
  $variables['language_rtl'] = (defined('LANGUAGE_RTL') &&
$language->direction == LANGUAGE_RTL) ? TRUE : FALSE;
9216
  $variables['head'] = drupal_get_html_head();
9226
}
923
924
/**
925
 * Traverse the book tree to build printable or exportable output.
926
 *
927
 * During the traversal, the $visit_func() callback is applied to each
928
 * node, and is called recursively for each child of the node (in weight,
929
 * title order).
930
 *
931
 * @param $tree
932
 *   A subtree of the book menu hierarchy, rooted at the current page.
933
 * @param $visit_func
934
 *   A function callback to be called upon visiting a node in the tree.
935
 * @return
936
 *   The output generated in visiting each node.
937
 */
93843
function book_export_traverse($tree, $visit_func) {
9396
  $output = '';
940
9416
  foreach ($tree as $data) {
942
    // Note- access checking is already performed when building the tree.
9436
    if ($node = node_load($data['link']['nid'], FALSE)) {
9446
      $children = '';
945
9466
      if ($data['below']) {
9472
        $children = book_export_traverse($data['below'], $visit_func);
9482
      }
949
9506
      if (function_exists($visit_func)) {
9516
        $output .= call_user_func($visit_func, $node, $children);
9526
      }
953
      else {
954
        // Use the default function.
9550
        $output .= book_node_export($node, $children);
956
      }
9576
    }
9586
  }
959
9606
  return $output;
9610
}
962
963
/**
964
 * Generates printer-friendly HTML for a node.
965
 *
966
 * @see book_export_traverse()
967
 *
968
 * @param $node
969
 *   The node that will be output.
970
 * @param $children
971
 *   All the rendered child nodes within the current node.
972
 * @return
973
 *   The HTML generated for the given node.
974
 */
97543
function book_node_export($node, $children = '') {
9766
  $node->build_mode = NODE_BUILD_PRINT;
9776
  $node = node_build_content($node, FALSE, FALSE);
9786
  $node->body = drupal_render($node->content);
979
9806
  return theme('book_node_export_html', $node, $children);
9810
}
982
983
/**
984
 * Process variables for book-node-export-html.tpl.php.
985
 *
986
 * The $variables array contains the following arguments:
987
 * - $node
988
 * - $children
989
 *
990
 * @see book-node-export-html.tpl.php
991
 */
99243
function template_preprocess_book_node_export_html(&$variables) {
9936
  $variables['depth'] = $variables['node']->book['depth'];
9946
  $variables['title'] = check_plain($variables['node']->title);
9956
  $variables['content'] = $variables['node']->body;
9966
}
997
998
/**
999
 * Determine if a given node type is in the list of types allowed for
books.
1000
 */
100143
function book_type_is_allowed($type) {
10028
  return in_array($type, variable_get('book_allowed_types',
array('book')));
10030
}
1004
1005
/**
1006
 * Implementation of hook_node_type().
1007
 *
1008
 * Update book module's persistent variables if the machine-readable name
of a
1009
 * node type is changed.
1010
 */
101143
function book_node_type($op, $type) {
1012
  switch ($op) {
10131
    case 'update':
10140
      if (!empty($type->old_type) && $type->old_type != $type->type) {
1015
        // Update the list of node types that are allowed to be added to
books.
10160
        $allowed_types = variable_get('book_allowed_types',
array('book'));
10170
        $key = array_search($type->old_type, $allowed_types);
1018
10190
        if ($key !== FALSE) {
10200
          $allowed_types[$type->type] = $allowed_types[$key] ? $type->type
: 0;
10210
          unset($allowed_types[$key]);
10220
          variable_set('book_allowed_types', $allowed_types);
10230
        }
1024
1025
        // Update the setting for the "Add child page" link.
10260
        if (variable_get('book_child_type', 'book') == $type->old_type) {
10270
          variable_set('book_child_type', $type->type);
10280
        }
10290
      }
10300
      break;
10310
  }
10321
}
1033
1034
/**
1035
 * Implementation of hook_help().
1036
 */
103743
function book_help($path, $arg) {
1038
  switch ($path) {
103927
    case 'admin/help#book':
10400
      $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>';
10410
      $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/build/block'))) . '</p>';
10420
      $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>';
10430
      $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>';
10440
      $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>';
10450
      $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>';
1046
10470
      return $output;
1048
104927
    case 'admin/content/book':
10500
      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>';
1051
105227
    case 'node/%/outline':
10530
      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>';
10540
  }
105527
}
1056
1057
/**
1058
 * Like menu_link_load(), but adds additional data from the {book} table.
1059
 *
1060
 * Do not call when loading a node, since this function may call
node_load().
1061
 */
106243
function book_link_load($mlid) {
106310
  if ($item = db_fetch_array(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 = %d", $mlid))) {
106410
    _menu_link_translate($item);
106510
    return $item;
10660
  }
1067
10680
  return FALSE;
10690
}
1070
1071
/**
1072
 * Get the data representing a subtree of the book hierarchy.
1073
 *
1074
 * The root of the subtree will be the link passed as a parameter, so the
1075
 * returned tree will contain this item and all its descendents in the menu
tree.
1076
 *
1077
 * @param $item
1078
 *   A fully loaded menu link.
1079
 * @return
1080
 *   An subtree of menu links in an array, in the order they should be
rendered.
1081
 */
108243
function book_menu_subtree_data($item) {
10838
  static $tree = array();
1084
1085
  // Generate a cache ID (cid) specific for this $menu_name and $item.
10868
  $cid = 'links:' . $item['menu_name'] . ':subtree-cid:' . $item['mlid'];
1087
10888
  if (!isset($tree[$cid])) {
10898
    $cache = cache_get($cid, 'cache_menu');
1090
10918
    if ($cache && isset($cache->data)) {
1092
      // If the cache entry exists, it will just be the cid for the actual
data.
1093
      // This avoids duplication of large amounts of data.
10941
      $cache = cache_get($cache->data, 'cache_menu');
1095
10961
      if ($cache && isset($cache->data)) {
10971
        $data = $cache->data;
10981
      }
10991
    }
1100
1101
    // If the subtree data was not in the cache, $data will be NULL.
11028
    if (!isset($data)) {
11037
      $match = array("menu_name = '%s'");
11047
      $args = array($item['menu_name']);
11057
      $i = 1;
11067
      while ($i <= MENU_MAX_DEPTH && $item["p$i"]) {
11077
        $match[] = "p$i = %d";
11087
        $args[] = $item["p$i"];
11097
        $i++;
11107
      }
1111
      $sql = "
1112
        SELECT b.*, m.load_functions, m.to_arg_functions,
m.access_callback, m.access_arguments, m.page_callback, m.page_arguments,
m.title, m.title_callback, m.title_arguments, m.type, ml.*
1113
        FROM {menu_links} ml INNER JOIN {menu_router} m ON m.path =
ml.router_path
1114
        INNER JOIN {book} b ON ml.mlid = b.mlid
11157
        WHERE " . implode(' AND ', $match) . "
11167
        ORDER BY p1 ASC, p2 ASC, p3 ASC, p4 ASC, p5 ASC, p6 ASC, p7 ASC, p8
ASC, p9 ASC";
1117
11187
      $data['tree'] = menu_tree_data(db_query($sql, $args), array(),
$item['depth']);
11197
      $data['node_links'] = array();
11207
      menu_tree_collect_node_links($data['tree'], $data['node_links']);
1121
      // Compute the real cid for book subtree data.
11227
      $tree_cid = 'links:' . $item['menu_name'] . ':subtree-data:' .
md5(serialize($data));
1123
      // Cache the data, if it is not already in the cache.
1124
11257
      if (!cache_get($tree_cid, 'cache_menu')) {
11267
        cache_set($tree_cid, $data, 'cache_menu');
11277
      }
1128
      // Cache the cid of the (shared) data using the menu and
item-specific cid.
11297
      cache_set($cid, $tree_cid, 'cache_menu');
11307
    }
1131
    // Check access for the current user to each item in the tree.
11328
    menu_tree_check_access($data['tree'], $data['node_links']);
11338
    $tree[$cid] = $data['tree'];
11348
  }
1135
11368
  return $tree[$cid];
11370
}
113843