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
 * Implementation of hook_node_info().
11
 */
12
function book_node_info() {
13
  return array('book' => array('name' => t('book page'), 'base' => 'book'));
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('outline posts in books', 'create book pages', 'create new books', 'edit book pages', 'edit own book pages', 'see printer-friendly version');
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') {
30
    // Only registered users can create book pages. Given the nature
Dries's avatar
 
Dries committed
31
    // 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') {
36
    // Only registered users can update book pages. Given the nature
Dries's avatar
 
Dries committed
37 38
    // 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
    // of that page waiting for approval. That is, only updates that
40
    // don't overwrite the current or pending information are allowed.
41

42
    if ((user_access('edit book pages') && !$node->moderate) || ($node->uid == $user->uid && user_access('edit own book pages'))) {
43 44 45 46 47
      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
      if (user_access('see printer-friendly version')) {
64 65 66
        $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.')));
67
      }
Dries's avatar
 
Dries committed
68
    }
Dries's avatar
 
Dries committed
69 70
  }

Dries's avatar
 
Dries committed
71
  return $links;
Dries's avatar
 
Dries committed
72 73
}

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

Dries's avatar
 
Dries committed
80
  if ($may_cache) {
81 82 83
    $items[] = array(
      'path' => 'node/add/book',
      'title' => t('book page'),
84
      'access' => user_access('create book pages'));
85 86 87
    $items[] = array(
      'path' => 'admin/node/book',
      'title' => t('books'),
Dries's avatar
 
Dries committed
88 89
      'callback' => 'book_admin',
      'access' => user_access('administer nodes'),
90 91 92 93 94 95
      'type' => MENU_LOCAL_TASK,
      'weight' => -1);
    $items[] = array(
      'path' => 'admin/node/book/list',
      'title' => t('list'),
      'type' => MENU_DEFAULT_LOCAL_TASK);
96 97 98
    $items[] = array(
      'path' => 'admin/node/book/orphan',
      'title' => t('orphan pages'),
Dries's avatar
 
Dries committed
99
      'callback' => 'book_admin_orphan',
100
      'type' => MENU_LOCAL_TASK,
Dries's avatar
 
Dries committed
101
      'weight' => 8);
102 103 104
    $items[] = array(
      'path' => 'book',
      'title' => t('books'),
Dries's avatar
 
Dries committed
105 106 107
      'callback' => 'book_render',
      'access' => user_access('access content'),
      'type' => MENU_SUGGESTED_ITEM);
108
    $items[] = array(
109 110
      'path' => 'book/export',
      'callback' => 'book_export',
111
      'access' => user_access('access content'),
Dries's avatar
 
Dries committed
112
      'type' => MENU_CALLBACK);
Dries's avatar
 
Dries committed
113
  }
Dries's avatar
 
Dries committed
114 115
  else {
    // To avoid SQL overhead, check whether we are on a node page and whether the
116 117
    // user is allowed to outline posts in books.
    if (arg(0) == 'node' && is_numeric(arg(1)) && user_access('outline posts in books')) {
Dries's avatar
 
Dries committed
118
      // Only add the outline-tab for non-book pages:
119
      $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
120
      if (db_num_rows($result) > 0) {
121 122 123 124
        $items[] = array(
          'path' => 'node/'. arg(1) .'/outline',
          'title' => t('outline'),
          'callback' => 'book_outline',
125
          'callback arguments' => array(arg(1)),
126
          'access' => user_access('outline posts in books'),
127 128
          'type' => MENU_LOCAL_TASK,
          'weight' => 2);
Dries's avatar
 
Dries committed
129 130 131
      }
    }
  }
Dries's avatar
 
Dries committed
132 133 134 135

  return $items;
}

136 137 138
/**
 * Implementation of hook_block().
 *
Dries's avatar
 
Dries committed
139 140
 * Displays the book table of contents in a block when the current page is a
 * single-node view of a book node.
141
 */
Dries's avatar
 
Dries committed
142
function book_block($op = 'list', $delta = 0) {
Dries's avatar
 
Dries committed
143
  $block = array();
Dries's avatar
 
Dries committed
144 145
  if ($op == 'list') {
    $block[0]['info'] = t('Book navigation');
146
    return $block;
Dries's avatar
 
Dries committed
147
  }
148
  else if ($op == 'view') {
Dries's avatar
 
Dries committed
149 150
    // Only display this block when the user is browsing a book:
    if (arg(0) == 'node' && is_numeric(arg(1))) {
151
      $result = db_query(db_rewrite_sql('SELECT n.nid, n.title, b.parent FROM {node} n INNER JOIN {book} b ON n.vid = b.vid WHERE n.nid = %d'), arg(1));
Dries's avatar
 
Dries committed
152 153 154 155 156 157 158 159 160 161 162
      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;
        }

163
        $block['subject'] = check_plain($path[0]->title);
Dries's avatar
 
Dries committed
164 165 166
        $block['content'] = book_tree($expand[0], 5, $expand);
      }
    }
Dries's avatar
 
Dries committed
167

168 169
    return $block;
  }
Dries's avatar
 
Dries committed
170 171
}

172 173 174
/**
 * Implementation of hook_load().
 */
Dries's avatar
 
Dries committed
175
function book_load($node) {
Dries's avatar
 
Dries committed
176
  global $user;
Dries's avatar
 
Dries committed
177

178
  $book = db_fetch_object(db_query('SELECT * FROM {book} WHERE vid = %d', $node->vid));
Dries's avatar
 
Dries committed
179

Dries's avatar
 
Dries committed
180
  if (arg(2) == 'edit' && !user_access('administer nodes')) {
Dries's avatar
 
Dries committed
181 182
    // If a user is about to update a book page, we overload some
    // fields to reflect the changes.
Dries's avatar
 
Dries committed
183 184 185 186 187 188
    if ($user->uid) {
      $book->uid = $user->uid;
      $book->name = $user->name;
    }
    else {
      $book->uid = 0;
189
      $book->name = '';
Dries's avatar
 
Dries committed
190
    }
Dries's avatar
 
Dries committed
191
  }
Dries's avatar
 
Dries committed
192

Dries's avatar
 
Dries committed
193
  return $book;
Dries's avatar
 
Dries committed
194 195
}

196 197 198
/**
 * Implementation of hook_insert().
 */
Dries's avatar
 
Dries committed
199
function book_insert($node) {
200
  db_query("INSERT INTO {book} (nid, vid, parent, weight) VALUES (%d, %d, %d, %d)", $node->nid, $node->vid, $node->parent, $node->weight);
Dries's avatar
 
Dries committed
201
}
Dries's avatar
 
Dries committed
202

203 204 205
/**
 * Implementation of hook_update().
 */
Dries's avatar
 
Dries committed
206
function book_update($node) {
207
  if ($node->revision) {
208
    db_query("INSERT INTO {book} (nid, vid, parent, weight) VALUES (%d, %d, %d, %d)", $node->nid, $node->vid, $node->parent, $node->weight);
209 210 211 212
  }
  else {
    db_query("UPDATE {book} SET parent = %d, weight = %d WHERE vid = %d", $node->parent, $node->weight, $node->vid);
  }
Dries's avatar
 
Dries committed
213
}
Dries's avatar
 
Dries committed
214

215 216 217
/**
 * Implementation of hook_delete().
 */
218
function book_delete(&$node) {
219
  db_query('DELETE FROM {book} WHERE nid = %d', $node->nid);
Dries's avatar
 
Dries committed
220 221
}

222
/**
223
 * Implementation of hook_submit().
224
 */
225
function book_submit(&$node) {
226 227
  // Set default values for non-administrators.
  if (!user_access('administer nodes')) {
228 229 230
    $node->weight = 0;
    $node->revision = 1;
  }
231
}
232

233 234 235
/**
 * Implementation of hook_form().
 */
Dries's avatar
 
Dries committed
236
function book_form(&$node) {
237
  if ($node->nid && !$node->parent && !user_access('create new books')) {
238
    $form['parent'] = array('#type' => 'value', '#value' => $node->parent);
239 240 241 242 243 244 245
  }
  else {
    $form['parent'] = array('#type' => 'select',
      '#title' => t('Parent'),
      '#default_value' => ($node->parent ? $node->parent : arg(4)),
      '#options' => book_toc($node->nid),
      '#weight' => -4,
246
      '#description' => user_access('create new books') ? t('The parent section in which to place this page. Note that each page whose parent is &lt;top-level&gt; is an independent, top-level book.') : t('The parent that this page belongs in.'),
247 248
    );
  }
249 250 251 252 253 254 255 256 257 258 259 260 261

  $form['title'] = array('#type' => 'textfield',
    '#title' => t('Title'),
    '#required' => TRUE,
    '#default_value' => $node->title,
    '#weight' => -5,
  );
  $form['body_filter']['body'] = array('#type' => 'textarea',
    '#title' => t('Body'),
    '#default_value' => $node->body,
    '#rows' => 20,
    '#required' => TRUE,
  );
262
  $form['body_filter']['format'] = filter_form($node->format);
263

264
  $form['log'] = array(
265 266 267 268 269
    '#type' => 'textarea',
    '#title' => t('Log message'),
    '#default_value' => $node->log,
    '#weight' => 5,
    '#description' => t('An explanation of the additions or updates being made to help other authors understand your motivations.'),
270
  );
Dries's avatar
 
Dries committed
271

272
  if (user_access('administer nodes')) {
273 274 275 276 277 278
    $form['weight'] = array('#type' => 'weight',
      '#title' => t('Weight'),
      '#default_value' => $node->weight,
      '#delta' => 15,
      '#weight' => 5,
      '#description' => t('Pages at a given level are ordered first by weight and then by title.'),
279
    );
Dries's avatar
 
Dries committed
280 281
  }
  else {
Dries's avatar
 
Dries committed
282 283
    // If a regular user updates a book page, we create a new revision
    // authored by that user:
284
    $form['revision'] = array('#type' => 'hidden', '#value' => 1);
Dries's avatar
 
Dries committed
285 286
  }

287
  return $form;
Dries's avatar
 
Dries committed
288 289
}

290
/**
Dries's avatar
 
Dries committed
291 292
 * Implementation of function book_outline()
 * Handles all book outline operations.
293
 */
294 295
function book_outline($nid) {
  $node = node_load($nid);
296
  $page = book_load($node);
Dries's avatar
 
Dries committed
297

298 299 300
  $form['parent'] = array('#type' => 'select',
    '#title' => t('Parent'),
    '#default_value' => $page->parent,
301
    '#options' => book_toc($node->nid),
302 303 304 305 306 307 308 309 310 311 312 313 314
    '#description' => t('The parent page in the book.'),
  );
  $form['weight'] = array('#type' => 'weight',
    '#title' => t('Weight'),
    '#default_value' => $page->weight,
    '#delta' => 15,
    '#description' => t('Pages at a given level are ordered first by weight and then by title.'),
  );
  $form['log'] = array('#type' => 'textarea',
    '#title' => t('Log message'),
    '#default_value' => $node->log,
    '#description' => t('An explanation to help other authors understand your motivations to put this post into the book.'),
  );
Dries's avatar
 
Dries committed
315

316 317 318 319 320 321 322 323 324 325 326 327
  $form['nid'] = array('#type' => 'value', '#value' => $nid);
  if ($page->nid) {
    $form['update'] = array('#type' => 'submit',
      '#value' => t('Update book outline'),
    );
    $form['remove'] = array('#type' => 'submit',
      '#value' => t('Remove from book outline'),
    );
  }
  else {
    $form['add'] = array('#type' => 'submit', '#value' => t('Add to book outline'));
  }
Dries's avatar
 
Dries committed
328

329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354
  drupal_set_title(check_plain($node->title));
  return drupal_get_form('book_outline', $form);
}

/**
 * Handles book outline form submissions.
 */
function book_outline_submit($form_id, $form_values) {
  $op = $_POST['op'];
  $node = node_load($form_values['nid']);

  switch ($op) {
    case t('Add to book outline'):
      db_query('INSERT INTO {book} (nid, vid, parent, weight) VALUES (%d, %d, %d, %d)', $node->nid, $node->vid, $form_values['parent'], $form_values['weight']);
      db_query("UPDATE {node_revisions} SET log = '%s' WHERE vid = %d", $form_values['log'], $node->vid);
      drupal_set_message(t('The post has been added to the book.'));
      break;
    case t('Update book outline'):
      db_query('UPDATE {book} SET parent = %d, weight = %d WHERE vid = %d', $form_values['parent'], $form_values['weight'], $node->vid);
      db_query("UPDATE {node_revisions} SET log = '%s' WHERE vid = %d", $form_values['log'], $node->vid);
      drupal_set_message(t('The book outline has been updated.'));
      break;
    case t('Remove from book outline'):
      db_query('DELETE FROM {book} WHERE nid = %d', $node->nid);
      drupal_set_message(t('The post has been removed from the book.'));
      break;
Dries's avatar
 
Dries committed
355
  }
356
  return "node/$node->nid";
Dries's avatar
 
Dries committed
357 358
}

359
/**
Dries's avatar
Dries committed
360 361
 * Given a node, this function returns an array of 'book node' objects
 * representing the path in the book tree from the root to the
362
 * parent of the given node.
Dries's avatar
Dries committed
363
 *
364 365 366 367 368 369
 * @param node - a book node object for which to compute the path
 *
 * @return - an array of book node objects representing the path of
 * nodes root to parent of the given node. Returns an empty array if
 * the node does not exist or is not part of a book hierarchy.
 *
370
 */
Dries's avatar
 
Dries committed
371
function book_location($node, $nodes = array()) {
372
  $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.vid = b.vid WHERE n.nid = %d'), $node->parent));
Dries's avatar
 
Dries committed
373 374
  if ($parent->title) {
    $nodes = book_location($parent, $nodes);
375
    $nodes[] = $parent;
Dries's avatar
 
Dries committed
376 377 378 379
  }
  return $nodes;
}

380 381 382
/**
 * Accumulates the nodes up to the root of the book from the given node in the $nodes array.
 */
Dries's avatar
 
Dries committed
383
function book_location_down($node, $nodes = array()) {
384
  $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 n.status = 1 AND b.parent = %d ORDER BY b.weight DESC, n.title DESC'), $node->nid));
Dries's avatar
 
Dries committed
385
  if ($last_direct_child) {
386
    $nodes[] = $last_direct_child;
Dries's avatar
 
Dries committed
387 388 389 390 391
    $nodes = book_location_down($last_direct_child, $nodes);
  }
  return $nodes;
}

392
/**
393
 * Fetches the node object of the previous page of the book.
394
 */
Dries's avatar
 
Dries committed
395
function book_prev($node) {
Dries's avatar
 
Dries committed
396
  // If the parent is zero, we are at the start of a book so there is no previous.
Dries's avatar
 
Dries committed
397 398 399 400
  if ($node->parent == 0) {
    return NULL;
  }

Dries's avatar
 
Dries committed
401
  // Previous on the same level:
402
  $direct_above = db_fetch_object(db_query(db_rewrite_sql("SELECT n.nid, n.title, b.weight FROM {node} n INNER JOIN {book} b ON n.vid = b.vid 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
403
  if ($direct_above) {
Dries's avatar
 
Dries committed
404
    // Get last leaf of $above.
Dries's avatar
 
Dries committed
405
    $path = book_location_down($direct_above);
Dries's avatar
 
Dries committed
406 407

    return $path ? (count($path) > 0 ? array_pop($path) : NULL) : $direct_above;
Dries's avatar
 
Dries committed
408 409
  }
  else {
Dries's avatar
 
Dries committed
410
    // Direct parent:
411
    $prev = db_fetch_object(db_query(db_rewrite_sql('SELECT n.nid, n.title FROM {node} n INNER JOIN {book} b ON n.vid = b.vid WHERE n.nid = %d AND n.status = 1 AND n.moderate = 0'), $node->parent));
Dries's avatar
 
Dries committed
412 413 414 415
    return $prev;
  }
}

416
/**
417
 * Fetches the node object of the next page of the book.
418
 */
Dries's avatar
 
Dries committed
419 420
function book_next($node) {
  // get first direct child
421
  $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.vid = b.vid 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
422 423 424 425
  if ($child) {
    return $child;
  }

Dries's avatar
 
Dries committed
426
  // No direct child: get next for this level or any parent in this book.
427
  $path = book_location($node); // Path to top-level node including this one.
428
  $path[] = $node;
429

Dries's avatar
 
Dries committed
430
  while (($leaf = array_pop($path)) && count($path)) {
431
    $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.vid = b.vid 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
432 433 434 435 436 437
    if ($next) {
      return $next;
    }
  }
}

438
/**
439 440
 * Returns the content of a given node. If $teaser if true, returns
 * the teaser rather than full content. Displays the most recently
441 442 443
 * 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
444
function book_content($node, $teaser = FALSE) {
445 446
  // Return the page body.
  return node_prepare($node, $teaser);
Dries's avatar
 
Dries committed
447 448
}

449 450 451 452 453 454
/**
 * 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
455
function book_view(&$node, $teaser = FALSE, $page = FALSE) {
456
  $node = node_prepare($node, $teaser);
Dries's avatar
 
Dries committed
457 458
}

459
/**
Dries's avatar
 
Dries committed
460 461 462
 * Implementation of hook_nodeapi().
 *
 * Appends book navigation to all nodes in the book.
463
 */
Dries's avatar
 
Dries committed
464 465 466 467
function book_nodeapi(&$node, $op, $teaser, $page) {
  switch ($op) {
    case 'view':
      if (!$teaser) {
468
        $book = db_fetch_array(db_query('SELECT * FROM {book} WHERE vid = %d', $node->vid));
Dries's avatar
 
Dries committed
469
        if ($book) {
470
          if ($node->moderate && user_access('administer nodes')) {
471
            drupal_set_message(t("The post has been submitted for moderation and won't be accessible until it has been approved."));
472 473
          }

Dries's avatar
 
Dries committed
474 475 476
          foreach ($book as $key => $value) {
            $node->$key = $value;
          }
477 478 479 480 481 482 483 484 485 486 487

          $path = book_location($node);
          // Construct the breadcrumb:
          $node->breadcrumb = array(); // Overwrite the trail with a book trail.
          foreach ($path as $level) {
            $node->breadcrumb[] = array('path' => 'node/'. $level->nid, 'title' =>  $level->title);
          }
          $node->breadcrumb[] = array('path' => 'node/'. $node->nid);

          $node->body .= theme('book_navigation', $node);

488 489 490
          if ($page) {
            menu_set_location($node->breadcrumb);
          }
Dries's avatar
 
Dries committed
491
        }
Dries's avatar
 
Dries committed
492
      }
Dries's avatar
 
Dries committed
493
      break;
494
    case 'delete revision':
495 496
      db_query('DELETE FROM {book} WHERE vid = %d', $node->vid);
      break;
497 498 499
    case 'delete':
      db_query('DELETE FROM {book} WHERE nid = %d', $node->nid);
      break;
Dries's avatar
 
Dries committed
500
  }
Dries's avatar
 
Dries committed
501
}
Dries's avatar
 
Dries committed
502

503
/**
504
 * Prepares the links to children (TOC) and forward/backward
505
 * navigation for a node presented as a book page.
506 507
 *
 * @ingroup themeable
508
 */
509
function theme_book_navigation($node) {
510 511
  $output = '';

Dries's avatar
 
Dries committed
512
  if ($node->nid) {
513
    $tree = book_tree($node->nid);
Dries's avatar
 
Dries committed
514

Dries's avatar
 
Dries committed
515
    if ($prev = book_prev($node)) {
516
      drupal_add_link(array('rel' => 'prev', 'href' => url('node/'. $prev->nid)));
517
      $links .= l(t('‹ ') . $prev->title, 'node/'. $prev->nid, array('class' => 'page-previous', 'title' => t('Go to previous page')));
Dries's avatar
 
Dries committed
518
    }
519 520 521
    if ($node->parent) {
      drupal_add_link(array('rel' => 'index', 'href' => url('node/'. $node->parent)));
      $links .= l(t('up'), 'node/'. $node->parent, array('class' => 'page-up', 'title' => t('Go to parent page')));
Dries's avatar
 
Dries committed
522 523
    }
    if ($next = book_next($node)) {
524
      drupal_add_link(array('rel' => 'next', 'href' => url('node/'. $next->nid)));
525
      $links .= l($next->title . t(' ›'), 'node/'. $next->nid, array('class' => 'page-next', 'title' => t('Go to next page')));
Dries's avatar
 
Dries committed
526
    }
Dries's avatar
 
Dries committed
527

528 529 530 531 532 533 534 535 536 537
    if (isset($tree) || isset($links)) {
      $output = '<div class="book-navigation">';
      if (isset($tree)) {
        $output .= $tree;
      }
      if (isset($links)) {
        $output .= '<div class="page-links">'. $links .'</div>';
      }
      $output .= '</div>';
    }
Dries's avatar
 
Dries committed
538
  }
Dries's avatar
 
Dries committed
539

540
  return $output;
Dries's avatar
 
Dries committed
541
}
Dries's avatar
 
Dries committed
542

543 544 545
/**
 * This is a helper function for book_toc().
 */
546
function book_toc_recurse($nid, $indent, $toc, $children, $exclude) {
Dries's avatar
 
Dries committed
547 548
  if ($children[$nid]) {
    foreach ($children[$nid] as $foo => $node) {
549 550 551 552
      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
553 554 555 556 557 558
    }
  }

  return $toc;
}

559 560 561
/**
 * Returns an array of titles and nid entries of book pages in table of contents order.
 */
562
function book_toc($exclude = 0) {
563
  $result = db_query(db_rewrite_sql('SELECT n.nid, n.title, b.parent, b.weight FROM {node} n INNER JOIN {book} b ON n.vid = b.vid WHERE n.status = 1 ORDER BY b.weight, n.title'));
Dries's avatar
 
Dries committed
564

Dries's avatar
 
Dries committed
565
  while ($node = db_fetch_object($result)) {
Dries's avatar
 
Dries committed
566 567 568
    if (!$children[$node->parent]) {
      $children[$node->parent] = array();
    }
569
    $children[$node->parent][] = $node;
Dries's avatar
 
Dries committed
570
  }
Dries's avatar
 
Dries committed
571

572
  $toc = array();
573
  // If the user has permission to create new books, add the top-level book page to the menu;
574
  if (user_access('create new books')) {
575
    $toc[0] = '<'. t('top-level') .'>';
Dries's avatar
 
Dries committed
576 577
  }

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

Dries's avatar
 
Dries committed
580 581 582
  return $toc;
}

583 584 585
/**
 * This is a helper function for book_tree()
 */
Dries's avatar
 
Dries committed
586
function book_tree_recurse($nid, $depth, $children, $unfold = array()) {
Dries's avatar
 
Dries committed
587
  if ($depth > 0) {
Dries's avatar
 
Dries committed
588 589
    if ($children[$nid]) {
      foreach ($children[$nid] as $foo => $node) {
Dries's avatar
 
Dries committed
590 591 592
        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
593
            $output .= l($node->title, 'node/'. $node->nid);
594
            $output .= '<ul class="menu">'. $tree .'</ul>';
Dries's avatar
 
Dries committed
595 596 597
            $output .= '</li>';
          }
          else {
Dries's avatar
 
Dries committed
598
            $output .= '<li class="leaf">'. l($node->title, 'node/'. $node->nid) .'</li>';
Dries's avatar
 
Dries committed
599 600 601 602
          }
        }
        else {
          if ($tree = book_tree_recurse($node->nid, 1, $children)) {
Dries's avatar
 
Dries committed
603
            $output .= '<li class="collapsed">'. l($node->title, 'node/'. $node->nid) .'</li>';
Dries's avatar
 
Dries committed
604 605
          }
          else {
Dries's avatar
 
Dries committed
606
            $output .= '<li class="leaf">'. l($node->title, 'node/'. $node->nid) .'</li>';
Dries's avatar
 
Dries committed
607
          }
Dries's avatar
 
Dries committed
608
        }
Dries's avatar
 
Dries committed
609 610
      }
    }
Dries's avatar
 
Dries committed
611 612 613 614 615
  }

  return $output;
}

616 617 618 619
/**
 * Returns an HTML nested list (wrapped in a menu-class div) representing the book nodes
 * as a tree.
 */
Dries's avatar
 
Dries committed
620
function book_tree($parent = 0, $depth = 3, $unfold = array()) {
621
  $result = db_query(db_rewrite_sql('SELECT n.nid, n.title, b.parent, b.weight FROM {node} n INNER JOIN {book} b ON n.vid = b.vid WHERE n.status = 1 AND n.moderate = 0 ORDER BY b.weight, n.title'));
Dries's avatar
 
Dries committed
622

Dries's avatar
 
Dries committed
623 624
  while ($node = db_fetch_object($result)) {
    $list = $children[$node->parent] ? $children[$node->parent] : array();
625
    $list[] = $node;
Dries's avatar
 
Dries committed
626
    $children[$node->parent] = $list;
Dries's avatar
 
Dries committed
627
  }
Dries's avatar
 
Dries committed
628

Dries's avatar
 
Dries committed
629
  if ($tree = book_tree_recurse($parent, $depth, $children, $unfold)) {
630
    return '<ul class="menu">'. $tree .'</ul>';
Dries's avatar
 
Dries committed
631
  }
Dries's avatar
 
Dries committed
632 633
}

634
/**
Dries's avatar
Dries committed
635
 * Menu callback; prints a listing of all books.
636
 */
Dries's avatar
 
Dries committed
637
function book_render() {
638
  $result = db_query(db_rewrite_sql('SELECT n.nid, n.title, b.weight FROM {node} n INNER JOIN {book} b ON n.vid = b.vid WHERE b.parent = 0 AND n.status = 1 AND n.moderate = 0 ORDER BY b.weight, n.title'));
Dries's avatar
 
Dries committed
639

640 641 642 643 644 645
  $books = array();
  while ($node = db_fetch_object($result)) {
    $books[] = l($node->title, 'node/'. $node->nid);
  }

  return theme('item_list', $books);
Dries's avatar
 
Dries committed
646 647
}

648
/**
649 650 651
 * Menu callback; Generates various representation of a book page with
 * all descendants and prints the requested representation to output.
 *
652 653
 * The function delegates the generation of output to helper functions.
 * The function name is derived by prepending 'book_export_' to the
654
 * given output type. So, e.g., a type of 'html' results in a call to
655
 * the function book_export_html().
656 657 658
 *
 * @param type
 *   - a string encoding the type of output requested.
659 660 661
 *       The following types are currently supported in book module
 *          html: HTML (printer friendly output)
 *       Other types are supported in contributed modules.
662 663 664
 * @param nid
 *   - an integer representing the node id (nid) of the node to export
 *
665
 */
666
function book_export($type = 'html', $nid = 0) {
667
  $type = drupal_strtolower($type);
668 669 670 671 672
  $node_result = db_query(db_rewrite_sql('SELECT n.nid, n.title, b.parent FROM {node} n INNER JOIN {book} b ON n.vid = b.vid WHERE n.nid = %d'), $nid);
  if (db_num_rows($node_result) > 0) {
      $node = db_fetch_object($node_result);
  }
  $depth = count(book_location($node)) + 1;
673 674 675 676
  $export_function = 'book_export_' . $type;

  if (function_exists($export_function)) {
    print call_user_func($export_function, $nid, $depth);
677 678
  }
  else {
679
    drupal_set_message(t('Unknown export format.'));
680
    drupal_not_found();
681
  }
682
}
Dries's avatar
 
Dries committed
683

684 685 686 687
/**
 * This function is called by book_export() to generate HTML for export.
 *
 * The given node is /embedded to its absolute depth in a top level
688
 * section/. For example, a child node with depth 2 in the hierarchy
689
 * is contained in (otherwise empty) &lt;div&gt; elements
690
 * corresponding to depth 0 and depth 1. This is intended to support
691 692 693 694 695 696 697 698 699 700 701 702 703 704 705 706 707 708 709 710 711 712 713 714 715 716 717 718 719 720 721 722 723 724 725 726 727 728
 * WYSIWYG output - e.g., level 3 sections always look like level 3
 * sections, no matter their depth relative to the node selected to be
 * exported as printer-friendly HTML.
 *
 * @param nid
 *   - an integer representing the node id (nid) of the node to export
 * @param depth
 * - an integer giving the depth in the book hierarchy of the node
       which is to be exported
 * @return
 * - string containing HTML representing the node and its children in
       the book hierarchy
*/
function book_export_html($nid, $depth) {
  if (user_access('see printer-friendly version')) {
    global $base_url;
    for ($i = 1; $i < $depth; $i++) {
      $output .= "<div class=\"section-$i\">\n";
    }
    $output .= book_recurse($nid, $depth, 'book_node_visitor_html_pre', 'book_node_visitor_html_post');
    for ($i = 1; $i < $depth; $i++) {
      $output .= "</div>\n";

    }
    $html = "<!DOCTYPE html PUBLIC \"-//W3C//DTD XHTML 1.0 Transitional//EN\" \"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd\">\n";
    $html .= '<html xmlns="http://www.w3.org/1999/xhtml" lang="en" xml:lang="en">';
    $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";
    return $html;
  }
  else {
    drupal_access_denied();
  }
}

729 730 731 732 733 734 735 736 737 738 739 740 741 742 743 744 745
/**
 * How the book's HTML export should be themed
 *
 * @ingroup themeable
 */
function theme_book_export_html($title, $content) {
  global $base_url;
  $html = "<!DOCTYPE html PUBLIC \"-//W3C//DTD XHTML 1.0 Transitional//EN\" \"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd\">\n";
  $html .= '<html xmlns="http://www.w3.org/1999/xhtml" lang="en" xml:lang="en">';
  $html .= "<head>\n<title>". $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". $content . "\n</body>\n</html>\n";
  return $html;
}

746
/**
747
 * Traverses the book tree. Applies the $visit_pre() callback to each
748
 * node, is called recursively for each child of the node (in weight,
749
 * title order). Finally appends the output of the $visit_post()
750 751 752 753 754 755 756 757 758 759 760 761 762 763 764
 * 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) {
765
  $result = db_query(db_rewrite_sql('SELECT n.nid, n.title, b.weight FROM {node} n INNER JOIN {book} b ON n.vid = b.vid WHERE n.status = 1 AND n.nid = %d AND n.moderate = 0 ORDER BY b.weight, n.title'), $nid);
Dries's avatar
 
Dries committed
766
  while ($page = db_fetch_object($result)) {
Dries's avatar
 
Dries committed
767
    // Load the node:
768
    $node = node_load($page->nid);
Dries's avatar
 
Dries committed
769

Dries's avatar
 
Dries committed
770
    if ($node) {
771 772
      if (function_exists($visit_pre)) {
        $output .= call_user_func($visit_pre, $node, $depth, $nid);
Dries's avatar
 
Dries committed
773
      }
774 775
      else {
        $output .= book_node_visitor_html_pre($node, $depth, $nid);
Dries's avatar
 
Dries committed
776
      }
Dries's avatar
 
Dries committed
777

778
      $children = db_query(db_rewrite_sql('SELECT n.nid, n.title, b.weight FROM {node} n INNER JOIN {book} b ON n.vid = b.vid WHERE n.status = 1 AND b.parent = %d AND n.moderate = 0 ORDER BY b.weight, n.title'), $node->nid);
779
      while ($childpage = db_fetch_object($children)) {
780
          $childnode = node_load($childpage->nid);
781
          if ($childnode->nid != $node->nid) {
782
              $output .= book_recurse($childnode->nid, $depth + 1, $visit_pre, $visit_post);
783 784 785
          }
      }
      if (function_exists($visit_post)) {
786
        $output .= call_user_func($visit_post, $node, $depth);
787
      }
788 789
      else {
        # default
790
        $output .= book_node_visitor_html_post($node, $depth);
791
      }
Dries's avatar
 
Dries committed
792
    }
Dries's avatar
 
Dries committed
793
  }
Dries's avatar
 
Dries committed
794

Dries's avatar
 
Dries committed
795 796
  return $output;
}
Dries's avatar
 
Dries committed
797

798
/**
799
 * Generates printer-friendly HTML for a node. This function
800 801 802 803 804 805 806 807 808 809 810 811 812
 * 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.
 */
813
function book_node_visitor_html_pre($node, $depth, $nid) {
814 815 816 817 818
  // Output the content:
  if (node_hook($node, 'content')) {
    $node = node_invoke($node, 'content');
  }
  // Allow modules to change $node->body before viewing.
819
  node_invoke_nodeapi($node, 'print', $node->body, false);
820

821 822
  $output .= "<div id=\"node-". $node->nid ."\" class=\"section-$depth\">\n";
  $output .= "<h1 class=\"book-heading\">". check_plain($node->title) ."</h1>\n";
823 824 825 826 827 828 829 830 831 832 833 834

  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().
 */
835
function book_node_visitor_html_post($node, $depth) {
836 837 838
  return "</div>\n";
}

839 840 841 842 843 844 845 846 847 848 849
function _book_admin_table($nodes = array()) {
  $form = array(
    '#theme' => 'book_admin_table',
    '#tree' => TRUE,
  );

  foreach ($nodes as $node) {
    $form = array_merge($form, _book_admin_table_tree($node, 0));
  }

  return $form;
850 851
}

852 853 854 855 856 857 858 859 860 861 862 863 864 865 866 867
function _book_admin_table_tree($node, $depth) {
  $form = array();

  $form[] = array(
    'nid' => array('#type' => 'value', '#value' => $node->nid),
    'depth' => array('#type' => 'value', '#value' => $depth),
    'title' => array(
      '#type' => 'textfield',
      '#default_value' => $node->title,
      '#maxlength' => 255,
    ),
    'weight' => array(
      '#type' => 'weight',
      '#default_value' => $node->weight,
      '#delta' => 15,
    ),
868
  );
869 870 871 872 873 874 875

  $children = db_query(db_rewrite_sql('SELECT n.nid, b.weight FROM {node} n INNER JOIN {book} b ON n.vid = b.vid WHERE b.parent = %d ORDER BY b.weight, n.title'), $node->nid);
  while ($child = db_fetch_object($children)) {
    $form = array_merge($form, _book_admin_table_tree(node_load($child->nid), $depth + 1));
  }

  return $form;
Dries's avatar
 
Dries committed
876 877
}

878 879
function theme_book_admin_table($form) {
  $header = array(t('Title'), t('Weight'), array('data' => t('Operations'), 'colspan' => '3'));
Dries's avatar
 
Dries committed
880

881
  $rows = array();
882 883 884 885 886 887 888 889 890
  foreach (element_children($form) as $key) {
    $nid = $form[$key]['nid']['#value'];
    $rows[] = array(
      '<div style="padding-left: '. (25 * $form[$key]['depth']['#value']) .'px;">'. form_render($form[$key]['title']) .'</div>',
      form_render($form[$key]['weight']),
      l(t('view'), 'node/'. $nid),
      l(t('edit'), 'node/'. $nid .'/edit'),
      l(t('delete'), 'node/'. $nid .'/delete')
    );
Dries's avatar
 
Dries committed
891 892
  }

893
  return theme('table', $header, $rows);
Dries's avatar
 
Dries committed
894 895
}

896 897 898
/**
 * Display an administrative view of the hierarchy of a book.
 */
899
function book_admin_edit($nid) {
900
  $node = node_load($nid);
901
  if ($node->nid) {
902
    drupal_set_title(check_plain($node->title));
903
    $form = array();
904

905 906 907 908 909
    $form['table'] = _book_admin_table(array($node));
    $form['save'] = array(
      '#type' => 'submit',
      '#value' => t('Save book pages'),
    );
Dries's avatar
 
Dries committed
910

911
    return drupal_get_form('book_admin_edit', $form);
Dries's avatar
 
Dries committed
912
  }
913 914 915
  else {
    drupal_not_found();
  }
Dries's avatar
 
Dries committed
916 917
}

918
/**
919
 * Menu callback; displays a listing of all orphaned book pages.
920
 */