book.module 38.1 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() {
Dries's avatar
Dries committed
43
  return array('add content to books', 'administer book outlines', 'create new books', 'access printer-friendly version');
Dries's avatar
 
Dries committed
44 45
}

Dries's avatar
 
Dries committed
46 47 48
/**
 * Implementation of hook_link().
 */
49
function book_link($type, $node = NULL, $teaser = FALSE) {
Dries's avatar
 
Dries committed
50 51
  $links = array();

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

Dries's avatar
 
Dries committed
74 75 76
/**
 * Implementation of hook_menu().
 */
77 78
function book_menu() {
  $items['admin/content/book'] = array(
79
    'title' => 'Books',
Dries's avatar
Dries committed
80 81 82
    'description' => "Manage your site's book outlines.",
    'page callback' => 'book_admin_overview',
    'access arguments' => array('administer book outlines'),
83
    'file' => 'book.admin.inc',
84 85
  );
  $items['admin/content/book/list'] = array(
86
    'title' => 'List',
87 88
    'type' => MENU_DEFAULT_LOCAL_TASK,
  );
Dries's avatar
Dries committed
89 90
  $items['admin/content/book/settings'] = array(
    'title' => 'Settings',
91
    'page callback' => 'drupal_get_form',
Dries's avatar
Dries committed
92 93
    'page arguments' => array('book_admin_settings'),
    'access arguments' => array('administer site configuration'),
94 95
    'type' => MENU_LOCAL_TASK,
    'weight' => 8,
96
    'file' => 'book.admin.inc',
97
  );
Dries's avatar
Dries committed
98 99 100 101 102 103 104
  $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,
105
    'file' => 'book.admin.inc',
Dries's avatar
Dries committed
106
  );
107
  $items['book'] = array(
108
    'title' => 'Books',
109 110 111
    'page callback' => 'book_render',
    'access arguments' => array('access content'),
    'type' => MENU_SUGGESTED_ITEM,
112
    'file' => 'book.pages.inc',
113 114 115 116
  );
  $items['book/export/%/%'] = array(
    'page callback' => 'book_export',
    'page arguments' => array(2, 3),
Dries's avatar
Dries committed
117
    'access arguments' => array('access printer-friendly version'),
118
    'type' => MENU_CALLBACK,
119
    'file' => 'book.pages.inc',
120
  );
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
    'access callback' => '_book_outline_access',
    'access arguments' => array(1),
    'type' => MENU_LOCAL_TASK,
    'weight' => 2,
129
    'file' => 'book.pages.inc',
130
  );
Dries's avatar
Dries committed
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
    'file' => 'book.pages.inc',
Dries's avatar
Dries committed
139
  );
140
  $items['book/js/form'] = array(
Dries's avatar
Dries committed
141 142 143
    'page callback' => 'book_form_update',
    'access arguments' => array('access content'),
    'type' => MENU_CALLBACK,
144
    'file' => 'book.pages.inc',
Dries's avatar
Dries committed
145
  );
Dries's avatar
 
Dries committed
146 147 148
  return $items;
}

Dries's avatar
Dries committed
149 150 151
/**
 * Menu item access callback - determine if the outline tab is accessible.
 */
152
function _book_outline_access($node) {
Dries's avatar
Dries committed
153 154 155 156 157 158 159 160
  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);
161 162
}

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

170 171 172
/**
 * Implementation of hook_block().
 *
Dries's avatar
 
Dries committed
173 174
 * Displays the book table of contents in a block when the current page is a
 * single-node view of a book node.
175
 */
Dries's avatar
Dries committed
176
function book_block($op = 'list', $delta = 0, $edit = array()) {
Dries's avatar
 
Dries committed
177
  $block = array();
Dries's avatar
Dries committed
178 179 180
  switch ($op) {
    case 'list':
      $block[0]['info'] = t('Book navigation');
181
      $block[0]['cache'] = BLOCK_CACHE_PER_PAGE | BLOCK_CACHE_PER_ROLE;
Dries's avatar
Dries committed
182 183
      return $block;
    case 'view':
184 185 186 187
      if (arg(0) == 'node' && is_numeric(arg(1))) {
        $node = node_load(arg(1));
      }
      $current_bid = empty($node->book['bid']) ? 0 : $node->book['bid'];
Dries's avatar
Dries committed
188 189 190 191
      $mode = variable_get('book_block_mode', 'all pages');
      if ($mode == 'all pages') {
        $block['subject'] = t('Book navigation');
        $book_menus = array();
192
        $pseudo_tree = array(0 => array('below' => FALSE));
193
        foreach (book_get_books() as $book_id => $book) {
194 195 196
          if ($book['bid'] == $current_bid) {
            // If the current page is a node associated with a book, the menu
            // needs to be retrieved.
197
            $book_menus[$book_id] = menu_tree_output(menu_tree_all_data($node->book['menu_name'], $node->book));
198 199 200 201 202 203
          }
          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;
204
            $book_menus[$book_id] = menu_tree_output($pseudo_tree);
205
          }
Dries's avatar
 
Dries committed
206
        }
Dries's avatar
Dries committed
207
        $block['content'] = theme('book_all_books_block', $book_menus);
Dries's avatar
 
Dries committed
208
      }
209
      elseif ($current_bid) {
Dries's avatar
Dries committed
210
        // Only display this block when the user is browsing a book.
211 212 213 214 215 216 217 218
        $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
219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239
        }
      }
      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'),
        '#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."),
        );
      return $form;
    case 'save':
      variable_set('book_block_mode', $edit['book_block_mode']);
      break;
  }
}
Dries's avatar
 
Dries committed
240

241
/**
Dries's avatar
Dries committed
242 243 244
 * Generate the HTML output for a link to a book title when used as a block title.
 *
 * @ingroup themeable
245
 */
Dries's avatar
Dries committed
246 247 248
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
249
}
Dries's avatar
 
Dries committed
250

251
/**
Dries's avatar
Dries committed
252 253 254 255
 * 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.
256
 */
Dries's avatar
Dries committed
257 258 259 260 261 262 263 264 265 266 267
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) {
268
      $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
269 270 271
      while ($link = db_fetch_array($result2)) {
        $link['href'] = $link['link_path'];
        $link['options'] = unserialize($link['options']);
272
        $all_books[$link['bid']] = $link;
Dries's avatar
Dries committed
273 274
      }
    }
275
  }
Dries's avatar
Dries committed
276
  return $all_books;
277
}
278

Dries's avatar
Dries committed
279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303
/**
 * 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) {

  if (isset($form['type']) && isset($form['#node']) && $form['type']['#value'] .'_node_form' == $form_id) {
    // 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)'),
304 305 306 307 308 309
         // 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
310 311 312
        '#weight' => 20,
      );
    }
313
  }
Dries's avatar
Dries committed
314
}
315

Dries's avatar
Dries committed
316 317 318 319 320 321 322 323
/**
 * 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) {
324 325 326
  if (variable_get('menu_override_parent_selector', FALSE)) {
    return array();
  }
Dries's avatar
Dries committed
327 328 329 330 331 332 333
  // 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>',
  );
334

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

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

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

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

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

397
  if (isset($node->nid) && ($nid == $node->book['original_bid']) && ($node->book['parent_depth_limit'] == 0)) {
398 399 400 401 402 403 404 405 406
    // 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
407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424
  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.
    $options = array($nid => '<'. t('create a new book') .'>') + $options;
  }
  if (!$node->book['mlid']) {
    // The node is not currently in a the hierarchy.
    $options = array(0 => '<'. t('none') .'>') + $options;
  }

  // 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,
425
    '#attributes' => array('class' => 'book-title-select'),
426 427 428 429 430
    '#ahah' => array(
      'path' => 'book/js/form',
      'wrapper' => 'edit-book-plid-wrapper',
      'effect' => 'slide',
    ),
431
  );
Dries's avatar
Dries committed
432
}
Dries's avatar
 
Dries committed
433

434
/**
Dries's avatar
Dries committed
435
 * Common helper function to handles additions and updates to the book outline.
Dries's avatar
Dries committed
436
 *
Dries's avatar
Dries committed
437 438 439 440 441 442 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 477 478 479 480
 * 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']);

  $node->book['link_path'] = 'node/'. $node->nid;
  $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;
}

481
/**
Dries's avatar
Dries committed
482
 * Update the bid for a page and its children when it is moved to a new book.
483
 *
Dries's avatar
Dries committed
484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505
 * @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"];
  }
  $result = db_query("SELECT mlid FROM {menu_links} WHERE ". implode(' AND ', $match), $args);

  $mlids = array();
  while ($a = db_fetch_array($result)) {
    $mlids[] = $a['mlid'];
  }
  if ($mlids) {
    db_query("UPDATE {book} SET bid = %d WHERE mlid IN (". implode(',', $mlids) .")", $book_link['bid']);
  }
}

/**
 * Get the book menu tree for a page, and return it as a linear array.
506
 *
Dries's avatar
Dries committed
507 508
 * @param $book_link
 *   A fully loaded menu link that is part of the book hierarchy.
509
 * @return
Dries's avatar
Dries committed
510 511 512 513
 *   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.
514
 */
Dries's avatar
Dries committed
515 516 517 518 519 520 521 522
function book_get_flat_menu($book_link) {
  static $flat = array();

  if (!isset($flat[$book_link['mlid']])) {
    // Call menu_tree_full_data() to take advantage of the menu system's caching.
    $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
523
  }
Dries's avatar
Dries committed
524
  return $flat[$book_link['mlid']];
Dries's avatar
 
Dries committed
525 526
}

527
/**
Dries's avatar
Dries committed
528
 * Recursive helper function for book_get_flat_menu().
529
 */
Dries's avatar
Dries committed
530 531 532 533 534 535 536 537
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
538
  }
Dries's avatar
Dries committed
539
}
Dries's avatar
 
Dries committed
540

Dries's avatar
Dries committed
541 542 543 544 545 546 547
/**
 * 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
548
  }
Dries's avatar
Dries committed
549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570
  $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
571 572 573
  }
}

574
/**
Dries's avatar
Dries committed
575
 * Fetches the menu link for the next page of the book.
576
 */
Dries's avatar
Dries committed
577 578 579 580 581 582 583 584
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
585
  }
Dries's avatar
Dries committed
586
}
Dries's avatar
 
Dries committed
587

Dries's avatar
Dries committed
588 589 590 591 592
/**
 * Format the menu links for the child pages of the current page.
 */
function book_children($book_link) {
  $flat = book_get_flat_menu($book_link);
593

Dries's avatar
Dries committed
594 595 596 597 598 599 600 601 602 603 604 605
  $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
606 607
    }
  }
Dries's avatar
Dries committed
608
  return $children ? menu_tree_output($children) : '';
Dries's avatar
 
Dries committed
609 610
}

611
/**
Dries's avatar
Dries committed
612
 * Generate the corresponding menu name from a book ID.
613
 */
Dries's avatar
Dries committed
614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 641 642 643 644 645
function book_menu_name($bid) {
  return 'book-toc-'. $bid;
}

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

  if (!isset($trail)) {
    $trail = array();
    $trail[] = array('title' => t('Home'), 'href' => '<front>', 'options' => array());

    $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
646 647
}

648
/**
Dries's avatar
 
Dries committed
649 650
 * Implementation of hook_nodeapi().
 *
Dries's avatar
Dries committed
651 652
 * Appends book navigation to all nodes in the book, and handles book outline
 * insertions and updates via the node form.
653
 */
Dries's avatar
 
Dries committed
654 655
function book_nodeapi(&$node, $op, $teaser, $page) {
  switch ($op) {
656
    case 'load':
Dries's avatar
Dries committed
657 658 659 660 661 662 663 664
      // 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;
      }
665
      break;
Dries's avatar
 
Dries committed
666
    case 'view':
Dries's avatar
Dries committed
667 668
    if (!$teaser) {
        if (!empty($node->book['bid']) && $node->build_mode == NODE_BUILD_NORMAL) {
669

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

675
          if ($page) {
Dries's avatar
Dries committed
676 677
            menu_set_active_trail(book_build_active_trail($node->book));
            menu_set_active_menu_name($node->book['menu_name']);
678
          }
Dries's avatar
 
Dries committed
679
        }
Dries's avatar
 
Dries committed
680
      }
Dries's avatar
 
Dries committed
681
      break;
Dries's avatar
Dries committed
682 683 684 685 686 687 688
    case 'presave':
      // Always save a revision for non-administrators.
      if (!empty($node->book['bid']) && !user_access('administer nodes')) {
        $node->revision = 1;
      }
      break;
    case 'insert':
689
    case 'update':
Dries's avatar
Dries committed
690 691 692 693
      if (!empty($node->book['bid'])) {
        if ($node->book['bid'] == 'new') {
          // New nodes that are their own book.
          $node->book['bid'] = $node->nid;
694
        }
Dries's avatar
Dries committed
695 696 697
        $node->book['nid'] = $node->nid;
        $node->book['menu_name'] = book_menu_name($node->book['bid']);
        _book_update_outline($node);
698 699
      }
      break;
700
    case 'delete':
Dries's avatar
Dries committed
701 702 703 704 705 706 707 708 709 710 711 712 713
      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']);
      }
714
      break;
Dries's avatar
Dries committed
715 716 717 718 719 720 721 722 723 724 725 726 727 728 729 730 731 732 733 734 735
    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'];
        }
      }
736 737 738 739
      // 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
740 741 742 743
      break;
  }
}

744 745 746 747 748 749 750
/**
 * 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
751 752 753 754 755 756 757 758 759 760 761 762
/**
 * 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(
      '#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>',
      '#weight' => -10,
    );
Dries's avatar
 
Dries committed
763
  }
Dries's avatar
 
Dries committed
764
}
Dries's avatar
 
Dries committed
765

766
/**
Dries's avatar
Dries committed
767 768 769 770 771 772 773
 * 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());
}

/**
774
 * Process variables for book-navigation.tpl.php.
Dries's avatar
Dries committed
775
 *
776 777
 * The $variables array contains the following arguments:
 * - $book_link
778
 *
779
 * @see book-navigation.tpl.php
780
 */
781 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']);
  $variables['book_url'] = 'node/'. $book_link['bid'];
  $variables['current_depth'] = $book_link['depth'];
789

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

Dries's avatar
Dries committed
794
    if ($prev = book_prev($book_link)) {
795 796 797 798
      $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
799
    }
Dries's avatar
Dries committed
800
    if ($book_link['plid'] && $parent = book_link_load($book_link['plid'])) {
801 802 803 804
      $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
805
    }
Dries's avatar
Dries committed
806
    if ($next = book_next($book_link)) {
807 808 809 810
      $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
811
    }
812
  }
Dries's avatar
 
Dries committed
813

814 815 816 817 818 819 820 821 822 823 824
  $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] = '';
825
    }
Dries's avatar
 
Dries committed
826
  }
Dries's avatar
 
Dries committed
827
}
Dries's avatar
 
Dries committed
828

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

847
/**
Dries's avatar
Dries committed
848 849 850 851 852 853 854
 * 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).
855 856
 * @param $depth_limit
 *   Any link deeper than this value will be excluded (along with its children).
Dries's avatar
Dries committed
857 858
 * @return
 *   An array of mlid, title pairs for use as options for selecting a book page.
859
 */
860
function book_toc($bid, $exclude = array(), $depth_limit) {
Dries's avatar
 
Dries committed
861

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

Dries's avatar
 
Dries committed
866 867 868
  return $toc;
}

869 870 871 872 873 874 875 876 877 878 879 880 881 882 883 884 885 886 887 888
/**
 * 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();
}

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

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

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

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

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

944
  return theme('book_node_export_html', $node, $children);
945 946 947
}

/**
948
 * Process variables for book-node-export-html.tpl.php.
Dries's avatar
Dries committed
949
 *
950 951 952
 * The $variables array contains the following arguments:
 * - $node
 * - $children
Dries's avatar
Dries committed
953
 *
954
 * @see book-node-export-html.tpl.php
955
 */
956 957 958 959
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;
960 961
}

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

Dries's avatar
Dries committed
969 970 971 972 973 974 975 976
/**
 * 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) {

977
  switch ($op) {
Dries's avatar
Dries committed
978 979 980 981 982 983 984 985 986 987 988 989 990 991 992 993 994
    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
995 996
}

997 998 999
/**
 * Implementation of hook_help().
 */
1000 1001
function book_help($path, $arg) {
  switch ($path) {
Dries's avatar
 
Dries committed
1002
    case 'admin/help#book':
1003
      $output = '<p>'. t('The <em>book</em> 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>';
Dries's avatar
Dries committed
1004
      $output .= '<p>'. t('Pages in the book hierarchy have navigation elements at the bottom of the page for moving through the text.  These link to the previous and next pages in the book, as well as a link labeled <em>up</em>, leading to the level above in the structure.  More comprehensive navigation may be provided by enabling the <em>book navigation block</em> on the <a href="@admin-block">block administration page</a>.', array('@admin-block' => url('admin/build/block'))) .'</p>';
1005
      $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>';
Dries's avatar
Dries committed
1006 1007
      $output .= '<p>'. t("Users with the <em>administer book outlines</em> permission can add content of any type to a book, placing it into the existing book structure through the edit form or through the interface that's available by clicking on the <em>outline</em> tab while viewing that post.", array('%book' => node_get_types('name', 'book'))) .'</p>';
      $output .= '<p>'. t('Administrators can view a list of all books on the <a href="@admin-node-book">book administration page</a>.  In this list there is a link to an outline page for each book, from which is it possible to change the titles of sections, or to change their weight, thus reordering sections.', array('@admin-node-book' => url('admin/content/book'))) .'</p>';
1008
      $output .= '<p>'. t('For more information please read the configuration and customization handbook <a href="@book">Book page</a>.', array('@book' => 'http://drupal.org/handbook/modules/book/')) .'</p>';
1009
      return $output;
1010
    case 'admin/content/book':
1011
      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>';
1012
    case 'node/%/outline':
Dries's avatar
Dries committed
1013 1014 1015 1016 1017 1018 1019 1020 1021 1022 1023 1024 1025 1026 1027 1028 1029 1030 1031 1032 1033 1034 1035 1036 1037 1038 1039 1040 1041 1042 1043 1044 1045 1046 1047 1048