book.module 39.4 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('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
      $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.')));
64
65
      $links[] = l(t('export DocBook XML'), 'book/export/docbook/'. $node->nid, array('title' => t('Export this book page and its sub-pages as DocBook XML.')));
      $links[] = l(t('export OPML'), 'book/export/opml/'. $node->nid, array('title' => t('Export this book page and its sub-pages as OPML.')));
Dries's avatar
   
Dries committed
66
    }
Dries's avatar
   
Dries committed
67
68
  }

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

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

Dries's avatar
   
Dries committed
78
  if ($may_cache) {
79
80
81
82
83
84
85
86
87
    $items[] = array(
      'path' => 'book',
      'title' => t('books'),
      'access' => user_access('access content'),
      'type' => MENU_NORMAL_ITEM,
      'weight' => 5);
    $items[] = array(
      'path' => 'node/add/book',
      'title' => t('book page'),
88
      'access' => user_access('create book pages'));
89
90
91
    $items[] = array(
      'path' => 'admin/node/book',
      'title' => t('books'),
Dries's avatar
   
Dries committed
92
93
      'callback' => 'book_admin',
      'access' => user_access('administer nodes'),
94
95
96
97
98
99
      'type' => MENU_LOCAL_TASK,
      'weight' => -1);
    $items[] = array(
      'path' => 'admin/node/book/list',
      'title' => t('list'),
      'type' => MENU_DEFAULT_LOCAL_TASK);
100
101
102
    $items[] = array(
      'path' => 'admin/node/book/orphan',
      'title' => t('orphan pages'),
Dries's avatar
   
Dries committed
103
      'callback' => 'book_admin_orphan',
104
      'type' => MENU_LOCAL_TASK,
Dries's avatar
   
Dries committed
105
106
107
108
109
      'weight' => 8);
    $items[] = array('path' => 'book', 'title' => t('books'),
      'callback' => 'book_render',
      'access' => user_access('access content'),
      'type' => MENU_SUGGESTED_ITEM);
110
    $items[] = array(
111
112
      'path' => 'book/export',
      'callback' => 'book_export',
Dries's avatar
   
Dries committed
113
114
      'access' => user_access('access content'),
      'type' => MENU_CALLBACK);
Dries's avatar
   
Dries committed
115
  }
Dries's avatar
   
Dries committed
116
117
118
119
120
  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:
121
      $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
122
123
124
125
126
127
128
      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
129
130
131
132

  return $items;
}

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

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

165
166
    return $block;
  }
Dries's avatar
   
Dries committed
167
168
}

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

175
  $book = db_fetch_object(db_query('SELECT parent, weight FROM {book} WHERE vid = %d', $node->vid));
Dries's avatar
   
Dries committed
176

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

Dries's avatar
   
Dries committed
190
  return $book;
Dries's avatar
   
Dries committed
191
192
}

193
194
195
/**
 * Implementation of hook_insert().
 */
Dries's avatar
   
Dries committed
196
function book_insert($node) {
197
  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
198
}
Dries's avatar
   
Dries committed
199

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

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

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

  node_validate_title($node);
Dries's avatar
   
Dries committed
230
231
}

232
233
234
/**
 * Implementation of hook_form().
 */
Dries's avatar
   
Dries committed
235
function book_form(&$node) {
236
237
  $output = form_textfield(t('Title'), 'title', $node->title, 60, 128, NULL, NULL, TRUE);
  $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
238

239
240
  if (function_exists('taxonomy_node_form')) {
    $output .= implode('', taxonomy_node_form('book', $node));
241
242
  }

243
  $output .= form_textarea(t('Body'), 'body', $node->body, 60, 20, '', NULL, TRUE);
244
  $output .= filter_form('format', $node->format);
245

Dries's avatar
   
Dries committed
246
  $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
247

248
  if (user_access('administer nodes')) {
Dries's avatar
   
Dries committed
249
    $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
250
251
  }
  else {
Dries's avatar
   
Dries committed
252
253
    // If a regular user updates a book page, we create a new revision
    // authored by that user:
254
    $output .= form_hidden('revision', 1);
Dries's avatar
   
Dries committed
255
256
257
258
259
  }

  return $output;
}

260
/**
Dries's avatar
   
Dries committed
261
262
 * Implementation of function book_outline()
 * Handles all book outline operations.
263
 */
Dries's avatar
   
Dries committed
264
function book_outline() {
Dries's avatar
   
Dries committed
265

266
267
  $op = $_POST['op'];
  $edit = $_POST['edit'];
268
  $node = node_load(arg(1));
Dries's avatar
   
Dries committed
269

Dries's avatar
   
Dries committed
270
271
272
  if ($node->nid) {
    switch ($op) {
      case t('Add to book outline'):
273
274
        db_query('INSERT INTO {book} (nid, vid, parent, weight) VALUES (%d, %d, %d, %d)', $node->nid, $node->vid, $edit['parent'], $edit['weight']);
        db_query("UPDATE {node_revisions} SET log = '%s' WHERE vid = %d", $edit['log'], $node->vid);
275
        drupal_set_message(t('The post has been added to the book.'));
Dries's avatar
   
Dries committed
276
277
278
279
        drupal_goto("node/$node->nid");
        break;

      case t('Update book outline'):
280
281
        db_query('UPDATE {book} SET parent = %d, weight = %d WHERE vid = %d', $edit['parent'], $edit['weight'], $node->vid);
        db_query("UPDATE {node_revisions} SET log = '%s' WHERE vid = %d", $edit['log'], $node->vid);
282
        drupal_set_message(t('The book outline has been updated.'));
Dries's avatar
   
Dries committed
283
284
285
286
287
        drupal_goto("node/$node->nid");
        break;

      case t('Remove from book outline'):
        db_query('DELETE FROM {book} WHERE nid = %d', $node->nid);
288
        drupal_set_message(t('The post has been removed from the book.'));
Dries's avatar
   
Dries committed
289
290
291
292
        drupal_goto("node/$node->nid");
        break;

      default:
293
        $page = db_fetch_object(db_query('SELECT * FROM {book} WHERE vid = %d', $node->vid));
Dries's avatar
   
Dries committed
294
295

        $output  = form_select(t('Parent'), 'parent', $page->parent, book_toc($node->nid), t('The parent page in the book.'));
296
        $output .= form_weight(t('Weight'), 'weight', $page->weight, 15, t('Pages at a given level are ordered first by weight and then by title.'));
297
        $output .= form_textarea(t('Log message'), 'log', $node->log, 60, 5, t('An explanation to help other authors understand your motivations to put this post into the book.'));
Dries's avatar
   
Dries committed
298
299
300
301
302
303
304
305

        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
306

307
        drupal_set_title(check_plain($node->title));
Dries's avatar
   
Dries committed
308
        return form($output);
Dries's avatar
   
Dries committed
309
310
311
312
    }
  }
}

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

325
326
327
/**
 * Accumulates the nodes up to the root of the book from the given node in the $nodes array.
 */
Dries's avatar
   
Dries committed
328
function book_location_down($node, $nodes = array()) {
329
  $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
330
331
332
333
334
335
336
  if ($last_direct_child) {
    array_push($nodes, $last_direct_child);
    $nodes = book_location_down($last_direct_child, $nodes);
  }
  return $nodes;
}

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

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

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

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

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

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

382
383
384
385
386
387
/**
 * 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
388
function book_content($node, $teaser = FALSE) {
389
  $op = $_POST['op'];
Dries's avatar
   
Dries committed
390

391
392
  // Extract the page body.
  $node = node_prepare($node, $teaser);
Dries's avatar
   
Dries committed
393

Dries's avatar
   
Dries committed
394
395
396
  return $node;
}

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

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

Dries's avatar
   
Dries committed
422
423
424
          foreach ($book as $key => $value) {
            $node->$key = $value;
          }
425
          $node = theme('book_navigation', $node);
426
427
428
          if ($page) {
            menu_set_location($node->breadcrumb);
          }
Dries's avatar
   
Dries committed
429
        }
Dries's avatar
   
Dries committed
430
      }
Dries's avatar
   
Dries committed
431
      break;
Dries's avatar
   
Dries committed
432
  }
Dries's avatar
   
Dries committed
433
}
Dries's avatar
   
Dries committed
434

435
436
437
/**
 * Prepares both the custom breadcrumb trail and the forward/backward
 * navigation for a node presented as a book page.
438
439
 *
 * @ingroup themeable
440
 */
441
function theme_book_navigation($node) {
Dries's avatar
   
Dries committed
442
  $path = book_location($node);
Dries's avatar
   
Dries committed
443

Dries's avatar
   
Dries committed
444
  // Construct the breadcrumb:
Dries's avatar
   
Dries committed
445

Dries's avatar
   
Dries committed
446
  $node->breadcrumb = array(); // Overwrite the trail with a book trail.
Dries's avatar
   
Dries committed
447
  foreach ($path as $level) {
Dries's avatar
   
Dries committed
448
    $node->breadcrumb[] = array('path' => 'node/'. $level->nid, 'title' =>  $level->title);
Dries's avatar
   
Dries committed
449
  }
Dries's avatar
   
Dries committed
450
  $node->breadcrumb[] = array('path' => 'node/'. $node->nid);
Dries's avatar
   
Dries committed
451

Dries's avatar
   
Dries committed
452
  if ($node->nid) {
453
    $output .= '<div class="book">';
Dries's avatar
   
Dries committed
454
455

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

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

483
    $output .= '<div class="nav">';
Dries's avatar
   
Dries committed
484
485
    $output .= ' <div class="links">'. $links .'</div>';
    $output .= ' <div class="titles">'. $titles .'</div>';
486
487
    $output .= '</div>';
    $output .= '</div>';
Dries's avatar
   
Dries committed
488
  }
Dries's avatar
   
Dries committed
489

Dries's avatar
   
Dries committed
490
  $node->body = $node->body.$output;
Dries's avatar
   
Dries committed
491

Dries's avatar
   
Dries committed
492
  return $node;
Dries's avatar
   
Dries committed
493
}
Dries's avatar
 
Dries committed
494

495
496
497
/**
 * This is a helper function for book_toc().
 */
498
function book_toc_recurse($nid, $indent, $toc, $children, $exclude) {
Dries's avatar
   
Dries committed
499
500
  if ($children[$nid]) {
    foreach ($children[$nid] as $foo => $node) {
501
502
503
504
      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
505
506
507
508
509
510
    }
  }

  return $toc;
}

511
512
513
/**
 * Returns an array of titles and nid entries of book pages in table of contents order.
 */
514
function book_toc($exclude = 0) {
515
  $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
516

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

524
525
  $toc = array();

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

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

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

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

  return $output;
}

570
571
572
573
/**
 * Returns an HTML nested list (wrapped in a menu-class div) representing the book nodes
 * as a tree.
 */
Dries's avatar
   
Dries committed
574
function book_tree($parent = 0, $depth = 3, $unfold = array()) {
575
  $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
576

Dries's avatar
   
Dries committed
577
578
579
580
  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
581
  }
Dries's avatar
   
Dries committed
582

Dries's avatar
   
Dries committed
583
  if ($tree = book_tree_recurse($parent, $depth, $children, $unfold)) {
584
    return '<div class="menu"><ul>'. $tree .'</ul></div>';
Dries's avatar
   
Dries committed
585
  }
Dries's avatar
   
Dries committed
586
587
}

588
/**
Dries's avatar
Dries committed
589
 * Menu callback; prints a listing of all books.
590
 */
Dries's avatar
   
Dries committed
591
function book_render() {
592
  $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
593

594
595
596
597
598
599
  $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
600
601
}

602
/**
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
 * Menu callback; Generates various representation of a book page with
 * all descendants and prints the requested representation to output.
 *
 * Notes: For HTML output, the given node is /embedded to its absolute
 * depth in a top level section/.  For example, a child node with
 * depth 2 in the hierarchy is contained in (otherwise empty) <div>
 * elements corresponding to depth 0 and depth 1.  This is intended to
 * support 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.
 *
 * DocBook XML and OPML outputs do not attempt to embed a node to its
 * absolute level in the parent book.

 *    For DocBook output, the exported node will be a document fragment
 * unless the node is a level 0 node (book), specifically
 *    <ul>
 *      <li>a <chapter> for level 1 elements, </li>
 *      <li>a <section> for levels 2 and deeper.</li>
 *    </ul>
 *
 *    For OPML output, the exported node will be the top level element
 * in the OPML outline.
 *
 * @param type
 *   - a string encoding the type of output requested.
 *       The following types are supported:
 *          1) HTML (printer friendly output)
 *          2) DocBook XML
 *          3) OPML (Outline Processor Markup Language) outlines
 * @param nid
 *   - an integer representing the node id (nid) of the node to export
 *
636
 */
637
function book_export($type = 'html', $nid = FALSE) {
Dries's avatar
   
Dries committed
638
  global $base_url;
639
  $type = drupal_strtolower($type);
640
641
  $node = node_load($nid);
  if ($node) {
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
    $depth = _book_get_depth($nid);
    switch ($type) {
      case 'docbook':
        $xml = "<?xml version='1.0'?>\n";
        $xml .= "<!DOCTYPE book PUBLIC \"-//OASIS//DTD Docbook XML V4.1.2//EN\" \"http://www.oasis-open.org/docbook/xml/4.1.2/docbookx.dtd\">\n";
        $xml .= book_recurse($nid, $depth, 'book_node_visitor_xml_pre', 'book_node_visitor_xml_post');
        drupal_set_header('Content-Type: text/xml; charset=utf-8');
        print $xml;
        break;
      case 'html':
        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";
        print $html;
        break;
      case 'opml':
        $output .= book_recurse($nid, $depth, 'book_node_visitor_opml_pre', 'book_node_visitor_opml_post');
        $ompl  = "<?xml version='1.0'?>\n";
        $opml .= "<opml version='1.0'>\n";
        $opml .= "<head>\n<title>". check_plain($node->title) ."</title>\n";
        $opml .= "</head>\n<body>\n". $output . "\n</body>\n</opml>\n";
        drupal_set_header('Content-Type: text/xml; charset=utf-8');
        print $opml;
        break;
      default:
        drupal_not_found();
    }
  }
  else {
    drupal_not_found();
683
  }
684
}
Dries's avatar
   
Dries committed
685

686
/**
687
688
689
690
691
692
693
694
 * Given a node, this function returns the depth of the node in its hierarchy.
 * A root node has depth 1, and children of a node of depth n have depth (n+1).
 *
 * @param node
 *   - the node whose depth to compute.
 * @return
 *   - the depth of the given node in its hierarchy.  Returns 0 if the node
 *  does not exist or is not part of a book hierarchy.
695
 */
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
function _book_get_depth($nid) {
  $depth = 0;
  if ($nid) {
    while ($nid) {
      $result = db_query(db_rewrite_sql('SELECT b.parent FROM {book} b WHERE b.nid = %d'), $nid);
      $obj =  db_fetch_object($result);
      $parent = $obj->parent;
      if ($nid == $parent->parent) {
        $nid = 0;
      }
      else {
        $nid = $parent;
      }
      $depth++;
    }
    return $depth;
  }
  else {
    return 0;
  }
Dries's avatar
   
Dries committed
716
717
}

718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
/**
 * 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) {
737
  $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
738
  while ($page = db_fetch_object($result)) {
Dries's avatar
   
Dries committed
739
    // Load the node:
740
    $node = node_load($page->nid);
Dries's avatar
   
Dries committed
741

Dries's avatar
   
Dries committed
742
    if ($node) {
743
744
      if (function_exists($visit_pre)) {
        $output .= call_user_func($visit_pre, $node, $depth, $nid);
Dries's avatar
   
Dries committed
745
      }
746
747
      else {
        $output .= book_node_visitor_html_pre($node, $depth, $nid);
Dries's avatar
   
Dries committed
748
      }
Dries's avatar
   
Dries committed
749

750
      $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);
751
      while ($childpage = db_fetch_object($children)) {
752
          $childnode = node_load($childpage->nid);
753
754
755
756
757
          if ($childnode->nid != $node->nid) {
              $output .= book_recurse($childnode->nid, $depth+1, $visit_pre, $visit_post);
          }
      }
      if (function_exists($visit_post)) {
758
        $output .= call_user_func($visit_post, $node, $depth);
759
760
      }
      else { # default
761
        $output .= book_node_visitor_html_post($node, $depth);
762
      }
Dries's avatar
   
Dries committed
763
    }
Dries's avatar
   
Dries committed
764
  }
Dries's avatar
   
Dries committed
765

Dries's avatar
   
Dries committed
766
767
  return $output;
}
Dries's avatar
   
Dries committed
768

769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
/**
 * 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.
 */
784
function book_node_visitor_html_pre($node, $depth, $nid) {
785
786
787
788
789
  // Output the content:
  if (node_hook($node, 'content')) {
    $node = node_invoke($node, 'content');
  }
  // Allow modules to change $node->body before viewing.
790
  node_invoke_nodeapi($node, 'print', $node->body, false);
791

792
793
  $output .= "<div id=\"node-". $node->nid ."\" class=\"section-$depth\">\n";
  $output .= "<h1 class=\"book-heading\">". check_plain($node->title) ."</h1>\n";
794
795
796
797
798
799
800
801
802
803
804
805

  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().
 */
806
function book_node_visitor_html_post($node, $depth) {
807
808
809
810
811
  return "</div>\n";
}

/**
 * Generates XML for a given node. This function is a 'pre-node'
812
813
814
815
816
817
818
 * visitor function for book_recurse().  The generated XML is valid
 * DocBook, but each node's HTML content is wrapped in a CDATA
 * section, and put inside a <literallayout> element.  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.  The weight of a node is also stored as metadata to
 * allow the node to be properly re-imported.
819
820
821
822
823
824
825
826
 *
 * @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
827
 *   is used only for generating output (e.g., id attribute)
828
829
830
831
832
833
834
835
836
 * @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.
837
838
839
840
841
842
843
844
845
846
  node_invoke_nodeapi($node, 'export_xml', $node->body, false);

  $releaseinfo  = "<releaseinfo>\n";
  $releaseinfo .= "md5-hash:" . md5($node->body) . "\n";
  $releaseinfo .= "weight:". $node->weight . "\n";
  $releaseinfo .= "depth:". $depth . "\n";
  $releaseinfo .= "</releaseinfo>\n";

  $title = "<title>". check_plain($node->title) ."</title>\n";

847
  // wrap the node body in a CDATA declaration
848
849
  $content = "<literallayout>";
  $content .= "<![CDATA[";
850
  if ($node->body) {
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
    $content .= $node->body;
  }
  $content .= "]]>";
  $content .= "</literallayout>\n";

  if ($depth == 1) {
      $output .= "<book>\n";
      $output .= $title;
      $output .= "<bookinfo>\n$releaseinfo</bookinfo>\n";
      $output .= "<preface>\n";
      $output .= "<title>Preface</title>\n";
      $output .= $content;
      $output .= "</preface>\n";
  }
  else if ($depth == 2) {
      $output .= "<chapter id=\"node-".$node->nid ."\">\n";
      $output .= "<chapterinfo>\n$releaseinfo</chapterinfo>\n";
      $output .= $title;
      $output .= $content;
  }
  else {
      $output .= "<section id=\"node-".$node->nid ."\">\n";
      $output .= "<sectioninfo>\n$releaseinfo</sectioninfo>\n";
      $output .= $title;
      $output .= $content;
876
  }
877

878
879
880
881
  return $output;
}

/**
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
 * Completes the XML generation for the node. This function is a
 * 'post-node' visitor function for book_recurse().
 */
function book_node_visitor_xml_post($node, $depth) {
  if ($depth == 1) {
    return "</book>\n";
  }
  else if ($depth == 2) {
    return "</chapter>\n";
  }
  else {
    return "</section>\n";
  }
}

/**
 * Generates OPML 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 OPML generated for the given node.
 */
function book_node_visitor_opml_pre($node, $depth, $nid) {
  // Output the content:
  if (node_hook($node, 'content')) {
    $node = node_invoke($node, 'content');
  }

  $output .= "<outline type=\"id:node-". $node->nid ."\"\n";
  $text = check_plain($node->title);
  $output .= "text=\"$text\">\n";
  return $output;
}

/**
 * Finishes up generation of OPML after visiting a node. This function
 * is a 'post-node' visitor function for book_recurse().
927
 */
928
929
function book_node_visitor_opml_post($node, $depth) {
  return "</outline>\n";
930
931
932
933
934
}

/**
 * Creates a row for the 'admin' view of a book.  Each row represents a page in the book, in the tree representing the book
 */
935
function book_admin_edit_line($node, $depth = 0) {
936
  return array('<div style="padding-left: '. (25 * $depth) .'px;">'. form_textfield(NULL, $node->nid .'][title', $node->title, 60, 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
937
938
}

939
function book_admin_edit_book($nid, $depth = 1) {
940
  $result = 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'), $nid);
Dries's avatar
   
Dries committed
941

942
943
  $rows = array();

Dries's avatar
   
Dries committed
944
  while ($node = db_fetch_object($result)) {
945
    $node = node_load($node->nid);
946
947
    $rows[] = book_admin_edit_line($node, $depth);
    $rows = array_merge($rows, book_admin_edit_book($node->nid, $depth + 1));
Dries's avatar
   
Dries committed
948
949
  }

Dries's avatar
   
Dries committed
950
  return $rows;
Dries's avatar
   
Dries committed
951
952
}

953
954
955
/**
 * Display an administrative view of the hierarchy of a book.
 */
956
function book_admin_edit($nid, $depth = 0) {
957
  $node = node_load($nid);
958
  if ($node->nid) {
Dries's avatar
   
Dries committed
959
    $header = array(t('Title'), t('Weight'), array('data' => t('Operations'), 'colspan' => '3'));
960
961
    $rows[] = book_admin_edit_line($node);
    $rows = array_merge($rows, book_admin_edit_book($nid));
Dries's avatar
   
Dries committed
962

Dries's avatar
   
Dries committed
963
964
    $output .= theme('table', $header, $rows);
    $output .= form_submit(t('Save book pages'));
Dries's avatar
   
Dries committed
965

966
    drupal_set_title(check_plain($node->title));
Dries's avatar
   
Dries committed
967
968
    return form($output);
  }
969
970
971
  else {
    drupal_not_found();
  }
Dries's avatar
   
Dries committed
972
973
}

974
975
976
/**
 * Saves the changes to a book made by an administrator in the book admin view.
 */
Dries's avatar
   
Dries committed
977
function book_admin_save($nid, $edit = array()) {
Dries's avatar
   
Dries committed
978
  if ($nid) {
979
    $book = node_load($nid);
Dries's avatar
   
Dries committed
980

Dries's avatar
   
Dries committed
981
    foreach ($edit as $nid => $value) {
Dries's avatar
   
Dries committed
982
      // Check to see whether the title needs updating:
983
984
      $node = db_fetch_object(db_query('SELECT title, vid FROM {node} WHERE nid = %d', $nid));
      if ($node->title != $value['title']) {
Dries's avatar
   
Dries committed
985
        db_query("UPDATE {node} SET title = '%s' WHERE nid = %d", $value['title'], $nid);
986
        db_query("UPDATE {book} SET title = '%s' WHERE vid = %d", $value['title'], $node->vid);
Dries's avatar
   
Dries committed
987
      }
Dries's avatar
   
Dries committed
988

Dries's avatar
   
Dries committed
989
      // Check to see whether the weight needs updating:
990
991
992
      $node = db_fetch_object(db_query('SELECT b.vid, b.weight FROM {book} b INNER JOIN {node} n ON n.vid = b.vid WHERE n.nid = %d', $nid));
      if ($node->weight != $value['weight']) {
        db_query('UPDATE {book} SET weight = %d WHERE vid = %d', $value['weight'], $node->vid);
Dries's avatar
   
Dries committed
993
      }
Dries's avatar
   
Dries committed
994
995
    }

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

Dries's avatar
   
Dries committed
999
1000
    return $message;
  }
Dries's avatar
   
Dries committed
1001
1002
}

1003
/**
Dries's avatar
Dries committed
1004
 * Menu callback; displays a listing of all orphaned book pages.
1005
 */
Dries's avatar
   
Dries committed
1006
function book_admin_orphan() {
1007
  $result = db_query(db_rewrite_sql('SELECT n.nid, n.title, n.status, b.parent FROM {node} n INNER JOIN {book} b ON n.vid = b.vid'));
Dries's avatar