book.module 41.4 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
6
 * Allows users to create and organize related content in an outline.
Dries's avatar
   
Dries committed
7
8
 */

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

39
/**
40
 * Implement hook_permission().
41
 */
42
function book_permission() {
43
  return array(
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
    'administer book outlines' => array(
      'title' => t('Administer book outlines'),
      'description' => t('Manage books through the administration panel.'),
    ),
    'create new books' => array(
      'title' => t('Create new books'),
      'description' => t('Add new top-level books.'),
    ),
    'add content to books' => array(
      'title' => t('Add content to books'),
      'description' => t('Add new content and child pages to books.'),
    ),
    'access printer-friendly version' => array(
      'title' => t('Access printer-friendly version'),
      'description' => t('View a book page and all of its sub-pages as a single document for ease of printing. Can be performance heavy.'),
    ),
60
  );
Dries's avatar
   
Dries committed
61
62
}

Dries's avatar
   
Dries committed
63
/**
Dries's avatar
Dries committed
64
 * Inject links into $node as needed.
Dries's avatar
   
Dries committed
65
 */
66
function book_node_view_link(stdClass $node, $build_mode) {
Dries's avatar
   
Dries committed
67
  $links = array();
Dries's avatar
Dries committed
68

69
  if (isset($node->book['depth'])) {
70
    if ($build_mode == 'full') {
Dries's avatar
Dries committed
71
72
      $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) {
73
        $links['book_add_child'] = array(
74
          'title' => t('Add child page'),
75
          'href' => 'node/add/' . str_replace('_', '-', $child_type),
76
          'query' => array('parent' => $node->book['mlid']),
77
        );
Dries's avatar
   
Dries committed
78
      }
79

Dries's avatar
Dries committed
80
      if (user_access('access printer-friendly version')) {
81
        $links['book_printer'] = array(
82
          'title' => t('Printer-friendly version'),
83
          'href' => 'book/export/html/' . $node->nid,
84
          'attributes' => array('title' => t('Show a printer-friendly version of this book page and its sub-pages.'))
85
        );
86
      }
Dries's avatar
   
Dries committed
87
    }
Dries's avatar
   
Dries committed
88
  }
89

90
91
  if (!empty($links)) {
    $node->content['links']['book'] = array(
92
93
      '#theme' => 'links',
      '#links' => $links,
94
      '#attributes' => array('class' => array('links', 'inline')),
95
96
    );
  }
Dries's avatar
   
Dries committed
97
98
}

Dries's avatar
   
Dries committed
99
/**
100
 * Implement hook_menu().
Dries's avatar
   
Dries committed
101
 */
102
103
function book_menu() {
  $items['admin/content/book'] = array(
104
    'title' => 'Books',
105
    'description' => "Manage your site's book outlines.",
Dries's avatar
Dries committed
106
107
    'page callback' => 'book_admin_overview',
    'access arguments' => array('administer book outlines'),
108
    'type' => MENU_LOCAL_TASK,
109
    'file' => 'book.admin.inc',
110
111
  );
  $items['admin/content/book/list'] = array(
112
    'title' => 'List',
113
114
    'type' => MENU_DEFAULT_LOCAL_TASK,
  );
Dries's avatar
Dries committed
115
116
  $items['admin/content/book/settings'] = array(
    'title' => 'Settings',
117
    'page callback' => 'drupal_get_form',
Dries's avatar
Dries committed
118
119
    'page arguments' => array('book_admin_settings'),
    'access arguments' => array('administer site configuration'),
120
121
    'type' => MENU_LOCAL_TASK,
    'weight' => 8,
122
    'file' => 'book.admin.inc',
123
  );
Dries's avatar
Dries committed
124
125
126
127
128
129
130
  $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,
131
    'file' => 'book.admin.inc',
Dries's avatar
Dries committed
132
  );
133
  $items['book'] = array(
134
    'title' => 'Books',
135
136
137
    'page callback' => 'book_render',
    'access arguments' => array('access content'),
    'type' => MENU_SUGGESTED_ITEM,
138
    'file' => 'book.pages.inc',
139
140
141
142
  );
  $items['book/export/%/%'] = array(
    'page callback' => 'book_export',
    'page arguments' => array(2, 3),
Dries's avatar
Dries committed
143
    'access arguments' => array('access printer-friendly version'),
144
    'type' => MENU_CALLBACK,
145
    'file' => 'book.pages.inc',
146
  );
147
  $items['node/%node/outline'] = array(
148
    'title' => 'Outline',
Dries's avatar
Dries committed
149
150
    'page callback' => 'book_outline',
    'page arguments' => array(1),
151
152
153
154
    'access callback' => '_book_outline_access',
    'access arguments' => array(1),
    'type' => MENU_LOCAL_TASK,
    'weight' => 2,
155
    'file' => 'book.pages.inc',
156
  );
Dries's avatar
Dries committed
157
158
159
160
161
162
163
  $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,
164
    'file' => 'book.pages.inc',
Dries's avatar
Dries committed
165
  );
166
  $items['book/js/form'] = array(
Dries's avatar
Dries committed
167
    'page callback' => 'book_form_update',
168
    'delivery callback' => 'ajax_deliver',
Dries's avatar
Dries committed
169
170
    'access arguments' => array('access content'),
    'type' => MENU_CALLBACK,
171
    'file' => 'book.pages.inc',
Dries's avatar
Dries committed
172
  );
173

Dries's avatar
   
Dries committed
174
175
176
  return $items;
}

Dries's avatar
Dries committed
177
178
179
/**
 * Menu item access callback - determine if the outline tab is accessible.
 */
180
function _book_outline_access(stdClass $node) {
Dries's avatar
Dries committed
181
182
183
184
185
186
  return user_access('administer book outlines') && node_access('view', $node);
}

/**
 * Menu item access callback - determine if the user can remove nodes from the outline.
 */
187
function _book_outline_remove_access(stdClass $node) {
Dries's avatar
Dries committed
188
  return isset($node->book) && ($node->book['bid'] != $node->nid) && _book_outline_access($node);
189
190
}

Dries's avatar
Dries committed
191
/**
192
 * Implement hook_init().
Dries's avatar
Dries committed
193
 */
194
function book_init() {
195
  drupal_add_css(drupal_get_path('module', 'book') . '/book.css');
196
197
}

198
199
200
201
202
203
204
205
206
207
208
209
210
/**
 * Implement hook_field_build_modes().
 */
function book_field_build_modes($obj_type) {
  $modes = array();
  if ($obj_type == 'node') {
    $modes = array(
      'print' => t('Print'),
    );
  }
  return $modes;
}

211
/**
212
 * Implement hook_block_info().
213
 */
214
function book_block_info() {
215
216
  $block = array();
  $block['navigation']['info'] = t('Book navigation');
217
  $block['navigation']['cache'] = DRUPAL_CACHE_PER_PAGE | DRUPAL_CACHE_PER_ROLE;
218
219
220
221
222

  return $block;
}

/**
223
 * Implement hook_block_view().
224
 *
Dries's avatar
   
Dries committed
225
226
 * Displays the book table of contents in a block when the current page is a
 * single-node view of a book node.
227
 */
228
function book_block_view($delta = '') {
Dries's avatar
   
Dries committed
229
  $block = array();
230
231
232
233
  $current_bid = 0;
  if ($node = menu_get_object()) {
    $current_bid = empty($node->book['bid']) ? 0 : $node->book['bid'];
  }
234

235
236
237
238
239
240
241
242
243
  if (variable_get('book_block_mode', 'all pages') == 'all pages') {
    $block['subject'] = t('Book navigation');
    $book_menus = array();
    $pseudo_tree = array(0 => array('below' => FALSE));
    foreach (book_get_books() as $book_id => $book) {
      if ($book['bid'] == $current_bid) {
        // If the current page is a node associated with a book, the menu
        // needs to be retrieved.
        $book_menus[$book_id] = menu_tree_output(menu_tree_all_data($node->book['menu_name'], $node->book));
Dries's avatar
   
Dries committed
244
      }
245
246
247
248
249
250
      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;
        $book_menus[$book_id] = menu_tree_output($pseudo_tree);
Dries's avatar
Dries committed
251
      }
252
    }
253
254
    $book_menus['#theme'] = 'book_all_books_block';
    $block['content'] = $book_menus;
255
256
257
258
259
260
261
262
263
264
265
266
267
  }
  elseif ($current_bid) {
    // Only display this block when the user is browsing a book.
    $select = db_select('node');
    $select->addField('node', 'title');
    $select->condition('nid', $node->book['bid']);
    $select->addTag('node_access');
    $title = $select->execute()->fetchField();
    // 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);
268
      $block['subject'] = theme('book_title_link', array('link' => $data['link']));
269
270
271
      $block['content'] = ($data['below']) ? menu_tree_output($data['below']) : '';
    }
  }
272

273
274
  return $block;
}
275

276
/**
277
 * Implement hook_block_configure().
278
279
280
281
282
283
284
285
286
287
288
289
290
291
 */
function book_block_configure($delta = '') {
  $block = array();
  $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."),
    );
292

293
294
  return $form;
}
295

296
/**
297
 * Implement hook_block_save().
298
299
300
301
 */
function book_block_save($delta = '', $edit = array()) {
  $block = array();
  variable_set('book_block_mode', $edit['book_block_mode']);
Dries's avatar
Dries committed
302
}
Dries's avatar
   
Dries committed
303

304
/**
Dries's avatar
Dries committed
305
306
307
 * Generate the HTML output for a link to a book title when used as a block title.
 *
 * @ingroup themeable
308
 */
309
310
311
function theme_book_title_link($variables) {
  $link = $variables['link'];

312
  $link['options']['attributes']['class'] = array('book-title');
313

Dries's avatar
Dries committed
314
  return l($link['title'], $link['href'], $link['options']);
Dries's avatar
   
Dries committed
315
}
Dries's avatar
   
Dries committed
316

317
/**
Dries's avatar
Dries committed
318
319
320
321
 * 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.
322
 */
Dries's avatar
Dries committed
323
function book_get_books() {
324
  $all_books = &drupal_static(__FUNCTION__);
Dries's avatar
Dries committed
325
326
327

  if (!isset($all_books)) {
    $all_books = array();
328
    $nids = db_query("SELECT DISTINCT(bid) FROM {book}")->fetchCol();
329

Dries's avatar
Dries committed
330
    if ($nids) {
331
332
333
334
335
336
337
338
339
340
341
342
343
344
      $query = db_select('book', 'b', array('fetch' => PDO::FETCH_ASSOC));
      $node_alias = $query->join('node', 'n', 'b.nid = n.nid');
      $menu_links_alias = $query->join('menu_links', 'ml', 'b.mlid = ml.mlid');
      $query->addField('n', 'type', 'type');
      $query->addField('n', 'title', 'title');
      $query->fields('b');
      $query->fields($menu_links_alias);
      $query->condition('n.nid', $nids, 'IN');
      $query->condition('n.status', 1);
      $query->orderBy('ml.weight');
      $query->orderBy('ml.link_title');
      $query->addTag('node_access');
      $result2 = $query->execute();
      foreach ($result2 as $link) {
Dries's avatar
Dries committed
345
346
        $link['href'] = $link['link_path'];
        $link['options'] = unserialize($link['options']);
347
        $all_books[$link['bid']] = $link;
Dries's avatar
Dries committed
348
349
      }
    }
350
  }
351

Dries's avatar
Dries committed
352
  return $all_books;
353
}
354

Dries's avatar
Dries committed
355
/**
356
 * Implement hook_form_alter().
357
358
 *
 * Adds the book fieldset to the node form.
Dries's avatar
Dries committed
359
360
361
362
363
364
 *
 * @see book_pick_book_submit()
 * @see book_submit()
 */
function book_form_alter(&$form, $form_state, $form_id) {

365
  if (!empty($form['#node_edit_form'])) {
366
    // Add elements to the node form.
Dries's avatar
Dries committed
367
368
369
370
371
    $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))) {
372
        // Already in the book hierarchy, or this node type is allowed.
Dries's avatar
Dries committed
373
374
375
376
377
378
379
380
381
        $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)'),
382
         // Submit the node form so the parent select options get updated.
383
         // This is typically only used when JS is disabled. Since the parent options
384
385
         // 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
386
         // selected book. This is similar to what happens during a node preview.
387
        '#submit' => array('node_form_submit_build_node'),
Dries's avatar
Dries committed
388
389
390
        '#weight' => 20,
      );
    }
391
  }
Dries's avatar
Dries committed
392
}
393

Dries's avatar
Dries committed
394
/**
395
 * Build the parent selection form element for the node form or outline tab.
Dries's avatar
Dries committed
396
397
398
399
400
401
 *
 * 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) {
402
403
404
  if (variable_get('menu_override_parent_selector', FALSE)) {
    return array();
  }
Dries's avatar
Dries committed
405
406
407
408
409
410
411
  // 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>',
  );
412

Dries's avatar
Dries committed
413
414
415
  if ($book_link['nid'] === $book_link['bid']) {
    // This is a book - at the top level.
    if ($book_link['original_bid'] === $book_link['bid']) {
416
      $form['#prefix'] .= '<em>' . t('This is the top-level page in this book.') . '</em>';
Dries's avatar
Dries committed
417
418
    }
    else {
419
      $form['#prefix'] .= '<em>' . t('This will be the top-level page in this book.') . '</em>';
Dries's avatar
Dries committed
420
421
422
    }
  }
  elseif (!$book_link['bid']) {
423
    $form['#prefix'] .= '<em>' . t('No book selected.') . '</em>';
424
425
  }
  else {
Dries's avatar
Dries committed
426
427
428
429
    $form = array(
      '#type' => 'select',
      '#title' => t('Parent item'),
      '#default_value' => $book_link['plid'],
430
      '#description' => t('The parent page in the book. The maximum depth for a book and all child pages is !maxdepth. Some pages in the selected book may not be available as parents if selecting them would exceed this limit.', array('!maxdepth' => MENU_MAX_DEPTH)),
431
      '#options' => book_toc($book_link['bid'], $book_link['parent_depth_limit'], array($book_link['mlid'])),
432
      '#attributes' => array('class' => array('book-title-select')),
433
    );
Dries's avatar
   
Dries committed
434
  }
435

436
  return $form;
Dries's avatar
   
Dries committed
437
438
}

439
/**
Dries's avatar
Dries committed
440
 * Build the common elements of the book form for the node and outline forms.
441
 */
442
function _book_add_form_elements(&$form, stdClass $node) {
Dries's avatar
Dries committed
443
444
445
446
447
448
449
450
451
  // Need this for AJAX.
  $form['#cache'] = TRUE;

  $form['book'] = array(
    '#type' => 'fieldset',
    '#title' => t('Book outline'),
    '#weight' => 10,
    '#collapsible' => TRUE,
    '#collapsed' => TRUE,
452
    '#group' => 'additional_settings',
453
454
455
    '#attached' => array(
      'js' => array(drupal_get_path('module', 'book') . '/book.js'),
    ),
Dries's avatar
Dries committed
456
    '#tree' => TRUE,
457
    '#attributes' => array('class' => array('book-outline-form')),
458
  );
459
  foreach (array('menu_name', 'mlid', 'nid', 'router_path', 'has_children', 'options', 'module', 'original_bid', 'parent_depth_limit') as $key) {
Dries's avatar
Dries committed
460
461
462
463
464
465
466
467
468
469
    $form['book'][$key] = array(
      '#type' => 'value',
      '#value' => $node->book[$key],
    );
  }

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

  $form['book']['weight'] = array(
    '#type' => 'weight',
470
    '#title' => t('Weight'),
Dries's avatar
Dries committed
471
    '#default_value' => $node->book['weight'],
472
    '#delta' => 15,
Dries's avatar
Dries committed
473
    '#weight' => 5,
474
475
    '#description' => t('Pages at a given level are ordered first by weight and then by title.'),
  );
Dries's avatar
Dries committed
476
477
  $options = array();
  $nid = isset($node->nid) ? $node->nid : 'new';
478

479
  if (isset($node->nid) && ($nid == $node->book['original_bid']) && ($node->book['parent_depth_limit'] == 0)) {
480
    // This is the top level node in a maximum depth book and thus cannot be moved.
481
    $options[$node->nid] = $node->title[FIELD_LANGUAGE_NONE][0]['value'];
482
483
484
485
486
487
488
  }
  else {
    foreach (book_get_books() as $book) {
      $options[$book['nid']] = $book['title'];
    }
  }

Dries's avatar
Dries committed
489
490
  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.
491
    $options = array($nid => '<' . t('create a new book') . '>') + $options;
Dries's avatar
Dries committed
492
493
  }
  if (!$node->book['mlid']) {
494
    // The node is not currently in the hierarchy.
495
    $options = array(0 => '<' . t('none') . '>') + $options;
Dries's avatar
Dries committed
496
497
498
499
500
501
502
503
504
505
506
  }

  // 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,
507
    '#attributes' => array('class' => array('book-title-select')),
508
    '#ajax' => array(
509
510
      'path' => 'book/js/form',
      'wrapper' => 'edit-book-plid-wrapper',
511
512
      'effect' => 'fade',
      'speed' => 'fast',
513
    ),
514
  );
Dries's avatar
Dries committed
515
}
Dries's avatar
   
Dries committed
516

517
/**
Dries's avatar
Dries committed
518
 * Common helper function to handles additions and updates to the book outline.
Dries's avatar
Dries committed
519
 *
Dries's avatar
Dries committed
520
521
522
 * Performs all additions and updates to the book outline through node addition,
 * node editing, node deletion, or the outline tab.
 */
523
function _book_update_outline(stdClass $node) {
Dries's avatar
Dries committed
524
525
526
527
528
  if (empty($node->book['bid'])) {
    return FALSE;
  }
  $new = empty($node->book['mlid']);

529
  $node->book['link_path'] = 'node/' . $node->nid;
530
  $node->book['link_title'] = $node->title[FIELD_LANGUAGE_NONE][0]['value'];
Dries's avatar
Dries committed
531
532
533
534
535
536
537
538
539
  $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'])) {
540
541
542
      $parent = db_query("SELECT * FROM {book} WHERE mlid = :mlid", array(
        ':mlid' => $node->book['plid'],
      ))->fetchAssoc();
Dries's avatar
Dries committed
543
544
    }
    if (empty($node->book['plid']) || !$parent || $parent['bid'] != $node->book['bid']) {
545
546
547
      $node->book['plid'] = db_query("SELECT mlid FROM {book} WHERE nid = :nid", array(
        ':nid' => $node->book['bid'],
      ))->fetchField();
Dries's avatar
Dries committed
548
549
550
      $node->book['parent_mismatch'] = TRUE; // Likely when JS is disabled.
    }
  }
551

Dries's avatar
Dries committed
552
553
554
  if (menu_link_save($node->book)) {
    if ($new) {
      // Insert new.
555
556
557
558
559
560
561
      db_insert('book')
        ->fields(array(
          'nid' => $node->nid,
          'mlid' => $node->book['mlid'],
          'bid' => $node->book['bid'],
        ))
        ->execute();
Dries's avatar
Dries committed
562
563
    }
    else {
564
565
566
      if ($node->book['bid'] != db_query("SELECT bid FROM {book} WHERE nid = :nid", array(
          ':nid' => $node->nid,
        ))->fetchField()) {
Dries's avatar
Dries committed
567
568
569
570
        // Update the bid for this page and all children.
        book_update_bid($node->book);
      }
    }
571

Dries's avatar
Dries committed
572
573
    return TRUE;
  }
574
575

  // Failed to save the menu link.
Dries's avatar
Dries committed
576
577
578
  return FALSE;
}

579
/**
Dries's avatar
Dries committed
580
 * Update the bid for a page and its children when it is moved to a new book.
581
 *
Dries's avatar
Dries committed
582
583
584
585
 * @param $book_link
 *   A fully loaded menu link that is part of the book hierarchy.
 */
function book_update_bid($book_link) {
586
587
  $query = db_select('menu_links');
  $query->addField('menu_links', 'mlid');
Dries's avatar
Dries committed
588
  for ($i = 1; $i <= MENU_MAX_DEPTH && $book_link["p$i"]; $i++) {
589
    $query->condition("p$i", $book_link["p$i"]);
Dries's avatar
Dries committed
590
  }
591
  $mlids = $query->execute()->fetchCol();
592

Dries's avatar
Dries committed
593
  if ($mlids) {
594
    db_update('book')
595
      ->fields(array('bid' => $book_link['bid']))
596
597
      ->condition('mlid', $mlids, 'IN')
      ->execute();
Dries's avatar
Dries committed
598
599
600
601
602
  }
}

/**
 * Get the book menu tree for a page, and return it as a linear array.
603
 *
Dries's avatar
Dries committed
604
605
 * @param $book_link
 *   A fully loaded menu link that is part of the book hierarchy.
606
 * @return
Dries's avatar
Dries committed
607
608
 *   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
609
 *   element corresponding to $node. The children of $node (if any) will come
610
611
 *   immediately after it in the array, and links will only be fetched as deep
 *   as one level deeper than $book_link.
612
 */
Dries's avatar
Dries committed
613
function book_get_flat_menu($book_link) {
614
  $flat = &drupal_static(__FUNCTION__, array());
Dries's avatar
Dries committed
615
616

  if (!isset($flat[$book_link['mlid']])) {
617
    // Call menu_tree_all_data() to take advantage of the menu system's caching.
618
    $tree = menu_tree_all_data($book_link['menu_name'], $book_link, $book_link['depth'] + 1);
Dries's avatar
Dries committed
619
620
    $flat[$book_link['mlid']] = array();
    _book_flatten_menu($tree, $flat[$book_link['mlid']]);
Dries's avatar
   
Dries committed
621
  }
622

Dries's avatar
Dries committed
623
  return $flat[$book_link['mlid']];
Dries's avatar
   
Dries committed
624
625
}

626
/**
Dries's avatar
Dries committed
627
 * Recursive helper function for book_get_flat_menu().
628
 */
Dries's avatar
Dries committed
629
630
631
632
633
634
635
636
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
637
  }
Dries's avatar
Dries committed
638
}
Dries's avatar
   
Dries committed
639

Dries's avatar
Dries committed
640
641
642
643
644
645
646
/**
 * 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
647
  }
Dries's avatar
Dries committed
648
649
650
651
652
653
654
655
656
657
658
659
  $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.
660
661
      $tree = book_menu_subtree_data($prev);
      $data = array_shift($tree);
Dries's avatar
Dries committed
662
663
664
665
      // The link of interest is the last child - iterate to find the deepest one.
      while ($data['below']) {
        $data = end($data['below']);
      }
666

Dries's avatar
Dries committed
667
668
669
670
671
      return $data['link'];
    }
    else {
      return $prev;
    }
Dries's avatar
   
Dries committed
672
673
674
  }
}

675
/**
Dries's avatar
Dries committed
676
 * Fetches the menu link for the next page of the book.
677
 */
Dries's avatar
Dries committed
678
679
680
681
682
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);
683
684
685
  }
  while ($key && $key != $book_link['mlid']);

Dries's avatar
Dries committed
686
687
  if ($key == $book_link['mlid']) {
    return current($flat);
Dries's avatar
   
Dries committed
688
  }
Dries's avatar
Dries committed
689
}
Dries's avatar
   
Dries committed
690

Dries's avatar
Dries committed
691
692
693
694
695
/**
 * Format the menu links for the child pages of the current page.
 */
function book_children($book_link) {
  $flat = book_get_flat_menu($book_link);
696

Dries's avatar
Dries committed
697
698
699
700
701
702
  $children = array();

  if ($book_link['has_children']) {
    // Walk through the array until we find the current page.
    do {
      $link = array_shift($flat);
703
704
    }
    while ($link && ($link['mlid'] != $book_link['mlid']));
Dries's avatar
Dries committed
705
706
707
708
709
    // 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
710
711
    }
  }
712

713
  return $children ? drupal_render(menu_tree_output($children)) : '';
Dries's avatar
   
Dries committed
714
715
}

716
/**
Dries's avatar
Dries committed
717
 * Generate the corresponding menu name from a book ID.
718
 */
Dries's avatar
Dries committed
719
function book_menu_name($bid) {
720
  return 'book-toc-' . $bid;
Dries's avatar
Dries committed
721
722
}

723
/**
724
 * Implement hook_node_load().
725
 */
726
function book_node_load($nodes, $types) {
727
  $result = db_query("SELECT * FROM {book} b INNER JOIN {menu_links} ml ON b.mlid = ml.mlid WHERE b.nid IN (:nids)", array(':nids' =>  array_keys($nodes)), array('fetch' => PDO::FETCH_ASSOC));
728
729
730
731
732
  foreach ($result as $record) {
    $nodes[$record['nid']]->book = $record;
    $nodes[$record['nid']]->book['href'] = $record['link_path'];
    $nodes[$record['nid']]->book['title'] = $record['link_title'];
    $nodes[$record['nid']]->book['options'] = unserialize($record['options']);
733
734
  }
}
735

736
/**
737
 * Implement hook_node_view().
738
 */
739
function book_node_view(stdClass $node, $build_mode) {
740
741
  if ($build_mode == 'full') {
    if (!empty($node->book['bid']) && empty($node->in_preview)) {
742
      $node->content['book_navigation'] = array(
743
        '#markup' => theme('book_navigation', array('book_link' => $node->book)),
744
745
746
747
        '#weight' => 100,
      );
    }
  }
Dries's avatar
Dries committed
748

749
750
  if ($build_mode != 'rss') {
    book_node_view_link($node, $build_mode);
751
  }
752
753
754
}

/**
755
 * Implement hook_page_alter().
756
757
758
759
760
761
762
763
764
765
 *
 * Add the book menu to the list of menus used to build the active trail when
 * viewing a book page.
 */
function book_page_alter(&$page) {
  if (($node = menu_get_object()) && !empty($node->book['bid'])) {
    $active_menus = menu_get_active_menu_names();
    $active_menus[] = $node->book['menu_name'];
    menu_set_active_menu_names($active_menus);
  }
766
}
767

768
/**
769
 * Implement hook_node_presave().
770
 */
771
function book_node_presave(stdClass $node) {
772
773
774
  // Always save a revision for non-administrators.
  if (!empty($node->book['bid']) && !user_access('administer nodes')) {
    $node->revision = 1;
775
776
777
778
    // The database schema requires a log message for every revision.
    if (!isset($node->log)) {
      $node->log = '';
    }
779
780
781
782
783
784
  }
  // Make sure a new node gets a new menu link.
  if (empty($node->nid)) {
    $node->book['mlid'] = NULL;
  }
}
785

786
/**
787
 * Implement hook_node_insert().
788
 */
789
function book_node_insert(stdClass $node) {
790
791
792
793
794
795
796
797
798
799
  if (!empty($node->book['bid'])) {
    if ($node->book['bid'] == 'new') {
      // New nodes that are their own book.
      $node->book['bid'] = $node->nid;
    }
    $node->book['nid'] = $node->nid;
    $node->book['menu_name'] = book_menu_name($node->book['bid']);
    _book_update_outline($node);
  }
}
800

801
/**
802
 * Implement hook_node_update().
803
 */
804
function book_node_update(stdClass $node) {
805
806
807
808
809
810
811
812
813
814
  if (!empty($node->book['bid'])) {
    if ($node->book['bid'] == 'new') {
      // New nodes that are their own book.
      $node->book['bid'] = $node->nid;
    }
    $node->book['nid'] = $node->nid;
    $node->book['menu_name'] = book_menu_name($node->book['bid']);
    _book_update_outline($node);
  }
}
815

816
/**
817
 * Implement hook_node_delete().
818
 */
819
function book_node_delete(stdClass $node) {
820
821
822
  if (!empty($node->book['bid'])) {
    if ($node->nid == $node->book['bid']) {
      // Handle deletion of a top-level post.
823
824
825
826
827
      $result = db_query("SELECT b.nid FROM {menu_links} ml INNER JOIN {book} b on b.mlid = ml.mlid WHERE ml.plid = :plid", array(
        ':plid' => $node->book['mlid']
      ));
      foreach ($result as $child) {
        $child_node = node_load($child->nid);
828
829
        $child_node->book['bid'] = $child_node->nid;
        _book_update_outline($child_node);
Dries's avatar
Dries committed
830
      }
831
832
    }
    menu_link_delete($node->book['mlid']);
833
834
835
    db_delete('book')
      ->condition('mlid', $node->book['mlid'])
      ->execute();
836
837
838
839
  }
}

/**
840
 * Implement hook_node_prepare().
841
 */
842
function book_node_prepare(stdClass $node) {
843
844
845
846
847
848
849
850
851
852
853
854
  // 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'];
855
      }
856
857
858
859
860
861
862
863
864
865
866
867
    }
    // 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'];
    }
  }
  // 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
868
869
870
  }
}

871
872
873
874
875
876
877
/**
 * 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
878
879
880
881
882
883
884
885
/**
 * 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(
886
      '#markup' => '<p>' . t('%title is part of a book outline, and has associated child pages. If you proceed with deletion, the child pages will be relocated automatically.', array('%title' => $node->title[FIELD_LANGUAGE_NONE][0]['value'])) . '</p>',
Dries's avatar
Dries committed
887
888
      '#weight' => -10,
    );
Dries's avatar
   
Dries committed
889
  }
Dries's avatar
   
Dries committed
890
}
Dries's avatar
   
Dries committed
891

892
/**
Dries's avatar
Dries committed
893
894
895
896
897
898
 * 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());
}

899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
/**
 * Process variables for book-all-books-block.tpl.php.
 *
 * The $variables array contains the following arguments:
 * - $book_menus
 *
 * All non-renderable elements are removed so that the template has full
 * access to the structured data but can also simply iterate over all
 * elements and render them (as in the default template).
 *
 * @see book-navigation.tpl.php
 */
function template_preprocess_book_all_books_block(&$variables) {
  // Remove all non-renderable elements.
  $elements = $variables['book_menus'];
  $variables['book_menus'] = array();
  foreach (element_children($elements) as $index) {
    $variables['book_menus'][$index] = $elements[$index];
  }
}

Dries's avatar
Dries committed
920
/**
921
 * Process variables for book-navigation.tpl.php.
Dries's avatar
Dries committed
922
 *
923
924
 * The $variables array contains the following arguments:
 * - $book_link
925
 *
926
 * @see book-navigation.tpl.php
927
 */
928
929
930
931
932
933
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']);
934
  $variables['book_url'] = 'node/' . $book_link['bid'];
935
936
  $variables['current_depth'] = $book_link['depth'];
  $variables['tree'] = '';
937

Dries's avatar
Dries committed
938
  if ($book_link['mlid']) {