book.module 31.5 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 43 44 45 46 47
    if ((user_access('maintain books') && !$node->moderate) || ($node->uid == $user->uid && user_access('edit own book pages'))) {
      return TRUE;
    }
    else {
       // do nothing. node-access() will determine further access
    }
Dries's avatar
 
Dries committed
48
  }
Dries's avatar
 
Dries committed
49 50
}

Dries's avatar
 
Dries committed
51 52 53
/**
 * Implementation of hook_link().
 */
Dries's avatar
 
Dries committed
54
function book_link($type, $node = 0, $main = 0) {
Dries's avatar
 
Dries committed
55 56 57

  $links = array();

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

Dries's avatar
 
Dries committed
67
  return $links;
Dries's avatar
 
Dries committed
68 69
}

Dries's avatar
 
Dries committed
70 71 72
/**
 * Implementation of hook_menu().
 */
Dries's avatar
 
Dries committed
73
function book_menu($may_cache) {
Dries's avatar
 
Dries committed
74 75
  $items = array();

Dries's avatar
 
Dries committed
76
  if ($may_cache) {
Dries's avatar
 
Dries committed
77 78
    $items[] = array('path' => 'book', 'title' => t('books'),
      'access' => user_access('access content'), 'type' => MENU_NORMAL_ITEM, 'weight' => 5);
Dries's avatar
 
Dries committed
79
    $items[] = array('path' => 'node/add/book', 'title' => t('book page'),
80
      'access' => user_access('create book pages'));
Dries's avatar
 
Dries committed
81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96
    $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
97
  }
Dries's avatar
 
Dries committed
98 99 100 101 102
  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:
103
      $result = db_query(db_rewrite_sql("SELECT n.nid FROM {node} n WHERE n.nid = %d AND n.type != 'book'"), arg(1));
Dries's avatar
 
Dries committed
104 105 106 107 108 109
      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
110 111 112 113

    // 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') {
114
      $result = db_query(db_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
115 116 117 118
      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
119
  }
Dries's avatar
 
Dries committed
120 121 122 123

  return $items;
}

124 125 126
/**
 * Implementation of hook_block().
 *
Dries's avatar
 
Dries committed
127 128
 * Displays the book table of contents in a block when the current page is a
 * single-node view of a book node.
129
 */
Dries's avatar
 
Dries committed
130
function book_block($op = 'list', $delta = 0) {
Dries's avatar
 
Dries committed
131
  $block = array();
Dries's avatar
 
Dries committed
132 133
  if ($op == 'list') {
    $block[0]['info'] = t('Book navigation');
134
    return $block;
Dries's avatar
 
Dries committed
135
  }
136
  else if ($op == 'view') {
Dries's avatar
 
Dries committed
137 138
    // Only display this block when the user is browsing a book:
    if (arg(0) == 'node' && is_numeric(arg(1))) {
139
      $result = db_query(db_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
140 141 142 143 144 145 146 147 148 149 150 151 152 153 154
      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
155

156 157
    return $block;
  }
Dries's avatar
 
Dries committed
158 159
}

160 161 162
/**
 * Implementation of hook_load().
 */
Dries's avatar
 
Dries committed
163
function book_load($node) {
Dries's avatar
 
Dries committed
164
  global $user;
Dries's avatar
 
Dries committed
165

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

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

Dries's avatar
 
Dries committed
181
  return $book;
Dries's avatar
 
Dries committed
182 183
}

184 185 186
/**
 * Implementation of hook_insert().
 */
Dries's avatar
 
Dries committed
187
function book_insert($node) {
188
  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
189
}
Dries's avatar
 
Dries committed
190

191 192 193
/**
 * Implementation of hook_update().
 */
Dries's avatar
 
Dries committed
194
function book_update($node) {
195
  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
196
}
Dries's avatar
 
Dries committed
197

198 199 200
/**
 * Implementation of hook_delete().
 */
201
function book_delete(&$node) {
202
  db_query('DELETE FROM {book} WHERE nid = %d', $node->nid);
Dries's avatar
 
Dries committed
203 204
}

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

216 217 218
/**
 * Implementation of hook_form().
 */
Dries's avatar
 
Dries committed
219
function book_form(&$node) {
Dries's avatar
 
Dries committed
220
  global $user;
Dries's avatar
 
Dries committed
221

222
  $op = $_POST['op'];
Dries's avatar
 
Dries committed
223

Dries's avatar
 
Dries committed
224
  $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
225

226 227
  if (function_exists('taxonomy_node_form')) {
    $output .= implode('', taxonomy_node_form('book', $node));
228 229
  }

230
  $output .= form_textarea(t('Body'), 'body', $node->body, 60, 20, '', NULL, TRUE);
231
  $output .= filter_form('format', $node->format);
Dries's avatar
 
Dries committed
232
  $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
233

234
  if (user_access('administer nodes')) {
Dries's avatar
 
Dries committed
235
    $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
236 237
  }
  else {
Dries's avatar
 
Dries committed
238 239
    // If a regular user updates a book page, we create a new revision
    // authored by that user:
240
    $output .= form_hidden('revision', 1);
Dries's avatar
 
Dries committed
241 242 243 244 245
  }

  return $output;
}

246
/**
Dries's avatar
 
Dries committed
247 248
 * Implementation of function book_outline()
 * Handles all book outline operations.
249
 */
Dries's avatar
 
Dries committed
250
function book_outline() {
Dries's avatar
 
Dries committed
251

252 253
  $op = $_POST['op'];
  $edit = $_POST['edit'];
Dries's avatar
 
Dries committed
254
  $node = node_load(array('nid' => arg(1)));
Dries's avatar
 
Dries committed
255

Dries's avatar
 
Dries committed
256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279
  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
280
        $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
281 282 283 284 285 286 287 288

        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
289

290 291
        drupal_set_title($node->title);
        print theme('page', form($output));
Dries's avatar
 
Dries committed
292 293 294 295
    }
  }
}

Dries's avatar
 
Dries committed
296

297 298 299
/**
 * Return the the most recent revision that matches the specified conditions.
 */
Dries's avatar
 
Dries committed
300 301 302 303 304 305
function book_revision_load($page, $conditions = array()) {

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

  foreach ($revisions as $revision) {

Dries's avatar
 
Dries committed
306
    // Extract the specified revision:
Dries's avatar
 
Dries committed
307 308
    $node = node_revision_load($page, $revision);

Dries's avatar
 
Dries committed
309 310
    // Check to see if the conditions are met:
    $status = TRUE;
Dries's avatar
 
Dries committed
311 312 313

    foreach ($conditions as $key => $value) {
      if ($node->$key != $value) {
Dries's avatar
 
Dries committed
314
        $status = FALSE;
Dries's avatar
 
Dries committed
315 316 317 318 319 320 321 322 323
      }
    }

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

324 325 326
/**
 * Return the path (call stack) to a certain book page.
 */
Dries's avatar
 
Dries committed
327
function book_location($node, $nodes = array()) {
328
  $parent = db_fetch_object(db_query(db_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
329 330 331 332 333 334 335
  if ($parent->title) {
    $nodes = book_location($parent, $nodes);
    array_push($nodes, $parent);
  }
  return $nodes;
}

Dries's avatar
 
Dries committed
336
function book_location_down($node, $nodes = array()) {
337
  $last_direct_child = db_fetch_object(db_query(db_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
338 339 340 341 342 343 344
  if ($last_direct_child) {
    array_push($nodes, $last_direct_child);
    $nodes = book_location_down($last_direct_child, $nodes);
  }
  return $nodes;
}

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

Dries's avatar
 
Dries committed
354
  // Previous on the same level:
355
  $direct_above = db_fetch_object(db_query(db_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
356
  if ($direct_above) {
Dries's avatar
 
Dries committed
357
    // Get last leaf of $above.
Dries's avatar
 
Dries committed
358
    $path = book_location_down($direct_above);
Dries's avatar
 
Dries committed
359 360

    return $path ? (count($path) > 0 ? array_pop($path) : NULL) : $direct_above;
Dries's avatar
 
Dries committed
361 362
  }
  else {
Dries's avatar
 
Dries committed
363
    // Direct parent:
364
    $prev = db_fetch_object(db_query(db_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
365 366 367 368
    return $prev;
  }
}

369 370 371
/**
 * Fetch the node object of the next page of the book.
 */
Dries's avatar
 
Dries committed
372 373
function book_next($node) {
  // get first direct child
374
  $child = db_fetch_object(db_query(db_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
375 376 377 378
  if ($child) {
    return $child;
  }

Dries's avatar
 
Dries committed
379 380
  // 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.
381

Dries's avatar
 
Dries committed
382
  while (($leaf = array_pop($path)) && count($path)) {
383
    $next = db_fetch_object(db_query(db_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
384 385 386 387 388 389
    if ($next) {
      return $next;
    }
  }
}

Dries's avatar
 
Dries committed
390
function book_content($node, $teaser = FALSE) {
391
  $op = $_POST['op'];
Dries's avatar
 
Dries committed
392

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

    if ($revision) {
      $node = $revision;
    }
Dries's avatar
 
Dries committed
402 403
  }

404 405
  // Extract the page body.
  $node = node_prepare($node, $teaser);
Dries's avatar
 
Dries committed
406

Dries's avatar
 
Dries committed
407 408 409
  return $node;
}

410 411 412 413 414 415
/**
 * 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
416
function book_view(&$node, $teaser = FALSE, $page = FALSE) {
Dries's avatar
 
Dries committed
417
  $node = book_content($node, $teaser);
Dries's avatar
 
Dries committed
418

Dries's avatar
 
Dries committed
419 420
  if (!$teaser && $node->moderate) {
    $node->body .= '<div class="log"><div class="title">'. t('Log') .':</div>'. $node->log .'</div>';
Dries's avatar
 
Dries committed
421 422 423
  }
}

424
/**
Dries's avatar
 
Dries committed
425 426 427
 * Implementation of hook_nodeapi().
 *
 * Appends book navigation to all nodes in the book.
428
 */
Dries's avatar
 
Dries committed
429 430 431 432
function book_nodeapi(&$node, $op, $teaser, $page) {
  switch ($op) {
    case 'view':
      if (!$teaser) {
Dries's avatar
 
Dries committed
433
        $book = db_fetch_array(db_query('SELECT * FROM {book} WHERE nid = %d', $node->nid));
Dries's avatar
 
Dries committed
434
        if ($book) {
435 436 437 438
          if ($node->moderate && user_access('administer nodes')) {
            drupal_set_message(t("This update/post awaits moderation and won't be accessible until approved."));
          }

Dries's avatar
 
Dries committed
439 440 441
          foreach ($book as $key => $value) {
            $node->$key = $value;
          }
442
          $node = theme('book_navigation', $node);
443 444 445
          if ($page) {
            menu_set_location($node->breadcrumb);
          }
Dries's avatar
 
Dries committed
446
        }
Dries's avatar
 
Dries committed
447
      }
Dries's avatar
 
Dries committed
448
      break;
Dries's avatar
 
Dries committed
449
  }
Dries's avatar
 
Dries committed
450
}
Dries's avatar
 
Dries committed
451

452 453 454
/**
 * Prepares both the custom breadcrumb trail and the forward/backward
 * navigation for a node presented as a book page.
455 456
 *
 * @ingroup themeable
457
 */
458
function theme_book_navigation($node) {
Dries's avatar
 
Dries committed
459
  $path = book_location($node);
Dries's avatar
 
Dries committed
460

Dries's avatar
 
Dries committed
461
  // Construct the breadcrumb:
Dries's avatar
 
Dries committed
462

Dries's avatar
 
Dries committed
463
  $node->breadcrumb = array(); // Overwrite the trail with a book trail.
Dries's avatar
 
Dries committed
464
  foreach ($path as $level) {
Dries's avatar
 
Dries committed
465
    $node->breadcrumb[] = array('path' => 'node/'. $level->nid, 'title' =>  $level->title);
Dries's avatar
 
Dries committed
466
  }
Dries's avatar
 
Dries committed
467
  $node->breadcrumb[] = array('path' => 'node/'. $node->nid);
Dries's avatar
 
Dries committed
468

Dries's avatar
 
Dries committed
469
  if ($node->nid) {
470
    $output .= '<div class="book">';
Dries's avatar
 
Dries committed
471 472

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

Dries's avatar
 
Dries committed
476
    if ($prev = book_prev($node)) {
477
      $links .= '<div class="prev">';
Dries's avatar
 
Dries committed
478
      $links .= l(t('previous'), 'node/'. $prev->nid, array('title' => t('View the previous page.')));
479
      $links .= '</div>';
Dries's avatar
 
Dries committed
480
      $titles .= '<div class="prev">'. $prev->title .'</div>';
Dries's avatar
 
Dries committed
481 482
    }
    else {
Dries's avatar
 
Dries committed
483
      $links .= '<div class="prev">&nbsp;</div>'; // Make an empty div to fill the space.
Dries's avatar
 
Dries committed
484 485
    }
    if ($next = book_next($node)) {
486
      $links .= '<div class="next">';
Dries's avatar
 
Dries committed
487
      $links .= l(t('next'), 'node/'. $next->nid, array('title' => t('View the next page.')));
488
      $links .= '</div>';
Dries's avatar
 
Dries committed
489
      $titles .= '<div class="next">'. $next->title .'</div>';
Dries's avatar
 
Dries committed
490 491
    }
    else {
Dries's avatar
 
Dries committed
492
      $links .= '<div class="next">&nbsp;</div>'; // Make an empty div to fill the space.
Dries's avatar
 
Dries committed
493 494
    }
    if ($node->parent) {
495
      $links .= '<div class="up">';
Dries's avatar
 
Dries committed
496
      $links .= l(t('up'), 'node/'. $node->parent, array('title' => t('View this page\'s parent section.')));
497
      $links .= '</div>';
Dries's avatar
 
Dries committed
498
    }
Dries's avatar
 
Dries committed
499

500
    $output .= '<div class="nav">';
Dries's avatar
 
Dries committed
501 502
    $output .= ' <div class="links">'. $links .'</div>';
    $output .= ' <div class="titles">'. $titles .'</div>';
503 504
    $output .= '</div>';
    $output .= '</div>';
Dries's avatar
 
Dries committed
505
  }
Dries's avatar
 
Dries committed
506

Dries's avatar
 
Dries committed
507
  $node->body = $node->body.$output;
Dries's avatar
 
Dries committed
508

Dries's avatar
 
Dries committed
509
  return $node;
Dries's avatar
 
Dries committed
510
}
Dries's avatar
 
Dries committed
511

512
function book_toc_recurse($nid, $indent, $toc, $children, $exclude) {
Dries's avatar
 
Dries committed
513 514
  if ($children[$nid]) {
    foreach ($children[$nid] as $foo => $node) {
515 516 517 518
      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
519 520 521 522 523 524
    }
  }

  return $toc;
}

525
function book_toc($exclude = 0) {
526
  $result = db_query(db_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
527

Dries's avatar
 
Dries committed
528
  while ($node = db_fetch_object($result)) {
Dries's avatar
 
Dries committed
529 530 531 532
    if (!$children[$node->parent]) {
      $children[$node->parent] = array();
    }
    array_push($children[$node->parent], $node);
Dries's avatar
 
Dries committed
533
  }
Dries's avatar
 
Dries committed
534

535 536
  $toc = array();

Dries's avatar
 
Dries committed
537 538
  // If the user is an administrator, add the top-level book page;
  // only administrators can start new books.
539 540
  if (user_access('administer nodes')) {
    $toc[0] = '<'. t('top-level') .'>';
Dries's avatar
 
Dries committed
541 542
  }

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

Dries's avatar
 
Dries committed
545 546 547
  return $toc;
}

Dries's avatar
 
Dries committed
548
function book_tree_recurse($nid, $depth, $children, $unfold = array()) {
Dries's avatar
 
Dries committed
549
  if ($depth > 0) {
Dries's avatar
 
Dries committed
550 551
    if ($children[$nid]) {
      foreach ($children[$nid] as $foo => $node) {
Dries's avatar
 
Dries committed
552 553 554
        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
555 556
            $output .= l($node->title, 'node/'. $node->nid);
            $output .= '<ul>'. $tree .'</ul>';
Dries's avatar
 
Dries committed
557 558 559
            $output .= '</li>';
          }
          else {
Dries's avatar
 
Dries committed
560
            $output .= '<li class="leaf">'. l($node->title, 'node/'. $node->nid) .'</li>';
Dries's avatar
 
Dries committed
561 562 563 564
          }
        }
        else {
          if ($tree = book_tree_recurse($node->nid, 1, $children)) {
Dries's avatar
 
Dries committed
565
            $output .= '<li class="collapsed">'. l($node->title, 'node/'. $node->nid) .'</li>';
Dries's avatar
 
Dries committed
566 567
          }
          else {
Dries's avatar
 
Dries committed
568
            $output .= '<li class="leaf">'. l($node->title, 'node/'. $node->nid) .'</li>';
Dries's avatar
 
Dries committed
569
          }
Dries's avatar
 
Dries committed
570
        }
Dries's avatar
 
Dries committed
571 572
      }
    }
Dries's avatar
 
Dries committed
573 574 575 576 577
  }

  return $output;
}

Dries's avatar
 
Dries committed
578
function book_tree($parent = 0, $depth = 3, $unfold = array()) {
579
  $result = db_query(db_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
580

Dries's avatar
 
Dries committed
581 582 583 584
  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
585
  }
Dries's avatar
 
Dries committed
586

Dries's avatar
 
Dries committed
587
  if ($tree = book_tree_recurse($parent, $depth, $children, $unfold)) {
Dries's avatar
 
Dries committed
588
    return '<ul>'. $tree .'</ul>';
Dries's avatar
 
Dries committed
589
  }
Dries's avatar
 
Dries committed
590 591
}

592
/**
Dries's avatar
Dries committed
593
 * Menu callback; prints a listing of all books.
594
 */
Dries's avatar
 
Dries committed
595
function book_render() {
596
  $result = db_query(db_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
597

Dries's avatar
 
Dries committed
598
  while ($page = db_fetch_object($result)) {
Dries's avatar
 
Dries committed
599
    // Load the node:
600
    $node = node_load(array('nid' => $page->nid));
Dries's avatar
 
Dries committed
601

Dries's avatar
 
Dries committed
602
    if ($node) {
Dries's avatar
 
Dries committed
603
      // Take the most recent approved revision, extract the page and check output:
604
      $node = book_content($node, TRUE);
Dries's avatar
 
Dries committed
605
      // Output the content:
606
      $output .= '<div class="book">';
Dries's avatar
 
Dries committed
607
      $output .= '<div class="title">'. l($node->title, 'node/'. $node->nid) .'</div>';
608
      $output .= '<div class="body">'. $node->teaser .'</div>';
609
      $output .= '</div>';
Dries's avatar
 
Dries committed
610
    }
Dries's avatar
 
Dries committed
611 612
  }

613 614
  drupal_set_title(t('Books'));
  print theme('page', $output);
Dries's avatar
 
Dries committed
615 616
}

617
/**
Dries's avatar
 
Dries committed
618
 * Menu callback; generates printer-friendly book page with all descendants.
619 620
 */
function book_print($nid = 0, $depth = 1) {
Dries's avatar
 
Dries committed
621
  global $base_url;
622
  $result = db_query(db_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
623

Dries's avatar
 
Dries committed
624 625
  while ($page = db_fetch_object($result)) {
    // load the node:
626
    $node = node_load(array('nid' => $page->nid));
Dries's avatar
 
Dries committed
627

Dries's avatar
 
Dries committed
628 629
    if ($node) {
      // output the content:
630 631
      if (node_hook($node, 'content')) {
        $node = node_invoke($node, 'content');
Dries's avatar
 
Dries committed
632
      }
633 634 635
      // Allow modules to change $node->body before viewing.
      node_invoke_nodeapi($node, 'view', $node->body, false);

Dries's avatar
 
Dries committed
636
      $output .= '<h1 id="'. $node->nid .'" name="'. $node->nid .'" class="book-h'. $depth .'">'. $node->title .'</h1>';
Dries's avatar
 
Dries committed
637

Dries's avatar
 
Dries committed
638
      if ($node->body) {
Dries's avatar
 
Dries committed
639
        $output .= $node->body;
Dries's avatar
 
Dries committed
640
      }
Dries's avatar
 
Dries committed
641
    }
Dries's avatar
 
Dries committed
642
  }
Dries's avatar
 
Dries committed
643

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

Dries's avatar
 
Dries committed
646 647
  $html = '<html><head><title>'. $node->title .'</title>';
  $html .= '<base href="'. $base_url .'/" />';
Dries's avatar
 
Dries committed
648
  $html .= "<style type=\"text/css\">\n@import url(misc/print.css);\n</style>";
649
  $html .= '</head><body>'. $output .'</body></html>';
Dries's avatar
 
Dries committed
650

651
  print $html;
Dries's avatar
 
Dries committed
652 653
}

654
function book_print_recurse($parent = '', $depth = 1) {
655
  $result = db_query(db_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
656

Dries's avatar
 
Dries committed
657
  while ($page = db_fetch_object($result)) {
Dries's avatar
 
Dries committed
658
    // Load the node:
659
    $node = node_load(array('nid' => $page->nid));
Dries's avatar
 
Dries committed
660

Dries's avatar
 
Dries committed
661
    // Take the most recent approved revision:
Dries's avatar
 
Dries committed
662
    if ($node->moderate) {
663
      $node = book_revision_load($node, array('moderate' => 0, 'status' => 1));
Dries's avatar
 
Dries committed
664 665
    }

Dries's avatar
 
Dries committed
666
    if ($node) {
Dries's avatar
 
Dries committed
667
      // Output the content:
668 669
      if (node_hook($node, 'content')) {
        $node = node_invoke($node, 'content');
Dries's avatar
 
Dries committed
670
      }
671 672 673
      // Allow modules to change $node->body before viewing.
      node_invoke_nodeapi($node, 'view', $node->body, false);

Dries's avatar
 
Dries committed
674
      $output .= '<h1 id="'. $node->nid .'" name="'. $node->nid .'" class="book-h'. $depth .'">'. $node->title .'</h1>';
Dries's avatar
 
Dries committed
675

Dries's avatar
 
Dries committed
676
      if ($node->body) {
677
        $output .= '<ul>'. $node->body .'</ul>';
Dries's avatar
 
Dries committed
678
      }
Dries's avatar
 
Dries committed
679

680
      $output .= book_print_recurse($node->nid, $depth + 1);
Dries's avatar
 
Dries committed
681
    }
Dries's avatar
 
Dries committed
682
  }
Dries's avatar
 
Dries committed
683

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

Dries's avatar
 
Dries committed
687
function book_admin_view_line($node, $depth = 0) {
688
  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, 15), 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
689 690 691
}

function book_admin_view_book($nid, $depth = 1) {
692
  $result = db_query(db_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
693 694

  while ($node = db_fetch_object($result)) {
Dries's avatar
 
Dries committed
695
    $node = node_load(array('nid' => $node->nid));
Dries's avatar
 
Dries committed
696 697
    $rows[] = book_admin_view_line($node, $depth);
    $rows = array_merge($rows, book_admin_view_book($node->nid, $depth + 1));
Dries's avatar
 
Dries committed
698 699
  }

Dries's avatar
 
Dries committed
700
  return $rows;
Dries's avatar
 
Dries committed
701 702
}

703 704 705
/**
 * Display an administrative view of the hierarchy of a book.
 */
Dries's avatar
 
Dries committed
706
function book_admin_view($nid, $depth = 0) {
Dries's avatar
 
Dries committed
707
  if ($nid) {
Dries's avatar
 
Dries committed
708
    $node = node_load(array('nid' => $nid));
Dries's avatar
 
Dries committed
709

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

Dries's avatar
 
Dries committed
712
    $header = array(t('Title'), t('Weight'), array('data' => t('Operations'), 'colspan' => '3'));
Dries's avatar
 
Dries committed
713 714
    $rows[] = book_admin_view_line($node);
    $rows = array_merge($rows, book_admin_view_book($nid));
Dries's avatar
 
Dries committed
715

Dries's avatar
 
Dries committed
716 717
    $output .= theme('table', $header, $rows);
    $output .= form_submit(t('Save book pages'));
Dries's avatar
 
Dries committed
718

Dries's avatar
 
Dries committed
719 720
    return form($output);
  }
Dries's avatar
 
Dries committed
721 722 723
}

function book_admin_save($nid, $edit = array()) {
Dries's avatar
 
Dries committed
724
  if ($nid) {
Dries's avatar
 
Dries committed
725
    $book = node_load(array('nid' => $nid));
Dries's avatar
 
Dries committed
726

Dries's avatar
 
Dries committed
727
    foreach ($edit as $nid => $value) {
Dries's avatar
 
Dries committed
728 729 730 731
      // 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
732
      }
Dries's avatar
 
Dries committed
733

Dries's avatar
 
Dries committed
734 735 736 737
      // 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
738
      }
Dries's avatar
 
Dries committed
739 740
    }

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

Dries's avatar
 
Dries committed
744 745
    return $message;
  }
Dries's avatar
 
Dries committed
746 747
}

748
/**
Dries's avatar
Dries committed
749
 * Menu callback; displays a listing of all orphaned book pages.
750
 */
Dries's avatar
 
Dries committed
751
function book_admin_orphan() {
752
  $result = db_query(db_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
753 754 755 756 757

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

Dries's avatar
 
Dries committed
758
  if ($pages) {
759
    $output .= '<h3>'. t('Orphan pages') .'</h3>';
Dries's avatar
 
Dries committed
760
    $header = array(t('Title'), t('Weight'), array('data' => t('Operations'), 'colspan' => '3'));
Dries's avatar
 
Dries committed
761 762
    foreach ($pages as $nid => $node) {
      if ($node->parent && empty($pages[$node->parent])) {
Dries's avatar
 
Dries committed
763 764
        $rows[] = book_admin_view_line($node, $depth);
        $rows = array_merge($rows, book_admin_view_book($node->nid, $depth + 1));
Dries's avatar
 
Dries committed
765
      }
Dries's avatar
 
Dries committed
766
    }
Dries's avatar
 
Dries committed
767
    $output .= theme('table', $header, $rows);
Dries's avatar
 
Dries committed
768 769
  }

Dries's avatar
 
Dries committed
770
  print theme('page', $output);
Dries's avatar
 
Dries committed
771 772
}

773
/**
Dries's avatar
Dries committed
774
 * Menu callback; displays the book administration page.
775
 */
Dries's avatar
Dries committed
776
function book_admin($nid = 0) {
Dries's avatar
 
Dries committed
777 778
  $op = $_POST['op'];
  $edit = $_POST['edit'];
Dries's avatar
 
Dries committed
779

Dries's avatar
 
Dries committed
780
  switch ($op) {
Dries's avatar
 
Dries committed
781
    case t('Save book pages'):
782
      drupal_set_message(book_admin_save($nid, $edit));
Dries's avatar
 
Dries committed
783 784
      // fall through:
    default:
785
      $output .= book_admin_view($nid);
Dries's avatar
 
Dries committed
786
      break;
Dries's avatar
 
Dries committed
787
  }
Dries's avatar
 
Dries committed
788
  print theme('page', $output);
Dries's avatar
 
Dries committed
789 790
}

791 792 793
/**
 * Implementation of hook_help().
 */
Dries's avatar
 
Dries committed
794
function book_help($section) {
Dries's avatar
 
Dries committed
795
  switch ($section) {
Dries's avatar
 
Dries committed
796
    case 'admin/help#book':
Dries's avatar
 
Dries committed
797
      return t("
Dries's avatar
 
Dries committed
798
      <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>
799
      <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>
800
      <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
801
      <p>Like other node types, book submissions and edits may be subject to moderation, depending on your configuration.  Similarly, books use <a href=\"%permissions\">permissions</a> to determine who may read and write to them. Only administrators are allowed to create new books, which are really just nodes whose parent is <em>&lt;top-level&gt;</em>.  To include an existing node in your book, click on the \"outline\"-tab on the node's page.  This enables you to place the node wherever you'd like within the book hierarchy. To add a new node into your book, use the <a href=\"%create\">create content &raquo; book page</a> link.</p>
802
      <p>Administrators may review the hierarchy of their books by clicking on the <a href=\"%collaborative-book\">collaborative book</a> link in the administration pages. There, nodes may be edited, reorganized, removed from book, and deleted. This behavior may change in the future. When a parent node is deleted, it may leave behind child nodes.  These nodes are now <em>orphans</em>. Administrators should periodically <a href=\"%orphans-book\">review their books for orphans</a> and reaffiliate those pages as desired. Finally, administrators may also <a href=\"%export-book\">export their books</a> to a single, flat HTML page which is suitable for printing.</p>
Dries's avatar
 
Dries committed
803 804
      <h3>Maintaining a FAQ using a collaborative book</h3>
      <p>Collaborative books let you easily set up a Frequently Asked Questions (FAQ) section on your web site. The main benefit is that you don't have to write all the questions/answers by yourself - let the community do it for you!</p>
805
      <p>In order to set up the FAQ, you have to create a new book which will hold all your content. To do so, click on the <a href=\"%create\">create content &raquo; book page</a> link. Give it a thoughtful title, and body. A title like \"Estonia Travel - FAQ\" is nice. You may always edit these fields later. You will probably want to designate <em>&lt;top-level&gt;</em> as the parent of this page. Leave the <em>log message</em> and <em>type</em> fields blank for now. After you have submitted this book page, you are ready to begin filling up your book with questions that are frequently asked.</p>
806
      <p>Whenever you come across a post which you want to include in your FAQ, click on the <em>administer</em> link. Then click on the <em>edit book outline</em> button at the bottom of the page. Then place the relevant post wherever is most appropriate in your book by selecting a <em>parent</em>. Books are quite flexible. They can have sections like <em>Flying to Estonia</em>, <em>Eating in Estonia</em> and so on. As you get more experienced with the book module, you can reorganize posts in your book so that it stays organized.</p>
807
      <p>Notes:</p><ul><li>Any comments attached to those relevant posts which you designate as book pages will also be transported into your book. This is a great feature, since much wisdom is shared via comments. Remember that all future comments and edits will automatically be reflected in your book.</li><li>You may wish to edit the title of posts when adding them to your FAQ. This is done on the same page as the <em>Edit book outline</em> button. Clear titles improve navigability enormously.</li><li>Book pages may come from any content type (blog, story, page, etc.). If you are creating a post solely for inclusion in your book, then use the <a href=\"%create\">create content &raquo; book page</a> link.</li><li>If you don't see the <em>administer</em> link, then you probably have insufficient <a href=\"%permissions\">permissions</a>.</li></ul>", array('%permissions' => url('admin/access/permissions'), "%create" => url('node/add/book'), '%collaborative-book' => url('admin/node/book'), '%orphans-book' => url('admin/node/book/orphan'), '%export-book' => url('book/print')));
Dries's avatar