book.module 31 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('create book pages', '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 == 'create') {
Dries's avatar
 
Dries committed
30 31
    // Only registered users can create book pages.  Given the nature
    // of the book module this is considered to be a good/safe idea.
32
    return user_access('create book pages');
Dries's avatar
 
Dries committed
33 34
  }

35
  if ($op == 'update') {
Dries's avatar
 
Dries committed
36 37 38
    // 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
39 40
    // of that page waiting for approval.  That is, only updates that
    // don't overwrite the current or pending information are allowed.
41

42
    return ((user_access('maintain books') && !$node->moderate) || ($node->uid == $user->uid && user_access('edit own book pages')));
Dries's avatar
 
Dries committed
43
  }
Dries's avatar
 
Dries committed
44 45
}

Dries's avatar
 
Dries committed
46 47 48
/**
 * Implementation of hook_link().
 */
Dries's avatar
 
Dries committed
49
function book_link($type, $node = 0, $main = 0) {
Dries's avatar
 
Dries committed
50 51 52

  $links = array();

Dries's avatar
 
Dries committed
53
  if ($type == 'node' && $node->type == 'book') {
Dries's avatar
 
Dries committed
54
    if (!$main) {
Dries's avatar
 
Dries committed
55 56 57
      if (book_access('create', $node)) {
        $links[] = l(t('add child page'), "node/add/book/parent/$node->nid");
      }
Dries's avatar
 
Dries committed
58
      $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
59
    }
Dries's avatar
 
Dries committed
60 61
  }

Dries's avatar
 
Dries committed
62
  return $links;
Dries's avatar
 
Dries committed
63 64
}

Dries's avatar
 
Dries committed
65 66 67
/**
 * Implementation of hook_menu().
 */
Dries's avatar
 
Dries committed
68
function book_menu($may_cache) {
Dries's avatar
 
Dries committed
69 70
  $items = array();

Dries's avatar
 
Dries committed
71
  if ($may_cache) {
Dries's avatar
 
Dries committed
72 73
    $items[] = array('path' => 'book', 'title' => t('books'),
      'access' => user_access('access content'), 'type' => MENU_NORMAL_ITEM, 'weight' => 5);
Dries's avatar
 
Dries committed
74
    $items[] = array('path' => 'node/add/book', 'title' => t('book page'),
75
      'access' => user_access('create book pages'));
Dries's avatar
 
Dries committed
76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91
    $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);
    $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
92
  }
Dries's avatar
 
Dries committed
93 94 95 96 97
  else {
    // To avoid SQL overhead, check whether we are on a node page and whether the
    // user is allowed to maintain books.
    if (arg(0) == 'node' && is_numeric(arg(1)) && user_access('maintain books')) {
      // Only add the outline-tab for non-book pages:
Dries's avatar
 
Dries committed
98
      $result = db_query(node_rewrite_sql("SELECT n.nid FROM {node} n WHERE n.nid = %d AND n.type != 'book'"), arg(1));
Dries's avatar
 
Dries committed
99 100 101 102 103 104
      if (db_num_rows($result) > 0) {
        $items[] = array('path' => 'node/'. arg(1) .'/outline', 'title' => t('outline'),
          'callback' => 'book_outline', 'access' => user_access('maintain books'),
          'type' => MENU_LOCAL_TASK, 'weight' => 2);
      }
    }
Dries's avatar
 
Dries committed
105 106 107 108

    // We don't want to cache these menu items because they could change whenever
    // a book page or outline node is edited.
    if (arg(0) == 'admin' && arg(1) == 'node' && arg(2) == 'book') {
Steven Wittens's avatar
Steven Wittens committed
109
      $result = db_query(node_rewrite_sql('SELECT n.nid, n.title FROM {node} n INNER JOIN {book} b ON n.nid = b.nid WHERE b.parent = 0 ORDER BY b.weight, n.title'));
Dries's avatar
 
Dries committed
110 111 112 113
      while ($book = db_fetch_object($result)) {
        $items[] = array('path' => 'admin/node/book/'. $book->nid, 'title' => t('"%title" book', array('%title' => $book->title)));
      }
    }
Dries's avatar
 
Dries committed
114
  }
Dries's avatar
 
Dries committed
115 116 117 118

  return $items;
}

119 120 121
/**
 * Implementation of hook_block().
 *
Dries's avatar
 
Dries committed
122 123
 * Displays the book table of contents in a block when the current page is a
 * single-node view of a book node.
124
 */
Dries's avatar
 
Dries committed
125
function book_block($op = 'list', $delta = 0) {
Dries's avatar
 
Dries committed
126
  $block = array();
Dries's avatar
 
Dries committed
127 128
  if ($op == 'list') {
    $block[0]['info'] = t('Book navigation');
129
    return $block;
Dries's avatar
 
Dries committed
130
  }
131
  else if ($op == 'view') {
Dries's avatar
 
Dries committed
132 133
    // Only display this block when the user is browsing a book:
    if (arg(0) == 'node' && is_numeric(arg(1))) {
Steven Wittens's avatar
Steven Wittens committed
134
      $result = db_query(node_rewrite_sql('SELECT n.nid, n.title, b.parent FROM {node} n INNER JOIN {book} b ON n.nid = b.nid WHERE n.nid = %d'), arg(1));
Dries's avatar
 
Dries committed
135 136 137 138 139 140 141 142 143 144 145 146 147 148 149
      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
150

151 152
    return $block;
  }
Dries's avatar
 
Dries committed
153 154
}

155 156 157
/**
 * Implementation of hook_load().
 */
Dries's avatar
 
Dries committed
158
function book_load($node) {
Dries's avatar
 
Dries committed
159
  global $user;
Dries's avatar
 
Dries committed
160

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

Dries's avatar
 
Dries committed
163
  if (arg(2) == 'edit' && !user_access('administer nodes')) {
Dries's avatar
 
Dries committed
164 165
    // If a user is about to update a book page, we overload some
    // fields to reflect the changes.
Dries's avatar
 
Dries committed
166 167 168 169 170 171
    if ($user->uid) {
      $book->uid = $user->uid;
      $book->name = $user->name;
    }
    else {
      $book->uid = 0;
172
      $book->name = '';
Dries's avatar
 
Dries committed
173
    }
Dries's avatar
 
Dries committed
174
  }
Dries's avatar
 
Dries committed
175

Dries's avatar
 
Dries committed
176
  return $book;
Dries's avatar
 
Dries committed
177 178
}

179 180 181
/**
 * Implementation of hook_insert().
 */
Dries's avatar
 
Dries committed
182
function book_insert($node) {
183
  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
184
}
Dries's avatar
 
Dries committed
185

186 187 188
/**
 * Implementation of hook_update().
 */
Dries's avatar
 
Dries committed
189
function book_update($node) {
190
  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
191
}
Dries's avatar
 
Dries committed
192

193 194 195
/**
 * Implementation of hook_delete().
 */
196
function book_delete(&$node) {
197
  db_query('DELETE FROM {book} WHERE nid = %d', $node->nid);
Dries's avatar
 
Dries committed
198 199
}

200 201 202
/**
 * Implementation of hook_validate().
 */
203
function book_validate(&$node) {
204 205
  // Set default values for non-administrators.
  if (!user_access('administer nodes')) {
206 207 208
    $node->weight = 0;
    $node->revision = 1;
  }
Dries's avatar
 
Dries committed
209 210
}

211 212 213
/**
 * Implementation of hook_form().
 */
Dries's avatar
 
Dries committed
214
function book_form(&$node) {
Dries's avatar
 
Dries committed
215
  global $user;
Dries's avatar
 
Dries committed
216

217
  $op = $_POST['op'];
Dries's avatar
 
Dries committed
218

Dries's avatar
 
Dries committed
219
  $output = form_select(t('Parent'), 'parent', ($node->parent ? $node->parent : arg(4)), book_toc($node->nid), t('The parent that this page belongs in. Note that pages whose parent is &lt;top-level&gt; are regarded as independent, top-level books.'));
Dries's avatar
 
Dries committed
220

221 222
  if (function_exists('taxonomy_node_form')) {
    $output .= implode('', taxonomy_node_form('book', $node));
223 224
  }

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

229
  if (user_access('administer nodes')) {
Dries's avatar
 
Dries committed
230
    $output .= form_weight(t('Weight'), 'weight', $node->weight, 15, t('Pages at a given level are ordered first by weight and then by title.'));
Dries's avatar
 
Dries committed
231 232
  }
  else {
Dries's avatar
 
Dries committed
233 234
    // If a regular user updates a book page, we create a new revision
    // authored by that user:
235
    $output .= form_hidden('revision', 1);
Dries's avatar
 
Dries committed
236 237 238 239 240
  }

  return $output;
}

241
/**
Dries's avatar
 
Dries committed
242 243
 * Implementation of function book_outline()
 * Handles all book outline operations.
244
 */
Dries's avatar
 
Dries committed
245
function book_outline() {
Dries's avatar
 
Dries committed
246

247 248
  $op = $_POST['op'];
  $edit = $_POST['edit'];
Dries's avatar
 
Dries committed
249
  $node = node_load(array('nid' => arg(1)));
Dries's avatar
 
Dries committed
250

Dries's avatar
 
Dries committed
251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274
  if ($node->nid) {
    switch ($op) {
      case t('Add to book outline'):
        db_query('INSERT INTO {book} (nid, parent, weight) VALUES (%d, %d, %d)', $node->nid, $edit['parent'], $edit['weight']);
        drupal_set_message(t('Added the post to the book.'));
        drupal_goto("node/$node->nid");
        break;

      case t('Update book outline'):
        db_query('UPDATE {book} SET parent = %d, weight = %d WHERE nid = %d', $edit['parent'], $edit['weight'], $node->nid);
        drupal_set_message(t('Updated the book outline.'));
        drupal_goto("node/$node->nid");
        break;

      case t('Remove from book outline'):
        db_query('DELETE FROM {book} WHERE nid = %d', $node->nid);
        drupal_set_message(t('Removed the post from the book.'));
        drupal_goto("node/$node->nid");
        break;

      default:
        $page = db_fetch_object(db_query('SELECT * FROM {book} WHERE nid = %d', $node->nid));

        $output  = form_select(t('Parent'), 'parent', $page->parent, book_toc($node->nid), t('The parent page in the book.'));
Dries's avatar
 
Dries committed
275
        $output .= form_weight(t('Weight'), 'weight', $node->weight, 15, t('Pages at a given level are ordered first by weight and then by title.'));
Dries's avatar
 
Dries committed
276 277 278 279 280 281 282 283

        if ($page->nid) {
          $output .= form_submit(t('Update book outline'));
          $output .= form_submit(t('Remove from book outline'));
        }
        else {
          $output .= form_submit(t('Add to book outline'));
        }
Dries's avatar
 
Dries committed
284

285 286
        drupal_set_title($node->title);
        print theme('page', form($output));
Dries's avatar
 
Dries committed
287 288 289 290
    }
  }
}

Dries's avatar
 
Dries committed
291

292 293 294
/**
 * Return the the most recent revision that matches the specified conditions.
 */
Dries's avatar
 
Dries committed
295 296 297 298 299 300
function book_revision_load($page, $conditions = array()) {

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

  foreach ($revisions as $revision) {

Dries's avatar
 
Dries committed
301
    // Extract the specified revision:
Dries's avatar
 
Dries committed
302 303
    $node = node_revision_load($page, $revision);

Dries's avatar
 
Dries committed
304 305
    // Check to see if the conditions are met:
    $status = TRUE;
Dries's avatar
 
Dries committed
306 307 308

    foreach ($conditions as $key => $value) {
      if ($node->$key != $value) {
Dries's avatar
 
Dries committed
309
        $status = FALSE;
Dries's avatar
 
Dries committed
310 311 312 313 314 315 316 317 318
      }
    }

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

319 320 321
/**
 * Return the path (call stack) to a certain book page.
 */
Dries's avatar
 
Dries committed
322
function book_location($node, $nodes = array()) {
Steven Wittens's avatar
Steven Wittens committed
323
  $parent = db_fetch_object(db_query(node_rewrite_sql('SELECT n.nid, n.title, b.parent, b.weight FROM {node} n INNER JOIN {book} b ON n.nid = b.nid WHERE n.nid = %d'), $node->parent));
Dries's avatar
 
Dries committed
324 325 326 327 328 329 330
  if ($parent->title) {
    $nodes = book_location($parent, $nodes);
    array_push($nodes, $parent);
  }
  return $nodes;
}

Dries's avatar
 
Dries committed
331
function book_location_down($node, $nodes = array()) {
Dries's avatar
 
Dries committed
332
  $last_direct_child = db_fetch_object(db_query(node_rewrite_sql('SELECT n.nid, n.title, b.parent, b.weight FROM {node} n INNER JOIN {book} b ON n.nid = b.nid WHERE b.parent = %d ORDER BY b.weight DESC, n.title DESC'), $node->nid));
Dries's avatar
 
Dries committed
333 334 335 336 337 338 339
  if ($last_direct_child) {
    array_push($nodes, $last_direct_child);
    $nodes = book_location_down($last_direct_child, $nodes);
  }
  return $nodes;
}

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

Dries's avatar
 
Dries committed
349
  // Previous on the same level:
Dries's avatar
 
Dries committed
350
  $direct_above = db_fetch_object(db_query(node_rewrite_sql("SELECT n.nid, n.title FROM {node} n INNER JOIN {book} b ON n.nid = b.nid WHERE b.parent = %d AND n.status = 1 AND n.moderate = 0 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
351
  if ($direct_above) {
Dries's avatar
 
Dries committed
352
    // Get last leaf of $above.
Dries's avatar
 
Dries committed
353
    $path = book_location_down($direct_above);
Dries's avatar
 
Dries committed
354 355

    return $path ? (count($path) > 0 ? array_pop($path) : NULL) : $direct_above;
Dries's avatar
 
Dries committed
356 357
  }
  else {
Dries's avatar
 
Dries committed
358
    // Direct parent:
Steven Wittens's avatar
Steven Wittens committed
359
    $prev = db_fetch_object(db_query(node_rewrite_sql('SELECT n.nid, n.title FROM {node} n INNER JOIN {book} b ON n.nid = b.nid WHERE n.nid = %d AND n.status = 1 AND n.moderate = 0'), $node->parent));
Dries's avatar
 
Dries committed
360 361 362 363
    return $prev;
  }
}

364 365 366
/**
 * Fetch the node object of the next page of the book.
 */
Dries's avatar
 
Dries committed
367 368
function book_next($node) {
  // get first direct child
Steven Wittens's avatar
Steven Wittens committed
369
  $child = db_fetch_object(db_query(node_rewrite_sql('SELECT n.nid, n.title, b.weight FROM {node} n INNER JOIN {book} b ON n.nid = b.nid WHERE b.parent = %d AND n.status = 1 AND n.moderate = 0 ORDER BY b.weight ASC, n.title ASC'), $node->nid));
Dries's avatar
 
Dries committed
370 371 372 373
  if ($child) {
    return $child;
  }

Dries's avatar
 
Dries committed
374 375
  // 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.
376

Dries's avatar
 
Dries committed
377
  while (($leaf = array_pop($path)) && count($path)) {
Dries's avatar
 
Dries committed
378
    $next = db_fetch_object(db_query(node_rewrite_sql("SELECT n.nid, n.title, b.weight FROM {node} n INNER JOIN {book} b ON n.nid = b.nid WHERE b.parent = %d AND n.status = 1 AND n.moderate = 0 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
379 380 381 382 383 384
    if ($next) {
      return $next;
    }
  }
}

Dries's avatar
 
Dries committed
385
function book_content($node, $teaser = FALSE) {
386
  $op = $_POST['op'];
Dries's avatar
 
Dries committed
387

Dries's avatar
 
Dries committed
388 389 390
  // 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.
391 392
  if ($op != t('Preview') && $node->moderate && arg(0) != 'queue') {
    $revision = book_revision_load($node, array('moderate' => 0, 'status' => 1));
Dries's avatar
 
Dries committed
393 394 395 396

    if ($revision) {
      $node = $revision;
    }
Dries's avatar
 
Dries committed
397 398
  }

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

Dries's avatar
 
Dries committed
402 403 404
  return $node;
}

405 406 407 408 409 410
/**
 * 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
411
function book_view(&$node, $teaser = FALSE, $page = FALSE) {
Dries's avatar
 
Dries committed
412
  $node = book_content($node, $teaser);
Dries's avatar
 
Dries committed
413

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

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

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

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

Dries's avatar
 
Dries committed
451
  // Construct the breadcrumb:
Dries's avatar
 
Dries committed
452

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

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

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

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

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

Dries's avatar
 
Dries committed
497
  $node->body = $node->body.$output;
Dries's avatar
 
Dries committed
498

Dries's avatar
 
Dries committed
499
  return $node;
Dries's avatar
 
Dries committed
500
}
Dries's avatar
 
Dries committed
501

502
function book_toc_recurse($nid, $indent, $toc, $children, $exclude) {
Dries's avatar
 
Dries committed
503 504
  if ($children[$nid]) {
    foreach ($children[$nid] as $foo => $node) {
505 506 507 508
      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
509 510 511 512 513 514
    }
  }

  return $toc;
}

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

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

525 526
  $toc = array();

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

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

Dries's avatar
 
Dries committed
535 536 537
  return $toc;
}

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

  return $output;
}

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

Dries's avatar
 
Dries committed
571 572 573 574
  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
575
  }
Dries's avatar
 
Dries committed
576

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

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

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

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

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

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

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

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

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

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

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

638
  print $html;
Dries's avatar
 
Dries committed
639 640
}

641
function book_print_recurse($parent = '', $depth = 1) {
Steven Wittens's avatar
Steven Wittens committed
642
  $result = db_query(node_rewrite_sql('SELECT n.nid, n.title, b.weight FROM {node} n INNER JOIN {book} b ON n.nid = b.nid WHERE n.status = 1 AND b.parent = %d AND n.moderate = 0 ORDER BY b.weight, n.title', $parent));
Dries's avatar
 
Dries committed
643

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

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

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

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

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

Dries's avatar
 
Dries committed
668 669
  return $output;
}
Dries's avatar
 
Dries committed
670

Dries's avatar
 
Dries committed
671
function book_admin_view_line($node, $depth = 0) {
Dries's avatar
 
Dries committed
672
  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
673 674 675
}

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

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

Dries's avatar
 
Dries committed
684
  return $rows;
Dries's avatar
 
Dries committed
685 686
}

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

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

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

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

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

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

Dries's avatar
 
Dries committed
711
    foreach ($edit as $nid => $value) {
Dries's avatar
 
Dries committed
712 713 714 715
      // 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
716
      }
Dries's avatar
 
Dries committed
717

Dries's avatar
 
Dries committed
718 719 720 721
      // 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
722
      }
Dries's avatar
 
Dries committed
723 724
    }

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

Dries's avatar
 
Dries committed
728 729
    return $message;
  }
Dries's avatar
 
Dries committed
730 731
}

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

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

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

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

757
/**
Dries's avatar
Dries committed
758
 * Menu callback; displays the book administration page.
759
 */
Dries's avatar
Dries committed
760
function book_admin($nid = 0) {
Dries's avatar
 
Dries committed
761 762
  $op = $_POST['op'];
  $edit = $_POST['edit'];
Dries's avatar
 
Dries committed
763

Dries's avatar
 
Dries committed
764
  switch ($op) {
Dries's avatar
 
Dries committed
765
    case t('Save book pages'):
766
      drupal_set_message(book_admin_save($nid, $edit));
Dries's avatar
 
Dries committed
767 768
      // fall through:
    default:
769
      $output .= book_admin_view($nid);
Dries's avatar
 
Dries committed
770
      break;
Dries's avatar
 
Dries committed
771
  }
Dries's avatar
 
Dries committed
772
  print theme('page', $output);
Dries's avatar
 
Dries committed
773 774
}

775 776 777
/**
 * Implementation of hook_help().
 */
Dries's avatar
 
Dries committed
778
function book_help($section) {
Dries's avatar
 
Dries committed
779
  switch ($section) {
Dries's avatar
 
Dries committed
780
    case 'admin/help#book':
Dries's avatar
 
Dries committed
781
      return t("
Dries's avatar
 
Dries committed
782
      <p>The book organises content into a nested hierarchical structure. It is particularly good for manuals, Frequently Asked Questions (FAQs) and the like, allowing you to have chapters, sections, etc.</p>
783
      <p>A book is simply a collection of nodes that have been linked together. These nodes are usually of type <em>book page</em>, but you can insert nodes of any type into a book outline. Every node in the book has a <em>parent</em> node which  \"contains\" it. This is how book.module establishes its hierarchy. At any given level in the hierarchy, a book can contain many nodes. All these sibling nodes are sorted according to the <em>weight</em> that you give them.</p>
784
      <p>Book pages contain a <em>log message</em> field which helps your users understand the motivation behind an edit of a book page. Each edited version of a book page is stored as a new revision of a node. This capability makes it easy to revert to an old version of a page, should that be desirable.</p>
Dries's avatar
 
Dries committed