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 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();

58
  if ($type == 'node' && isset($node->parent)) {
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 110
      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
111 112 113 114

  return $items;
}

115 116 117
/**
 * Implementation of hook_block().
 *
Dries's avatar
 
Dries committed
118 119
 * Displays the book table of contents in a block when the current page is a
 * single-node view of a book node.
120
 */
Dries's avatar
 
Dries committed
121
function book_block($op = 'list', $delta = 0) {
Dries's avatar
 
Dries committed
122
  $block = array();
Dries's avatar
 
Dries committed
123 124
  if ($op == 'list') {
    $block[0]['info'] = t('Book navigation');
125
    return $block;
Dries's avatar
 
Dries committed
126
  }
127
  else if ($op == 'view') {
Dries's avatar
 
Dries committed
128 129
    // Only display this block when the user is browsing a book:
    if (arg(0) == 'node' && is_numeric(arg(1))) {
130
      $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
131 132 133 134 135 136 137 138 139 140 141
      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;
        }

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

147 148
    return $block;
  }
Dries's avatar
 
Dries committed
149 150
}

151 152 153
/**
 * Implementation of hook_load().
 */
Dries's avatar
 
Dries committed
154
function book_load($node) {
Dries's avatar
 
Dries committed
155
  global $user;
Dries's avatar
 
Dries committed
156

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

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

Dries's avatar
 
Dries committed
172
  return $book;
Dries's avatar
 
Dries committed
173 174
}

175 176 177
/**
 * Implementation of hook_insert().
 */
Dries's avatar
 
Dries committed
178
function book_insert($node) {
179
  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
180
}
Dries's avatar
 
Dries committed
181

182 183 184
/**
 * Implementation of hook_update().
 */
Dries's avatar
 
Dries committed
185
function book_update($node) {
186
  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
187
}
Dries's avatar
 
Dries committed
188

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

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

207 208 209
/**
 * Implementation of hook_form().
 */
Dries's avatar
 
Dries committed
210
function book_form(&$node) {
Dries's avatar
 
Dries committed
211
  global $user;
Dries's avatar
 
Dries committed
212

213
  $op = $_POST['op'];
Dries's avatar
 
Dries committed
214

Dries's avatar
 
Dries committed
215
  $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
216

217 218
  if (function_exists('taxonomy_node_form')) {
    $output .= implode('', taxonomy_node_form('book', $node));
219 220
  }

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

225
  if (user_access('administer nodes')) {
Dries's avatar
 
Dries committed
226
    $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
227 228
  }
  else {
Dries's avatar
 
Dries committed
229 230
    // If a regular user updates a book page, we create a new revision
    // authored by that user:
231
    $output .= form_hidden('revision', 1);
Dries's avatar
 
Dries committed
232 233 234 235 236
  }

  return $output;
}

237
/**
Dries's avatar
 
Dries committed
238 239
 * Implementation of function book_outline()
 * Handles all book outline operations.
240
 */
Dries's avatar
 
Dries committed
241
function book_outline() {
Dries's avatar
 
Dries committed
242

243 244
  $op = $_POST['op'];
  $edit = $_POST['edit'];
Dries's avatar
 
Dries committed
245
  $node = node_load(array('nid' => arg(1)));
Dries's avatar
 
Dries committed
246

Dries's avatar
 
Dries committed
247 248 249 250
  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']);
251
        drupal_set_message(t('The post has been added to the book.'));
Dries's avatar
 
Dries committed
252 253 254 255 256
        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);
257
        drupal_set_message(t('The book outline has been updated.'));
Dries's avatar
 
Dries committed
258 259 260 261 262
        drupal_goto("node/$node->nid");
        break;

      case t('Remove from book outline'):
        db_query('DELETE FROM {book} WHERE nid = %d', $node->nid);
263
        drupal_set_message(t('The post has been removed from the book.'));
Dries's avatar
 
Dries committed
264 265 266 267 268 269 270
        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
271
        $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
272 273 274 275 276 277 278 279

        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
280

281
        drupal_set_title(check_plain($node->title));
Dries's avatar
 
Dries committed
282
        return form($output);
Dries's avatar
 
Dries committed
283 284 285 286
    }
  }
}

Dries's avatar
 
Dries committed
287

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

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

  foreach ($revisions as $revision) {

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

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

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

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

315 316 317
/**
 * Return the path (call stack) to a certain book page.
 */
Dries's avatar
 
Dries committed
318
function book_location($node, $nodes = array()) {
319
  $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
320 321 322 323 324 325 326
  if ($parent->title) {
    $nodes = book_location($parent, $nodes);
    array_push($nodes, $parent);
  }
  return $nodes;
}

Dries's avatar
 
Dries committed
327
function book_location_down($node, $nodes = array()) {
328
  $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
329 330 331 332 333 334 335
  if ($last_direct_child) {
    array_push($nodes, $last_direct_child);
    $nodes = book_location_down($last_direct_child, $nodes);
  }
  return $nodes;
}

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

Dries's avatar
 
Dries committed
345
  // Previous on the same level:
346
  $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
347
  if ($direct_above) {
Dries's avatar
 
Dries committed
348
    // Get last leaf of $above.
Dries's avatar
 
Dries committed
349
    $path = book_location_down($direct_above);
Dries's avatar
 
Dries committed
350 351

    return $path ? (count($path) > 0 ? array_pop($path) : NULL) : $direct_above;
Dries's avatar
 
Dries committed
352 353
  }
  else {
Dries's avatar
 
Dries committed
354
    // Direct parent:
355
    $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
356 357 358 359
    return $prev;
  }
}

360 361 362
/**
 * Fetch the node object of the next page of the book.
 */
Dries's avatar
 
Dries committed
363 364
function book_next($node) {
  // get first direct child
365
  $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
366 367 368 369
  if ($child) {
    return $child;
  }

Dries's avatar
 
Dries committed
370 371
  // 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.
372

Dries's avatar
 
Dries committed
373
  while (($leaf = array_pop($path)) && count($path)) {
374
    $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
375 376 377 378 379 380
    if ($next) {
      return $next;
    }
  }
}

Dries's avatar
 
Dries committed
381
function book_content($node, $teaser = FALSE) {
382
  $op = $_POST['op'];
Dries's avatar
 
Dries committed
383

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

    if ($revision) {
      $node = $revision;
    }
Dries's avatar
 
Dries committed
393 394
  }

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

Dries's avatar
 
Dries committed
398 399 400
  return $node;
}

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

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

415
/**
Dries's avatar
 
Dries committed
416 417 418
 * Implementation of hook_nodeapi().
 *
 * Appends book navigation to all nodes in the book.
419
 */
Dries's avatar
 
Dries committed
420 421 422 423
function book_nodeapi(&$node, $op, $teaser, $page) {
  switch ($op) {
    case 'view':
      if (!$teaser) {
Dries's avatar
 
Dries committed
424
        $book = db_fetch_array(db_query('SELECT * FROM {book} WHERE nid = %d', $node->nid));
Dries's avatar
 
Dries committed
425
        if ($book) {
426
          if ($node->moderate && user_access('administer nodes')) {
427
            drupal_set_message(t("The post has been submitted for moderation and won't be accessible until it has been approved."));
428 429
          }

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

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

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

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

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

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

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

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

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

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

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

  return $toc;
}

516
function book_toc($exclude = 0) {
517
  $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
518

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

526 527
  $toc = array();

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

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

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

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

  return $output;
}

Dries's avatar
 
Dries committed
569
function book_tree($parent = 0, $depth = 3, $unfold = array()) {
570
  $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
571

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

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

583
/**
Dries's avatar
Dries committed
584
 * Menu callback; prints a listing of all books.
585
 */
Dries's avatar
 
Dries committed
586
function book_render() {
587
  $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 AND n.status = 1 AND n.moderate = 0 ORDER BY b.weight, n.title'));
Dries's avatar
 
Dries committed
588

Dries's avatar
 
Dries committed
589
  return $output;
Dries's avatar
 
Dries committed
590 591
}

592
/**
Dries's avatar
 
Dries committed
593
 * Menu callback; generates printer-friendly book page with all descendants.
594 595
 */
function book_print($nid = 0, $depth = 1) {
Dries's avatar
 
Dries committed
596
  global $base_url;
597
  $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 n.nid = %d AND n.moderate = 0 ORDER BY b.weight, n.title'), $nid);
Dries's avatar
 
Dries committed
598

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

Dries's avatar
 
Dries committed
603 604
    if ($node) {
      // output the content:
605 606
      if (node_hook($node, 'content')) {
        $node = node_invoke($node, 'content');
Dries's avatar
 
Dries committed
607
      }
608 609 610
      // Allow modules to change $node->body before viewing.
      node_invoke_nodeapi($node, 'view', $node->body, false);

611
      $output .= '<h1 id="'. $node->nid .'" name="'. $node->nid .'" class="book-h'. $depth .'">'. check_plain($node->title) .'</h1>';
Dries's avatar
 
Dries committed
612

Dries's avatar
 
Dries committed
613
      if ($node->body) {
Dries's avatar
 
Dries committed
614
        $output .= $node->body;
Dries's avatar
 
Dries committed
615
      }
Dries's avatar
 
Dries committed
616
    }
Dries's avatar
 
Dries committed
617
  }
Dries's avatar
 
Dries committed
618

619
  $output .= book_print_recurse($nid, $depth + 1);
Dries's avatar
 
Dries committed
620

621
  $html = '<html><head><title>'. check_plain($node->title) .'</title>';
Dries's avatar
 
Dries committed
622
  $html .= '<base href="'. $base_url .'/" />';
623
  $html .= theme_stylesheet_import('misc/print.css', 'print');
624
  $html .= '</head><body>'. $output .'</body></html>';
Dries's avatar
 
Dries committed
625

626
  print $html;
Dries's avatar
 
Dries committed
627 628
}

629
function book_print_recurse($parent = '', $depth = 1) {
630
  $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
631

Dries's avatar
 
Dries committed
632
  while ($page = db_fetch_object($result)) {
Dries's avatar
 
Dries committed
633
    // Load the node:
634
    $node = node_load(array('nid' => $page->nid));
Dries's avatar
 
Dries committed
635

Dries's avatar
 
Dries committed
636
    // Take the most recent approved revision:
Dries's avatar
 
Dries committed
637
    if ($node->moderate) {
638
      $node = book_revision_load($node, array('moderate' => 0, 'status' => 1));
Dries's avatar
 
Dries committed
639 640
    }

Dries's avatar
 
Dries committed
641
    if ($node) {
Dries's avatar
 
Dries committed
642
      // Output the content:
643 644
      if (node_hook($node, 'content')) {
        $node = node_invoke($node, 'content');
Dries's avatar
 
Dries committed
645
      }
646 647 648
      // Allow modules to change $node->body before viewing.
      node_invoke_nodeapi($node, 'view', $node->body, false);

649
      $output .= '<h1 id="'. $node->nid .'" name="'. $node->nid .'" class="book-h'. $depth .'">'. check_plain($node->title) .'</h1>';
Dries's avatar
 
Dries committed
650

Dries's avatar
 
Dries committed
651
      if ($node->body) {
652
        $output .= '<ul>'. $node->body .'</ul>';
Dries's avatar
 
Dries committed
653
      }
Dries's avatar
 
Dries committed
654

655
      $output .= book_print_recurse($node->nid, $depth + 1);
Dries's avatar
 
Dries committed
656
    }
Dries's avatar
 
Dries committed
657
  }
Dries's avatar
 
Dries committed
658

Dries's avatar
 
Dries committed
659 660
  return $output;
}
Dries's avatar
 
Dries committed
661

662
function book_admin_edit_line($node, $depth = 0) {
663
  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'), 'node/'.$node->nid.'/delete'));
Dries's avatar
 
Dries committed
664 665
}

666
function book_admin_edit_book($nid, $depth = 1) {
667
  $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
668 669

  while ($node = db_fetch_object($result)) {
Dries's avatar
 
Dries committed
670
    $node = node_load(array('nid' => $node->nid));
671 672
    $rows[] = book_admin_edit_line($node, $depth);
    $rows = array_merge($rows, book_admin_edit_book($node->nid, $depth + 1));
Dries's avatar
 
Dries committed
673 674
  }

Dries's avatar
 
Dries committed
675
  return $rows;
Dries's avatar
 
Dries committed
676 677
}

678 679 680
/**
 * Display an administrative view of the hierarchy of a book.
 */
681 682 683
function book_admin_edit($nid, $depth = 0) {
  $node = node_load(array('nid' => $nid));
  if ($node->nid) {
Dries's avatar
 
Dries committed
684
    $header = array(t('Title'), t('Weight'), array('data' => t('Operations'), 'colspan' => '3'));
685 686
    $rows[] = book_admin_edit_line($node);
    $rows = array_merge($rows, book_admin_edit_book($nid));
Dries's avatar
 
Dries committed
687

Dries's avatar
 
Dries committed
688 689
    $output .= theme('table', $header, $rows);
    $output .= form_submit(t('Save book pages'));
Dries's avatar
 
Dries committed
690

691
    drupal_set_title(check_plain($node->title));
Dries's avatar
 
Dries committed
692 693
    return form($output);
  }
694 695 696
  else {
    drupal_not_found();
  }
Dries's avatar
 
Dries committed
697 698 699
}

function book_admin_save($nid, $edit = array()) {
Dries's avatar
 
Dries committed
700
  if ($nid) {
Dries's avatar
 
Dries committed
701
    $book = node_load(array('nid' => $nid));
Dries's avatar
 
Dries committed
702

Dries's avatar
 
Dries committed
703
    foreach ($edit as $nid => $value) {
Dries's avatar
 
Dries committed
704 705 706 707
      // 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
708
      }
Dries's avatar
 
Dries committed
709

Dries's avatar
 
Dries committed
710 711 712 713
      // 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
714
      }
Dries's avatar
 
Dries committed
715 716
    }

717
    $message = t('The book %title has been updated.', array('%title' => theme('placeholder', $book->title)));
718
    watchdog('content', $message);
Dries's avatar
 
Dries committed
719

Dries's avatar
 
Dries committed
720 721
    return $message;
  }
Dries's avatar
 
Dries committed
722 723
}

724
/**
Dries's avatar
Dries committed
725
 * Menu callback; displays a listing of all orphaned book pages.
726
 */
Dries's avatar
 
Dries committed
727
function book_admin_orphan() {
728
  $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
729 730 731 732 733

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

Dries's avatar
 
Dries committed
734
  if ($pages) {
735
    $output .= '<h3>'. t('Orphan pages') .'</h3>';
Dries's avatar
 
Dries committed
736
    $header = array(t('Title'), t('Weight'), array('data' => t('Operations'), 'colspan' => '3'));
Dries's avatar
 
Dries committed
737 738
    foreach ($pages as $nid => $node) {
      if ($node->parent && empty($pages[$node->parent])) {
739 740
        $rows[] = book_admin_edit_line($node, $depth);
        $rows = array_merge($rows, book_admin_edit_book($node->nid, $depth + 1));
Dries's avatar
 
Dries committed
741
      }
Dries's avatar
 
Dries committed
742
    }
Dries's avatar
 
Dries committed
743
    $output .= theme('table', $header, $rows);
Dries's avatar
 
Dries committed
744 745
  }

Dries's avatar
 
Dries committed
746
  return $output;
Dries's avatar
 
Dries committed
747 748
}

749
/**
Dries's avatar
Dries committed
750
 * Menu callback; displays the book administration page.
751
 */
Dries's avatar
Dries committed
752
function book_admin($nid = 0) {
Dries's avatar
 
Dries committed
753 754
  $op = $_POST['op'];
  $edit = $_POST['edit'];
Dries's avatar
 
Dries committed
755

756 757
  if ($op == t('Save book pages')) {
    drupal_set_message(book_admin_save($nid, $edit));
Dries's avatar
 
Dries committed
758
  }
759 760 761 762 763 764 765 766 767 768 769 770 771 772 773 774 775

  if ($nid) {
    return book_admin_edit($nid);
  }
  else {
    return book_admin_overview();
  }
}

function book_admin_overview() {
  $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'));
  while ($book = db_fetch_object($result)) {
    $rows[] = array(l($book->title, "node/$book->nid"), l(t('outline'), "admin/node/book/$book->nid"));
  }
  $headers = array(t('Book'), t('Operations'));

  return theme('table', $headers, $rows);
Dries's avatar
 
Dries committed
776 777
}

778 779 780
/**
 * Implementation of hook_help().
 */
Dries's avatar
 
Dries committed
781
function book_help($section) {
Dries's avatar
 
Dries committed
782
  switch ($section) {
Dries's avatar
 
Dries committed
783
    case 'admin/help#book':
Dries's avatar
 
Dries committed
784
      return t("
Dries's avatar
 
Dries committed
785
      <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>
786
      <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>
787
      <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
788
      <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>
789
      <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
790 791
      <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>
792
      <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>
793
      <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>
794
      <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
 
Dries committed
795
    case 'admin/modules#description':
Dries's avatar
 
Dries committed
796
      return t('Allows users to collaboratively author a book.');
Dries's avatar
 
Dries committed
797
    case 'admin/node/book':
798
      return t('<p>The book module offers a mean to organize content, authored by many users, in an online manual, outline or FAQ.</p>');
Dries's avatar
 
Dries committed
799
    case 'admin/node/book/orphan':
800
      return t('<p>Pages in a book are like a tree. As pages are edited, reorganized and removed, child pages might be left with no link to the rest of the book.  Such pages are referred to as "orphan pages".  On this page, administrators can review their books for orphans and reattach those pages as desired.</p>');
Dries's avatar
 
Dries committed
801
    case 'node/add#book':
Dries's avatar
 
Dries committed
802
      return t("A book is a collaborative writing effort: users can collaborate writing the pages of the book, positioning the pages in the right order, and reviewing or modifying pages previously written.  So when you have some information to share or when you read a page of the book and you didn't like it, or if you think a certain page could have been written better, you can do something about it.");
Dries's avatar
 
Dries committed
803
  }
Dries's avatar
 
Dries committed
804 805

  if (arg(0) == 'node' && is_numeric(arg(1)) && arg(2) == 'outline') {
Steven Wittens's avatar
Steven Wittens committed
806
    return t('The outline feature allows you to include posts in the <a href="%book">book hierarchy</a>.', array('%book' => url('book')));
Dries's avatar
 
Dries committed