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

Line #Times calledCode
1
<?php
2
// $Id: book.module,v 1.473 2008/10/29 10:08:51 dries Exp $
3
4
/**
5
 * @file
6
 * Allows users to create and organize related content in an 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(
44
    'administer book outlines' => array(
451
      'title' => t('Administer book outlines'),
461
      'description' => t('Manage books through the administration panel.'),
471
    ),
48
    'create new books' => array(
491
      'title' => t('Create new books'),
501
      'description' => t('Add new top-level books.'),
511
    ),
52
    'add content to books' => array(
531
      'title' => t('Add content to books'),
541
      'description' => t('Add new content and child pages to books.'),
551
    ),
56
    'access printer-friendly version' => array(
571
      'title' => t('Access printer-friendly version'),
581
      'description' => t('View a book page and all of its sub-pages as a
single document for ease of printing. Can be performance heavy.'),
591
    ),
601
  );
610
}
62
63
/**
64
 * Implementation of hook_link().
65
 */
6643
function book_link($type, $node = NULL, $teaser = FALSE) {
6712
  $links = array();
68
6912
  if ($type == 'node' && isset($node->book)) {
7012
    if (!$teaser) {
7112
      $child_type = variable_get('book_child_type', 'book');
7212
      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) {
736
        $links['book_add_child'] = array(
746
          'title' => t('Add child page'),
756
          'href' => "node/add/" . str_replace('_', '-', $child_type),
766
          'query' => "parent=" . $node->book['mlid'],
77
        );
786
      }
79
8012
      if (user_access('access printer-friendly version')) {
816
        $links['book_printer'] = array(
826
          'title' => t('Printer-friendly version'),
836
          'href' => 'book/export/html/' . $node->nid,
846
          'attributes' => array('title' => t('Show a printer-friendly
version of this book page and its sub-pages.'))
856
        );
866
      }
8712
    }
8812
  }
89
9012
  return $links;
910
}
92
93
/**
94
 * Implementation of hook_menu().
95
 */
9643
function book_menu() {
971
  $items['admin/content/book'] = array(
981
    'title' => 'Books',
991
    'description' => "Manage your site's book outlines.",
1001
    'page callback' => 'book_admin_overview',
1011
    'access arguments' => array('administer book outlines'),
102
  );
1031
  $items['admin/content/book/list'] = array(
1041
    'title' => 'List',
1051
    'type' => MENU_DEFAULT_LOCAL_TASK,
106
  );
1071
  $items['admin/content/book/settings'] = array(
1081
    'title' => 'Settings',
1091
    'page callback' => 'drupal_get_form',
1101
    'page arguments' => array('book_admin_settings'),
1111
    'access arguments' => array('administer site configuration'),
1121
    'type' => MENU_LOCAL_TASK,
1131
    'weight' => 8,
114
  );
1151
  $items['admin/content/book/%node'] = array(
1161
    'title' => 'Re-order book pages and change titles',
1171
    'page callback' => 'drupal_get_form',
1181
    'page arguments' => array('book_admin_edit', 3),
1191
    'access callback' => '_book_outline_access',
1201
    'access arguments' => array(3),
1211
    'type' => MENU_CALLBACK,
122
  );
1231
  $items['book'] = array(
1241
    'title' => 'Books',
1251
    'page callback' => 'book_render',
1261
    'access arguments' => array('access content'),
1271
    'type' => MENU_SUGGESTED_ITEM,
128
  );
1291
  $items['book/export/%/%'] = array(
1301
    'page callback' => 'book_export',
1311
    'page arguments' => array(2, 3),
1321
    'access arguments' => array('access printer-friendly version'),
1331
    'type' => MENU_CALLBACK,
134
  );
1351
  $items['node/%node/outline'] = array(
1361
    'title' => 'Outline',
1371
    'page callback' => 'book_outline',
1381
    'page arguments' => array(1),
1391
    'access callback' => '_book_outline_access',
1401
    'access arguments' => array(1),
1411
    'type' => MENU_LOCAL_TASK,
1421
    'weight' => 2,
143
  );
1441
  $items['node/%node/outline/remove'] = array(
1451
    'title' => 'Remove from outline',
1461
    'page callback' => 'drupal_get_form',
1471
    'page arguments' => array('book_remove_form', 1),
1481
    'access callback' => '_book_outline_remove_access',
1491
    'access arguments' => array(1),
1501
    'type' => MENU_CALLBACK,
151
  );
1521
  $items['book/js/form'] = array(
1531
    'page callback' => 'book_form_update',
1541
    'access arguments' => array('access content'),
1551
    'type' => MENU_CALLBACK,
156
  );
157
1581
  return $items;
1590
}
160
161
/**
162
 * Menu item access callback - determine if the outline tab is accessible.
163
 */
16443
function _book_outline_access($node) {
16512
  return user_access('administer book outlines') && node_access('view',
$node);
1660
}
167
168
/**
169
 * Menu item access callback - determine if the user can remove nodes from
the outline.
170
 */
17143
function _book_outline_remove_access($node) {
1720
  return isset($node->book) && ($node->book['bid'] != $node->nid) &&
_book_outline_access($node);
1730
}
174
175
/**
176
 * Implementation of hook_init().
177
 */
17843
function book_init() {
17942
  drupal_add_css(drupal_get_path('module', 'book') . '/book.css');
18042
}
181
182
/**
183
 * Implementation of hook_block().
184
 *
185
 * Displays the book table of contents in a block when the current page is
a
186
 * single-node view of a book node.
187
 */
18843
function book_block($op = 'list', $delta = '', $edit = array()) {
1890
  $block = array();
190
  switch ($op) {
1910
    case 'list':
1920
      $block['navigation']['info'] = t('Book navigation');
1930
      $block['navigation']['cache'] = BLOCK_CACHE_PER_PAGE |
BLOCK_CACHE_PER_ROLE;
194
1950
      return $block;
196
1970
    case 'view':
1980
      $current_bid = 0;
1990
      if ($node = menu_get_object()) {
2000
        $current_bid = empty($node->book['bid']) ? 0 : $node->book['bid'];
2010
      }
202
2030
      if (variable_get('book_block_mode', 'all pages') == 'all pages') {
2040
        $block['subject'] = t('Book navigation');
2050
        $book_menus = array();
2060
        $pseudo_tree = array(0 => array('below' => FALSE));
2070
        foreach (book_get_books() as $book_id => $book) {
2080
          if ($book['bid'] == $current_bid) {
209
            // If the current page is a node associated with a book, the
menu
210
            // needs to be retrieved.
2110
            $book_menus[$book_id] =
menu_tree_output(menu_tree_all_data($node->book['menu_name'],
$node->book));
2120
          }
213
          else {
214
            // Since we know we will only display a link to the top node,
there
215
            // is no reason to run an additional menu tree query for each
book.
2160
            $book['in_active_trail'] = FALSE;
2170
            $pseudo_tree[0]['link'] = $book;
2180
            $book_menus[$book_id] = menu_tree_output($pseudo_tree);
219
          }
2200
        }
2210
        $block['content'] = theme('book_all_books_block', $book_menus);
2220
      }
2230
      elseif ($current_bid) {
224
        // Only display this block when the user is browsing a book.
2250
        $title = db_result(db_query(db_rewrite_sql('SELECT n.title FROM
{node} n WHERE n.nid = %d'), $node->book['bid']));
226
        // Only show the block if the user has view access for the
top-level node.
2270
        if ($title) {
2280
          $tree = menu_tree_all_data($node->book['menu_name'],
$node->book);
229
          // There should only be one element at the top level.
2300
          $data = array_shift($tree);
2310
          $block['subject'] = theme('book_title_link', $data['link']);
2320
          $block['content'] = ($data['below']) ?
menu_tree_output($data['below']) : '';
2330
        }
2340
      }
235
2360
      return $block;
237
2380
    case 'configure':
239
      $options = array(
2400
        'all pages' => t('Show block on all pages'),
2410
        'book pages' => t('Show block only on book pages'),
2420
      );
2430
      $form['book_block_mode'] = array(
2440
        '#type' => 'radios',
2450
        '#title' => t('Book navigation block display'),
2460
        '#options' => $options,
2470
        '#default_value' => variable_get('book_block_mode', 'all pages'),
2480
        '#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."),
249
        );
250
2510
      return $form;
252
2530
    case 'save':
2540
      variable_set('book_block_mode', $edit['book_block_mode']);
2550
      break;
2560
  }
2570
}
258
259
/**
260
 * Generate the HTML output for a link to a book title when used as a block
title.
261
 *
262
 * @ingroup themeable
263
 */
26443
function theme_book_title_link($link) {
2650
  $link['options']['attributes']['class'] =  'book-title';
266
2670
  return l($link['title'], $link['href'], $link['options']);
2680
}
269
270
/**
271
 * Returns an array of all books.
272
 *
273
 * This list may be used for generating a list of all the books, or for
building
274
 * the options for a form select.
275
 */
27643
function book_get_books() {
2778
  static $all_books;
278
2798
  if (!isset($all_books)) {
2808
    $all_books = array();
2818
    $result = db_query("SELECT DISTINCT(bid) FROM {book}");
2828
    $nids = array();
2838
    while ($book = db_fetch_array($result)) {
2847
      $nids[] = $book['bid'];
2857
    }
286
2878
    if ($nids) {
2887
      $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"));
2897
      while ($link = db_fetch_array($result2)) {
2907
        $link['href'] = $link['link_path'];
2917
        $link['options'] = unserialize($link['options']);
2927
        $all_books[$link['bid']] = $link;
2937
      }
2947
    }
2958
  }
296
2978
  return $all_books;
2980
}
299
300
/**
301
 * Implementation of hook_form_alter().
302
 *
303
 * Adds the book fieldset to the node form.
304
 *
305
 * @see book_pick_book_submit()
306
 * @see book_submit()
307
 */
30843
function book_form_alter(&$form, $form_state, $form_id) {
309
31015
  if (!empty($form['#node_edit_form'])) {
311
    // Add elements to the node form.
3128
    $node = $form['#node'];
313
3148
    $access = user_access('administer book outlines');
3158
    if (!$access) {
3168
      if (user_access('add content to books') &&
((!empty($node->book['mlid']) && !empty($node->nid)) ||
book_type_is_allowed($node->type))) {
317
        // Already in the book hierarchy, or this node type is allowed.
3188
        $access = TRUE;
3198
      }
3208
    }
321
3228
    if ($access) {
3238
      _book_add_form_elements($form, $node);
3248
      $form['book']['pick-book'] = array(
3258
        '#type' => 'submit',
3268
        '#value' => t('Change book (update list of parents)'),
327
         // Submit the node form so the parent select options get updated.
328
         // This is typically only used when JS is disabled. Since the
parent options
329
         // won't be changed via AJAX, a button is provided in the node
form to submit
330
         // the form and generate options in the parent select
corresponding to the
331
         // selected book. This is similar to what happens during a node
preview.
3328
        '#submit' => array('node_form_submit_build_node'),
3338
        '#weight' => 20,
334
      );
3358
    }
3368
  }
33715
}
338
339
/**
340
 * Build the parent selection form element for the node form or outline
tab.
341
 *
342
 * This function is also called when generating a new set of options during
the
343
 * AJAX callback, so an array is returned that can be used to replace an
existing
344
 * form element.
345
 */
34643
function _book_parent_select($book_link) {
3478
  if (variable_get('menu_override_parent_selector', FALSE)) {
3480
    return array();
3490
  }
350
  // Offer a message or a drop-down to choose a different parent page.
351
  $form = array(
3528
    '#type' => 'hidden',
3538
    '#value' => -1,
3548
    '#prefix' => '<div id="edit-book-plid-wrapper">',
3558
    '#suffix' => '</div>',
3568
  );
357
3588
  if ($book_link['nid'] === $book_link['bid']) {
359
    // This is a book - at the top level.
3600
    if ($book_link['original_bid'] === $book_link['bid']) {
3610
      $form['#prefix'] .= '<em>' . t('This is the top-level page in this
book.') . '</em>';
3620
    }
363
    else {
3640
      $form['#prefix'] .= '<em>' . t('This will be the top-level page in
this book.') . '</em>';
365
    }
3660
  }
3678
  elseif (!$book_link['bid']) {
3686
    $form['#prefix'] .= '<em>' . t('No book selected.') . '</em>';
3696
  }
370
  else {
371
    $form = array(
3722
      '#type' => 'select',
3732
      '#title' => t('Parent item'),
3742
      '#default_value' => $book_link['plid'],
3752
      '#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)),
3762
      '#options' => book_toc($book_link['bid'], array($book_link['mlid']),
$book_link['parent_depth_limit']),
3772
      '#attributes' => array('class' => 'book-title-select'),
3782
    );
379
  }
380
3818
  return $form;
3820
}
383
384
/**
385
 * Build the common elements of the book form for the node and outline
forms.
386
 */
38743
function _book_add_form_elements(&$form, $node) {
388
  // Need this for AJAX.
3898
  $form['#cache'] = TRUE;
3908
  drupal_add_js("if (Drupal.jsEnabled) { $(document).ready(function() {
$('#edit-book-pick-book').css('display', 'none'); }); }", 'inline');
391
3928
  $form['book'] = array(
3938
    '#type' => 'fieldset',
3948
    '#title' => t('Book outline'),
3958
    '#weight' => 10,
3968
    '#collapsible' => TRUE,
3978
    '#collapsed' => TRUE,
3988
    '#tree' => TRUE,
3998
    '#attributes' => array('class' => 'book-outline-form'),
400
  );
4018
  foreach (array('menu_name', 'mlid', 'nid', 'router_path', 'has_children',
'options', 'module', 'original_bid', 'parent_depth_limit') as $key) {
4028
    $form['book'][$key] = array(
4038
      '#type' => 'value',
4048
      '#value' => $node->book[$key],
405
    );
4068
  }
407
4088
  $form['book']['plid'] = _book_parent_select($node->book);
409
4108
  $form['book']['weight'] = array(
4118
    '#type' => 'weight',
4128
    '#title' => t('Weight'),
4138
    '#default_value' => $node->book['weight'],
4148
    '#delta' => 15,
4158
    '#weight' => 5,
4168
    '#description' => t('Pages at a given level are ordered first by weight
and then by title.'),
417
  );
4188
  $options = array();
4198
  $nid = isset($node->nid) ? $node->nid : 'new';
420
4218
  if (isset($node->nid) && ($nid == $node->book['original_bid']) &&
($node->book['parent_depth_limit'] == 0)) {
422
    // This is the top level node in a maximum depth book and thus cannot
be moved.
4230
    $options[$node->nid] = $node->title;
4240
  }
425
  else {
4268
    foreach (book_get_books() as $book) {
4277
      $options[$book['nid']] = $book['title'];
4287
    }
429
  }
430
4318
  if (user_access('create new books') && ($nid == 'new' || ($nid !=
$node->book['original_bid']))) {
432
    // The node can become a new book, if it is not one already.
4338
    $options = array($nid => '<' . t('create a new book') . '>') +
$options;
4348
  }
4358
  if (!$node->book['mlid']) {
436
    // The node is not currently in the hierarchy.
4378
    $options = array(0 => '<' . t('none') . '>') + $options;
4388
  }
439
440
  // Add a drop-down to select the destination book.
4418
  $form['book']['bid'] = array(
4428
    '#type' => 'select',
4438
    '#title' => t('Book'),
4448
    '#default_value' => $node->book['bid'],
4458
    '#options' => $options,
4468
    '#access' => (bool)$options,
4478
    '#description' => t('Your page will be a part of the selected book.'),
4488
    '#weight' => -5,
4498
    '#attributes' => array('class' => 'book-title-select'),
450
    '#ahah' => array(
4518
      'path' => 'book/js/form',
4528
      'wrapper' => 'edit-book-plid-wrapper',
4538
      'effect' => 'slide',
4548
    ),
455
  );
4568
}
457
458
/**
459
 * Common helper function to handles additions and updates to the book
outline.
460
 *
461
 * Performs all additions and updates to the book outline through node
addition,
462
 * node editing, node deletion, or the outline tab.
463
 */
46443
function _book_update_outline(&$node) {
4656
  if (empty($node->book['bid'])) {
4660
    return FALSE;
4670
  }
4686
  $new = empty($node->book['mlid']);
469
4706
  $node->book['link_path'] = 'node/' . $node->nid;
4716
  $node->book['link_title'] = $node->title;
4726
  $node->book['parent_mismatch'] = FALSE; // The normal case.
473
4746
  if ($node->book['bid'] == $node->nid) {
4751
    $node->book['plid'] = 0;
4761
    $node->book['menu_name'] = book_menu_name($node->nid);
4771
  }
478
  else {
479
    // Check in case the parent is not is this book; the book takes
precedence.
4805
    if (!empty($node->book['plid'])) {
4815
      $parent = db_fetch_array(db_query("SELECT * FROM {book} WHERE mlid =
%d", $node->book['plid']));
4825
    }
4835
    if (empty($node->book['plid']) || !$parent || $parent['bid'] !=
$node->book['bid']) {
4843
      $node->book['plid'] = db_result(db_query("SELECT mlid FROM {book}
WHERE nid = %d", $node->book['bid']));
4853
      $node->book['parent_mismatch'] = TRUE; // Likely when JS is disabled.
4863
    }
487
  }
488
4896
  if (menu_link_save($node->book)) {
4906
    if ($new) {
491
      // Insert new.
4926
      db_query("INSERT INTO {book} (nid, mlid, bid) VALUES (%d, %d, %d)",
$node->nid, $node->book['mlid'], $node->book['bid']);
4936
    }
494
    else {
4950
      if ($node->book['bid'] != db_result(db_query("SELECT bid FROM {book}
WHERE nid = %d", $node->nid))) {
496
        // Update the bid for this page and all children.
4970
        book_update_bid($node->book);
4980
      }
499
    }
500
5016
    return TRUE;
5020
  }
503
504
  // Failed to save the menu link.
5050
  return FALSE;
5060
}
507
508
/**
509
 * Update the bid for a page and its children when it is moved to a new
book.
510
 *
511
 * @param $book_link
512
 *   A fully loaded menu link that is part of the book hierarchy.
513
 */
51443
function book_update_bid($book_link) {
5150
  for ($i = 1; $i <= MENU_MAX_DEPTH && $book_link["p$i"]; $i++) {
5160
    $match[] = "p$i = %d";
5170
    $args[] = $book_link["p$i"];
5180
  }
5190
  $result = db_query("SELECT mlid FROM {menu_links} WHERE " . implode(' AND
', $match), $args);
520
5210
  $mlids = array();
5220
  while ($a = db_fetch_array($result)) {
5230
    $mlids[] = $a['mlid'];
5240
  }
525
5260
  if ($mlids) {
5270
    db_query("UPDATE {book} SET bid = %d WHERE mlid IN (" . implode(',',
$mlids) . ")", $book_link['bid']);
5280
  }
5290
}
530
531
/**
532
 * Get the book menu tree for a page, and return it as a linear array.
533
 *
534
 * @param $book_link
535
 *   A fully loaded menu link that is part of the book hierarchy.
536
 * @return
537
 *   A linear array of menu links in the order that the links are shown in
the
538
 *   menu, so the previous and next pages are the elements before and after
the
539
 *   element corresponding to $node.  The children of $node (if any) will
come
540
 *   immediately after it in the array.
541
 */
54243
function book_get_flat_menu($book_link) {
54312
  static $flat = array();
544
54512
  if (!isset($flat[$book_link['mlid']])) {
546
    // Call menu_tree_all_data() to take advantage of the menu system's
caching.
54712
    $tree = menu_tree_all_data($book_link['menu_name'], $book_link);
54812
    $flat[$book_link['mlid']] = array();
54912
    _book_flatten_menu($tree, $flat[$book_link['mlid']]);
55012
  }
551
55212
  return $flat[$book_link['mlid']];
5530
}
554
555
/**
556
 * Recursive helper function for book_get_flat_menu().
557
 */
55843
function _book_flatten_menu($tree, &$flat) {
55912
  foreach ($tree as $data) {
56012
    if (!$data['link']['hidden']) {
56112
      $flat[$data['link']['mlid']] = $data['link'];
56212
      if ($data['below']) {
56311
        _book_flatten_menu($data['below'], $flat);
56411
      }
56512
    }
56612
  }
56712
}
568
569
/**
570
 * Fetches the menu link for the previous page of the book.
571
 */
57243
function book_prev($book_link) {
573
  // If the parent is zero, we are at the start of a book.
57412
  if ($book_link['plid'] == 0) {
5752
    return NULL;
5760
  }
57710
  $flat = book_get_flat_menu($book_link);
578
  // Assigning the array to $flat resets the array pointer for use with
each().
57910
  $curr = NULL;
580
  do {
58110
    $prev = $curr;
58210
    list($key, $curr) = each($flat);
58310
  } while ($key && $key != $book_link['mlid']);
584
58510
  if ($key == $book_link['mlid']) {
586
    // The previous page in the book may be a child of the previous visible
link.
58710
    if ($prev['depth'] == $book_link['depth'] && $prev['has_children']) {
588
      // The subtree will have only one link at the top level - get its
data.
5892
      $data = array_shift(book_menu_subtree_data($prev));
590
      // The link of interest is the last child - iterate to find the
deepest one.
5912
      while ($data['below']) {
5922
        $data = end($data['below']);
5932
      }
594
5952
      return $data['link'];
5960
    }
597
    else {
5988
      return $prev;
599
    }
6000
  }
6010
}
602
603
/**
604
 * Fetches the menu link for the next page of the book.
605
 */
60643
function book_next($book_link) {
60712
  $flat = book_get_flat_menu($book_link);
608
  // Assigning the array to $flat resets the array pointer for use with
each().
609
  do {
61012
    list($key, $curr) = each($flat);
611
  }
61212
  while ($key && $key != $book_link['mlid']);
613
61412
  if ($key == $book_link['mlid']) {
61512
    return current($flat);
6160
  }
6170
}
618
619
/**
620
 * Format the menu links for the child pages of the current page.
621
 */
62243
function book_children($book_link) {
62312
  $flat = book_get_flat_menu($book_link);
624
62512
  $children = array();
626
62712
  if ($book_link['has_children']) {
628
    // Walk through the array until we find the current page.
629
    do {
6302
      $link = array_shift($flat);
631
    }
6322
    while ($link && ($link['mlid'] != $book_link['mlid']));
633
    // Continue though the array and collect the links whose parent is this
page.
6342
    while (($link = array_shift($flat)) && $link['plid'] ==
$book_link['mlid']) {
6352
      $data['link'] = $link;
6362
      $data['below'] = '';
6372
      $children[] = $data;
6382
    }
6392
  }
640
64112
  return $children ? menu_tree_output($children) : '';
6420
}
643
644
/**
645
 * Generate the corresponding menu name from a book ID.
646
 */
64743
function book_menu_name($bid) {
6488
  return 'book-toc-' . $bid;
6490
}
650
651
/**
652
 * Build an active trail to show in the breadcrumb.
653
 */
65443
function book_build_active_trail($book_link) {
65512
  static $trail;
656
65712
  if (!isset($trail)) {
65812
    $trail = array();
65912
    $trail[] = array('title' => t('Home'), 'href' => '<front>',
'localized_options' => array());
660
66112
    $tree = menu_tree_all_data($book_link['menu_name'], $book_link);
66212
    $curr = array_shift($tree);
663
66412
    while ($curr) {
66512
      if ($curr['link']['href'] == $book_link['href']) {
66612
        $trail[] = $curr['link'];
66712
        $curr = FALSE;
66812
      }
669
      else {
67010
        if ($curr['below'] && $curr['link']['in_active_trail']) {
67110
          $trail[] = $curr['link'];
67210
          $tree = $curr['below'];
67310
        }
67410
        $curr = array_shift($tree);
675
      }
67612
    }
67712
  }
678
67912
  return $trail;
6800
}
681
682
/**
683
 * Implementation of hook_nodeapi_load().
684
 */
68543
function book_nodeapi_load(&$node, $teaser, $page) {
686
  // Note - we cannot use book_link_load() because it will call
node_load().
68719
  $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));
688
68919
  if ($info['book']) {
69019
    $info['book']['href'] = $info['book']['link_path'];
69119
    $info['book']['title'] = $info['book']['link_title'];
69219
    $info['book']['options'] = unserialize($info['book']['options']);
693
69419
    return $info;
6950
  }
6960
}
697
698
/**
699
 * Implementation of hook_nodeapi_view().
700
 */
70143
function book_nodeapi_view(&$node, $teaser, $page) {
70218
  if (!$teaser) {
70318
    if (!empty($node->book['bid']) && $node->build_mode ==
NODE_BUILD_NORMAL) {
70412
      $node->content['book_navigation'] = array(
70512
        '#markup' => theme('book_navigation', $node->book),
70612
        '#weight' => 100,
707
      );
708
70912
      if ($page) {
71012
        menu_set_active_trail(book_build_active_trail($node->book));
71112
        menu_set_active_menu_name($node->book['menu_name']);
71212
      }
71312
    }
71418
  }
71518
}
716
717
/**
718
 * Implementation of hook_nodeapi_presave().
719
 */
72043
function book_nodeapi_presave(&$node, $teaser, $page) {
721
  // Always save a revision for non-administrators.
7226
  if (!empty($node->book['bid']) && !user_access('administer nodes')) {
7236
    $node->revision = 1;
7246
  }
725
  // Make sure a new node gets a new menu link.
7266
  if (empty($node->nid)) {
7276
    $node->book['mlid'] = NULL;
7286
  }
7296
}
730
731
/**
732
 * Implementation of hook_nodeapi_insert().
733
 */
73443
function book_nodeapi_insert(&$node, $teaser, $page) {
7356
  if (!empty($node->book['bid'])) {
7366
    if ($node->book['bid'] == 'new') {
737
      // New nodes that are their own book.
7381
      $node->book['bid'] = $node->nid;
7391
    }
7406
    $node->book['nid'] = $node->nid;
7416
    $node->book['menu_name'] = book_menu_name($node->book['bid']);
7426
    _book_update_outline($node);
7436
  }
7446
}
745
746
/**
747
 * Implementation of hook_nodeapi_update().
748
 */
74943
function book_nodeapi_update(&$node, $teaser, $page) {
7500
  if (!empty($node->book['bid'])) {
7510
    if ($node->book['bid'] == 'new') {
752
      // New nodes that are their own book.
7530
      $node->book['bid'] = $node->nid;
7540
    }
7550
    $node->book['nid'] = $node->nid;
7560
    $node->book['menu_name'] = book_menu_name($node->book['bid']);
7570
    _book_update_outline($node);
7580
  }
7590
}
760
761
/**
762
 * Implementation of hook_nodeapi_delete().
763
 */
76443
function book_nodeapi_delete(&$node, $teaser, $page) {
7650
  if (!empty($node->book['bid'])) {
7660
    if ($node->nid == $node->book['bid']) {
767
      // Handle deletion of a top-level post.
7680
      $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']);
7690
      while ($child = db_fetch_array($result)) {
7700
        $child_node = node_load($child['nid']);
7710
        $child_node->book['bid'] = $child_node->nid;
7720
        _book_update_outline($child_node);
7730
      }
7740
    }
7750
    menu_link_delete($node->book['mlid']);
7760
    db_query('DELETE FROM {book} WHERE mlid = %d', $node->book['mlid']);
7770
  }
7780
}
779
780
/**
781
 * Implementation of hook_nodeapi_prepare().
782
 */
78343
function book_nodeapi_prepare(&$node, $teaser, $page) {
784
  // Prepare defaults for the add/edit form.
7858
  if (empty($node->book) && (user_access('add content to books') ||
user_access('administer book outlines'))) {
7866
    $node->book = array();
787
7886
    if (empty($node->nid) && isset($_GET['parent']) &&
is_numeric($_GET['parent'])) {
789
      // Handle "Add child page" links:
7900
      $parent = book_link_load($_GET['parent']);
791
7920
      if ($parent && $parent['access']) {
7930
        $node->book['bid'] = $parent['bid'];
7940
        $node->book['plid'] = $parent['mlid'];
7950
        $node->book['menu_name'] = $parent['menu_name'];
7960
      }
7970
    }
798
    // Set defaults.
7996
    $node->book += _book_link_defaults(!empty($node->nid) ? $node->nid :
'new');
8006
  }
801
  else {
8022
    if (isset($node->book['bid']) && !isset($node->book['original_bid'])) {
8030
      $node->book['original_bid'] = $node->book['bid'];
8040
    }
805
  }
806
  // Find the depth limit for the parent select.
8078
  if (isset($node->book['bid']) &&
!isset($node->book['parent_depth_limit'])) {
8086
    $node->book['parent_depth_limit'] =
_book_parent_depth_limit($node->book);
8096
  }
8108
}
811
812
/**
813
 * Find the depth limit for items in the parent select.
814
 */
81543
function _book_parent_depth_limit($book_link) {
8166
  return MENU_MAX_DEPTH - 1 - (($book_link['mlid'] &&
$book_link['has_children']) ? menu_link_children_relative_depth($book_link)
: 0);
8170
}
818
819
/**
820
 * Form altering function for the confirm form for a single node deletion.
821
 */
82243
function book_form_node_delete_confirm_alter(&$form, $form_state) {
8230
  $node = node_load($form['nid']['#value']);
824
8250
  if (isset($node->book) && $node->book['has_children']) {
8260
    $form['book_warning'] = array(
8270
      '#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>',
8280
      '#weight' => -10,
829
    );
8300
  }
8310
}
832
833
/**
834
 * Return an array with default values for a book link.
835
 */
83643
function _book_link_defaults($nid) {
8376
  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());
8380
}
839
840
/**
841
 * Process variables for book-navigation.tpl.php.
842
 *
843
 * The $variables array contains the following arguments:
844
 * - $book_link
845
 *
846
 * @see book-navigation.tpl.php
847
 */
84843
function template_preprocess_book_navigation(&$variables) {
84912
  $book_link = $variables['book_link'];
850
851
  // Provide extra variables for themers. Not needed by default.
85212
  $variables['book_id'] = $book_link['bid'];
85312
  $variables['book_title'] = check_plain($book_link['link_title']);
85412
  $variables['book_url'] = 'node/' . $book_link['bid'];
85512
  $variables['current_depth'] = $book_link['depth'];
85612
  $variables['tree'] = '';
857
85812
  if ($book_link['mlid']) {
85912
    $variables['tree'] = book_children($book_link);
860
86112
    if ($prev = book_prev($book_link)) {
86210
      $prev_href = url($prev['href']);
86310
      drupal_add_link(array('rel' => 'prev', 'href' => $prev_href));
86410
      $variables['prev_url'] = $prev_href;
86510
      $variables['prev_title'] = check_plain($prev['title']);
86610
    }
867
86812
    if ($book_link['plid'] && $parent = book_link_load($book_link['plid']))
{
86910
      $parent_href = url($parent['href']);
87010
      drupal_add_link(array('rel' => 'up', 'href' => $parent_href));
87110
      $variables['parent_url'] = $parent_href;
87210
      $variables['parent_title'] = check_plain($parent['title']);
87310
    }
874
87512
    if ($next = book_next($book_link)) {
8765
      $next_href = url($next['href']);
8775
      drupal_add_link(array('rel' => 'next', 'href' => $next_href));
8785
      $variables['next_url'] = $next_href;
8795
      $variables['next_title'] = check_plain($next['title']);
8805
    }
88112
  }
882
88312
  $variables['has_links'] = FALSE;
884
  // Link variables to filter for values and set state of the flag
variable.
88512
  $links = array('prev_url', 'prev_title', 'parent_url', 'parent_title',
'next_url', 'next_title');
88612
  foreach ($links as $link) {
88712
    if (isset($variables[$link])) {
888
      // Flag when there is a value.
88911
      $variables['has_links'] = TRUE;
89011
    }
891
    else {
892
      // Set empty to prevent notices.
8938
      $variables[$link] = '';
894
    }
89512
  }
89612
}
897
898
/**
899
 * A recursive helper function for book_toc().
900
 */
90143
function _book_toc_recurse($tree, $indent, &$toc, $exclude, $depth_limit) {
9022
  foreach ($tree as $data) {
9032
    if ($data['link']['depth'] > $depth_limit) {
904
      // Don't iterate through any links on this level.
9050
      break;
9060
    }
907
9082
    if (!in_array($data['link']['mlid'], $exclude)) {
9092
      $toc[$data['link']['mlid']] = $indent . ' ' .
truncate_utf8($data['link']['title'], 30, TRUE, TRUE);
9102
      if ($data['below']) {
9112
        _book_toc_recurse($data['below'], $indent . '--', $toc, $exclude,
$depth_limit);
9122
      }
9132
    }
9142
  }
9152
}
916
917
/**
918
 * Returns an array of book pages in table of contents order.
919
 *
920
 * @param $bid
921
 *   The ID of the book whose pages are to be listed.
922
 * @param $exclude
923
 *   Optional array of mlid values.  Any link whose mlid is in this array
924
 *   will be excluded (along with its children).
925
 * @param $depth_limit
926
 *   Any link deeper than this value will be excluded (along with its
children).
927
 * @return
928
 *   An array of mlid, title pairs for use as options for selecting a book
page.
929
 */
93043
function book_toc($bid, $exclude = array(), $depth_limit) {
9312
  $tree = menu_tree_all_data(book_menu_name($bid));
9322
  $toc = array();
9332
  _book_toc_recurse($tree, '', $toc, $exclude, $depth_limit);
934
9352
  return $toc;
9360
}
937
938
/**
939
 * Process variables for book-export-html.tpl.php.
940
 *
941
 * The $variables array contains the following arguments:
942
 * - $title
943
 * - $contents
944
 * - $depth
945
 *
946
 * @see book-export-html.tpl.php
947
 */
94843
function template_preprocess_book_export_html(&$variables) {
9496
  global $base_url, $language;
950
9516
  $variables['title'] = check_plain($variables['title']);
9526
  $variables['base_url'] = $base_url;
9536
  $variables['language'] = $language;
9546
  $variables['language_rtl'] = defined('LANGUAGE_RTL') &&
$language->direction == LANGUAGE_RTL;
9556
  $variables['head'] = drupal_get_html_head();
9566
}
957
958
/**
959
 * Traverse the book tree to build printable or exportable output.
960
 *
961
 * During the traversal, the $visit_func() callback is applied to each
962
 * node, and is called recursively for each child of the node (in weight,
963
 * title order).
964
 *
965
 * @param $tree
966
 *   A subtree of the book menu hierarchy, rooted at the current page.
967
 * @param $visit_func
968
 *   A function callback to be called upon visiting a node in the tree.
969
 * @return
970
 *   The output generated in visiting each node.
971
 */
97243
function book_export_traverse($tree, $visit_func) {
9736
  $output = '';
974
9756
  foreach ($tree as $data) {
976
    // Note- access checking is already performed when building the tree.
9776
    if ($node = node_load($data['link']['nid'], FALSE)) {
9786
      $children = '';
979
9806
      if ($data['below']) {
9812
        $children = book_export_traverse($data['below'], $visit_func);
9822
      }
983
9846
      if (function_exists($visit_func)) {
9856
        $output .= call_user_func($visit_func, $node, $children);
9866
      }
987
      else {
988
        // Use the default function.
9890
        $output .= book_node_export($node, $children);
990
      }
9916
    }
9926
  }
993
9946
  return $output;
9950
}
996
997
/**
998
 * Generates printer-friendly HTML for a node.
999
 *
1000
 * @see book_export_traverse()
1001
 *
1002
 * @param $node
1003
 *   The node that will be output.
1004
 * @param $children
1005
 *   All the rendered child nodes within the current node.
1006
 * @return
1007
 *   The HTML generated for the given node.
1008
 */
100943
function book_node_export($node, $children = '') {
10106
  $node->build_mode = NODE_BUILD_PRINT;
10116
  $node = node_build_content($node, FALSE, FALSE);
10126
  $node->body = drupal_render($node->content);
1013
10146
  return theme('book_node_export_html', $node, $children);
10150
}
1016
1017
/**
1018
 * Process variables for book-node-export-html.tpl.php.
1019
 *
1020
 * The $variables array contains the following arguments:
1021
 * - $node
1022
 * - $children
1023
 *
1024
 * @see book-node-export-html.tpl.php
1025
 */
102643
function template_preprocess_book_node_export_html(&$variables) {
10276
  $variables['depth'] = $variables['node']->book['depth'];
10286
  $variables['title'] = check_plain($variables['node']->title);
10296
  $variables['content'] = $variables['node']->body;
10306
}
1031
1032
/**
1033
 * Determine if a given node type is in the list of types allowed for
books.
1034
 */
103543
function book_type_is_allowed($type) {
10368
  return in_array($type, variable_get('book_allowed_types',
array('book')));
10370
}
1038
1039
/**
1040
 * Implementation of hook_node_type().
1041
 *
1042
 * Update book module's persistent variables if the machine-readable name
of a
1043
 * node type is changed.
1044
 */
104543
function book_node_type($op, $type) {
1046
  switch ($op) {
10471
    case 'update':
10480
      if (!empty($type->old_type) && $type->old_type != $type->type) {
1049
        // Update the list of node types that are allowed to be added to
books.
10500
        $allowed_types = variable_get('book_allowed_types', array('book'));
10510
        $key = array_search($type->old_type, $allowed_types);
1052
10530
        if ($key !== FALSE) {
10540
          $allowed_types[$type->type] = $allowed_types[$key] ? $type->type
: 0;
10550
          unset($allowed_types[$key]);
10560
          variable_set('book_allowed_types', $allowed_types);
10570
        }
1058
1059
        // Update the setting for the "Add child page" link.
10600
        if (variable_get('book_child_type', 'book') == $type->old_type) {
10610
          variable_set('book_child_type', $type->type);
10620
        }
10630
      }
10640
      break;
10650
  }
10661
}
1067
1068
/**
1069
 * Implementation of hook_help().
1070
 */
107143
function book_help($path, $arg) {
1072
  switch ($path) {
107327
    case 'admin/help#book':
10740
      $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>';
10750
      $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>';
10760
      $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>';
10770
      $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>';
10780
      $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>';
10790
      $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>';
1080
10810
      return $output;
1082
108327
    case 'admin/content/book':
10840
      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>';
1085
108627
    case 'node/%/outline':
10870
      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>';
10880
  }
108927
}
1090
1091
/**
1092
 * Like menu_link_load(), but adds additional data from the {book} table.
1093
 *
1094
 * Do not call when loading a node, since this function may call
node_load().
1095
 */
109643
function book_link_load($mlid) {
109710
  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))) {
109810
    _menu_link_translate($item);
109910
    return $item;
11000
  }
1101
11020
  return FALSE;
11030
}
1104
1105
/**
1106
 * Get the data representing a subtree of the book hierarchy.
1107
 *
1108
 * The root of the subtree will be the link passed as a parameter, so the
1109
 * returned tree will contain this item and all its descendents in the menu
tree.
1110
 *
1111
 * @param $item
1112
 *   A fully loaded menu link.
1113
 * @return
1114
 *   An subtree of menu links in an array, in the order they should be
rendered.
1115
 */
111643
function book_menu_subtree_data($item) {
11178
  static $tree = array();
1118
1119
  // Generate a cache ID (cid) specific for this $menu_name and $item.
11208
  $cid = 'links:' . $item['menu_name'] . ':subtree-cid:' . $item['mlid'];
1121
11228
  if (!isset($tree[$cid])) {
11238
    $cache = cache_get($cid, 'cache_menu');
1124
11258
    if ($cache && isset($cache->data)) {
1126
      // If the cache entry exists, it will just be the cid for the actual
data.
1127
      // This avoids duplication of large amounts of data.
11281
      $cache = cache_get($cache->data, 'cache_menu');
1129
11301
      if ($cache && isset($cache->data)) {
11311
        $data = $cache->data;
11321
      }
11331
    }
1134
1135
    // If the subtree data was not in the cache, $data will be NULL.
11368
    if (!isset($data)) {
11377
      $match = array("menu_name = '%s'");
11387
      $args = array($item['menu_name']);
11397
      $i = 1;
11407
      while ($i <= MENU_MAX_DEPTH && $item["p$i"]) {
11417
        $match[] = "p$i = %d";
11427
        $args[] = $item["p$i"];
11437
        $i++;
11447
      }
1145
      $sql = "
1146
        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.*
1147
        FROM {menu_links} ml INNER JOIN {menu_router} m ON m.path =
ml.router_path
1148
        INNER JOIN {book} b ON ml.mlid = b.mlid
11497
        WHERE " . implode(' AND ', $match) . "
11507
        ORDER BY p1 ASC, p2 ASC, p3 ASC, p4 ASC, p5 ASC, p6 ASC, p7 ASC, p8
ASC, p9 ASC";
1151
11527
      $data['tree'] = menu_tree_data(db_query($sql, $args), array(),
$item['depth']);
11537
      $data['node_links'] = array();
11547
      menu_tree_collect_node_links($data['tree'], $data['node_links']);
1155
      // Compute the real cid for book subtree data.
11567
      $tree_cid = 'links:' . $item['menu_name'] . ':subtree-data:' .
md5(serialize($data));
1157
      // Cache the data, if it is not already in the cache.
1158
11597
      if (!cache_get($tree_cid, 'cache_menu')) {
11607
        cache_set($tree_cid, $data, 'cache_menu');
11617
      }
1162
      // Cache the cid of the (shared) data using the menu and
item-specific cid.
11637
      cache_set($cid, $tree_cid, 'cache_menu');
11647
    }
1165
    // Check access for the current user to each item in the tree.
11668
    menu_tree_check_access($data['tree'], $data['node_links']);
11678
    $tree[$cid] = $data['tree'];
11688
  }
1169
11708
  return $tree[$cid];
11710
}
117243