book.module 38.9 KB
Newer Older
Dries's avatar
 
Dries committed
1
<?php
2
// $Id$
Dries's avatar
 
Dries committed
3

Dries's avatar
 
Dries committed
4 5
/**
 * @file
Dries's avatar
Dries committed
6
 * Allows users to structure the pages of a site in a hierarchy or outline.
Dries's avatar
 
Dries committed
7 8
 */

9 10 11 12 13 14
/**
 * Implementation of hook_theme()
 */
function book_theme() {
  return array(
    'book_navigation' => array(
Dries's avatar
Dries committed
15
      'arguments' => array('book_link' => NULL),
16
      'template' => 'book-navigation',
17 18
    ),
    'book_export_html' => array(
19 20
      'arguments' => array('title' => NULL, 'contents' => NULL, 'depth' => NULL),
      'template' => 'book-export-html',
21 22 23 24
    ),
    'book_admin_table' => array(
      'arguments' => array('form' => NULL),
    ),
Dries's avatar
Dries committed
25 26 27 28 29
    'book_title_link' => array(
      'arguments' => array('link' => NULL),
    ),
    'book_all_books_block' => array(
      'arguments' => array('book_menus' => array()),
30 31 32 33 34
      'template' => 'book-all-books-block',
    ),
    'book_node_export_html' => array(
      'arguments' => array('node' => NULL, 'children' => NULL),
      'template' => 'book-node-export-html',
Dries's avatar
Dries committed
35
    ),
36 37 38
  );
}

39 40 41
/**
 * Implementation of hook_perm().
 */
Dries's avatar
 
Dries committed
42
function book_perm() {
43 44 45 46 47 48
  return array(
    'add content to books' => t('Add new content and child pages to books.'),
    'administer book outlines' => t('Manage books through the administration panel.'),
    'create new books' => t('Add new top-level books.'),
    '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.'),
  );
Dries's avatar
 
Dries committed
49 50
}

Dries's avatar
 
Dries committed
51 52 53
/**
 * Implementation of hook_link().
 */
54
function book_link($type, $node = NULL, $teaser = FALSE) {
Dries's avatar
 
Dries committed
55 56
  $links = array();

Dries's avatar
Dries committed
57
  if ($type == 'node' && isset($node->book)) {
58
    if (!$teaser) {
Dries's avatar
Dries committed
59 60
      $child_type = variable_get('book_child_type', 'book');
      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) {
61
        $links['book_add_child'] = array(
62
          'title' => t('Add child page'),
63 64
          'href' => "node/add/" . str_replace('_', '-', $child_type),
          'query' => "parent=" . $node->book['mlid'],
65
        );
Dries's avatar
 
Dries committed
66
      }
Dries's avatar
Dries committed
67
      if (user_access('access printer-friendly version')) {
68
        $links['book_printer'] = array(
69
          'title' => t('Printer-friendly version'),
70
          'href' => 'book/export/html/' . $node->nid,
71
          'attributes' => array('title' => t('Show a printer-friendly version of this book page and its sub-pages.'))
72
        );
73
      }
Dries's avatar
 
Dries committed
74
    }
Dries's avatar
 
Dries committed
75
  }
Dries's avatar
 
Dries committed
76
  return $links;
Dries's avatar
 
Dries committed
77 78
}

Dries's avatar
 
Dries committed
79 80 81
/**
 * Implementation of hook_menu().
 */
82 83
function book_menu() {
  $items['admin/content/book'] = array(
84
    'title' => 'Books',
Dries's avatar
Dries committed
85 86 87
    'description' => "Manage your site's book outlines.",
    'page callback' => 'book_admin_overview',
    'access arguments' => array('administer book outlines'),
88 89
  );
  $items['admin/content/book/list'] = array(
90
    'title' => 'List',
91 92
    'type' => MENU_DEFAULT_LOCAL_TASK,
  );
Dries's avatar
Dries committed
93 94
  $items['admin/content/book/settings'] = array(
    'title' => 'Settings',
95
    'page callback' => 'drupal_get_form',
Dries's avatar
Dries committed
96 97
    'page arguments' => array('book_admin_settings'),
    'access arguments' => array('administer site configuration'),
98 99 100
    'type' => MENU_LOCAL_TASK,
    'weight' => 8,
  );
Dries's avatar
Dries committed
101 102 103 104 105 106 107 108
  $items['admin/content/book/%node'] = array(
    'title' => 'Re-order book pages and change titles',
    'page callback' => 'drupal_get_form',
    'page arguments' => array('book_admin_edit', 3),
    'access callback' => '_book_outline_access',
    'access arguments' => array(3),
    'type' => MENU_CALLBACK,
  );
109
  $items['book'] = array(
110
    'title' => 'Books',
111 112 113 114 115 116 117
    'page callback' => 'book_render',
    'access arguments' => array('access content'),
    'type' => MENU_SUGGESTED_ITEM,
  );
  $items['book/export/%/%'] = array(
    'page callback' => 'book_export',
    'page arguments' => array(2, 3),
Dries's avatar
Dries committed
118
    'access arguments' => array('access printer-friendly version'),
119 120
    'type' => MENU_CALLBACK,
  );
121
  $items['node/%node/outline'] = array(
122
    'title' => 'Outline',
Dries's avatar
Dries committed
123 124
    'page callback' => 'book_outline',
    'page arguments' => array(1),
125 126 127 128 129
    'access callback' => '_book_outline_access',
    'access arguments' => array(1),
    'type' => MENU_LOCAL_TASK,
    'weight' => 2,
  );
Dries's avatar
Dries committed
130 131 132 133 134 135 136 137
  $items['node/%node/outline/remove'] = array(
    'title' => 'Remove from outline',
    'page callback' => 'drupal_get_form',
    'page arguments' => array('book_remove_form', 1),
    'access callback' => '_book_outline_remove_access',
    'access arguments' => array(1),
    'type' => MENU_CALLBACK,
  );
138
  $items['book/js/form'] = array(
Dries's avatar
Dries committed
139 140 141 142
    'page callback' => 'book_form_update',
    'access arguments' => array('access content'),
    'type' => MENU_CALLBACK,
  );
Dries's avatar
 
Dries committed
143 144 145
  return $items;
}

Dries's avatar
Dries committed
146 147 148
/**
 * Menu item access callback - determine if the outline tab is accessible.
 */
149
function _book_outline_access($node) {
Dries's avatar
Dries committed
150 151 152 153 154 155 156 157
  return user_access('administer book outlines') && node_access('view', $node);
}

/**
 * Menu item access callback - determine if the user can remove nodes from the outline.
 */
function _book_outline_remove_access($node) {
  return isset($node->book) && ($node->book['bid'] != $node->nid) && _book_outline_access($node);
158 159
}

Dries's avatar
Dries committed
160 161 162
/**
 * Implementation of hook_init(). Add's the book module's CSS.
 */
163
function book_init() {
164
  drupal_add_css(drupal_get_path('module', 'book') . '/book.css');
165 166
}

167 168 169
/**
 * Implementation of hook_block().
 *
Dries's avatar
 
Dries committed
170 171
 * Displays the book table of contents in a block when the current page is a
 * single-node view of a book node.
172
 */
173
function book_block($op = 'list', $delta = '', $edit = array()) {
Dries's avatar
 
Dries committed
174
  $block = array();
Dries's avatar
Dries committed
175 176
  switch ($op) {
    case 'list':
177 178
      $block['navigation']['info'] = t('Book navigation');
      $block['navigation']['cache'] = BLOCK_CACHE_PER_PAGE | BLOCK_CACHE_PER_ROLE;
Dries's avatar
Dries committed
179 180
      return $block;
    case 'view':
181 182 183
      $current_bid = 0;
      if ($node = menu_get_object()) {
        $current_bid = empty($node->book['bid']) ? 0 : $node->book['bid'];
184
      }
185
      if (variable_get('book_block_mode', 'all pages') == 'all pages') {
Dries's avatar
Dries committed
186 187
        $block['subject'] = t('Book navigation');
        $book_menus = array();
188
        $pseudo_tree = array(0 => array('below' => FALSE));
189
        foreach (book_get_books() as $book_id => $book) {
190 191 192
          if ($book['bid'] == $current_bid) {
            // If the current page is a node associated with a book, the menu
            // needs to be retrieved.
193
            $book_menus[$book_id] = menu_tree_output(menu_tree_all_data($node->book['menu_name'], $node->book));
194 195 196 197 198 199
          }
          else {
            // Since we know we will only display a link to the top node, there
            // is no reason to run an additional menu tree query for each book.
            $book['in_active_trail'] = FALSE;
            $pseudo_tree[0]['link'] = $book;
200
            $book_menus[$book_id] = menu_tree_output($pseudo_tree);
201
          }
Dries's avatar
 
Dries committed
202
        }
Dries's avatar
Dries committed
203
        $block['content'] = theme('book_all_books_block', $book_menus);
Dries's avatar
 
Dries committed
204
      }
205
      elseif ($current_bid) {
Dries's avatar
Dries committed
206
        // Only display this block when the user is browsing a book.
207 208 209 210 211 212 213 214
        $title = db_result(db_query(db_rewrite_sql('SELECT n.title FROM {node} n WHERE n.nid = %d'), $node->book['bid']));
        // Only show the block if the user has view access for the top-level node.
        if ($title) {
          $tree = menu_tree_all_data($node->book['menu_name'], $node->book);
          // There should only be one element at the top level.
          $data = array_shift($tree);
          $block['subject'] = theme('book_title_link', $data['link']);
          $block['content'] = ($data['below']) ? menu_tree_output($data['below']) : '';
Dries's avatar
Dries committed
215 216 217 218 219 220 221 222 223 224 225 226 227
        }
      }
      return $block;
    case 'configure':
      $options = array(
        'all pages' => t('Show block on all pages'),
        'book pages' => t('Show block only on book pages'),
      );
      $form['book_block_mode'] = array(
        '#type' => 'radios',
        '#title' => t('Book navigation block display'),
        '#options' => $options,
        '#default_value' => variable_get('book_block_mode', 'all pages'),
228
        '#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."),
Dries's avatar
Dries committed
229 230 231 232 233 234 235
        );
      return $form;
    case 'save':
      variable_set('book_block_mode', $edit['book_block_mode']);
      break;
  }
}
Dries's avatar
 
Dries committed
236

237
/**
Dries's avatar
Dries committed
238 239 240
 * Generate the HTML output for a link to a book title when used as a block title.
 *
 * @ingroup themeable
241
 */
Dries's avatar
Dries committed
242 243 244
function theme_book_title_link($link) {
  $link['options']['attributes']['class'] =  'book-title';
  return l($link['title'], $link['href'], $link['options']);
Dries's avatar
 
Dries committed
245
}
Dries's avatar
 
Dries committed
246

247
/**
Dries's avatar
Dries committed
248 249 250 251
 * Returns an array of all books.
 *
 * This list may be used for generating a list of all the books, or for building
 * the options for a form select.
252
 */
Dries's avatar
Dries committed
253 254 255 256 257 258 259 260 261 262 263
function book_get_books() {
  static $all_books;

  if (!isset($all_books)) {
    $all_books = array();
    $result = db_query("SELECT DISTINCT(bid) FROM {book}");
    $nids = array();
    while ($book = db_fetch_array($result)) {
      $nids[] = $book['bid'];
    }
    if ($nids) {
264
      $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"));
Dries's avatar
Dries committed
265 266 267
      while ($link = db_fetch_array($result2)) {
        $link['href'] = $link['link_path'];
        $link['options'] = unserialize($link['options']);
268
        $all_books[$link['bid']] = $link;
Dries's avatar
Dries committed
269 270
      }
    }
271
  }
Dries's avatar
Dries committed
272
  return $all_books;
273
}
274

Dries's avatar
Dries committed
275 276 277 278 279 280 281 282
/**
 * Implementation of hook_form_alter(). Adds the book fieldset to the node form.
 *
 * @see book_pick_book_submit()
 * @see book_submit()
 */
function book_form_alter(&$form, $form_state, $form_id) {

283
  if (isset($form['type']) && isset($form['#node']) && $form['type']['#value'] . '_node_form' == $form_id) {
Dries's avatar
Dries committed
284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299
    // Add elements to the node form
    $node = $form['#node'];

    $access = user_access('administer book outlines');
    if (!$access) {
      if (user_access('add content to books') && ((!empty($node->book['mlid']) && !empty($node->nid)) || book_type_is_allowed($node->type))) {
        // Already in the book hierarchy or this node type is allowed
        $access = TRUE;
      }
    }

    if ($access) {
      _book_add_form_elements($form, $node);
      $form['book']['pick-book'] = array(
        '#type' => 'submit',
        '#value' => t('Change book (update list of parents)'),
300 301 302 303 304 305
         // Submit the node form so the parent select options get updated.
         // This is typically only used when JS is disabled.  Since the parent options
         // won't be changed via AJAX, a button is provided in the node form to submit
         // the form and generate options in the parent select corresponding to the
         // selected book.  This is similar to what happens during a node preview.
        '#submit' => array('node_form_submit_build_node'),
Dries's avatar
Dries committed
306 307 308
        '#weight' => 20,
      );
    }
309
  }
Dries's avatar
Dries committed
310
}
311

Dries's avatar
Dries committed
312 313 314 315 316 317 318 319
/**
 * Build the parent selection form element for the node form or outline tab
 *
 * This function is also called when generating a new set of options during the
 * AJAX callback, so an array is returned that can be used to replace an existing
 * form element.
 */
function _book_parent_select($book_link) {
320 321 322
  if (variable_get('menu_override_parent_selector', FALSE)) {
    return array();
  }
Dries's avatar
Dries committed
323 324 325 326 327 328 329
  // Offer a message or a drop-down to choose a different parent page.
  $form = array(
    '#type' => 'hidden',
    '#value' => -1,
    '#prefix' => '<div id="edit-book-plid-wrapper">',
    '#suffix' => '</div>',
  );
330

Dries's avatar
Dries committed
331 332 333
  if ($book_link['nid'] === $book_link['bid']) {
    // This is a book - at the top level.
    if ($book_link['original_bid'] === $book_link['bid']) {
334
      $form['#prefix'] .= '<em>' . t('This is the top-level page in this book.') . '</em>';
Dries's avatar
Dries committed
335 336
    }
    else {
337
      $form['#prefix'] .= '<em>' . t('This will be the top-level page in this book.') . '</em>';
Dries's avatar
Dries committed
338 339 340
    }
  }
  elseif (!$book_link['bid']) {
341
    $form['#prefix'] .= '<em>' . t('No book selected.') . '</em>';
342 343
  }
  else {
Dries's avatar
Dries committed
344 345 346 347
    $form = array(
      '#type' => 'select',
      '#title' => t('Parent item'),
      '#default_value' => $book_link['plid'],
348
      '#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)),
349
      '#options' => book_toc($book_link['bid'], array($book_link['mlid']), $book_link['parent_depth_limit']),
350
      '#attributes' => array('class' => 'book-title-select'),
351
    );
Dries's avatar
 
Dries committed
352
  }
353
  return $form;
Dries's avatar
 
Dries committed
354 355
}

356
/**
Dries's avatar
Dries committed
357
 * Build the common elements of the book form for the node and outline forms.
358
 */
Dries's avatar
Dries committed
359 360 361
function _book_add_form_elements(&$form, $node) {
  // Need this for AJAX.
  $form['#cache'] = TRUE;
362
  drupal_add_js("if (Drupal.jsEnabled) { $(document).ready(function() { $('#edit-book-pick-book').css('display', 'none'); }); }", 'inline');
Dries's avatar
Dries committed
363 364 365 366 367 368 369 370

  $form['book'] = array(
    '#type' => 'fieldset',
    '#title' => t('Book outline'),
    '#weight' => 10,
    '#collapsible' => TRUE,
    '#collapsed' => TRUE,
    '#tree' => TRUE,
371
    '#attributes' => array('class' => 'book-outline-form'),
372
  );
373
  foreach (array('menu_name', 'mlid', 'nid', 'router_path', 'has_children', 'options', 'module', 'original_bid', 'parent_depth_limit') as $key) {
Dries's avatar
Dries committed
374 375 376 377 378 379 380 381 382 383
    $form['book'][$key] = array(
      '#type' => 'value',
      '#value' => $node->book[$key],
    );
  }

  $form['book']['plid'] = _book_parent_select($node->book);

  $form['book']['weight'] = array(
    '#type' => 'weight',
384
    '#title' => t('Weight'),
Dries's avatar
Dries committed
385
    '#default_value' => $node->book['weight'],
386
    '#delta' => 15,
Dries's avatar
Dries committed
387
    '#weight' => 5,
388 389
    '#description' => t('Pages at a given level are ordered first by weight and then by title.'),
  );
Dries's avatar
Dries committed
390 391
  $options = array();
  $nid = isset($node->nid) ? $node->nid : 'new';
392

393
  if (isset($node->nid) && ($nid == $node->book['original_bid']) && ($node->book['parent_depth_limit'] == 0)) {
394 395 396 397 398 399 400 401 402
    // This is the top level node in a maximum depth book and thus cannot be moved.
    $options[$node->nid] = $node->title;
  }
  else {
    foreach (book_get_books() as $book) {
      $options[$book['nid']] = $book['title'];
    }
  }

Dries's avatar
Dries committed
403 404
  if (user_access('create new books') && ($nid == 'new' || ($nid != $node->book['original_bid']))) {
    // The node can become a new book, if it is not one already.
405
    $options = array($nid => '<' . t('create a new book') . '>') + $options;
Dries's avatar
Dries committed
406 407 408
  }
  if (!$node->book['mlid']) {
    // The node is not currently in a the hierarchy.
409
    $options = array(0 => '<' . t('none') . '>') + $options;
Dries's avatar
Dries committed
410 411 412 413 414 415 416 417 418 419 420
  }

  // Add a drop-down to select the destination book.
  $form['book']['bid'] = array(
    '#type' => 'select',
    '#title' => t('Book'),
    '#default_value' => $node->book['bid'],
    '#options' => $options,
    '#access' => (bool)$options,
    '#description' => t('Your page will be a part of the selected book.'),
    '#weight' => -5,
421
    '#attributes' => array('class' => 'book-title-select'),
422 423 424 425 426
    '#ahah' => array(
      'path' => 'book/js/form',
      'wrapper' => 'edit-book-plid-wrapper',
      'effect' => 'slide',
    ),
427
  );
Dries's avatar
Dries committed
428
}
Dries's avatar
 
Dries committed
429

430
/**
Dries's avatar
Dries committed
431
 * Common helper function to handles additions and updates to the book outline.
Dries's avatar
Dries committed
432
 *
Dries's avatar
Dries committed
433 434 435 436 437 438 439 440 441
 * Performs all additions and updates to the book outline through node addition,
 * node editing, node deletion, or the outline tab.
 */
function _book_update_outline(&$node) {
  if (empty($node->book['bid'])) {
    return FALSE;
  }
  $new = empty($node->book['mlid']);

442
  $node->book['link_path'] = 'node/' . $node->nid;
Dries's avatar
Dries committed
443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476
  $node->book['link_title'] = $node->title;
  $node->book['parent_mismatch'] = FALSE; // The normal case.

  if ($node->book['bid'] == $node->nid) {
    $node->book['plid'] = 0;
    $node->book['menu_name'] = book_menu_name($node->nid);
  }
  else {
    // Check in case the parent is not is this book; the book takes precedence.
    if (!empty($node->book['plid'])) {
      $parent = db_fetch_array(db_query("SELECT * FROM {book} WHERE mlid = %d", $node->book['plid']));
    }
    if (empty($node->book['plid']) || !$parent || $parent['bid'] != $node->book['bid']) {
      $node->book['plid'] = db_result(db_query("SELECT mlid FROM {book} WHERE nid = %d", $node->book['bid']));
      $node->book['parent_mismatch'] = TRUE; // Likely when JS is disabled.
    }
  }
  if (menu_link_save($node->book)) {
    if ($new) {
      // Insert new.
      db_query("INSERT INTO {book} (nid, mlid, bid) VALUES (%d, %d, %d)", $node->nid, $node->book['mlid'], $node->book['bid']);
    }
    else {
      if ($node->book['bid'] != db_result(db_query("SELECT bid FROM {book} WHERE nid = %d", $node->nid))) {
        // Update the bid for this page and all children.
        book_update_bid($node->book);
      }
    }
    return TRUE;
  }
  // Failed to save the menu link
  return FALSE;
}

477
/**
Dries's avatar
Dries committed
478
 * Update the bid for a page and its children when it is moved to a new book.
479
 *
Dries's avatar
Dries committed
480 481 482 483 484 485 486 487 488
 * @param $book_link
 *   A fully loaded menu link that is part of the book hierarchy.
 */
function book_update_bid($book_link) {

  for ($i = 1; $i <= MENU_MAX_DEPTH && $book_link["p$i"]; $i++) {
    $match[] = "p$i = %d";
    $args[] = $book_link["p$i"];
  }
489
  $result = db_query("SELECT mlid FROM {menu_links} WHERE " . implode(' AND ', $match), $args);
Dries's avatar
Dries committed
490 491 492 493 494 495

  $mlids = array();
  while ($a = db_fetch_array($result)) {
    $mlids[] = $a['mlid'];
  }
  if ($mlids) {
496
    db_query("UPDATE {book} SET bid = %d WHERE mlid IN (" . implode(',', $mlids) . ")", $book_link['bid']);
Dries's avatar
Dries committed
497 498 499 500 501
  }
}

/**
 * Get the book menu tree for a page, and return it as a linear array.
502
 *
Dries's avatar
Dries committed
503 504
 * @param $book_link
 *   A fully loaded menu link that is part of the book hierarchy.
505
 * @return
Dries's avatar
Dries committed
506 507 508 509
 *   A linear array of menu links in the order that the links are shown in the
 *   menu, so the previous and next pages are the elements before and after the
 *   element corresponding to $node.  The children of $node (if any) will come
 *   immediately after it in the array.
510
 */
Dries's avatar
Dries committed
511 512 513 514
function book_get_flat_menu($book_link) {
  static $flat = array();

  if (!isset($flat[$book_link['mlid']])) {
515
    // Call menu_tree_all_data() to take advantage of the menu system's caching.
Dries's avatar
Dries committed
516 517 518
    $tree = menu_tree_all_data($book_link['menu_name'], $book_link);
    $flat[$book_link['mlid']] = array();
    _book_flatten_menu($tree, $flat[$book_link['mlid']]);
Dries's avatar
 
Dries committed
519
  }
Dries's avatar
Dries committed
520
  return $flat[$book_link['mlid']];
Dries's avatar
 
Dries committed
521 522
}

523
/**
Dries's avatar
Dries committed
524
 * Recursive helper function for book_get_flat_menu().
525
 */
Dries's avatar
Dries committed
526 527 528 529 530 531 532 533
function _book_flatten_menu($tree, &$flat) {
  foreach ($tree as $data) {
    if (!$data['link']['hidden']) {
      $flat[$data['link']['mlid']] = $data['link'];
      if ($data['below']) {
        _book_flatten_menu($data['below'], $flat);
      }
    }
Dries's avatar
 
Dries committed
534
  }
Dries's avatar
Dries committed
535
}
Dries's avatar
 
Dries committed
536

Dries's avatar
Dries committed
537 538 539 540 541 542 543
/**
 * Fetches the menu link for the previous page of the book.
 */
function book_prev($book_link) {
  // If the parent is zero, we are at the start of a book.
  if ($book_link['plid'] == 0) {
    return NULL;
Dries's avatar
 
Dries committed
544
  }
Dries's avatar
Dries committed
545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566
  $flat = book_get_flat_menu($book_link);
  // Assigning the array to $flat resets the array pointer for use with each().
  $curr = NULL;
  do {
    $prev = $curr;
    list($key, $curr) = each($flat);
  } while ($key && $key != $book_link['mlid']);

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

570
/**
Dries's avatar
Dries committed
571
 * Fetches the menu link for the next page of the book.
572
 */
Dries's avatar
Dries committed
573 574 575 576 577 578 579 580
function book_next($book_link) {
  $flat = book_get_flat_menu($book_link);
  // Assigning the array to $flat resets the array pointer for use with each().
  do {
    list($key, $curr) = each($flat);
  } while ($key && $key != $book_link['mlid']);
  if ($key == $book_link['mlid']) {
    return current($flat);
Dries's avatar
 
Dries committed
581
  }
Dries's avatar
Dries committed
582
}
Dries's avatar
 
Dries committed
583

Dries's avatar
Dries committed
584 585 586 587 588
/**
 * Format the menu links for the child pages of the current page.
 */
function book_children($book_link) {
  $flat = book_get_flat_menu($book_link);
589

Dries's avatar
Dries committed
590 591 592 593 594 595 596 597 598 599 600 601
  $children = array();

  if ($book_link['has_children']) {
    // Walk through the array until we find the current page.
    do {
      $link = array_shift($flat);
    } while ($link && ($link['mlid'] != $book_link['mlid']));
    // Continue though the array and collect the links whose parent is this page.
    while (($link = array_shift($flat)) && $link['plid'] == $book_link['mlid']) {
      $data['link'] = $link;
      $data['below'] = '';
      $children[] = $data;
Dries's avatar
 
Dries committed
602 603
    }
  }
Dries's avatar
Dries committed
604
  return $children ? menu_tree_output($children) : '';
Dries's avatar
 
Dries committed
605 606
}

607
/**
Dries's avatar
Dries committed
608
 * Generate the corresponding menu name from a book ID.
609
 */
Dries's avatar
Dries committed
610
function book_menu_name($bid) {
611
  return 'book-toc-' . $bid;
Dries's avatar
Dries committed
612 613 614 615 616 617 618 619 620 621
}

/**
 * Build an active trail to show in the breadcrumb.
 */
function book_build_active_trail($book_link) {
  static $trail;

  if (!isset($trail)) {
    $trail = array();
622
    $trail[] = array('title' => t('Home'), 'href' => '<front>', 'localized_options' => array());
Dries's avatar
Dries committed
623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 641

    $tree = menu_tree_all_data($book_link['menu_name'], $book_link);
    $curr = array_shift($tree);

    while ($curr) {
      if ($curr['link']['href'] == $book_link['href']) {
        $trail[] = $curr['link'];
        $curr = FALSE;
      }
      else {
        if ($curr['below'] && $curr['link']['in_active_trail']) {
          $trail[] = $curr['link'];
          $tree = $curr['below'];
        }
        $curr = array_shift($tree);
      }
    }
  }
  return $trail;
Dries's avatar
 
Dries committed
642 643
}

644
/**
Dries's avatar
 
Dries committed
645 646
 * Implementation of hook_nodeapi().
 *
Dries's avatar
Dries committed
647 648
 * Appends book navigation to all nodes in the book, and handles book outline
 * insertions and updates via the node form.
649
 */
Dries's avatar
 
Dries committed
650 651
function book_nodeapi(&$node, $op, $teaser, $page) {
  switch ($op) {
652
    case 'load':
653 654 655 656 657 658 659 660 661
      if (in_array($node->type, variable_get('book_allowed_types', array('book')))) {
        // Note - we cannot use book_link_load() because it will call node_load()
        $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));
        if ($info['book']) {
          $info['book']['href'] = $info['book']['link_path'];
          $info['book']['title'] = $info['book']['link_title'];
          $info['book']['options'] = unserialize($info['book']['options']);
          return $info;
        }
Dries's avatar
Dries committed
662
      }
663
      break;
Dries's avatar
 
Dries committed
664
    case 'view':
Dries's avatar
Dries committed
665 666
    if (!$teaser) {
        if (!empty($node->book['bid']) && $node->build_mode == NODE_BUILD_NORMAL) {
667

668
          $node->content['book_navigation'] = array(
Dries's avatar
Dries committed
669
            '#value' => theme('book_navigation', $node->book),
670 671
            '#weight' => 100,
          );
672

673
          if ($page) {
Dries's avatar
Dries committed
674 675
            menu_set_active_trail(book_build_active_trail($node->book));
            menu_set_active_menu_name($node->book['menu_name']);
676
          }
Dries's avatar
 
Dries committed
677
        }
Dries's avatar
 
Dries committed
678
      }
Dries's avatar
 
Dries committed
679
      break;
Dries's avatar
Dries committed
680 681 682 683 684
    case 'presave':
      // Always save a revision for non-administrators.
      if (!empty($node->book['bid']) && !user_access('administer nodes')) {
        $node->revision = 1;
      }
685 686 687 688
      // Make sure a new node gets a new menu link.
      if (empty($node->nid)) {
        $node->book['mlid'] = NULL;
      }
Dries's avatar
Dries committed
689 690
      break;
    case 'insert':
691
    case 'update':
Dries's avatar
Dries committed
692 693 694 695
      if (!empty($node->book['bid'])) {
        if ($node->book['bid'] == 'new') {
          // New nodes that are their own book.
          $node->book['bid'] = $node->nid;
696
        }
Dries's avatar
Dries committed
697 698 699
        $node->book['nid'] = $node->nid;
        $node->book['menu_name'] = book_menu_name($node->book['bid']);
        _book_update_outline($node);
700 701
      }
      break;
702
    case 'delete':
Dries's avatar
Dries committed
703 704 705 706 707 708 709 710 711 712 713 714 715
      if (!empty($node->book['bid'])) {
        if ($node->nid == $node->book['bid']) {
          // Handle deletion of a top-level post.
          $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']);
          while ($child = db_fetch_array($result)) {
            $child_node = node_load($child['nid']);
            $child_node->book['bid'] = $child_node->nid;
            _book_update_outline($child_node);
          }
        }
        menu_link_delete($node->book['mlid']);
        db_query('DELETE FROM {book} WHERE mlid = %d', $node->book['mlid']);
      }
716
      break;
Dries's avatar
Dries committed
717 718 719 720 721 722 723 724 725 726 727 728 729 730 731 732 733 734 735 736 737
    case 'prepare':
      // Prepare defaults for the add/edit form.
      if (empty($node->book) && (user_access('add content to books') || user_access('administer book outlines'))) {
        $node->book = array();
        if (empty($node->nid) && isset($_GET['parent']) && is_numeric($_GET['parent'])) {
          // Handle "Add child page" links:
          $parent = book_link_load($_GET['parent']);
          if ($parent && $parent['access']) {
            $node->book['bid'] = $parent['bid'];
            $node->book['plid'] = $parent['mlid'];
            $node->book['menu_name'] = $parent['menu_name'];
          }
        }
        // Set defaults.
        $node->book += _book_link_defaults(!empty($node->nid) ? $node->nid : 'new');
      }
      else {
        if (isset($node->book['bid']) && !isset($node->book['original_bid'])) {
          $node->book['original_bid'] = $node->book['bid'];
        }
      }
738 739 740 741
      // Find the depth limit for the parent select.
      if (isset($node->book['bid']) && !isset($node->book['parent_depth_limit'])) {
        $node->book['parent_depth_limit'] = _book_parent_depth_limit($node->book);
      }
Dries's avatar
Dries committed
742 743 744 745
      break;
  }
}

746 747 748 749 750 751 752
/**
 * Find the depth limit for items in the parent select.
 */
function _book_parent_depth_limit($book_link) {
  return MENU_MAX_DEPTH - 1 - (($book_link['mlid'] && $book_link['has_children']) ? menu_link_children_relative_depth($book_link) : 0);
}

Dries's avatar
Dries committed
753 754 755 756 757 758 759 760 761
/**
 * Form altering function for the confirm form for a single node deletion.
 */
function book_form_node_delete_confirm_alter(&$form, $form_state) {

  $node = node_load($form['nid']['#value']);

  if (isset($node->book) && $node->book['has_children']) {
    $form['book_warning'] = array(
762
      '#value' => '<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>',
Dries's avatar
Dries committed
763 764
      '#weight' => -10,
    );
Dries's avatar
 
Dries committed
765
  }
Dries's avatar
 
Dries committed
766
}
Dries's avatar
 
Dries committed
767

768
/**
Dries's avatar
Dries committed
769 770 771 772 773 774 775
 * Return an array with default values for a book link.
 */
function _book_link_defaults($nid) {
  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());
}

/**
776
 * Process variables for book-navigation.tpl.php.
Dries's avatar
Dries committed
777
 *
778 779
 * The $variables array contains the following arguments:
 * - $book_link
780
 *
781
 * @see book-navigation.tpl.php
782
 */
783 784 785 786 787 788
function template_preprocess_book_navigation(&$variables) {
  $book_link = $variables['book_link'];

  // Provide extra variables for themers. Not needed by default.
  $variables['book_id'] = $book_link['bid'];
  $variables['book_title'] = check_plain($book_link['link_title']);
789
  $variables['book_url'] = 'node/' . $book_link['bid'];
790
  $variables['current_depth'] = $book_link['depth'];
791

792
  $variables['tree'] = '';
Dries's avatar
Dries committed
793
  if ($book_link['mlid']) {
794
    $variables['tree'] = book_children($book_link);
Dries's avatar
 
Dries committed
795

Dries's avatar
Dries committed
796
    if ($prev = book_prev($book_link)) {
797 798 799 800
      $prev_href = url($prev['href']);
      drupal_add_link(array('rel' => 'prev', 'href' => $prev_href));
      $variables['prev_url'] = $prev_href;
      $variables['prev_title'] = check_plain($prev['title']);
Dries's avatar
 
Dries committed
801
    }
Dries's avatar
Dries committed
802
    if ($book_link['plid'] && $parent = book_link_load($book_link['plid'])) {
803 804 805 806
      $parent_href = url($parent['href']);
      drupal_add_link(array('rel' => 'up', 'href' => $parent_href));
      $variables['parent_url'] = $parent_href;
      $variables['parent_title'] = check_plain($parent['title']);
Dries's avatar
 
Dries committed
807
    }
Dries's avatar
Dries committed
808
    if ($next = book_next($book_link)) {
809 810 811 812
      $next_href = url($next['href']);
      drupal_add_link(array('rel' => 'next', 'href' => $next_href));
      $variables['next_url'] = $next_href;
      $variables['next_title'] = check_plain($next['title']);
Dries's avatar
 
Dries committed
813
    }
814
  }
Dries's avatar
 
Dries committed
815

816 817 818 819 820 821 822 823 824 825 826
  $variables['has_links'] = FALSE;
  // Link variables to filter for values and set state of the flag variable.
  $links = array('prev_url', 'prev_title', 'parent_url', 'parent_title', 'next_url', 'next_title');
  foreach ($links as $link) {
    if (isset($variables[$link])) {
      // Flag when there is a value.
      $variables['has_links'] = TRUE;
    }
    else {
      // Set empty to prevent notices.
      $variables[$link] = '';
827
    }
Dries's avatar
 
Dries committed
828
  }
Dries's avatar
 
Dries committed
829
}
Dries's avatar
 
Dries committed
830

831
/**
Dries's avatar
Dries committed
832
 * A recursive helper function for book_toc().
833
 */
834
function _book_toc_recurse($tree, $indent, &$toc, $exclude, $depth_limit) {
Dries's avatar
Dries committed
835
  foreach ($tree as $data) {
836 837 838 839
    if ($data['link']['depth'] > $depth_limit) {
      // Don't iterate through any links on this level.
      break;
    }
Dries's avatar
Dries committed
840
    if (!in_array($data['link']['mlid'], $exclude)) {
841
      $toc[$data['link']['mlid']] = $indent . ' ' . truncate_utf8($data['link']['title'], 30, TRUE, TRUE);
842
      if ($data['below']) {
843
        _book_toc_recurse($data['below'], $indent . '--', $toc, $exclude, $depth_limit);
844
      }
Dries's avatar
 
Dries committed
845 846 847 848
    }
  }
}

849
/**
Dries's avatar
Dries committed
850 851 852 853 854 855 856
 * Returns an array of book pages in table of contents order.
 *
 * @param $bid
 *   The ID of the book whose pages are to be listed.
 * @param $exclude
 *   Optional array of mlid values.  Any link whose mlid is in this array
 *   will be excluded (along with its children).
857 858
 * @param $depth_limit
 *   Any link deeper than this value will be excluded (along with its children).
Dries's avatar
Dries committed
859 860
 * @return
 *   An array of mlid, title pairs for use as options for selecting a book page.
861
 */
862
function book_toc($bid, $exclude = array(), $depth_limit) {
Dries's avatar
 
Dries committed
863

Dries's avatar
Dries committed
864
  $tree = menu_tree_all_data(book_menu_name($bid));
865
  $toc = array();
866
  _book_toc_recurse($tree, '', $toc, $exclude, $depth_limit);
Dries's avatar
 
Dries committed
867

Dries's avatar
 
Dries committed
868 869 870
  return $toc;
}

871 872 873 874 875 876 877 878 879 880 881 882 883 884 885 886 887 888 889 890
/**
 * Process variables for book-export-html.tpl.php.
 *
 * The $variables array contains the following arguments:
 * - $title
 * - $contents
 * - $depth
 *
 * @see book-export-html.tpl.php
 */
function template_preprocess_book_export_html(&$variables) {
  global $base_url, $language;

  $variables['title'] = check_plain($variables['title']);
  $variables['base_url'] = $base_url;
  $variables['language'] = $language;
  $variables['language_rtl'] = (defined('LANGUAGE_RTL') && $language->direction == LANGUAGE_RTL) ? TRUE : FALSE;
  $variables['head'] = drupal_get_html_head();
}

891
/**
Dries's avatar
Dries committed
892
 * Traverse the book tree to build printable or exportable output.
893
 *
894
 * During the traversal, the $visit_func() callback is applied to each
Dries's avatar
Dries committed
895
 * node, and is called recursively for each child of the node (in weight,
896
 * title order).
897
 *
Dries's avatar
Dries committed
898 899
 * @param $tree
 *   A subtree of the book menu hierarchy, rooted at the current page.
900
 * @param $visit_func
Dries's avatar
Dries committed
901
 *   A function callback to be called upon visiting a node in the tree.
902
 * @return
Dries's avatar
Dries committed
903
 *   The output generated in visiting each node.
904
 */
905
function book_export_traverse($tree, $visit_func) {
906
  $output = '';
Dries's avatar
Dries committed
907 908 909

  foreach ($tree as $data) {
    // Note- access checking is already performed when building the tree.
910 911
    if ($node = node_load($data['link']['nid'], FALSE)) {
      $children = '';
Dries's avatar
Dries committed
912
      if ($data['below']) {
913
        $children = book_export_traverse($data['below'], $visit_func);
914
      }
Dries's avatar
Dries committed
915

916 917
      if (function_exists($visit_func)) {
        $output .= call_user_func($visit_func, $node, $children);
918
      }
919
      else {
Dries's avatar
Dries committed
920
        // Use the default function.
921
        $output .= book_node_export($node, $children);
922
      }
Dries's avatar
 
Dries committed
923
    }
Dries's avatar
 
Dries committed
924 925 926
  }
  return $output;
}
Dries's avatar
 
Dries committed
927

928
/**
Dries's avatar
Dries committed
929 930
 * Generates printer-friendly HTML for a node.
 *
931
 * @see book_export_traverse()
932 933
 *
 * @param $node
Dries's avatar
Dries committed
934
 *   The node to generate output for.
935 936
 * @param $children
 *   All the rendered child nodes within the current node.
937
 * @return
Dries's avatar
Dries committed
938
 *   The HTML generated for the given node.
939
 */
940
function book_node_export($node, $children = '') {
941

Dries's avatar
Dries committed
942 943
  $node->build_mode = NODE_BUILD_PRINT;
  $node = node_build_content($node, FALSE, FALSE);
944
  $node->body = drupal_render($node->content);
945

946
  return theme('book_node_export_html', $node, $children);
947 948 949
}

/**
950
 * Process variables for book-node-export-html.tpl.php.
Dries's avatar
Dries committed
951
 *
952 953 954
 * The $variables array contains the following arguments:
 * - $node
 * - $children
Dries's avatar
Dries committed
955
 *
956
 * @see book-node-export-html.tpl.php
957
 */
958 959 960 961
function template_preprocess_book_node_export_html(&$variables) {
  $variables['depth'] = $variables['node']->book['depth'];
  $variables['title'] = check_plain($variables['node']->title);
  $variables['content'] = $variables['node']->body;
962 963
}

964
/**
Dries's avatar
Dries committed
965
 * Determine if a given node type is in the list of types allowed for books.
966
 */
Dries's avatar
Dries committed
967 968
function book_type_is_allowed($type) {
  return in_array($type, variable_get('book_allowed_types', array('book')));
969 970
}

Dries's avatar
Dries committed
971 972 973 974 975 976 977 978
/**
 * Implementation of hook_node_type().
 *
 * Update book module's persistent variables if the machine-readable name of a
 * node type is changed.
 */
function book_node_type($op, $type) {

979
  switch ($op) {
Dries's avatar
Dries committed
980 981 982 983 984 985 986 987 988 989 990 991 992 993 994 995 996
    case 'update':
      if (!empty($type->old_type) && $type->old_type != $type->type) {
        // Update the list of node types that are allowed to be added to books.
        $allowed_types = variable_get('book_allowed_types', array('book'));
        $key = array_search($type->old_type, $allowed_types);
        if ($key !== FALSE) {
          $allowed_types[$type->type] = $allowed_types[$key] ? $type->type : 0;
          unset($allowed_types[$key]);
          variable_set('book_allowed_types', $allowed_types);
        }
        // Update the setting for the "Add child page" link.
        if (variable_get('book_child_type', 'book') == $type->old_type) {
          variable_set('book_child_type', $type->type);
        }
      }
      break;
  }
Dries's avatar
 
Dries committed
997 998
}