book.module 36.3 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");
      }
63 64
      $links[] = l(t('printer-friendly version'), 'book/export/html/'. $node->nid, array('title' => t('Show a printer-friendly version of this book page and its sub-pages.')));
      $links[] = l(t('export as XML'), 'book/export/docbook/'. $node->nid, array('title' => t('Export this book page and its sub-pages as Docbook-like XML.')));
Dries's avatar
 
Dries committed
65
    }
Dries's avatar
 
Dries committed
66 67
  }

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

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

Dries's avatar
 
Dries committed
77
  if ($may_cache) {
Dries's avatar
 
Dries committed
78 79
    $items[] = array('path' => 'book', 'title' => t('books'),
      'access' => user_access('access content'), 'type' => MENU_NORMAL_ITEM, 'weight' => 5);
Dries's avatar
 
Dries committed
80
    $items[] = array('path' => 'node/add/book', 'title' => t('book page'),
81
      'access' => user_access('create book pages'));
Dries's avatar
 
Dries committed
82 83 84 85 86 87 88 89 90 91 92 93
    $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);
94 95 96 97 98 99 100 101
    $items[] = array(
      'path' => 'book/export/docbook',
      'title' => t('export XML'),
      'callback' => 'book_export_docbook',
      'access' => user_access('access content'),
      'type' => MENU_CALLBACK);
    $items[] = array('path' => 'book/export/printer', 'title' => t('printer-friendly version'),
      'callback' => 'book_export_html',
Dries's avatar
 
Dries committed
102 103
      'access' => user_access('access content'),
      'type' => MENU_CALLBACK);
Dries's avatar
 
Dries committed
104
  }
Dries's avatar
 
Dries committed
105 106 107 108 109
  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:
110
      $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
111 112 113 114 115 116 117
      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
118 119 120 121

  return $items;
}

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

149
        $block['subject'] = check_plain($path[0]->title);
Dries's avatar
 
Dries committed
150 151 152
        $block['content'] = book_tree($expand[0], 5, $expand);
      }
    }
Dries's avatar
 
Dries committed
153

154 155
    return $block;
  }
Dries's avatar
 
Dries committed
156 157
}

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

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

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

Dries's avatar
 
Dries committed
179
  return $book;
Dries's avatar
 
Dries committed
180 181
}

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

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

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

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

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

220
  $op = $_POST['op'];
Dries's avatar
 
Dries committed
221

Dries's avatar
 
Dries committed
222
  $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
223

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

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

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

  return $output;
}

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

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

Dries's avatar
 
Dries committed
254 255 256 257
  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']);
258
        drupal_set_message(t('The post has been added to the book.'));
Dries's avatar
 
Dries committed
259 260 261 262 263
        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);
264
        drupal_set_message(t('The book outline has been updated.'));
Dries's avatar
 
Dries committed
265 266 267 268 269
        drupal_goto("node/$node->nid");
        break;

      case t('Remove from book outline'):
        db_query('DELETE FROM {book} WHERE nid = %d', $node->nid);
270
        drupal_set_message(t('The post has been removed from the book.'));
Dries's avatar
 
Dries committed
271 272 273 274 275 276 277
        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
278
        $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
279 280 281 282 283 284 285 286

        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
287

288
        drupal_set_title(check_plain($node->title));
Dries's avatar
 
Dries committed
289
        return form($output);
Dries's avatar
 
Dries committed
290 291 292 293
    }
  }
}

Dries's avatar
 
Dries committed
294

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

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

  foreach ($revisions as $revision) {

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

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

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

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

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

334 335 336
/**
 * Accumulates the nodes up to the root of the book from the given node in the $nodes array.
 */
Dries's avatar
 
Dries committed
337
function book_location_down($node, $nodes = array()) {
338
  $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
339 340 341 342 343 344 345
  if ($last_direct_child) {
    array_push($nodes, $last_direct_child);
    $nodes = book_location_down($last_direct_child, $nodes);
  }
  return $nodes;
}

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

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

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

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

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

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

391 392 393 394 395 396
/**
 * Returns the content of a given node.  If $teaser if true, returns
 * the teaser rather than full content.  Displays the most recently
 * approved revision of a node (if any) unless we have to display this
 * page in the context of the moderation queue.
 */
Dries's avatar
 
Dries committed
397
function book_content($node, $teaser = FALSE) {
398
  $op = $_POST['op'];
Dries's avatar
 
Dries committed
399

Dries's avatar
 
Dries committed
400 401 402
  // 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.
403 404
  if ($op != t('Preview') && $node->moderate && arg(0) != 'queue') {
    $revision = book_revision_load($node, array('moderate' => 0, 'status' => 1));
Dries's avatar
 
Dries committed
405 406 407 408

    if ($revision) {
      $node = $revision;
    }
Dries's avatar
 
Dries committed
409 410
  }

411 412
  // Extract the page body.
  $node = node_prepare($node, $teaser);
Dries's avatar
 
Dries committed
413

Dries's avatar
 
Dries committed
414 415 416
  return $node;
}

417 418 419 420 421 422
/**
 * 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
423
function book_view(&$node, $teaser = FALSE, $page = FALSE) {
Dries's avatar
 
Dries committed
424
  $node = book_content($node, $teaser);
Dries's avatar
 
Dries committed
425

Dries's avatar
 
Dries committed
426 427
  if (!$teaser && $node->moderate) {
    $node->body .= '<div class="log"><div class="title">'. t('Log') .':</div>'. $node->log .'</div>';
Dries's avatar
 
Dries committed
428 429 430
  }
}

431
/**
Dries's avatar
 
Dries committed
432 433 434
 * Implementation of hook_nodeapi().
 *
 * Appends book navigation to all nodes in the book.
435
 */
Dries's avatar
 
Dries committed
436 437 438 439
function book_nodeapi(&$node, $op, $teaser, $page) {
  switch ($op) {
    case 'view':
      if (!$teaser) {
Dries's avatar
 
Dries committed
440
        $book = db_fetch_array(db_query('SELECT * FROM {book} WHERE nid = %d', $node->nid));
Dries's avatar
 
Dries committed
441
        if ($book) {
442
          if ($node->moderate && user_access('administer nodes')) {
443
            drupal_set_message(t("The post has been submitted for moderation and won't be accessible until it has been approved."));
444 445
          }

Dries's avatar
 
Dries committed
446 447 448
          foreach ($book as $key => $value) {
            $node->$key = $value;
          }
449
          $node = theme('book_navigation', $node);
450 451 452
          if ($page) {
            menu_set_location($node->breadcrumb);
          }
Dries's avatar
 
Dries committed
453
        }
Dries's avatar
 
Dries committed
454
      }
Dries's avatar
 
Dries committed
455
      break;
Dries's avatar
 
Dries committed
456
  }
Dries's avatar
 
Dries committed
457
}
Dries's avatar
 
Dries committed
458

459 460 461
/**
 * Prepares both the custom breadcrumb trail and the forward/backward
 * navigation for a node presented as a book page.
462 463
 *
 * @ingroup themeable
464
 */
465
function theme_book_navigation($node) {
Dries's avatar
 
Dries committed
466
  $path = book_location($node);
Dries's avatar
 
Dries committed
467

Dries's avatar
 
Dries committed
468
  // Construct the breadcrumb:
Dries's avatar
 
Dries committed
469

Dries's avatar
 
Dries committed
470
  $node->breadcrumb = array(); // Overwrite the trail with a book trail.
Dries's avatar
 
Dries committed
471
  foreach ($path as $level) {
Dries's avatar
 
Dries committed
472
    $node->breadcrumb[] = array('path' => 'node/'. $level->nid, 'title' =>  $level->title);
Dries's avatar
 
Dries committed
473
  }
Dries's avatar
 
Dries committed
474
  $node->breadcrumb[] = array('path' => 'node/'. $node->nid);
Dries's avatar
 
Dries committed
475

Dries's avatar
 
Dries committed
476
  if ($node->nid) {
477
    $output .= '<div class="book">';
Dries's avatar
 
Dries committed
478 479

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

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

507
    $output .= '<div class="nav">';
Dries's avatar
 
Dries committed
508 509
    $output .= ' <div class="links">'. $links .'</div>';
    $output .= ' <div class="titles">'. $titles .'</div>';
510 511
    $output .= '</div>';
    $output .= '</div>';
Dries's avatar
 
Dries committed
512
  }
Dries's avatar
 
Dries committed
513

Dries's avatar
 
Dries committed
514
  $node->body = $node->body.$output;
Dries's avatar
 
Dries committed
515

Dries's avatar
 
Dries committed
516
  return $node;
Dries's avatar
 
Dries committed
517
}
Dries's avatar
 
Dries committed
518

519 520 521
/**
 * This is a helper function for book_toc().
 */
522
function book_toc_recurse($nid, $indent, $toc, $children, $exclude) {
Dries's avatar
 
Dries committed
523 524
  if ($children[$nid]) {
    foreach ($children[$nid] as $foo => $node) {
525 526 527 528
      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
529 530 531 532 533 534
    }
  }

  return $toc;
}

535 536 537
/**
 * Returns an array of titles and nid entries of book pages in table of contents order.
 */
538
function book_toc($exclude = 0) {
539
  $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
540

Dries's avatar
 
Dries committed
541
  while ($node = db_fetch_object($result)) {
Dries's avatar
 
Dries committed
542 543 544 545
    if (!$children[$node->parent]) {
      $children[$node->parent] = array();
    }
    array_push($children[$node->parent], $node);
Dries's avatar
 
Dries committed
546
  }
Dries's avatar
 
Dries committed
547

548 549
  $toc = array();

Dries's avatar
 
Dries committed
550 551
  // If the user is an administrator, add the top-level book page;
  // only administrators can start new books.
552 553
  if (user_access('administer nodes')) {
    $toc[0] = '<'. t('top-level') .'>';
Dries's avatar
 
Dries committed
554 555
  }

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

Dries's avatar
 
Dries committed
558 559 560
  return $toc;
}

561 562 563
/**
 * This is a helper function for book_tree()
 */
Dries's avatar
 
Dries committed
564
function book_tree_recurse($nid, $depth, $children, $unfold = array()) {
Dries's avatar
 
Dries committed
565
  if ($depth > 0) {
Dries's avatar
 
Dries committed
566 567
    if ($children[$nid]) {
      foreach ($children[$nid] as $foo => $node) {
Dries's avatar
 
Dries committed
568 569 570
        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
571 572
            $output .= l($node->title, 'node/'. $node->nid);
            $output .= '<ul>'. $tree .'</ul>';
Dries's avatar
 
Dries committed
573 574 575
            $output .= '</li>';
          }
          else {
Dries's avatar
 
Dries committed
576
            $output .= '<li class="leaf">'. l($node->title, 'node/'. $node->nid) .'</li>';
Dries's avatar
 
Dries committed
577 578 579 580
          }
        }
        else {
          if ($tree = book_tree_recurse($node->nid, 1, $children)) {
Dries's avatar
 
Dries committed
581
            $output .= '<li class="collapsed">'. l($node->title, 'node/'. $node->nid) .'</li>';
Dries's avatar
 
Dries committed
582 583
          }
          else {
Dries's avatar
 
Dries committed
584
            $output .= '<li class="leaf">'. l($node->title, 'node/'. $node->nid) .'</li>';
Dries's avatar
 
Dries committed
585
          }
Dries's avatar
 
Dries committed
586
        }
Dries's avatar
 
Dries committed
587 588
      }
    }
Dries's avatar
 
Dries committed
589 590 591 592 593
  }

  return $output;
}

594 595 596 597
/**
 * Returns an HTML nested list (wrapped in a menu-class div) representing the book nodes
 * as a tree.
 */
Dries's avatar
 
Dries committed
598
function book_tree($parent = 0, $depth = 3, $unfold = array()) {
599
  $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
600

Dries's avatar
 
Dries committed
601 602 603 604
  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
605
  }
Dries's avatar
 
Dries committed
606

Dries's avatar
 
Dries committed
607
  if ($tree = book_tree_recurse($parent, $depth, $children, $unfold)) {
608
    return '<div class="menu"><ul>'. $tree .'</ul></div>';
Dries's avatar
 
Dries committed
609
  }
Dries's avatar
 
Dries committed
610 611
}

612
/**
Dries's avatar
Dries committed
613
 * Menu callback; prints a listing of all books.
614
 */
Dries's avatar
 
Dries committed
615
function book_render() {
616
  $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
617

Dries's avatar
 
Dries committed
618
  return $output;
Dries's avatar
 
Dries committed
619 620
}

621
/**
622
 * Menu callback; generates a printer-friendly book page with all descendants.
623
 */
624
function book_export_html($nid = 0, $depth = 1) {
Dries's avatar
 
Dries committed
625
  global $base_url;
Dries's avatar
 
Dries committed
626

627
  $output .= book_recurse($nid, $depth, 'book_node_visitor_print_pre', 'book_node_visitor_print_post');
Dries's avatar
 
Dries committed
628

629 630
  $html = '<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">';
  $html .= '<html xmlns="http://www.w3.org/1999/xhtml" lang="en" xml:lang="en">';
Dries's avatar
 
Dries committed
631

632 633 634 635 636
  $html .= "<head>\n<title>". check_plain($node->title) ."</title>\n";
  $html .= '<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />';
  $html .= '<base href="'. $base_url .'/" />' . "\n";
  $html .= "<style type=\"text/css\">\n@import url(misc/print.css);\n</style>\n";
  $html .= "</head>\n<body>\n". $output . "\n</body>\n</html>\n";
Dries's avatar
 
Dries committed
637

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

641 642 643 644 645 646 647 648 649 650
/**
 * Menu callback; generates XML output of entire book hierarchy beneath
 * the given node.
 */
function book_export_docbook($nid = 0, $depth = 1) {
  $xml = "<?xml version='1.0'?>\n";
  $xml .= "<book>\n";
  $xml .= book_recurse($nid, $depth, 'book_node_visitor_xml_pre', 'book_node_visitor_xml_post');
  $xml .= "</book>\n";
  print $xml;
Dries's avatar
 
Dries committed
651

Dries's avatar
 
Dries committed
652 653
}

654 655 656 657 658 659 660 661 662 663 664 665 666 667 668 669 670 671 672 673
/**
 * Traverses the book tree.  Applies the $visit_pre() callback to each
 * node, is called recursively for each child of the node (in weight,
 * title order).  Finally appends the output of the $visit_post()
 * callback to the output before returning the generated output.
 *
 * @param nid
 *  - the node id (nid) of the root node of the book hierarchy.
 * @param depth
 *  - the depth of the given node in the book hierarchy.
 * @param visit_pre
 *  - a function callback to be called upon visiting a node in the tree
 * @param visit_post
 *  - a function callback to be called after visiting a node in the tree,
 *    but before recursively visiting children.
 * @return
 *  - the output generated in visiting each node
 */
function book_recurse($nid = 0, $depth = 1, $visit_pre, $visit_post) {
  $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
674

Dries's avatar
 
Dries committed
675
  while ($page = db_fetch_object($result)) {
Dries's avatar
 
Dries committed
676
    // Load the node:
677
    $node = node_load(array('nid' => $page->nid));
Dries's avatar
 
Dries committed
678

Dries's avatar
 
Dries committed
679
    // Take the most recent approved revision:
Dries's avatar
 
Dries committed
680
    if ($node->moderate) {
681
      $node = book_revision_load($node, array('moderate' => 0, 'status' => 1));
Dries's avatar
 
Dries committed
682 683
    }

Dries's avatar
 
Dries committed
684
    if ($node) {
685 686
      if (function_exists($visit_pre)) {
        $output .= call_user_func($visit_pre, $node, $depth, $nid);
Dries's avatar
 
Dries committed
687
      }
688 689
      else { # default
        $output .= book_node_visitor_print_pre($node, $depth, $nid);
Dries's avatar
 
Dries committed
690
      }
Dries's avatar
 
Dries committed
691

692 693 694 695 696 697 698 699 700 701 702 703 704
      $children = 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'), $node->nid);
      while ($childpage = db_fetch_object($children)) {
          $childnode = node_load(array('nid' => $childpage->nid));
          if ($childnode->nid != $node->nid) {
              $output .= book_recurse($childnode->nid, $depth+1, $visit_pre, $visit_post);
          }
      }
      if (function_exists($visit_post)) {
        $output .= call_user_func($visit_post, $node);
      }
      else { # default
        $output .= book_node_visitor_print_post();
      }
Dries's avatar
 
Dries committed
705
    }
Dries's avatar
 
Dries committed
706
  }
Dries's avatar
 
Dries committed
707

Dries's avatar
 
Dries committed
708 709
  return $output;
}
Dries's avatar
 
Dries committed
710

711 712 713 714 715 716 717 718 719 720 721 722 723 724 725 726 727 728 729 730 731 732 733 734 735 736 737 738 739 740 741 742 743 744 745 746 747 748 749 750 751 752 753 754 755 756 757 758 759 760 761 762 763 764 765 766 767 768 769 770 771 772 773 774 775 776 777 778 779 780 781 782 783 784 785 786 787 788 789 790 791 792 793 794 795 796 797 798 799 800 801 802 803 804 805 806 807 808 809 810
/**
 * Generates printer-friendly HTML for a node.  This function
 * is a 'pre-node' visitor function for book_recurse().
 *
 * @param $node
 *   - the node to generate output for.
 * @param $depth
 *   - the depth of the given node in the hierarchy. This
 *   is used only for generating output.
 * @param $nid
 *   - the node id (nid) of the given node. This
 *   is used only for generating output.
 * @return
 *   - the HTML generated for the given node.
 */
function book_node_visitor_print_pre($node, $depth, $nid) {
  // Output the content:
  if (node_hook($node, 'content')) {
    $node = node_invoke($node, 'content');
  }
  // Allow modules to change $node->body before viewing.
  node_invoke_nodeapi($node, 'view', $node->body, false);

  $output .= '<div id="node-'.$node->nid. '" class="section-'.$depth.'">'."\n";
  $output .= '<h1 class="book-heading">'. check_plain($node->title) ."</h1>\n";

  if ($node->body) {
    $output .= $node->body;
  }
  return $output;
}

/**
 * Finishes up generation of printer-friendly HTML after visiting a
 * node. This function is a 'post-node' visitor function for
 * book_recurse().
 */
function book_node_visitor_print_post() {
  return "</div>\n";
}

/**
 * Generates XML for a given node. This function is a 'pre-node'
 * visitor function for book_recurse().  The generated XML is
 * DocBook-like - the node's HTML content wrapped in a CDATA
 * processing instruction, and put inside a <literallayout> tag.  The
 * node body has an md5-hash applied; the value of this is stored as
 * node metadata to allow importing code to determine if contents have
 * changed.
 *
 * @param $node
 *   - the node to generate output for.
 * @param $depth
 *   - the depth of the given node in the hierarchy. This
 *   is currently not used.
 * @param $nid
 *   - the node id (nid) of the given node. This
 *   is used only for generating output (e.g., ID attribute)
 * @return
 *   - the generated XML for the given node.
 */
function book_node_visitor_xml_pre($node, $depth, $nid) {
  // Output the content:
  if (node_hook($node, 'content')) {
    $node = node_invoke($node, 'content');
  }
  // Allow modules to change $node->body before viewing.
  node_invoke_nodeapi($node, 'view', $node->body, false);

  $output .= '<section id="node-'.$node->nid .'">'."\n";
  $output .= "<sectioninfo>\n";
  $output .= "<releaseinfo>\n";
  $output .= "md5-hash:" . md5($node->body) . "\n";
  $output .= "weight:". $node->weight . "\n";
  $output .= "</releaseinfo>\n";
  $output .= "</sectioninfo>\n";
  $output .= '<title>'. check_plain($node->title) ."</title>\n";
  // wrap the node body in a CDATA declaration
  $output .= "<literallayout>";
  $output .= "<![CDATA[";
  if ($node->body) {
    $output .= $node->body;
  }
  $output .= "]]>";
  $output .= "</literallayout>\n";
  return $output;
}

/**
 * Completes the XML generated for the node. This
 * function is a 'post-node' visitor function for
 * book_recurse().
 */
function book_node_visitor_xml_post() {
  return "</section>\n";
}

/**
 * Creates a row for the 'admin' view of a book.  Each row represents a page in the book, in the tree representing the book
 */
811
function book_admin_edit_line($node, $depth = 0) {
812
  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
813 814
}

815
function book_admin_edit_book($nid, $depth = 1) {
816
  $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
817

818 819
  $rows = array();

Dries's avatar
 
Dries committed
820
  while ($node = db_fetch_object($result)) {
Dries's avatar
 
Dries committed
821
    $node = node_load(array('nid' => $node->nid));
822 823
    $rows[] = book_admin_edit_line($node, $depth);
    $rows = array_merge($rows, book_admin_edit_book($node->nid, $depth + 1));
Dries's avatar
 
Dries committed
824 825
  }

Dries's avatar
 
Dries committed
826
  return $rows;
Dries's avatar
 
Dries committed
827 828
}

829 830 831
/**
 * Display an administrative view of the hierarchy of a book.
 */
832 833 834
function book_admin_edit($nid, $depth = 0) {
  $node = node_load(array('nid' => $nid));
  if ($node->nid) {
Dries's avatar
 
Dries committed
835
    $header = array(t('Title'), t('Weight'), array('data' => t('Operations'), 'colspan' => '3'));
836 837
    $rows[] = book_admin_edit_line($node);
    $rows = array_merge($rows, book_admin_edit_book($nid));
Dries's avatar
 
Dries committed
838

Dries's avatar
 
Dries committed
839 840
    $output .= theme('table', $header, $rows);
    $output .= form_submit(t('Save book pages'));
Dries's avatar
 
Dries committed
841

842
    drupal_set_title(check_plain($node->title));
Dries's avatar
 
Dries committed
843 844
    return form($output);
  }
845 846 847
  else {
    drupal_not_found();
  }
Dries's avatar
 
Dries committed
848 849
}

850 851 852
/**
 * Saves the changes to a book made by an administrator in the book admin view.
 */
Dries's avatar
 
Dries committed
853
function book_admin_save($nid, $edit = array()) {
Dries's avatar
 
Dries committed
854
  if ($nid) {
Dries's avatar
 
Dries committed
855
    $book = node_load(array('nid' => $nid));
Dries's avatar
 
Dries committed
856

Dries's avatar
 
Dries committed
857
    foreach ($edit as $nid => $value) {
Dries's avatar
 
Dries committed
858 859 860 861
      // 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
862
      }
Dries's avatar
 
Dries committed
863

Dries's avatar
 
Dries committed
864 865 866 867
      // 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
868
      }
Dries's avatar
 
Dries committed
869 870
    }

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

Dries's avatar
 
Dries committed
874 875
    return $message;
  }
Dries's avatar
 
Dries committed
876 877
}

878
/**
Dries's avatar
Dries committed
879
 * Menu callback; displays a listing of all orphaned book pages.
880
 */
Dries's avatar
 
Dries committed
881
function book_admin_orphan() {
882
  $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
883 884 885 886 887

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

Dries's avatar
 
Dries committed
888
  if ($pages) {
889
    $output .= '<h3>'. t('Orphan pages') .'</h3>';
Dries's avatar
 
Dries committed
890
    $header = array(t('Title'), t('Weight'), array('data' => t('Operations'), 'colspan' => '3'));
Dries's avatar
 
Dries committed
891 892
    foreach ($pages as $nid => $node) {
      if ($node->parent && empty($pages[$node->parent])) {
893 894
        $rows[] = book_admin_edit_line($node, $depth);
        $rows = array_merge($rows, book_admin_edit_book($node->nid, $depth + 1));
Dries's avatar
 
Dries committed
895
      }
Dries's avatar
 
Dries committed
896
    }
Dries's avatar
 
Dries committed
897
    $output .= theme('table', $header, $rows);
Dries's avatar
 
Dries committed
898 899
  }

Dries's avatar
 
Dries committed
900
  return $output;
Dries's avatar
 
Dries committed
901 902
}

903
/**
Dries's avatar
Dries committed
904
 * Menu callback; displays the book administration page.
905
 */
Dries's avatar
Dries committed
906
function book_admin($nid = 0) {
Dries's avatar
 
Dries committed
907 908
  $op = $_POST['op'];
  $edit = $_POST['edit'];
Dries's avatar
 
Dries committed
909

910 911
  if ($op == t('Save book pages')) {
    drupal_set_message(book_admin_save($nid, $edit));
Dries's avatar
 
Dries committed
912
  }
913 914 915 916 917 918 919 920 921

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

922 923 924
/**
 * Returns an administrative overview of all books.
 */