book.module 40.2 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
205
206
207
208
209
  if ($node->revision) {
    db_query("INSERT INTO {book} (nid, vid, parent, weight) VALUES (%d, %d, %d, %d, '%s')", $node->nid, $node->vid, $node->parent, $node->weight);
  }
  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;
  }
Dries's avatar
   
Dries committed
228
229
}

230
231
232
/**
 * Implementation of hook_form().
 */
Dries's avatar
   
Dries committed
233
function book_form(&$node) {
Dries's avatar
   
Dries committed
234
  $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
235

236
237
  if (function_exists('taxonomy_node_form')) {
    $output .= implode('', taxonomy_node_form('book', $node));
238
239
  }

240
  $output .= form_textarea(t('Body'), 'body', $node->body, 60, 20, '', NULL, TRUE);
241
  $output .= filter_form('format', $node->format);
242

Dries's avatar
   
Dries committed
243
  $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
244

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

  return $output;
}

257
/**
Dries's avatar
   
Dries committed
258
259
 * Implementation of function book_outline()
 * Handles all book outline operations.
260
 */
Dries's avatar
   
Dries committed
261
function book_outline() {
Dries's avatar
   
Dries committed
262

263
264
  $op = $_POST['op'];
  $edit = $_POST['edit'];
265
  $node = node_load(arg(1));
Dries's avatar
   
Dries committed
266

Dries's avatar
   
Dries committed
267
268
269
  if ($node->nid) {
    switch ($op) {
      case t('Add to book outline'):
270
271
        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);
272
        drupal_set_message(t('The post has been added to the book.'));
Dries's avatar
   
Dries committed
273
274
275
276
        drupal_goto("node/$node->nid");
        break;

      case t('Update book outline'):
277
278
        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);
279
        drupal_set_message(t('The book outline has been updated.'));
Dries's avatar
   
Dries committed
280
281
282
283
284
        drupal_goto("node/$node->nid");
        break;

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

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

        $output  = form_select(t('Parent'), 'parent', $page->parent, book_toc($node->nid), t('The parent page in the book.'));
293
        $output .= form_weight(t('Weight'), 'weight', $page->weight, 15, t('Pages at a given level are ordered first by weight and then by title.'));
294
        $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
295
296
297
298
299
300
301
302

        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
303

304
        drupal_set_title(check_plain($node->title));
Dries's avatar
   
Dries committed
305
        return form($output);
Dries's avatar
   
Dries committed
306
307
308
309
    }
  }
}

Dries's avatar
   
Dries committed
310

311
/**
312
 * Return the most recent revision that matches the specified conditions.
313
 */
Dries's avatar
   
Dries committed
314
315
316
317
318
319
function book_revision_load($page, $conditions = array()) {

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

  foreach ($revisions as $revision) {

Dries's avatar
   
Dries committed
320
    // Extract the specified revision:
Dries's avatar
   
Dries committed
321
322
    $node = node_revision_load($page, $revision);

Dries's avatar
   
Dries committed
323
324
    // Check to see if the conditions are met:
    $status = TRUE;
Dries's avatar
   
Dries committed
325
326
327

    foreach ($conditions as $key => $value) {
      if ($node->$key != $value) {
Dries's avatar
   
Dries committed
328
        $status = FALSE;
Dries's avatar
   
Dries committed
329
330
331
332
333
334
335
336
337
      }
    }

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

338
339
340
/**
 * Return the path (call stack) to a certain book page.
 */
Dries's avatar
   
Dries committed
341
function book_location($node, $nodes = array()) {
342
  $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
343
344
345
346
347
348
349
  if ($parent->title) {
    $nodes = book_location($parent, $nodes);
    array_push($nodes, $parent);
  }
  return $nodes;
}

350
351
352
/**
 * Accumulates the nodes up to the root of the book from the given node in the $nodes array.
 */
Dries's avatar
   
Dries committed
353
function book_location_down($node, $nodes = array()) {
354
  $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
355
356
357
358
359
360
361
  if ($last_direct_child) {
    array_push($nodes, $last_direct_child);
    $nodes = book_location_down($last_direct_child, $nodes);
  }
  return $nodes;
}

362
/**
363
 * Fetches the node object of the previous page of the book.
364
 */
Dries's avatar
   
Dries committed
365
function book_prev($node) {
Dries's avatar
   
Dries committed
366
  // If the parent is zero, we are at the start of a book so there is no previous.
Dries's avatar
   
Dries committed
367
368
369
370
  if ($node->parent == 0) {
    return NULL;
  }

Dries's avatar
   
Dries committed
371
  // Previous on the same level:
372
  $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
373
  if ($direct_above) {
Dries's avatar
   
Dries committed
374
    // Get last leaf of $above.
Dries's avatar
   
Dries committed
375
    $path = book_location_down($direct_above);
Dries's avatar
   
Dries committed
376
377

    return $path ? (count($path) > 0 ? array_pop($path) : NULL) : $direct_above;
Dries's avatar
   
Dries committed
378
379
  }
  else {
Dries's avatar
   
Dries committed
380
    // Direct parent:
381
    $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
382
383
384
385
    return $prev;
  }
}

386
/**
387
 * Fetches the node object of the next page of the book.
388
 */
Dries's avatar
   
Dries committed
389
390
function book_next($node) {
  // get first direct child
391
  $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
392
393
394
395
  if ($child) {
    return $child;
  }

Dries's avatar
   
Dries committed
396
397
  // 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.
398

Dries's avatar
   
Dries committed
399
  while (($leaf = array_pop($path)) && count($path)) {
400
    $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
401
402
403
404
405
406
    if ($next) {
      return $next;
    }
  }
}

407
408
409
410
411
412
/**
 * 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
413
function book_content($node, $teaser = FALSE) {
414
  $op = $_POST['op'];
Dries's avatar
   
Dries committed
415

Dries's avatar
   
Dries committed
416
417
418
  // 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.
419
420
  if ($op != t('Preview') && $node->moderate && arg(0) != 'queue') {
    $revision = book_revision_load($node, array('moderate' => 0, 'status' => 1));
Dries's avatar
   
Dries committed
421
422
423
424

    if ($revision) {
      $node = $revision;
    }
Dries's avatar
   
Dries committed
425
426
  }

427
428
  // Extract the page body.
  $node = node_prepare($node, $teaser);
Dries's avatar
   
Dries committed
429

Dries's avatar
   
Dries committed
430
431
432
  return $node;
}

433
434
435
436
437
438
/**
 * 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
439
function book_view(&$node, $teaser = FALSE, $page = FALSE) {
Dries's avatar
   
Dries committed
440
  $node = book_content($node, $teaser);
Dries's avatar
   
Dries committed
441
442
}

443
/**
Dries's avatar
   
Dries committed
444
445
446
 * Implementation of hook_nodeapi().
 *
 * Appends book navigation to all nodes in the book.
447
 */
Dries's avatar
   
Dries committed
448
449
450
451
function book_nodeapi(&$node, $op, $teaser, $page) {
  switch ($op) {
    case 'view':
      if (!$teaser) {
452
        $book = db_fetch_array(db_query('SELECT * FROM {book} WHERE vid = %d', $node->vid));
Dries's avatar
   
Dries committed
453
        if ($book) {
454
          if ($node->moderate && user_access('administer nodes')) {
455
            drupal_set_message(t("The post has been submitted for moderation and won't be accessible until it has been approved."));
456
457
          }

Dries's avatar
   
Dries committed
458
459
460
          foreach ($book as $key => $value) {
            $node->$key = $value;
          }
461
          $node = theme('book_navigation', $node);
462
463
464
          if ($page) {
            menu_set_location($node->breadcrumb);
          }
Dries's avatar
   
Dries committed
465
        }
Dries's avatar
   
Dries committed
466
      }
Dries's avatar
   
Dries committed
467
      break;
Dries's avatar
   
Dries committed
468
  }
Dries's avatar
   
Dries committed
469
}
Dries's avatar
   
Dries committed
470

471
472
473
/**
 * Prepares both the custom breadcrumb trail and the forward/backward
 * navigation for a node presented as a book page.
474
475
 *
 * @ingroup themeable
476
 */
477
function theme_book_navigation($node) {
Dries's avatar
   
Dries committed
478
  $path = book_location($node);
Dries's avatar
   
Dries committed
479

Dries's avatar
   
Dries committed
480
  // Construct the breadcrumb:
Dries's avatar
   
Dries committed
481

Dries's avatar
   
Dries committed
482
  $node->breadcrumb = array(); // Overwrite the trail with a book trail.
Dries's avatar
   
Dries committed
483
  foreach ($path as $level) {
Dries's avatar
   
Dries committed
484
    $node->breadcrumb[] = array('path' => 'node/'. $level->nid, 'title' =>  $level->title);
Dries's avatar
   
Dries committed
485
  }
Dries's avatar
   
Dries committed
486
  $node->breadcrumb[] = array('path' => 'node/'. $node->nid);
Dries's avatar
   
Dries committed
487

Dries's avatar
   
Dries committed
488
  if ($node->nid) {
489
    $output .= '<div class="book">';
Dries's avatar
   
Dries committed
490
491

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

Dries's avatar
   
Dries committed
495
    if ($prev = book_prev($node)) {
496
      $links .= '<div class="prev">';
Dries's avatar
   
Dries committed
497
      $links .= l(t('previous'), 'node/'. $prev->nid, array('title' => t('View the previous page.')));
498
      $links .= '</div>';
499
      $titles .= '<div class="prev">'. check_plain($prev->title) .'</div>';
Dries's avatar
   
Dries committed
500
501
    }
    else {
Dries's avatar
   
Dries committed
502
      $links .= '<div class="prev">&nbsp;</div>'; // Make an empty div to fill the space.
Dries's avatar
   
Dries committed
503
504
    }
    if ($next = book_next($node)) {
505
      $links .= '<div class="next">';
Dries's avatar
   
Dries committed
506
      $links .= l(t('next'), 'node/'. $next->nid, array('title' => t('View the next page.')));
507
      $links .= '</div>';
508
      $titles .= '<div class="next">'. check_plain($next->title) .'</div>';
Dries's avatar
   
Dries committed
509
510
    }
    else {
Dries's avatar
   
Dries committed
511
      $links .= '<div class="next">&nbsp;</div>'; // Make an empty div to fill the space.
Dries's avatar
   
Dries committed
512
513
    }
    if ($node->parent) {
514
      $links .= '<div class="up">';
Dries's avatar
   
Dries committed
515
      $links .= l(t('up'), 'node/'. $node->parent, array('title' => t('View this page\'s parent section.')));
516
      $links .= '</div>';
Dries's avatar
   
Dries committed
517
    }
Dries's avatar
   
Dries committed
518

519
    $output .= '<div class="nav">';
Dries's avatar
   
Dries committed
520
521
    $output .= ' <div class="links">'. $links .'</div>';
    $output .= ' <div class="titles">'. $titles .'</div>';
522
523
    $output .= '</div>';
    $output .= '</div>';
Dries's avatar
   
Dries committed
524
  }
Dries's avatar
   
Dries committed
525

Dries's avatar
   
Dries committed
526
  $node->body = $node->body.$output;
Dries's avatar
   
Dries committed
527

Dries's avatar
   
Dries committed
528
  return $node;
Dries's avatar
   
Dries committed
529
}
Dries's avatar
 
Dries committed
530

531
532
533
/**
 * This is a helper function for book_toc().
 */
534
function book_toc_recurse($nid, $indent, $toc, $children, $exclude) {
Dries's avatar
   
Dries committed
535
536
  if ($children[$nid]) {
    foreach ($children[$nid] as $foo => $node) {
537
538
539
540
      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
541
542
543
544
545
546
    }
  }

  return $toc;
}

547
548
549
/**
 * Returns an array of titles and nid entries of book pages in table of contents order.
 */
550
function book_toc($exclude = 0) {
551
  $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
552

Dries's avatar
   
Dries committed
553
  while ($node = db_fetch_object($result)) {
Dries's avatar
   
Dries committed
554
555
556
557
    if (!$children[$node->parent]) {
      $children[$node->parent] = array();
    }
    array_push($children[$node->parent], $node);
Dries's avatar
   
Dries committed
558
  }
Dries's avatar
   
Dries committed
559

560
561
  $toc = array();

Dries's avatar
   
Dries committed
562
563
  // If the user is an administrator, add the top-level book page;
  // only administrators can start new books.
564
565
  if (user_access('administer nodes')) {
    $toc[0] = '<'. t('top-level') .'>';
Dries's avatar
   
Dries committed
566
567
  }

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

Dries's avatar
   
Dries committed
570
571
572
  return $toc;
}

573
574
575
/**
 * This is a helper function for book_tree()
 */
Dries's avatar
   
Dries committed
576
function book_tree_recurse($nid, $depth, $children, $unfold = array()) {
Dries's avatar
   
Dries committed
577
  if ($depth > 0) {
Dries's avatar
   
Dries committed
578
579
    if ($children[$nid]) {
      foreach ($children[$nid] as $foo => $node) {
Dries's avatar
   
Dries committed
580
581
582
        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
583
584
            $output .= l($node->title, 'node/'. $node->nid);
            $output .= '<ul>'. $tree .'</ul>';
Dries's avatar
   
Dries committed
585
586
587
            $output .= '</li>';
          }
          else {
Dries's avatar
   
Dries committed
588
            $output .= '<li class="leaf">'. l($node->title, 'node/'. $node->nid) .'</li>';
Dries's avatar
   
Dries committed
589
590
591
592
          }
        }
        else {
          if ($tree = book_tree_recurse($node->nid, 1, $children)) {
Dries's avatar
   
Dries committed
593
            $output .= '<li class="collapsed">'. l($node->title, 'node/'. $node->nid) .'</li>';
Dries's avatar
   
Dries committed
594
595
          }
          else {
Dries's avatar
   
Dries committed
596
            $output .= '<li class="leaf">'. l($node->title, 'node/'. $node->nid) .'</li>';
Dries's avatar
   
Dries committed
597
          }
Dries's avatar
   
Dries committed
598
        }
Dries's avatar
   
Dries committed
599
600
      }
    }
Dries's avatar
   
Dries committed
601
602
603
604
605
  }

  return $output;
}

606
607
608
609
/**
 * Returns an HTML nested list (wrapped in a menu-class div) representing the book nodes
 * as a tree.
 */
Dries's avatar
   
Dries committed
610
function book_tree($parent = 0, $depth = 3, $unfold = array()) {
611
  $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
612

Dries's avatar
   
Dries committed
613
614
615
616
  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
617
  }
Dries's avatar
   
Dries committed
618

Dries's avatar
   
Dries committed
619
  if ($tree = book_tree_recurse($parent, $depth, $children, $unfold)) {
620
    return '<div class="menu"><ul>'. $tree .'</ul></div>';
Dries's avatar
   
Dries committed
621
  }
Dries's avatar
   
Dries committed
622
623
}

624
/**
Dries's avatar
Dries committed
625
 * Menu callback; prints a listing of all books.
626
 */
Dries's avatar
   
Dries committed
627
function book_render() {
628
  $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
629

630
631
632
633
634
635
  $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
636
637
}

638
/**
639
640
641
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
 * 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
 *
672
 */
673
function book_export($type = 'html', $nid = 0) {
Dries's avatar
   
Dries committed
674
  global $base_url;
675
  $type = drupal_strtolower($type);
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
  $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;
  }
712
}
Dries's avatar
   
Dries committed
713

714
/**
715
716
717
718
719
720
721
722
 * 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.
723
 */
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
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
744
745
}

746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
/**
 * 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) {
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
    // Take the most recent approved revision:
Dries's avatar
   
Dries committed
771
    if ($node->moderate) {
772
      $node = book_revision_load($node, array('moderate' => 0, 'status' => 1));
Dries's avatar
   
Dries committed
773
774
    }

Dries's avatar
   
Dries committed
775
    if ($node) {
776
777
      if (function_exists($visit_pre)) {
        $output .= call_user_func($visit_pre, $node, $depth, $nid);
Dries's avatar
   
Dries committed
778
      }
779
780
      else {
        $output .= book_node_visitor_html_pre($node, $depth, $nid);
Dries's avatar
   
Dries committed
781
      }
Dries's avatar
   
Dries committed
782

783
      $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);
784
      while ($childpage = db_fetch_object($children)) {
785
          $childnode = node_load($childpage->nid);
786
787
788
789
790
          if ($childnode->nid != $node->nid) {
              $output .= book_recurse($childnode->nid, $depth+1, $visit_pre, $visit_post);
          }
      }
      if (function_exists($visit_post)) {
791
        $output .= call_user_func($visit_post, $node, $depth);
792
793
      }
      else { # default
794
        $output .= book_node_visitor_html_post($node, $depth);
795
      }
Dries's avatar
   
Dries committed
796
    }
Dries's avatar
   
Dries committed
797
  }
Dries's avatar
   
Dries committed
798

Dries's avatar
   
Dries committed
799
800
  return $output;
}
Dries's avatar
   
Dries committed
801

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

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

  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().
 */
839
function book_node_visitor_html_post($node, $depth) {
840
841
842
843
844
  return "</div>\n";
}

/**
 * Generates XML for a given node. This function is a 'pre-node'
845
846
847
848
849
850
851
 * 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.
852
853
854
855
856
857
858
859
 *
 * @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
860
 *   is used only for generating output (e.g., id attribute)
861
862
863
864
865
866
867
868
869
 * @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.
870
871
872
873
874
875
876
877
878
879
  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";

880
  // wrap the node body in a CDATA declaration
881
882
  $content = "<literallayout>";
  $content .= "<![CDATA[";
883
  if ($node->body) {
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
    $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;
909
  }
910

911
912
913
914
  return $output;
}

/**
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
 * 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().
960
 */
961
962
function book_node_visitor_opml_post($node, $depth) {
  return "</outline>\n";
963
964
965
966
967
}

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

972
function book_admin_edit_book($nid, $depth = 1) {
973
  $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
974

975
976
  $rows = array();

Dries's avatar
   
Dries committed
977
  while ($node = db_fetch_object($result)) {
978
    $node = node_load($node->nid);
979
980
    $rows[] = book_admin_edit_line($node, $depth);
    $rows = array_merge($rows, book_admin_edit_book($node->nid, $depth + 1));
Dries's avatar
   
Dries committed
981
982
  }

Dries's avatar
   
Dries committed
983
  return $rows;
Dries's avatar
   
Dries committed
984
985
}

986
987
988
/**
 * Display an administrative view of the hierarchy of a book.
 */
989
function book_admin_edit($nid, $depth = 0) {
990
  $node = node_load($nid);
991
  if ($node->nid) {
Dries's avatar
   
Dries committed
992
    $header = array(t('Title'), t('Weight'), array('data' => t('Operations'), 'colspan' => '3'));
993
994
    $rows[] = book_admin_edit_line($node);
    $rows = array_merge($rows, book_admin_edit_book($nid));
Dries's avatar
   
Dries committed
995

Dries's avatar
   
Dries committed
996
997
    $output .= theme('table', $header, $rows);
    $output .= form_submit(t('Save book pages'));
Dries's avatar
   
Dries committed
998

999
    drupal_set_title(check_plain($node->title));
Dries's avatar
   
Dries committed
1000
    return form($output);