book.module 36.9 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', 'see printer-friendly version');
Dries's avatar
   
Dries committed
21
22
}

23
24
25
/**
 * Implementation of hook_access().
 */
Dries's avatar
   
Dries committed
26
function book_access($op, $node) {
Dries's avatar
   
Dries committed
27
  global $user;
Dries's avatar
   
Dries committed
28

29
  if ($op == 'create') {
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
      if (user_access('see printer-friendly version')) {
64
65
66
        $links[] = l(t('printer-friendly version'),
                     'book/export/html/'. $node->nid,
                     array('title' => t('Show a printer-friendly version of this book page and its sub-pages.')));
67
      }
Dries's avatar
   
Dries committed
68
    }
Dries's avatar
   
Dries committed
69
70
  }

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

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

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

  return $items;
}

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

168
        $block['subject'] = check_plain($path[0]->title);
Dries's avatar
   
Dries committed
169
170
171
        $block['content'] = book_tree($expand[0], 5, $expand);
      }
    }
Dries's avatar
   
Dries committed
172

173
174
    return $block;
  }
Dries's avatar
   
Dries committed
175
176
}

177
178
179
/**
 * Implementation of hook_load().
 */
Dries's avatar
   
Dries committed
180
function book_load($node) {
Dries's avatar
   
Dries committed
181
  global $user;
Dries's avatar
   
Dries committed
182

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

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

Dries's avatar
   
Dries committed
198
  return $book;
Dries's avatar
   
Dries committed
199
200
}

201
202
203
/**
 * Implementation of hook_insert().
 */
Dries's avatar
   
Dries committed
204
function book_insert($node) {
205
  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
206
}
Dries's avatar
   
Dries committed
207

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

220
221
222
/**
 * Implementation of hook_delete().
 */
223
function book_delete(&$node) {
224
  db_query('DELETE FROM {book} WHERE nid = %d', $node->nid);
Dries's avatar
   
Dries committed
225
226
}

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

238
239
240
241
/**
 * Implementation of hook_validate().
 */
function book_validate($node) {
242
  node_validate_title($node);
Dries's avatar
   
Dries committed
243
244
}

245
246
247
/**
 * Implementation of hook_form().
 */
Dries's avatar
   
Dries committed
248
function book_form(&$node) {
249
  $form['parent'] = array(
250
    '#type' => 'select', '#title' => t('Parent'), '#default_value' => ($node->parent ? $node->parent : arg(4)), '#options' => book_toc($node->nid), '#weight' => -4,
251
    '#description' => 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.')
252
  );
Dries's avatar
   
Dries committed
253

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

258
  $form['log'] = array(
259
    '#type' => 'textarea', '#title' => t('Log message'), '#default_value' => $node->log, '#weight' => 5,
260
    '#description' => t('An explanation of the additions or updates being made to help other authors understand your motivations.')
261
  );
Dries's avatar
   
Dries committed
262

263
  if (user_access('administer nodes')) {
264
    $form['weight'] = array(
265
        '#type' => 'weight', '#title' => t('Weight'), '#default_value' => $node->weight, '#delta' => 15, '#weight' => 5,
266
        '#description' => t('Pages at a given level are ordered first by weight and then by title.')
267
    );
Dries's avatar
   
Dries committed
268
269
  }
  else {
Dries's avatar
   
Dries committed
270
271
    // If a regular user updates a book page, we create a new revision
    // authored by that user:
272
    $form['revision'] = array('#type' => 'hidden', '#value' => 1);
Dries's avatar
   
Dries committed
273
274
  }

275
  return $form;
Dries's avatar
   
Dries committed
276
277
}

278
/**
Dries's avatar
   
Dries committed
279
280
 * Implementation of function book_outline()
 * Handles all book outline operations.
281
 */
Dries's avatar
   
Dries committed
282
function book_outline() {
Dries's avatar
   
Dries committed
283

284
285
  $op = $_POST['op'];
  $edit = $_POST['edit'];
286
  $node = node_load(arg(1));
Dries's avatar
   
Dries committed
287

Dries's avatar
   
Dries committed
288
289
290
  if ($node->nid) {
    switch ($op) {
      case t('Add to book outline'):
291
292
        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);
293
        drupal_set_message(t('The post has been added to the book.'));
Dries's avatar
   
Dries committed
294
295
296
297
        drupal_goto("node/$node->nid");
        break;

      case t('Update book outline'):
298
299
        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);
300
        drupal_set_message(t('The book outline has been updated.'));
Dries's avatar
   
Dries committed
301
302
303
304
305
        drupal_goto("node/$node->nid");
        break;

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

      default:
311
        $page = db_fetch_object(db_query('SELECT * FROM {book} WHERE vid = %d', $node->vid));
312

313
        $form['parent'] = array(
314
315
          '#type' => 'select', '#title' => t('Parent'), '#default_value' => $page->parent,
          '#options' => book_toc($node->nid), '#description' => t('The parent page in the book.')
316
        );
317

318
        $form['weight'] = array(
319
320
          '#type' => 'weight', '#title' => t('Weight'), '#default_value' => $page->weight, '#delta' => 15,
          '#description' => t('Pages at a given level are ordered first by weight and then by title.')
321
        );
322

323
        $form['log'] = array(
324
          '#type' => 'textarea', '#title' => t('Log message'),
325
          '#default_value' => $node->log, '#description' => t('An explanation to help other authors understand your motivations to put this post into the book.')
326
        );
Dries's avatar
   
Dries committed
327
328

        if ($page->nid) {
329
330
          $form['update'] = array('#type' => 'submit', '#value' => t('Update book outline'));
          $form['remove'] = array('#type' => 'submit', '#value' => t('Remove from book outline'));
Dries's avatar
   
Dries committed
331
332
        }
        else {
333
          $form['add'] = array('#type' => 'submit', '#value' => t('Add to book outline'));
Dries's avatar
   
Dries committed
334
        }
Dries's avatar
   
Dries committed
335

336
        drupal_set_title(check_plain($node->title));
337
        return drupal_get_form('book_outline', $form);
Dries's avatar
   
Dries committed
338
339
340
341
    }
  }
}

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

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

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

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

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

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

Dries's avatar
   
Dries committed
400
401
  // 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.
402

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

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

420
421
  // Extract the page body.
  $node = node_prepare($node, $teaser);
Dries's avatar
   
Dries committed
422

Dries's avatar
   
Dries committed
423
424
425
  return $node;
}

426
427
428
429
430
431
/**
 * 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
432
function book_view(&$node, $teaser = FALSE, $page = FALSE) {
Dries's avatar
   
Dries committed
433
  $node = book_content($node, $teaser);
Dries's avatar
   
Dries committed
434
435
}

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

Dries's avatar
   
Dries committed
451
452
453
          foreach ($book as $key => $value) {
            $node->$key = $value;
          }
454
455
456
457
458
459
460
461
462
463
464

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

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

465
466
467
          if ($page) {
            menu_set_location($node->breadcrumb);
          }
Dries's avatar
   
Dries committed
468
        }
Dries's avatar
   
Dries committed
469
      }
Dries's avatar
   
Dries committed
470
      break;
471
    case 'delete revision':
472
473
      db_query('DELETE FROM {book} WHERE vid = %d', $node->vid);
      break;
Dries's avatar
   
Dries committed
474
  }
Dries's avatar
   
Dries committed
475
}
Dries's avatar
   
Dries committed
476

477
478
479
/**
 * Prepares both the custom breadcrumb trail and the forward/backward
 * navigation for a node presented as a book page.
480
481
 *
 * @ingroup themeable
482
 */
483
function theme_book_navigation($node) {
Dries's avatar
   
Dries committed
484
  if ($node->nid) {
485
    $output .= '<div class="book">';
Dries's avatar
   
Dries committed
486
487

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

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

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

525
  return $output;
Dries's avatar
   
Dries committed
526
}
Dries's avatar
 
Dries committed
527

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

  return $toc;
}

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

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

557
558
  $toc = array();

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

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

Dries's avatar
   
Dries committed
567
568
569
  return $toc;
}

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

  return $output;
}

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

Dries's avatar
   
Dries committed
610
611
612
613
  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
614
  }
Dries's avatar
   
Dries committed
615

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

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

627
628
629
630
631
632
  $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
633
634
}

635
/**
636
637
638
 * Menu callback; Generates various representation of a book page with
 * all descendants and prints the requested representation to output.
 *
639
640
641
642
 * The function delegates the generation of output to helper functions.
 * The function name is derived by prepending 'book_export_' to the
 * given output type.  So, e.g., a type of 'html' results in a call to
 * the function book_export_html().
643
644
645
 *
 * @param type
 *   - a string encoding the type of output requested.
646
647
648
 *       The following types are currently supported in book module
 *          html: HTML (printer friendly output)
 *       Other types are supported in contributed modules.
649
650
651
 * @param nid
 *   - an integer representing the node id (nid) of the node to export
 *
652
 */
653
function book_export($type = 'html', $nid = 0) {
654
  $type = drupal_strtolower($type);
655
656
657
658
659
  $depth = _book_get_depth($nid);
  $export_function = 'book_export_' . $type;

  if (function_exists($export_function)) {
    print call_user_func($export_function, $nid, $depth);
660
661
  }
  else {
662
    drupal_set_message('Unknown export format');
663
    drupal_not_found();
664
  }
665
}
Dries's avatar
   
Dries committed
666

667
668
669
670
671
672
673
674
675
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
/**
 * This function is called by book_export() to generate HTML for export.
 *
 * The given node is /embedded to its absolute depth in a top level
 * section/.  For example, a child node with depth 2 in the hierarchy
 * is contained in (otherwise empty) &lt;div&gt; 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.
 *
 * @param nid
 *   - an integer representing the node id (nid) of the node to export
 * @param depth
 * - an integer giving the depth in the book hierarchy of the node
       which is to be exported
 * @return
 * - string containing HTML representing the node and its children in
       the book hierarchy
*/
function book_export_html($nid, $depth) {
  if (user_access('see printer-friendly version')) {
    global $base_url;
    for ($i = 1; $i < $depth; $i++) {
      $output .= "<div class=\"section-$i\">\n";
    }
    $output .= book_recurse($nid, $depth, 'book_node_visitor_html_pre', 'book_node_visitor_html_post');
    for ($i = 1; $i < $depth; $i++) {
      $output .= "</div>\n";

    }
    $html = "<!DOCTYPE html PUBLIC \"-//W3C//DTD XHTML 1.0 Transitional//EN\" \"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd\">\n";
    $html .= '<html xmlns="http://www.w3.org/1999/xhtml" lang="en" xml:lang="en">';
    $html .= "<head>\n<title>". check_plain($node->title) ."</title>\n";
    $html .= '<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />';
    $html .= '<base href="'. $base_url .'/" />' . "\n";
    $html .= "<style type=\"text/css\">\n@import url(misc/print.css);\n</style>\n";
    $html .= "</head>\n<body>\n". $output . "\n</body>\n</html>\n";
    return $html;
  }
  else {
    drupal_access_denied();
  }
}

712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
/**
 * How the book's HTML export should be themed
 *
 * @ingroup themeable
 */
function theme_book_export_html($title, $content) {
  global $base_url;
  $html = "<!DOCTYPE html PUBLIC \"-//W3C//DTD XHTML 1.0 Transitional//EN\" \"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd\">\n";
  $html .= '<html xmlns="http://www.w3.org/1999/xhtml" lang="en" xml:lang="en">';
  $html .= "<head>\n<title>". $title ."</title>\n";
  $html .= '<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />';
  $html .= '<base href="'. $base_url .'/" />' . "\n";
  $html .= "<style type=\"text/css\">\n@import url(misc/print.css);\n</style>\n";
  $html .= "</head>\n<body>\n". $content . "\n</body>\n</html>\n";
  return $html;
}

729
/**
730
731
732
 * 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).
 *
733
734
 * @param nid
 *   - the nid of the node whose depth to compute.
735
736
737
 * @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.
738
 */
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
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
759
760
}

761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
/**
 * 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) {
780
  $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
781
  while ($page = db_fetch_object($result)) {
Dries's avatar
   
Dries committed
782
    // Load the node:
783
    $node = node_load($page->nid);
Dries's avatar
   
Dries committed
784

Dries's avatar
   
Dries committed
785
    if ($node) {
786
787
      if (function_exists($visit_pre)) {
        $output .= call_user_func($visit_pre, $node, $depth, $nid);
Dries's avatar
   
Dries committed
788
      }
789
790
      else {
        $output .= book_node_visitor_html_pre($node, $depth, $nid);
Dries's avatar
   
Dries committed
791
      }
Dries's avatar
   
Dries committed
792

793
      $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);
794
      while ($childpage = db_fetch_object($children)) {
795
          $childnode = node_load($childpage->nid);
796
          if ($childnode->nid != $node->nid) {
797
              $output .= book_recurse($childnode->nid, $depth + 1, $visit_pre, $visit_post);
798
799
800
          }
      }
      if (function_exists($visit_post)) {
801
        $output .= call_user_func($visit_post, $node, $depth);
802
      }
803
804
      else {
        # default
805
        $output .= book_node_visitor_html_post($node, $depth);
806
      }
Dries's avatar
   
Dries committed
807
    }
Dries's avatar
   
Dries committed
808
  }
Dries's avatar
   
Dries committed
809

Dries's avatar
   
Dries committed
810
811
  return $output;
}
Dries's avatar
   
Dries committed
812

813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
/**
 * 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.
 */
828
function book_node_visitor_html_pre($node, $depth, $nid) {
829
830
831
832
833
  // Output the content:
  if (node_hook($node, 'content')) {
    $node = node_invoke($node, 'content');
  }
  // Allow modules to change $node->body before viewing.
834
  node_invoke_nodeapi($node, 'print', $node->body, false);
835

836
837
  $output .= "<div id=\"node-". $node->nid ."\" class=\"section-$depth\">\n";
  $output .= "<h1 class=\"book-heading\">". check_plain($node->title) ."</h1>\n";
838
839
840
841
842
843
844
845
846
847
848
849

  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().
 */
850
function book_node_visitor_html_post($node, $depth) {
851
852
853
  return "</div>\n";
}

854
855
856
857
858
859
860
861
862
863
864
function _book_admin_table($nodes = array()) {
  $form = array(
    '#theme' => 'book_admin_table',
    '#tree' => TRUE,
  );

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

  return $form;
865
866
}

867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
function _book_admin_table_tree($node, $depth) {
  $form = array();

  $form[] = array(
    'nid' => array('#type' => 'value', '#value' => $node->nid),
    'depth' => array('#type' => 'value', '#value' => $depth),
    'title' => array(
      '#type' => 'textfield',
      '#default_value' => $node->title,
      '#maxlength' => 255,
    ),
    'weight' => array(
      '#type' => 'weight',
      '#default_value' => $node->weight,
      '#delta' => 15,
    ),
883
  );
884
885
886
887
888
889
890

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

  return $form;
Dries's avatar
   
Dries committed
891
892
}

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

896
  $rows = array();
897
898
899
900
901
902
903
904
905
  foreach (element_children($form) as $key) {
    $nid = $form[$key]['nid']['#value'];
    $rows[] = array(
      '<div style="padding-left: '. (25 * $form[$key]['depth']['#value']) .'px;">'. form_render($form[$key]['title']) .'</div>',
      form_render($form[$key]['weight']),
      l(t('view'), 'node/'. $nid),
      l(t('edit'), 'node/'. $nid .'/edit'),
      l(t('delete'), 'node/'. $nid .'/delete')
    );
Dries's avatar
   
Dries committed
906
907
  }

908
  return theme('table', $header, $rows);
Dries's avatar
   
Dries committed
909
910
}

911
912
913
/**
 * Display an administrative view of the hierarchy of a book.
 */
914
function book_admin_edit($nid) {
915
  $node = node_load($nid);
916
  if ($node->nid) {
917
    drupal_set_title(check_plain($node->title));
918
    $form = array();
919

920
921
922
923
924
    $form['table'] = _book_admin_table(array($node));
    $form['save'] = array(
      '#type' => 'submit',
      '#value' => t('Save book pages'),
    );
Dries's avatar
   
Dries committed
925

926
    return drupal_get_form('book_admin_edit', $form);
Dries's avatar
   
Dries committed
927
  }
928
929
930
  else {
    drupal_not_found();
  }
Dries's avatar
   
Dries committed
931
932
}

933
/**
934
 * Menu callback; displays a listing of all orphaned book pages.
935
 */
936
937
function book_admin_orphan() {
  $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
   
Dries committed
938

939
940
941
942
943
944
945
946
947
948
  $pages = array();
  while ($page = db_fetch_object($result)) {
    $pages[$page->nid] = $page;
  }

  $orphans = array();
  if (count($pages)) {
    foreach ($pages as $page) {
      if ($page->parent && empty($pages[$page->parent])) {
        $orphans[] = node_load($page->nid);
Dries's avatar
   
Dries committed
949
      }
Dries's avatar
   
Dries committed
950
    }
951
  }
Dries's avatar
   
Dries committed
952

953
954
  if (count($orphans)) {
    $form = array();
Dries's avatar
   
Dries committed
955

956
957
958
959
960
961
962
963