book.module 31.8 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 6 7 8
/**
 * @file
 * Allows users to collaboratively author a book.
 */

9 10 11
/**
 * Implementation of hook_node_name().
 */
Dries's avatar
 
Dries committed
12
function book_node_name($node) {
13
  return t('book page');
Dries's avatar
 
Dries committed
14 15
}

16 17 18
/**
 * Implementation of hook_perm().
 */
Dries's avatar
 
Dries committed
19
function book_perm() {
20
  return array('maintain books', 'edit own book pages');
Dries's avatar
 
Dries committed
21 22
}

23 24 25
/**
 * Implementation of hook_access().
 */
Dries's avatar
 
Dries committed
26
function book_access($op, $node) {
Dries's avatar
 
Dries committed
27
  global $user;
Dries's avatar
 
Dries committed
28

29
  if ($op == 'view') {
Dries's avatar
 
Dries committed
30 31 32 33
    // Everyone can access all published book pages whether these pages
    // are still waiting for approval or not.  We might not always want
    // to display pages that are waiting for approval, but we take care
    // of that problem in the book_content() function.
34
    return ($node->status ? $node->status : ($node->uid == $user->uid && user_access('edit own book pages')));
Dries's avatar
 
Dries committed
35 36
  }

37
  if ($op == 'create') {
Dries's avatar
 
Dries committed
38 39
    // Only registered users can create book pages.  Given the nature
    // of the book module this is considered to be a good/safe idea.
40
    return user_access('maintain books');
Dries's avatar
 
Dries committed
41 42
  }

43
  if ($op == 'update') {
Dries's avatar
 
Dries committed
44 45 46
    // Only registered users can update book pages.  Given the nature
    // of the book module this is considered to be a good/safe idea.
    // One can only update a book page if there are no suggested updates
47 48 49 50
    // of that page waiting for approval and as long as the "create new
    // revision"-bit is set.  That is, only updates that don't overwrite
    // the current or pending information are allowed.

51 52
    return (user_access('maintain books') && !$node->moderate && $node->revision)
      || ($node->uid == $user->uid && user_access('edit own book pages'));
Dries's avatar
 
Dries committed
53
  }
Dries's avatar
 
Dries committed
54 55
}

Dries's avatar
 
Dries committed
56 57 58
/**
 * Implementation of hook_link().
 */
Dries's avatar
 
Dries committed
59
function book_link($type, $node = 0, $main = 0) {
Dries's avatar
 
Dries committed
60 61 62

  $links = array();

Dries's avatar
 
Dries committed
63 64
  if ($type == 'page' && user_access('access content')) {
    $links[] = l(t('books'), 'book', array('title' => t('Read and contribute to the collaborative books.')));
Dries's avatar
 
Dries committed
65 66
  }

Dries's avatar
 
Dries committed
67
  if ($type == 'node' && $node->type == 'book') {
Dries's avatar
 
Dries committed
68
    if (!$main) {
Dries's avatar
 
Dries committed
69
      $links[] = l(t('printer-friendly version'), 'book/print/'. $node->nid, array('title' => t('Show a printer-friendly version of this book page and its sub-pages.')));
Dries's avatar
 
Dries committed
70
    }
Dries's avatar
 
Dries committed
71 72
  }

Dries's avatar
 
Dries committed
73
  return $links;
Dries's avatar
 
Dries committed
74 75
}

Dries's avatar
 
Dries committed
76 77 78
/**
 * Implementation of hook_menu().
 */
Dries's avatar
 
Dries committed
79
function book_menu($may_cache) {
Dries's avatar
 
Dries committed
80 81
  $items = array();

Dries's avatar
 
Dries committed
82 83 84 85 86 87 88 89 90 91 92
  if ($may_cache) {
    $items[] = array('path' => 'node/add/book', 'title' => t('book page'),
      'access' => user_access('maintain books'));
    $items[] = array('path' => 'admin/node/book', 'title' => t('books'),
      'callback' => 'book_admin',
      'access' => user_access('administer nodes'),
      'weight' => 4);
    $items[] = array('path' => 'admin/node/book/orphan', 'title' => t('orphan pages'),
      'callback' => 'book_admin_orphan',
      'access' => user_access('administer nodes'),
      'weight' => 8);
93
    $result = db_query('SELECT n.nid, n.title FROM {node} n '. node_access_join_sql() .' INNER JOIN {book} b ON n.nid = b.nid WHERE '. node_access_where_sql() .' AND b.parent = 0 ORDER BY b.weight, n.title');
Dries's avatar
 
Dries committed
94 95 96 97 98 99 100 101 102 103 104
    while ($book = db_fetch_object($result)) {
      $items[] = array('path' => 'admin/node/book/'. $book->nid, 'title' => t('"%title" book', array('%title' => $book->title)));
    }
    $items[] = array('path' => 'book', 'title' => t('books'),
      'callback' => 'book_render',
      'access' => user_access('access content'),
      'type' => MENU_SUGGESTED_ITEM);
    $items[] = array('path' => 'book/print', 'title' => t('printer-friendly version'),
      'callback' => 'book_print',
      'access' => user_access('access content'),
      'type' => MENU_CALLBACK);
Dries's avatar
 
Dries committed
105 106 107 108 109
  }

  return $items;
}

110 111 112
/**
 * Implementation of hook_block().
 *
Dries's avatar
 
Dries committed
113 114
 * Displays the book table of contents in a block when the current page is a
 * single-node view of a book node.
115
 */
Dries's avatar
 
Dries committed
116
function book_block($op = 'list', $delta = 0) {
Dries's avatar
 
Dries committed
117
  $block = array();
Dries's avatar
 
Dries committed
118 119 120 121
  if ($op == 'list') {
    $block[0]['info'] = t('Book navigation');
  }
  else {
Dries's avatar
 
Dries committed
122 123
    // Only display this block when the user is browsing a book:
    if (arg(0) == 'node' && is_numeric(arg(1))) {
124
      $result = db_query('SELECT n.nid, n.title, b.parent FROM {node} n '. node_access_join_sql() .' INNER JOIN {book} b ON n.nid = b.nid WHERE '. node_access_where_sql() .' n.nid = %d', arg(1));
Dries's avatar
 
Dries committed
125 126 127 128 129 130 131 132 133 134 135 136 137 138 139
      if (db_num_rows($result) > 0) {
        $node = db_fetch_object($result);

        $path = book_location($node);
        $path[] = $node;

        $expand = array();
        foreach ($path as $key => $node) {
          $expand[] = $node->nid;
        }

        $block['subject'] = $path[0]->title;
        $block['content'] = book_tree($expand[0], 5, $expand);
      }
    }
Dries's avatar
 
Dries committed
140 141 142 143 144
  }

   return $block;
}

145 146 147
/**
 * Implementation of hook_load().
 */
Dries's avatar
 
Dries committed
148
function book_load($node) {
Dries's avatar
 
Dries committed
149
  global $user;
Dries's avatar
 
Dries committed
150

151
  $book = db_fetch_object(db_query('SELECT parent, weight, log FROM {book} WHERE nid = %d', $node->nid));
Dries's avatar
 
Dries committed
152

Dries's avatar
 
Dries committed
153
  if (arg(2) == 'edit' && !user_access('administer nodes')) {
Dries's avatar
 
Dries committed
154 155
    // If a user is about to update a book page, we overload some
    // fields to reflect the changes.
Dries's avatar
 
Dries committed
156 157 158 159 160 161
    if ($user->uid) {
      $book->uid = $user->uid;
      $book->name = $user->name;
    }
    else {
      $book->uid = 0;
162
      $book->name = '';
Dries's avatar
 
Dries committed
163
    }
Dries's avatar
 
Dries committed
164
  }
Dries's avatar
 
Dries committed
165

Dries's avatar
 
Dries committed
166
  return $book;
Dries's avatar
 
Dries committed
167 168
}

169 170 171
/**
 * Implementation of hook_insert().
 */
Dries's avatar
 
Dries committed
172
function book_insert($node) {
173
  db_query("INSERT INTO {book} (nid, parent, weight, log) VALUES (%d, %d, %d, '%s')", $node->nid, $node->parent, $node->weight, $node->log);
Dries's avatar
 
Dries committed
174
}
Dries's avatar
 
Dries committed
175

176 177 178
/**
 * Implementation of hook_update().
 */
Dries's avatar
 
Dries committed
179
function book_update($node) {
180
  db_query("UPDATE {book} SET parent = %d, weight = %d, log = '%s' WHERE nid = %d", $node->parent, $node->weight, $node->log, $node->nid);
Dries's avatar
 
Dries committed
181
}
Dries's avatar
 
Dries committed
182

183 184 185
/**
 * Implementation of hook_delete().
 */
186
function book_delete(&$node) {
187
  db_query('DELETE FROM {book} WHERE nid = %d', $node->nid);
Dries's avatar
 
Dries committed
188 189
}

190 191 192
/**
 * Implementation of hook_validate().
 */
193
function book_validate(&$node) {
194 195
  // Set default values for non-administrators.
  if (!user_access('administer nodes')) {
196 197 198
    $node->weight = 0;
    $node->revision = 1;
  }
Dries's avatar
 
Dries committed
199 200
}

201 202 203
/**
 * Implementation of hook_form().
 */
Dries's avatar
 
Dries committed
204
function book_form(&$node) {
Dries's avatar
 
Dries committed
205
  global $user;
Dries's avatar
 
Dries committed
206

207
  $op = $_POST['op'];
208
  $output = form_select(t('Parent'), 'parent', $node->parent, book_toc($node->nid), t('The parent subject or category the page belongs in.'));
Dries's avatar
 
Dries committed
209

210 211
  if (function_exists('taxonomy_node_form')) {
    $output .= implode('', taxonomy_node_form('book', $node));
212 213
  }

214
  $output .= form_textarea(t('Body'), 'body', $node->body, 60, 20, '', NULL, TRUE);
215
  $output .= filter_form('format', $node->format);
216
  $output .= form_textarea(t('Log message'), 'log', $node->log, 60, 5, t('An explanation of the additions or updates being made to help the group understand your motivations.'));
Dries's avatar
 
Dries committed
217

218 219
  if (user_access('administer nodes')) {
    $output .= form_weight(t('Weight'), 'weight', $node->weight, 15, t('The heavier pages will sink and the lighter pages will be positioned nearer the top.'));
Dries's avatar
 
Dries committed
220 221
  }
  else {
Dries's avatar
 
Dries committed
222 223
    // If a regular user updates a book page, we create a new revision
    // authored by that user:
224
    $output .= form_hidden('revision', 1);
Dries's avatar
 
Dries committed
225 226 227 228 229
  }

  return $output;
}

230 231 232
/**
 * Implementation of hook_node_link().
 */
Dries's avatar
 
Dries committed
233
function book_node_link($node = 0) {
Dries's avatar
 
Dries committed
234 235
  global $user;

236 237
  $op = $_POST['op'];
  $edit = $_POST['edit'];
Dries's avatar
 
Dries committed
238

239
  if ($node->type != 'book') {
Dries's avatar
 
Dries committed
240

241 242
    if ($edit['nid']) {
      $node = node_load(array('nid' => $edit['nid']));
Dries's avatar
 
Dries committed
243 244
    }

245 246
    if ($op == t('Add to book outline')) {
      db_query('INSERT INTO {book} (nid, parent, weight) VALUES (%d, %d, %d)', $node->nid, $edit['parent'], $edit['weight']);
Dries's avatar
 
Dries committed
247
      drupal_set_message(t('Added the node to the book.'));
Dries's avatar
 
Dries committed
248 249
    }

250 251
    if ($op == t('Update book outline')) {
      db_query('UPDATE {book} SET parent = %d, weight = %d WHERE nid = %d', $edit['parent'], $edit['weight'], $node->nid);
Dries's avatar
 
Dries committed
252
      drupal_set_message(t('Updated the book outline.'));
Dries's avatar
 
Dries committed
253 254
    }

255 256
    if ($op == t('Remove from book outline')) {
      db_query('DELETE FROM {book} WHERE nid = %d', $node->nid);
257
      drupal_set_message(t('Removed the post from the book.'));
Dries's avatar
 
Dries committed
258 259
    }

Dries's avatar
 
Dries committed
260
    $output .= '<h3>'. t('Edit book outline for node "%booktitle"', array('%booktitle' => '<em>'. $node->title .'</em>')) .'</h3>';
Dries's avatar
 
Dries committed
261

262 263
    if ($edit['nid']) {
      $page = db_fetch_object(db_query('SELECT * FROM {book} WHERE nid = %d', $node->nid));
Dries's avatar
 
Dries committed
264

265
      $output .= form_select(t('Parent'), 'parent', $page->parent, book_toc($node->nid), t('The parent subject or category the page belongs in.'));
266
      $output .= form_weight(t('Weight'), 'weight', $node->weight, 15, t('The heavier pages will sink and the lighter pages will be positioned nearer the top.'));
Dries's avatar
 
Dries committed
267 268

      if ($page->nid) {
269 270
        $output .= form_submit(t('Update book outline'));
        $output .= form_submit(t('Remove from book outline'));
Dries's avatar
 
Dries committed
271 272
      }
      else {
273
        $output .= form_submit(t('Add to book outline'));
Dries's avatar
 
Dries committed
274 275 276 277
      }

    }
    else {
278
      $output .= form_submit(t('Edit book outline'));
Dries's avatar
 
Dries committed
279 280
    }

281
    $output .= form_hidden('nid', $node->nid);
Dries's avatar
 
Dries committed
282

283
    return form($output, 'post', url('admin/node/book'));
Dries's avatar
 
Dries committed
284 285 286
  }
}

287 288 289
/**
 * Return the the most recent revision that matches the specified conditions.
 */
Dries's avatar
 
Dries committed
290 291 292 293 294 295
function book_revision_load($page, $conditions = array()) {

  $revisions = array_reverse(node_revision_list($page));

  foreach ($revisions as $revision) {

Dries's avatar
 
Dries committed
296
    // Extract the specified revision:
Dries's avatar
 
Dries committed
297 298
    $node = node_revision_load($page, $revision);

Dries's avatar
 
Dries committed
299 300
    // Check to see if the conditions are met:
    $status = TRUE;
Dries's avatar
 
Dries committed
301 302 303

    foreach ($conditions as $key => $value) {
      if ($node->$key != $value) {
Dries's avatar
 
Dries committed
304
        $status = FALSE;
Dries's avatar
 
Dries committed
305 306 307 308 309 310 311 312 313
      }
    }

    if ($status) {
      return $node;
    }
  }
}

314 315 316
/**
 * Return the path (call stack) to a certain book page.
 */
Dries's avatar
 
Dries committed
317
function book_location($node, $nodes = array()) {
318
  $parent = db_fetch_object(db_query('SELECT n.nid, n.title, b.parent FROM {node} n '. node_access_join_sql() .' INNER JOIN {book} b ON n.nid = b.nid WHERE '. node_access_where_sql() .' AND n.nid = %d', $node->parent));
Dries's avatar
 
Dries committed
319 320 321 322 323 324 325
  if ($parent->title) {
    $nodes = book_location($parent, $nodes);
    array_push($nodes, $parent);
  }
  return $nodes;
}

Dries's avatar
 
Dries committed
326
function book_location_down($node, $nodes = array()) {
327
  $last_direct_child = db_fetch_object(db_query('SELECT n.nid, n.title, b.parent FROM {node} n '. node_access_join_sql() .' INNER JOIN {book} b ON n.nid = b.nid WHERE '. node_access_where_sql() .' AND b.parent = %d ORDER BY b.weight DESC, n.title DESC', $node->nid));
Dries's avatar
 
Dries committed
328 329 330 331 332 333 334
  if ($last_direct_child) {
    array_push($nodes, $last_direct_child);
    $nodes = book_location_down($last_direct_child, $nodes);
  }
  return $nodes;
}

335 336 337
/**
 * Fetch the node object of the previous page of the book.
 */
Dries's avatar
 
Dries committed
338
function book_prev($node) {
Dries's avatar
 
Dries committed
339
  // If the parent is zero, we are at the start of a book so there is no previous.
Dries's avatar
 
Dries committed
340 341 342 343
  if ($node->parent == 0) {
    return NULL;
  }

Dries's avatar
 
Dries committed
344
  // Previous on the same level:
345
  $direct_above = db_fetch_object(db_query('SELECT n.nid, n.title FROM {node} n '. node_access_join_sql() .' INNER JOIN {book} b ON n.nid = b.nid WHERE '. node_access_where_sql() ." AND b.parent = %d AND n.status = 1 AND (n.moderate = 0 OR n.revisions != '') AND (b.weight < %d OR (b.weight = %d AND n.title < '%s')) ORDER BY b.weight DESC, n.title DESC", $node->parent, $node->weight, $node->weight, $node->title));
Dries's avatar
 
Dries committed
346
  if ($direct_above) {
Dries's avatar
 
Dries committed
347
    // Get last leaf of $above.
Dries's avatar
 
Dries committed
348
    $path = book_location_down($direct_above);
Dries's avatar
 
Dries committed
349 350

    return $path ? (count($path) > 0 ? array_pop($path) : NULL) : $direct_above;
Dries's avatar
 
Dries committed
351 352
  }
  else {
Dries's avatar
 
Dries committed
353
    // Direct parent:
354
    $prev = db_fetch_object(db_query('SELECT n.nid, n.title FROM {node} n '. node_access_join_sql() .' INNER JOIN {book} b ON n.nid = b.nid WHERE '. node_access_where_sql() ." AND n.nid = %d AND n.status = 1 AND (n.moderate = 0 OR n.revisions != '')", $node->parent));
Dries's avatar
 
Dries committed
355 356 357 358
    return $prev;
  }
}

359 360 361
/**
 * Fetch the node object of the next page of the book.
 */
Dries's avatar
 
Dries committed
362 363
function book_next($node) {
  // get first direct child
Dries's avatar
 
Dries committed
364
  $child = db_fetch_object(db_query("SELECT DISTINCT(n.nid), n.title, b.weight FROM {node} n ". node_access_join_sql() ." INNER JOIN {book} b ON n.nid = b.nid WHERE b.parent = %d AND n.status = 1 AND ". node_access_where_sql() ." AND (n.moderate = 0 OR n.revisions != '') ORDER BY b.weight ASC, n.title ASC", $node->nid));
Dries's avatar
 
Dries committed
365 366 367 368
  if ($child) {
    return $child;
  }

Dries's avatar
 
Dries committed
369 370
  // No direct child: get next for this level or any parent in this book.
  array_push($path = book_location($node), $node); // Path to top-level node including this one.
371

Dries's avatar
 
Dries committed
372
  while (($leaf = array_pop($path)) && count($path)) {
Dries's avatar
 
Dries committed
373
    $next = db_fetch_object(db_query("SELECT DISTINCT(n.nid), n.title, b.weight FROM {node} n ". node_access_join_sql() ." INNER JOIN {book} b ON n.nid = b.nid WHERE b.parent = %d AND n.status = 1 AND ". node_access_where_sql() ." AND (n.moderate = 0 OR n.revisions != '') AND (b.weight > %d OR (b.weight = %d AND n.title > '%s')) ORDER BY b.weight ASC, n.title ASC", $leaf->parent, $leaf->weight, $leaf->weight, $leaf->title));
Dries's avatar
 
Dries committed
374 375 376 377 378 379
    if ($next) {
      return $next;
    }
  }
}

380 381 382
/**
 * Implementation of hook_content().
 */
Dries's avatar
 
Dries committed
383
function book_content($node, $teaser = FALSE) {
384
  $op = $_POST['op'];
Dries's avatar
 
Dries committed
385

Dries's avatar
 
Dries committed
386 387 388
  // Always display the most recently approved revision of a node
  // (if any) unless we have to display this page in the context of
  // the moderation queue.
389 390
  if ($op != t('Preview') && $node->moderate && arg(0) != 'queue') {
    $revision = book_revision_load($node, array('moderate' => 0, 'status' => 1));
Dries's avatar
 
Dries committed
391 392 393 394

    if ($revision) {
      $node = $revision;
    }
Dries's avatar
 
Dries committed
395 396
  }

397 398
  // Extract the page body.
  $node = node_prepare($node, $teaser);
Dries's avatar
 
Dries committed
399

Dries's avatar
 
Dries committed
400 401 402
  return $node;
}

403 404 405 406 407 408
/**
 * Implementation of hook_view().
 *
 * If not displayed on the main page, we render the node as a page in the
 * book with extra links to the previous and next pages.
 */
Dries's avatar
 
Dries committed
409
function book_view(&$node, $teaser = FALSE, $page = FALSE) {
Dries's avatar
 
Dries committed
410
  $node = book_content($node, $teaser);
Dries's avatar
 
Dries committed
411

Dries's avatar
 
Dries committed
412 413
  if (!$teaser && $node->moderate) {
    $node->body .= '<div class="log"><div class="title">'. t('Log') .':</div>'. $node->log .'</div>';
Dries's avatar
 
Dries committed
414 415 416
  }
}

417
/**
Dries's avatar
 
Dries committed
418 419 420
 * Implementation of hook_nodeapi().
 *
 * Appends book navigation to all nodes in the book.
421
 */
Dries's avatar
 
Dries committed
422 423 424 425
function book_nodeapi(&$node, $op, $teaser, $page) {
  switch ($op) {
    case 'view':
      if (!$teaser) {
Dries's avatar
 
Dries committed
426
        $book = db_fetch_array(db_query('SELECT * FROM {book} WHERE nid = %d', $node->nid));
Dries's avatar
 
Dries committed
427

Dries's avatar
 
Dries committed
428 429 430 431 432
        if ($book) {
          foreach ($book as $key => $value) {
            $node->$key = $value;
          }
          $node = book_navigation($node);
433 434 435
          if ($page) {
            menu_set_location($node->breadcrumb);
          }
Dries's avatar
 
Dries committed
436
        }
Dries's avatar
 
Dries committed
437
      }
Dries's avatar
 
Dries committed
438
      break;
Dries's avatar
 
Dries committed
439
  }
Dries's avatar
 
Dries committed
440
}
Dries's avatar
 
Dries committed
441

442 443 444 445
/**
 * Prepares both the custom breadcrumb trail and the forward/backward
 * navigation for a node presented as a book page.
 */
Dries's avatar
 
Dries committed
446
function book_navigation($node) {
Dries's avatar
 
Dries committed
447
  $path = book_location($node);
Dries's avatar
 
Dries committed
448

Dries's avatar
 
Dries committed
449
  // Construct the breadcrumb:
Dries's avatar
 
Dries committed
450

Dries's avatar
 
Dries committed
451
  $node->breadcrumb = array(); // Overwrite the trail with a book trail.
Dries's avatar
 
Dries committed
452
  foreach ($path as $level) {
Dries's avatar
 
Dries committed
453
    $node->breadcrumb[] = array('path' => 'node/'. $level->nid, 'title' =>  $level->title);
Dries's avatar
 
Dries committed
454
  }
Dries's avatar
 
Dries committed
455
  $node->breadcrumb[] = array('path' => 'node/'. $node->nid);
Dries's avatar
 
Dries committed
456

Dries's avatar
 
Dries committed
457
  if ($node->nid) {
458
    $output .= '<div class="book">';
Dries's avatar
 
Dries committed
459 460

    if ($tree = book_tree($node->nid)) {
Dries's avatar
 
Dries committed
461
      $output .= '<div class="tree">'. $tree .'</div>';
Dries's avatar
 
Dries committed
462
    }
Dries's avatar
 
Dries committed
463

Dries's avatar
 
Dries committed
464
    if ($prev = book_prev($node)) {
465
      $links .= '<div class="prev">';
Dries's avatar
 
Dries committed
466
      $links .= l(t('previous'), 'node/'. $prev->nid, array('title' => t('View the previous page.')));
467
      $links .= '</div>';
Dries's avatar
 
Dries committed
468
      $titles .= '<div class="prev">'. $prev->title .'</div>';
Dries's avatar
 
Dries committed
469 470
    }
    else {
Dries's avatar
 
Dries committed
471
      $links .= '<div class="prev">&nbsp;</div>'; // Make an empty div to fill the space.
Dries's avatar
 
Dries committed
472 473
    }
    if ($next = book_next($node)) {
474
      $links .= '<div class="next">';
Dries's avatar
 
Dries committed
475
      $links .= l(t('next'), 'node/'. $next->nid, array('title' => t('View the next page.')));
476
      $links .= '</div>';
Dries's avatar
 
Dries committed
477
      $titles .= '<div class="next">'. $next->title .'</div>';
Dries's avatar
 
Dries committed
478 479
    }
    else {
Dries's avatar
 
Dries committed
480
      $links .= '<div class="next">&nbsp;</div>'; // Make an empty div to fill the space.
Dries's avatar
 
Dries committed
481 482
    }
    if ($node->parent) {
483
      $links .= '<div class="up">';
Dries's avatar
 
Dries committed
484
      $links .= l(t('up'), 'node/'. $node->parent, array('title' => t('View this page\'s parent section.')));
485
      $links .= '</div>';
Dries's avatar
 
Dries committed
486
    }
Dries's avatar
 
Dries committed
487

488
    $output .= '<div class="nav">';
Dries's avatar
 
Dries committed
489 490
    $output .= ' <div class="links">'. $links .'</div>';
    $output .= ' <div class="titles">'. $titles .'</div>';
491 492
    $output .= '</div>';
    $output .= '</div>';
Dries's avatar
 
Dries committed
493
  }
Dries's avatar
 
Dries committed
494

Dries's avatar
 
Dries committed
495
  $node->body = $node->body.$output;
Dries's avatar
 
Dries committed
496

Dries's avatar
 
Dries committed
497
  return $node;
Dries's avatar
 
Dries committed
498
}
Dries's avatar
 
Dries committed
499

500
function book_toc_recurse($nid, $indent, $toc, $children, $exclude) {
Dries's avatar
 
Dries committed
501 502
  if ($children[$nid]) {
    foreach ($children[$nid] as $foo => $node) {
503 504 505 506
      if (!$exclude || $exclude != $node->nid) {
        $toc[$node->nid] = $indent .' '. $node->title;
        $toc = book_toc_recurse($node->nid, $indent .'--', $toc, $children, $exclude);
      }
Dries's avatar
 
Dries committed
507 508 509 510 511 512
    }
  }

  return $toc;
}

513
function book_toc($exclude = 0) {
Dries's avatar
 
Dries committed
514
  $result = db_query('SELECT DISTINCT(n.nid), n.title, b.parent, b.weight FROM {node} n '. node_access_join_sql() .' INNER JOIN {book} b ON n.nid = b.nid WHERE n.status = 1 AND '. node_access_where_sql() .' ORDER BY b.weight, n.title');
Dries's avatar
 
Dries committed
515

Dries's avatar
 
Dries committed
516
  while ($node = db_fetch_object($result)) {
Dries's avatar
 
Dries committed
517 518 519 520
    if (!$children[$node->parent]) {
      $children[$node->parent] = array();
    }
    array_push($children[$node->parent], $node);
Dries's avatar
 
Dries committed
521
  }
Dries's avatar
 
Dries committed
522

523 524
  $toc = array();

Dries's avatar
 
Dries committed
525 526
  // If the user is an administrator, add the top-level book page;
  // only administrators can start new books.
527 528
  if (user_access('administer nodes')) {
    $toc[0] = '<'. t('top-level') .'>';
Dries's avatar
 
Dries committed
529 530
  }

531
  $toc = book_toc_recurse(0, '', $toc, $children, $exclude);
Dries's avatar
 
Dries committed
532

Dries's avatar
 
Dries committed
533 534 535
  return $toc;
}

Dries's avatar
 
Dries committed
536
function book_tree_recurse($nid, $depth, $children, $unfold = array()) {
Dries's avatar
 
Dries committed
537
  if ($depth > 0) {
Dries's avatar
 
Dries committed
538 539
    if ($children[$nid]) {
      foreach ($children[$nid] as $foo => $node) {
Dries's avatar
 
Dries committed
540 541 542
        if (in_array($node->nid, $unfold)) {
          if ($tree = book_tree_recurse($node->nid, $depth - 1, $children, $unfold)) {
            $output .= '<li class="expanded">';
Dries's avatar
 
Dries committed
543 544
            $output .= l($node->title, 'node/'. $node->nid);
            $output .= '<ul>'. $tree .'</ul>';
Dries's avatar
 
Dries committed
545 546 547
            $output .= '</li>';
          }
          else {
Dries's avatar
 
Dries committed
548
            $output .= '<li class="leaf">'. l($node->title, 'node/'. $node->nid) .'</li>';
Dries's avatar
 
Dries committed
549 550 551 552
          }
        }
        else {
          if ($tree = book_tree_recurse($node->nid, 1, $children)) {
Dries's avatar
 
Dries committed
553
            $output .= '<li class="collapsed">'. l($node->title, 'node/'. $node->nid) .'</li>';
Dries's avatar
 
Dries committed
554 555
          }
          else {
Dries's avatar
 
Dries committed
556
            $output .= '<li class="leaf">'. l($node->title, 'node/'. $node->nid) .'</li>';
Dries's avatar
 
Dries committed
557
          }
Dries's avatar
 
Dries committed
558
        }
Dries's avatar
 
Dries committed
559 560
      }
    }
Dries's avatar
 
Dries committed
561 562 563 564 565
  }

  return $output;
}

Dries's avatar
 
Dries committed
566
function book_tree($parent = 0, $depth = 3, $unfold = array()) {
Dries's avatar
 
Dries committed
567
  $result = db_query('SELECT DISTINCT(n.nid), n.title, b.parent, b.weight FROM {node} n '. node_access_join_sql() .' INNER JOIN {book} b ON n.nid = b.nid WHERE n.status = 1 AND '. node_access_where_sql() .' AND n.moderate = 0 ORDER BY b.weight, n.title');
Dries's avatar
 
Dries committed
568

Dries's avatar
 
Dries committed
569 570 571 572
  while ($node = db_fetch_object($result)) {
    $list = $children[$node->parent] ? $children[$node->parent] : array();
    array_push($list, $node);
    $children[$node->parent] = $list;
Dries's avatar
 
Dries committed
573
  }
Dries's avatar
 
Dries committed
574

Dries's avatar
 
Dries committed
575
  if ($tree = book_tree_recurse($parent, $depth, $children, $unfold)) {
Dries's avatar
 
Dries committed
576
    return '<ul>'. $tree .'</ul>';
Dries's avatar
 
Dries committed
577
  }
Dries's avatar
 
Dries committed
578 579
}

580
/**
Dries's avatar
Dries committed
581
 * Menu callback; prints a listing of all books.
582
 */
Dries's avatar
 
Dries committed
583
function book_render() {
584
  $result = db_query('SELECT n.nid FROM {node} n '. node_access_join_sql() .' INNER JOIN {book} b ON n.nid = b.nid WHERE '. node_access_where_sql() .' AND b.parent = 0 AND n.status = 1 AND (n.moderate = 0 OR n.revisions IS NOT NULL) ORDER BY b.weight, n.title');
Dries's avatar
 
Dries committed
585

Dries's avatar
 
Dries committed
586
  while ($page = db_fetch_object($result)) {
Dries's avatar
 
Dries committed
587
    // Load the node:
588
    $node = node_load(array('nid' => $page->nid));
Dries's avatar
 
Dries committed
589

Dries's avatar
 
Dries committed
590
    if ($node) {
Dries's avatar
 
Dries committed
591
      // Take the most recent approved revision, extract the page and check output:
592
      $node = book_content($node, TRUE);
Dries's avatar
 
Dries committed
593
      // Output the content:
594
      $output .= '<div class="book">';
Dries's avatar
 
Dries committed
595
      $output .= '<div class="title">'. l($node->title, 'node/'. $node->nid) .'</div>';
596
      $output .= '<div class="body">'. $node->teaser .'</div>';
597
      $output .= '</div>';
Dries's avatar
 
Dries committed
598
    }
Dries's avatar
 
Dries committed
599 600
  }

601 602
  drupal_set_title(t('Books'));
  print theme('page', $output);
Dries's avatar
 
Dries committed
603 604
}

605
/**
Dries's avatar
 
Dries committed
606
 * Menu callback; generates printer-friendly book page with all descendants.
607 608
 */
function book_print($nid = 0, $depth = 1) {
Dries's avatar
 
Dries committed
609
  global $base_url;
Dries's avatar
 
Dries committed
610
  $result = db_query('SELECT DISTINCT(n.nid), n.title, b.weight FROM {node} n '. node_access_join_sql() .' INNER JOIN {book} b ON n.nid = b.nid WHERE n.status = 1 AND '. node_access_where_sql() .' AND n.nid = %d AND (n.moderate = 0 OR n.revisions IS NOT NULL) ORDER BY b.weight, n.title', $nid);
Dries's avatar
 
Dries committed
611

Dries's avatar
 
Dries committed
612 613
  while ($page = db_fetch_object($result)) {
    // load the node:
614
    $node = node_load(array('nid' => $page->nid));
Dries's avatar
 
Dries committed
615

Dries's avatar
 
Dries committed
616 617
    if ($node) {
      // output the content:
618 619
      if (node_hook($node, 'content')) {
        $node = node_invoke($node, 'content');
Dries's avatar
 
Dries committed
620
      }
Dries's avatar
 
Dries committed
621
      $output .= '<h1 id="'. $node->nid .'" name="'. $node->nid .'" class="book-h'. $depth .'">'. $node->title .'</h1>';
Dries's avatar
 
Dries committed
622

Dries's avatar
 
Dries committed
623
      if ($node->body) {
Dries's avatar
 
Dries committed
624
        $output .= $node->body;
Dries's avatar
 
Dries committed
625
      }
Dries's avatar
 
Dries committed
626
    }
Dries's avatar
 
Dries committed
627
  }
Dries's avatar
 
Dries committed
628

629
  $output .= book_print_recurse($nid, $depth);
Dries's avatar
 
Dries committed
630

Dries's avatar
 
Dries committed
631 632
  $html = '<html><head><title>'. $node->title .'</title>';
  $html .= '<base href="'. $base_url .'/" />';
Dries's avatar
 
Dries committed
633
  $html .= "<style type=\"text/css\">\n@import url(misc/print.css);\n</style>";
634
  $html .= '</head><body>'. $output .'</body></html>';
Dries's avatar
 
Dries committed
635

636
  print $html;
Dries's avatar
 
Dries committed
637 638
}

639
function book_print_recurse($parent = '', $depth = 1) {
Dries's avatar
 
Dries committed
640
  $result = db_query("SELECT DISTINCT(n.nid), n.title, b.weight FROM {node} n ". node_access_join_sql() ." INNER JOIN {book} b ON n.nid = b.nid WHERE n.status = 1 AND ". node_access_where_sql() ." AND b.parent = '$parent' AND (n.moderate = 0 OR n.revisions IS NOT NULL) ORDER BY b.weight, n.title");
Dries's avatar
 
Dries committed
641

Dries's avatar
 
Dries committed
642
  while ($page = db_fetch_object($result)) {
Dries's avatar
 
Dries committed
643
    // Load the node:
644
    $node = node_load(array('nid' => $page->nid));
Dries's avatar
 
Dries committed
645

Dries's avatar
 
Dries committed
646
    // Take the most recent approved revision:
Dries's avatar
 
Dries committed
647
    if ($node->moderate) {
648
      $node = book_revision_load($node, array('moderate' => 0, 'status' => 1));
Dries's avatar
 
Dries committed
649 650
    }

Dries's avatar
 
Dries committed
651
    if ($node) {
Dries's avatar
 
Dries committed
652
      // Output the content:
653 654
      if (node_hook($node, 'content')) {
        $node = node_invoke($node, 'content');
Dries's avatar
 
Dries committed
655
      }
Dries's avatar
 
Dries committed
656
      $output .= '<h1 id="'. $node->nid .'" name="'. $node->nid .'" class="book-h'. $depth .'">'. $node->title .'</h1>';
Dries's avatar
 
Dries committed
657

Dries's avatar
 
Dries committed
658
      if ($node->body) {
659
        $output .= '<ul>'. $node->body .'</ul>';
Dries's avatar
 
Dries committed
660
      }
Dries's avatar
 
Dries committed
661

662
      $output .= book_print_recurse($node->nid, $depth + 1);
Dries's avatar
 
Dries committed
663
    }
Dries's avatar
 
Dries committed
664
  }
Dries's avatar
 
Dries committed
665

Dries's avatar
 
Dries committed
666 667
  return $output;
}
Dries's avatar
 
Dries committed
668

Dries's avatar
 
Dries committed
669
function book_admin_view_line($node, $depth = 0) {
Dries's avatar
 
Dries committed
670
  return array('<div style="padding-left: '. (25 * $depth) .'px;">'. form_textfield(NULL, $node->nid .'][title', $node->title, 64, 255) .'</div>', form_weight(NULL, $node->nid .'][weight', $node->weight), l(t('view'), 'node/'. $node->nid), l(t('edit'), 'node/'. $node->nid .'/edit'), l(t('delete'), 'admin/node/delete/'. $node->nid));
Dries's avatar
 
Dries committed
671 672 673
}

function book_admin_view_book($nid, $depth = 1) {
674
  $result = db_query('SELECT n.nid FROM {node} n '. node_access_join_sql() .' INNER JOIN {book} b ON n.nid = b.nid WHERE '. node_access_where_sql() .' AND b.parent = %d ORDER BY b.weight, n.title', $nid);
Dries's avatar
 
Dries committed
675 676

  while ($node = db_fetch_object($result)) {
Dries's avatar
 
Dries committed
677
    $node = node_load(array('nid' => $node->nid));
Dries's avatar
 
Dries committed
678 679
    $rows[] = book_admin_view_line($node, $depth);
    $rows = array_merge($rows, book_admin_view_book($node->nid, $depth + 1));
Dries's avatar
 
Dries committed
680 681
  }

Dries's avatar
 
Dries committed
682
  return $rows;
Dries's avatar
 
Dries committed
683 684
}

685 686 687
/**
 * Display an administrative view of the hierarchy of a book.
 */
Dries's avatar
 
Dries committed
688
function book_admin_view($nid, $depth = 0) {
Dries's avatar
 
Dries committed
689
  if ($nid) {
Dries's avatar
 
Dries committed
690
    $node = node_load(array('nid' => $nid));
Dries's avatar
 
Dries committed
691

Dries's avatar
 
Dries committed
692
    $output .= '<h3>'. $node->title .'</h3>';
Dries's avatar
 
Dries committed
693

Dries's avatar
 
Dries committed
694
    $header = array(t('Title'), t('Weight'), array('data' => t('Operations'), 'colspan' => 3));
Dries's avatar
 
Dries committed
695 696
    $rows[] = book_admin_view_line($node);
    $rows = array_merge($rows, book_admin_view_book($nid));
Dries's avatar
 
Dries committed
697

Dries's avatar
 
Dries committed
698 699
    $output .= theme('table', $header, $rows);
    $output .= form_submit(t('Save book pages'));
Dries's avatar
 
Dries committed
700

Dries's avatar
 
Dries committed
701 702
    return form($output);
  }
Dries's avatar
 
Dries committed
703 704 705
}

function book_admin_save($nid, $edit = array()) {
Dries's avatar
 
Dries committed
706
  if ($nid) {
Dries's avatar
 
Dries committed
707
    $book = node_load(array('nid' => $nid));
Dries's avatar
 
Dries committed
708

Dries's avatar
 
Dries committed
709
    foreach ($edit as $nid => $value) {
Dries's avatar
 
Dries committed
710 711 712 713
      // Check to see whether the title needs updating:
      $title = db_result(db_query('SELECT title FROM {node} WHERE nid = %d', $nid));
      if ($title != $value['title']) {
        db_query("UPDATE {node} SET title = '%s' WHERE nid = %d", $value['title'], $nid);
Dries's avatar
 
Dries committed
714
      }
Dries's avatar
 
Dries committed
715

Dries's avatar
 
Dries committed
716 717 718 719
      // Check to see whether the weight needs updating:
      $weight = db_result(db_query('SELECT weight FROM {book} WHERE nid = %d', $nid));
      if ($weight != $value['weight']) {
        db_query('UPDATE {book} SET weight = %d WHERE nid = %d', $value['weight'], $nid);
Dries's avatar
 
Dries committed
720
      }
Dries's avatar
 
Dries committed
721 722
    }

Dries's avatar
 
Dries committed
723
    $message = t('Updated book %title.', array('%title' => "<em>$book->title</em>"));
724
    watchdog('special', $message);
Dries's avatar
 
Dries committed
725

Dries's avatar
 
Dries committed
726 727
    return $message;
  }
Dries's avatar
 
Dries committed
728 729
}

730
/**
Dries's avatar
Dries committed
731
 * Menu callback; displays a listing of all orphaned book pages.
732
 */
Dries's avatar
 
Dries committed
733
function book_admin_orphan() {
734
  $result = db_query('SELECT n.nid, n.title, n.status, b.parent FROM {node} n '. node_access_join_sql() .' INNER JOIN {book} b ON n.nid = b.nid WHERE '. node_access_where_sql());
Dries's avatar
 
Dries committed
735 736 737 738 739

  while ($page = db_fetch_object($result)) {
    $pages[$page->nid] = $page;
  }

Dries's avatar
 
Dries committed
740
  if ($pages) {
741
    $output .= '<h3>'. t('Orphan pages') .'</h3>';
Dries's avatar
 
Dries committed
742
    $header = array(t('Title'), t('Weight'), array('data' => t('Operations'), 'colspan' => 3));
Dries's avatar
 
Dries committed
743 744
    foreach ($pages as $nid => $node) {
      if ($node->parent && empty($pages[$node->parent])) {
Dries's avatar
 
Dries committed
745 746
        $rows[] = book_admin_view_line($node, $depth);
        $rows = array_merge($rows, book_admin_view_book($node->nid, $depth + 1));
Dries's avatar
 
Dries committed
747
      }
Dries's avatar
 
Dries committed
748
    }
Dries's avatar
 
Dries committed
749
    $output .= theme('table', $header, $rows);
Dries's avatar
 
Dries committed
750 751
  }

Dries's avatar
 
Dries committed
752
  print theme('page', $output);
Dries's avatar
 
Dries committed
753 754
}

755
/**