book.module 31 KB
Newer Older
Dries's avatar
 
Dries committed
1
<?php
2
// $Id$
Dries's avatar
 
Dries committed
3

Dries's avatar
   
Dries committed
4
5
6
7
8
/**
 * @file
 * Allows users to collaboratively author a book.
 */

9
10
11
/**
 * Implementation of hook_node_name().
 */
Dries's avatar
   
Dries committed
12
function book_node_name($node) {
13
  return t('book page');
Dries's avatar
   
Dries committed
14
15
}

16
17
18
/**
 * Implementation of hook_perm().
 */
Dries's avatar
   
Dries committed
19
function book_perm() {
20
  return array('create book pages', 'maintain books', 'edit own book pages');
Dries's avatar
   
Dries committed
21
22
}

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

29
  if ($op == 'create') {
Dries's avatar
   
Dries committed
30
31
    // Only registered users can create book pages.  Given the nature
    // of the book module this is considered to be a good/safe idea.
32
    return user_access('create book pages');
Dries's avatar
   
Dries committed
33
34
  }

35
  if ($op == 'update') {
Dries's avatar
   
Dries committed
36
37
38
    // Only registered users can update book pages.  Given the nature
    // of the book module this is considered to be a good/safe idea.
    // One can only update a book page if there are no suggested updates
39
40
    // of that page waiting for approval.  That is, only updates that
    // don't overwrite the current or pending information are allowed.
41

42
43
44
45
46
47
    if ((user_access('maintain books') && !$node->moderate) || ($node->uid == $user->uid && user_access('edit own book pages'))) {
      return TRUE;
    }
    else {
       // do nothing. node-access() will determine further access
    }
Dries's avatar
   
Dries committed
48
  }
Dries's avatar
   
Dries committed
49
50
}

Dries's avatar
   
Dries committed
51
52
53
/**
 * Implementation of hook_link().
 */
Dries's avatar
   
Dries committed
54
function book_link($type, $node = 0, $main = 0) {
Dries's avatar
   
Dries committed
55
56
57

  $links = array();

58
  if ($type == 'node' && isset($node->parent)) {
Dries's avatar
   
Dries committed
59
    if (!$main) {
Dries's avatar
   
Dries committed
60
61
62
      if (book_access('create', $node)) {
        $links[] = l(t('add child page'), "node/add/book/parent/$node->nid");
      }
Dries's avatar
   
Dries committed
63
      $links[] = l(t('printer-friendly version'), 'book/print/'. $node->nid, array('title' => t('Show a printer-friendly version of this book page and its sub-pages.')));
Dries's avatar
   
Dries committed
64
    }
Dries's avatar
   
Dries committed
65
66
  }

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

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

Dries's avatar
   
Dries committed
76
  if ($may_cache) {
Dries's avatar
   
Dries committed
77
78
    $items[] = array('path' => 'book', 'title' => t('books'),
      'access' => user_access('access content'), 'type' => MENU_NORMAL_ITEM, 'weight' => 5);
Dries's avatar
   
Dries committed
79
    $items[] = array('path' => 'node/add/book', 'title' => t('book page'),
80
      'access' => user_access('create book pages'));
Dries's avatar
   
Dries committed
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
    $items[] = array('path' => 'admin/node/book', 'title' => t('books'),
      'callback' => 'book_admin',
      'access' => user_access('administer nodes'),
      'weight' => 4);
    $items[] = array('path' => 'admin/node/book/orphan', 'title' => t('orphan pages'),
      'callback' => 'book_admin_orphan',
      'access' => user_access('administer nodes'),
      'weight' => 8);
    $items[] = array('path' => 'book', 'title' => t('books'),
      'callback' => 'book_render',
      'access' => user_access('access content'),
      'type' => MENU_SUGGESTED_ITEM);
    $items[] = array('path' => 'book/print', 'title' => t('printer-friendly version'),
      'callback' => 'book_print',
      'access' => user_access('access content'),
      'type' => MENU_CALLBACK);
Dries's avatar
   
Dries committed
97
  }
Dries's avatar
   
Dries committed
98
99
100
101
102
  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:
103
      $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
104
105
106
107
108
109
110
      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
111
112
113
114

  return $items;
}

115
116
117
/**
 * Implementation of hook_block().
 *
Dries's avatar
   
Dries committed
118
119
 * Displays the book table of contents in a block when the current page is a
 * single-node view of a book node.
120
 */
Dries's avatar
   
Dries committed
121
function book_block($op = 'list', $delta = 0) {
Dries's avatar
   
Dries committed
122
  $block = array();
Dries's avatar
   
Dries committed
123
124
  if ($op == 'list') {
    $block[0]['info'] = t('Book navigation');
125
    return $block;
Dries's avatar
   
Dries committed
126
  }
127
  else if ($op == 'view') {
Dries's avatar
   
Dries committed
128
129
    // Only display this block when the user is browsing a book:
    if (arg(0) == 'node' && is_numeric(arg(1))) {
130
      $result = db_query(db_rewrite_sql('SELECT n.nid, n.title, b.parent FROM {node} n INNER JOIN {book} b ON n.nid = b.nid WHERE n.nid = %d'), arg(1));
Dries's avatar
   
Dries committed
131
132
133
134
135
136
137
138
139
140
141
      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;
        }

142
        $block['subject'] = check_plain($path[0]->title);
Dries's avatar
   
Dries committed
143
144
145
        $block['content'] = book_tree($expand[0], 5, $expand);
      }
    }
Dries's avatar
   
Dries committed
146

147
148
    return $block;
  }
Dries's avatar
   
Dries committed
149
150
}

151
152
153
/**
 * Implementation of hook_load().
 */
Dries's avatar
   
Dries committed
154
function book_load($node) {
Dries's avatar
   
Dries committed
155
  global $user;
Dries's avatar
   
Dries committed
156

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

Dries's avatar
   
Dries committed
159
  if (arg(2) == 'edit' && !user_access('administer nodes')) {
Dries's avatar
   
Dries committed
160
161
    // If a user is about to update a book page, we overload some
    // fields to reflect the changes.
Dries's avatar
   
Dries committed
162
163
164
165
166
167
    if ($user->uid) {
      $book->uid = $user->uid;
      $book->name = $user->name;
    }
    else {
      $book->uid = 0;
168
      $book->name = '';
Dries's avatar
   
Dries committed
169
    }
Dries's avatar
   
Dries committed
170
  }
Dries's avatar
   
Dries committed
171

Dries's avatar
   
Dries committed
172
  return $book;
Dries's avatar
   
Dries committed
173
174
}

175
176
177
/**
 * Implementation of hook_insert().
 */
Dries's avatar
   
Dries committed
178
function book_insert($node) {
179
  db_query("INSERT INTO {book} (nid, parent, weight, log) VALUES (%d, %d, %d, '%s')", $node->nid, $node->parent, $node->weight, $node->log);
Dries's avatar
   
Dries committed
180
}
Dries's avatar
   
Dries committed
181

182
183
184
/**
 * Implementation of hook_update().
 */
Dries's avatar
   
Dries committed
185
function book_update($node) {
186
  db_query("UPDATE {book} SET parent = %d, weight = %d, log = '%s' WHERE nid = %d", $node->parent, $node->weight, $node->log, $node->nid);
Dries's avatar
   
Dries committed
187
}
Dries's avatar
   
Dries committed
188

189
190
191
/**
 * Implementation of hook_delete().
 */
192
function book_delete(&$node) {
193
  db_query('DELETE FROM {book} WHERE nid = %d', $node->nid);
Dries's avatar
   
Dries committed
194
195
}

196
197
198
/**
 * Implementation of hook_validate().
 */
199
function book_validate(&$node) {
200
201
  // Set default values for non-administrators.
  if (!user_access('administer nodes')) {
202
203
204
    $node->weight = 0;
    $node->revision = 1;
  }
Dries's avatar
   
Dries committed
205
206
}

207
208
209
/**
 * Implementation of hook_form().
 */
Dries's avatar
   
Dries committed
210
function book_form(&$node) {
Dries's avatar
   
Dries committed
211
  global $user;
Dries's avatar
   
Dries committed
212

213
  $op = $_POST['op'];
Dries's avatar
   
Dries committed
214

Dries's avatar
   
Dries committed
215
  $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
216

217
218
  if (function_exists('taxonomy_node_form')) {
    $output .= implode('', taxonomy_node_form('book', $node));
219
220
  }

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

225
  if (user_access('administer nodes')) {
Dries's avatar
   
Dries committed
226
    $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
227
228
  }
  else {
Dries's avatar
   
Dries committed
229
230
    // If a regular user updates a book page, we create a new revision
    // authored by that user:
231
    $output .= form_hidden('revision', 1);
Dries's avatar
   
Dries committed
232
233
234
235
236
  }

  return $output;
}

237
/**
Dries's avatar
   
Dries committed
238
239
 * Implementation of function book_outline()
 * Handles all book outline operations.
240
 */
Dries's avatar
   
Dries committed
241
function book_outline() {
Dries's avatar
   
Dries committed
242

243
244
  $op = $_POST['op'];
  $edit = $_POST['edit'];
Dries's avatar
   
Dries committed
245
  $node = node_load(array('nid' => arg(1)));
Dries's avatar
   
Dries committed
246

Dries's avatar
   
Dries committed
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
  if ($node->nid) {
    switch ($op) {
      case t('Add to book outline'):
        db_query('INSERT INTO {book} (nid, parent, weight) VALUES (%d, %d, %d)', $node->nid, $edit['parent'], $edit['weight']);
        drupal_set_message(t('Added the post to the book.'));
        drupal_goto("node/$node->nid");
        break;

      case t('Update book outline'):
        db_query('UPDATE {book} SET parent = %d, weight = %d WHERE nid = %d', $edit['parent'], $edit['weight'], $node->nid);
        drupal_set_message(t('Updated the book outline.'));
        drupal_goto("node/$node->nid");
        break;

      case t('Remove from book outline'):
        db_query('DELETE FROM {book} WHERE nid = %d', $node->nid);
        drupal_set_message(t('Removed the post from the book.'));
        drupal_goto("node/$node->nid");
        break;

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

        $output  = form_select(t('Parent'), 'parent', $page->parent, book_toc($node->nid), t('The parent page in the book.'));
Dries's avatar
   
Dries committed
271
        $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
272
273
274
275
276
277
278
279

        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
280

281
        drupal_set_title(check_plain($node->title));
Dries's avatar
   
Dries committed
282
        return form($output);
Dries's avatar
   
Dries committed
283
284
285
286
    }
  }
}

Dries's avatar
   
Dries committed
287

288
289
290
/**
 * Return the the most recent revision that matches the specified conditions.
 */
Dries's avatar
   
Dries committed
291
292
293
294
295
296
function book_revision_load($page, $conditions = array()) {

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

  foreach ($revisions as $revision) {

Dries's avatar
   
Dries committed
297
    // Extract the specified revision:
Dries's avatar
   
Dries committed
298
299
    $node = node_revision_load($page, $revision);

Dries's avatar
   
Dries committed
300
301
    // Check to see if the conditions are met:
    $status = TRUE;
Dries's avatar
   
Dries committed
302
303
304

    foreach ($conditions as $key => $value) {
      if ($node->$key != $value) {
Dries's avatar
   
Dries committed
305
        $status = FALSE;
Dries's avatar
   
Dries committed
306
307
308
309
310
311
312
313
314
      }
    }

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

315
316
317
/**
 * Return the path (call stack) to a certain book page.
 */
Dries's avatar
   
Dries committed
318
function book_location($node, $nodes = array()) {
319
  $parent = db_fetch_object(db_query(db_rewrite_sql('SELECT n.nid, n.title, b.parent, b.weight FROM {node} n INNER JOIN {book} b ON n.nid = b.nid WHERE n.nid = %d'), $node->parent));
Dries's avatar
   
Dries committed
320
321
322
323
324
325
326
  if ($parent->title) {
    $nodes = book_location($parent, $nodes);
    array_push($nodes, $parent);
  }
  return $nodes;
}

Dries's avatar
   
Dries committed
327
function book_location_down($node, $nodes = array()) {
328
  $last_direct_child = db_fetch_object(db_query(db_rewrite_sql('SELECT n.nid, n.title, b.parent, b.weight FROM {node} n INNER JOIN {book} b ON n.nid = b.nid WHERE b.parent = %d ORDER BY b.weight DESC, n.title DESC'), $node->nid));
Dries's avatar
   
Dries committed
329
330
331
332
333
334
335
  if ($last_direct_child) {
    array_push($nodes, $last_direct_child);
    $nodes = book_location_down($last_direct_child, $nodes);
  }
  return $nodes;
}

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

Dries's avatar
   
Dries committed
345
  // Previous on the same level:
346
  $direct_above = db_fetch_object(db_query(db_rewrite_sql("SELECT n.nid, n.title FROM {node} n INNER JOIN {book} b ON n.nid = b.nid WHERE b.parent = %d AND n.status = 1 AND n.moderate = 0 AND (b.weight < %d OR (b.weight = %d AND n.title < '%s')) ORDER BY b.weight DESC, n.title DESC"), $node->parent, $node->weight, $node->weight, $node->title));
Dries's avatar
   
Dries committed
347
  if ($direct_above) {
Dries's avatar
   
Dries committed
348
    // Get last leaf of $above.
Dries's avatar
   
Dries committed
349
    $path = book_location_down($direct_above);
Dries's avatar
   
Dries committed
350
351

    return $path ? (count($path) > 0 ? array_pop($path) : NULL) : $direct_above;
Dries's avatar
   
Dries committed
352
353
  }
  else {
Dries's avatar
   
Dries committed
354
    // Direct parent:
355
    $prev = db_fetch_object(db_query(db_rewrite_sql('SELECT n.nid, n.title FROM {node} n INNER JOIN {book} b ON n.nid = b.nid WHERE n.nid = %d AND n.status = 1 AND n.moderate = 0'), $node->parent));
Dries's avatar
   
Dries committed
356
357
358
359
    return $prev;
  }
}

360
361
362
/**
 * Fetch the node object of the next page of the book.
 */
Dries's avatar
   
Dries committed
363
364
function book_next($node) {
  // get first direct child
365
  $child = db_fetch_object(db_query(db_rewrite_sql('SELECT n.nid, n.title, b.weight FROM {node} n INNER JOIN {book} b ON n.nid = b.nid WHERE b.parent = %d AND n.status = 1 AND n.moderate = 0 ORDER BY b.weight ASC, n.title ASC'), $node->nid));
Dries's avatar
   
Dries committed
366
367
368
369
  if ($child) {
    return $child;
  }

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

Dries's avatar
   
Dries committed
373
  while (($leaf = array_pop($path)) && count($path)) {
374
    $next = db_fetch_object(db_query(db_rewrite_sql("SELECT n.nid, n.title, b.weight FROM {node} n INNER JOIN {book} b ON n.nid = b.nid WHERE b.parent = %d AND n.status = 1 AND n.moderate = 0 AND (b.weight > %d OR (b.weight = %d AND n.title > '%s')) ORDER BY b.weight ASC, n.title ASC"), $leaf->parent, $leaf->weight, $leaf->weight, $leaf->title));
Dries's avatar
   
Dries committed
375
376
377
378
379
380
    if ($next) {
      return $next;
    }
  }
}

Dries's avatar
   
Dries committed
381
function book_content($node, $teaser = FALSE) {
382
  $op = $_POST['op'];
Dries's avatar
   
Dries committed
383

Dries's avatar
   
Dries committed
384
385
386
  // 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.
387
388
  if ($op != t('Preview') && $node->moderate && arg(0) != 'queue') {
    $revision = book_revision_load($node, array('moderate' => 0, 'status' => 1));
Dries's avatar
   
Dries committed
389
390
391
392

    if ($revision) {
      $node = $revision;
    }
Dries's avatar
   
Dries committed
393
394
  }

395
396
  // Extract the page body.
  $node = node_prepare($node, $teaser);
Dries's avatar
   
Dries committed
397

Dries's avatar
   
Dries committed
398
399
400
  return $node;
}

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

Dries's avatar
   
Dries committed
410
411
  if (!$teaser && $node->moderate) {
    $node->body .= '<div class="log"><div class="title">'. t('Log') .':</div>'. $node->log .'</div>';
Dries's avatar
   
Dries committed
412
413
414
  }
}

415
/**
Dries's avatar
   
Dries committed
416
417
418
 * Implementation of hook_nodeapi().
 *
 * Appends book navigation to all nodes in the book.
419
 */
Dries's avatar
   
Dries committed
420
421
422
423
function book_nodeapi(&$node, $op, $teaser, $page) {
  switch ($op) {
    case 'view':
      if (!$teaser) {
Dries's avatar
   
Dries committed
424
        $book = db_fetch_array(db_query('SELECT * FROM {book} WHERE nid = %d', $node->nid));
Dries's avatar
   
Dries committed
425
        if ($book) {
426
427
428
429
          if ($node->moderate && user_access('administer nodes')) {
            drupal_set_message(t("This update/post awaits moderation and won't be accessible until approved."));
          }

Dries's avatar
   
Dries committed
430
431
432
          foreach ($book as $key => $value) {
            $node->$key = $value;
          }
433
          $node = theme('book_navigation', $node);
434
435
436
          if ($page) {
            menu_set_location($node->breadcrumb);
          }
Dries's avatar
   
Dries committed
437
        }
Dries's avatar
   
Dries committed
438
      }
Dries's avatar
   
Dries committed
439
      break;
Dries's avatar
   
Dries committed
440
  }
Dries's avatar
   
Dries committed
441
}
Dries's avatar
   
Dries committed
442

443
444
445
/**
 * Prepares both the custom breadcrumb trail and the forward/backward
 * navigation for a node presented as a book page.
446
447
 *
 * @ingroup themeable
448
 */
449
function theme_book_navigation($node) {
Dries's avatar
   
Dries committed
450
  $path = book_location($node);
Dries's avatar
   
Dries committed
451

Dries's avatar
   
Dries committed
452
  // Construct the breadcrumb:
Dries's avatar
   
Dries committed
453

Dries's avatar
   
Dries committed
454
  $node->breadcrumb = array(); // Overwrite the trail with a book trail.
Dries's avatar
   
Dries committed
455
  foreach ($path as $level) {
Dries's avatar
   
Dries committed
456
    $node->breadcrumb[] = array('path' => 'node/'. $level->nid, 'title' =>  $level->title);
Dries's avatar
   
Dries committed
457
  }
Dries's avatar
   
Dries committed
458
  $node->breadcrumb[] = array('path' => 'node/'. $node->nid);
Dries's avatar
   
Dries committed
459

Dries's avatar
   
Dries committed
460
  if ($node->nid) {
461
    $output .= '<div class="book">';
Dries's avatar
   
Dries committed
462
463

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

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

491
    $output .= '<div class="nav">';
Dries's avatar
   
Dries committed
492
493
    $output .= ' <div class="links">'. $links .'</div>';
    $output .= ' <div class="titles">'. $titles .'</div>';
494
495
    $output .= '</div>';
    $output .= '</div>';
Dries's avatar
   
Dries committed
496
  }
Dries's avatar
   
Dries committed
497

Dries's avatar
   
Dries committed
498
  $node->body = $node->body.$output;
Dries's avatar
   
Dries committed
499

Dries's avatar
   
Dries committed
500
  return $node;
Dries's avatar
   
Dries committed
501
}
Dries's avatar
 
Dries committed
502

503
function book_toc_recurse($nid, $indent, $toc, $children, $exclude) {
Dries's avatar
   
Dries committed
504
505
  if ($children[$nid]) {
    foreach ($children[$nid] as $foo => $node) {
506
507
508
509
      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
510
511
512
513
514
515
    }
  }

  return $toc;
}

516
function book_toc($exclude = 0) {
517
  $result = db_query(db_rewrite_sql('SELECT n.nid, n.title, b.parent, b.weight FROM {node} n INNER JOIN {book} b ON n.nid = b.nid WHERE n.status = 1 ORDER BY b.weight, n.title'));
Dries's avatar
   
Dries committed
518

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

526
527
  $toc = array();

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

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

Dries's avatar
   
Dries committed
536
537
538
  return $toc;
}

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

  return $output;
}

Dries's avatar
   
Dries committed
569
function book_tree($parent = 0, $depth = 3, $unfold = array()) {
570
  $result = db_query(db_rewrite_sql('SELECT n.nid, n.title, b.parent, b.weight FROM {node} n INNER JOIN {book} b ON n.nid = b.nid WHERE n.status = 1 AND n.moderate = 0 ORDER BY b.weight, n.title'));
Dries's avatar
   
Dries committed
571

Dries's avatar
   
Dries committed
572
573
574
575
  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
576
  }
Dries's avatar
   
Dries committed
577

Dries's avatar
   
Dries committed
578
  if ($tree = book_tree_recurse($parent, $depth, $children, $unfold)) {
579
    return '<div class="menu"><ul>'. $tree .'</ul></div>';
Dries's avatar
   
Dries committed
580
  }
Dries's avatar
   
Dries committed
581
582
}

583
/**
Dries's avatar
Dries committed
584
 * Menu callback; prints a listing of all books.
585
 */
Dries's avatar
   
Dries committed
586
function book_render() {
587
  $result = db_query(db_rewrite_sql('SELECT n.nid, n.title FROM {node} n INNER JOIN {book} b ON n.nid = b.nid WHERE b.parent = 0 AND n.status = 1 AND n.moderate = 0 ORDER BY b.weight, n.title'));
Dries's avatar
   
Dries committed
588

Dries's avatar
   
Dries committed
589
  return $output;
Dries's avatar
   
Dries committed
590
591
}

592
/**
Dries's avatar
   
Dries committed
593
 * Menu callback; generates printer-friendly book page with all descendants.
594
595
 */
function book_print($nid = 0, $depth = 1) {
Dries's avatar
   
Dries committed
596
  global $base_url;
597
  $result = db_query(db_rewrite_sql('SELECT n.nid, n.title, b.weight FROM {node} n INNER JOIN {book} b ON n.nid = b.nid WHERE n.status = 1 AND n.nid = %d AND n.moderate = 0 ORDER BY b.weight, n.title'), $nid);
Dries's avatar
   
Dries committed
598

Dries's avatar
   
Dries committed
599
600
  while ($page = db_fetch_object($result)) {
    // load the node:
601
    $node = node_load(array('nid' => $page->nid));
Dries's avatar
   
Dries committed
602

Dries's avatar
   
Dries committed
603
604
    if ($node) {
      // output the content:
605
606
      if (node_hook($node, 'content')) {
        $node = node_invoke($node, 'content');
Dries's avatar
   
Dries committed
607
      }
608
609
610
      // Allow modules to change $node->body before viewing.
      node_invoke_nodeapi($node, 'view', $node->body, false);

611
      $output .= '<h1 id="'. $node->nid .'" name="'. $node->nid .'" class="book-h'. $depth .'">'. check_plain($node->title) .'</h1>';
Dries's avatar
   
Dries committed
612

Dries's avatar
   
Dries committed
613
      if ($node->body) {
Dries's avatar
   
Dries committed
614
        $output .= $node->body;
Dries's avatar
   
Dries committed
615
      }
Dries's avatar
   
Dries committed
616
    }
Dries's avatar
   
Dries committed
617
  }
Dries's avatar
   
Dries committed
618

619
  $output .= book_print_recurse($nid, $depth);
Dries's avatar
   
Dries committed
620

621
  $html = '<html><head><title>'. check_plain($node->title) .'</title>';
Dries's avatar
   
Dries committed
622
  $html .= '<base href="'. $base_url .'/" />';
Dries's avatar
   
Dries committed
623
  $html .= "<style type=\"text/css\">\n@import url(misc/print.css);\n</style>";
624
  $html .= '</head><body>'. $output .'</body></html>';
Dries's avatar
   
Dries committed
625

626
  print $html;
Dries's avatar
   
Dries committed
627
628
}

629
function book_print_recurse($parent = '', $depth = 1) {
630
  $result = db_query(db_rewrite_sql('SELECT n.nid, n.title, b.weight FROM {node} n INNER JOIN {book} b ON n.nid = b.nid WHERE n.status = 1 AND b.parent = %d AND n.moderate = 0 ORDER BY b.weight, n.title'), $parent);
Dries's avatar
   
Dries committed
631

Dries's avatar
   
Dries committed
632
  while ($page = db_fetch_object($result)) {
Dries's avatar
   
Dries committed
633
    // Load the node:
634
    $node = node_load(array('nid' => $page->nid));
Dries's avatar
   
Dries committed
635

Dries's avatar
   
Dries committed
636
    // Take the most recent approved revision:
Dries's avatar
   
Dries committed
637
    if ($node->moderate) {
638
      $node = book_revision_load($node, array('moderate' => 0, 'status' => 1));
Dries's avatar
   
Dries committed
639
640
    }

Dries's avatar
   
Dries committed
641
    if ($node) {
Dries's avatar
   
Dries committed
642
      // Output the content:
643
644
      if (node_hook($node, 'content')) {
        $node = node_invoke($node, 'content');
Dries's avatar
   
Dries committed
645
      }
646
647
648
      // Allow modules to change $node->body before viewing.
      node_invoke_nodeapi($node, 'view', $node->body, false);

649
      $output .= '<h1 id="'. $node->nid .'" name="'. $node->nid .'" class="book-h'. $depth .'">'. check_plain($node->title) .'</h1>';
Dries's avatar
   
Dries committed
650

Dries's avatar
   
Dries committed
651
      if ($node->body) {
652
        $output .= '<ul>'. $node->body .'</ul>';
Dries's avatar
   
Dries committed
653
      }
Dries's avatar
   
Dries committed
654

655
      $output .= book_print_recurse($node->nid, $depth + 1);
Dries's avatar
   
Dries committed
656
    }
Dries's avatar
   
Dries committed
657
  }
Dries's avatar
   
Dries committed
658

Dries's avatar
   
Dries committed
659
660
  return $output;
}
Dries's avatar
   
Dries committed
661

662
function book_admin_edit_line($node, $depth = 0) {
663
  return array('<div style="padding-left: '. (25 * $depth) .'px;">'. form_textfield(NULL, $node->nid .'][title', $node->title, 64, 255) .'</div>', form_weight(NULL, $node->nid .'][weight', $node->weight, 15), l(t('view'), 'node/'. $node->nid), l(t('edit'), 'node/'. $node->nid .'/edit'), l(t('delete'), 'node/'.$node->nid.'/delete'));
Dries's avatar
   
Dries committed
664
665
}

666
function book_admin_edit_book($nid, $depth = 1) {
667
  $result = db_query(db_rewrite_sql('SELECT n.nid FROM {node} n INNER JOIN {book} b ON n.nid = b.nid WHERE b.parent = %d ORDER BY b.weight, n.title'), $nid);
Dries's avatar
   
Dries committed
668

669
670
  $rows = array();

Dries's avatar
   
Dries committed
671
  while ($node = db_fetch_object($result)) {
Dries's avatar
   
Dries committed
672
    $node = node_load(array('nid' => $node->nid));
673
674
    $rows[] = book_admin_edit_line($node, $depth);
    $rows = array_merge($rows, book_admin_edit_book($node->nid, $depth + 1));
Dries's avatar
   
Dries committed
675
676
  }

Dries's avatar
   
Dries committed
677
  return $rows;
Dries's avatar
   
Dries committed
678
679
}

680
681
682
/**
 * Display an administrative view of the hierarchy of a book.
 */
683
684
685
function book_admin_edit($nid, $depth = 0) {
  $node = node_load(array('nid' => $nid));
  if ($node->nid) {
Dries's avatar
   
Dries committed
686
    $header = array(t('Title'), t('Weight'), array('data' => t('Operations'), 'colspan' => '3'));
687
688
    $rows[] = book_admin_edit_line($node);
    $rows = array_merge($rows, book_admin_edit_book($nid));
Dries's avatar
   
Dries committed
689

Dries's avatar
   
Dries committed
690
691
    $output .= theme('table', $header, $rows);
    $output .= form_submit(t('Save book pages'));
Dries's avatar
   
Dries committed
692

693
    drupal_set_title(check_plain($node->title));
Dries's avatar
   
Dries committed
694
695
    return form($output);
  }
696
697
698
  else {
    drupal_not_found();
  }
Dries's avatar
   
Dries committed
699
700
701
}

function book_admin_save($nid, $edit = array()) {
Dries's avatar
   
Dries committed
702
  if ($nid) {
Dries's avatar
   
Dries committed
703
    $book = node_load(array('nid' => $nid));
Dries's avatar
   
Dries committed
704

Dries's avatar
   
Dries committed
705
    foreach ($edit as $nid => $value) {
Dries's avatar
   
Dries committed
706
707
708
709
      // Check to see whether the title needs updating:
      $title = db_result(db_query('SELECT title FROM {node} WHERE nid = %d', $nid));
      if ($title != $value['title']) {
        db_query("UPDATE {node} SET title = '%s' WHERE nid = %d", $value['title'], $nid);
Dries's avatar
   
Dries committed
710
      }
Dries's avatar
   
Dries committed
711

Dries's avatar
   
Dries committed
712
713
714
715
      // Check to see whether the weight needs updating:
      $weight = db_result(db_query('SELECT weight FROM {book} WHERE nid = %d', $nid));
      if ($weight != $value['weight']) {
        db_query('UPDATE {book} SET weight = %d WHERE nid = %d', $value['weight'], $nid);
Dries's avatar
   
Dries committed
716
      }
Dries's avatar
   
Dries committed
717
718
    }

719
    $message = t('Updated book %title.', array('%title' => theme('placeholder', $book->title)));
720
    watchdog('content', $message);
Dries's avatar
   
Dries committed
721

Dries's avatar
   
Dries committed
722
723
    return $message;
  }
Dries's avatar
   
Dries committed
724
725
}

726
/**
Dries's avatar
Dries committed
727
 * Menu callback; displays a listing of all orphaned book pages.
728
 */
Dries's avatar
   
Dries committed
729
function book_admin_orphan() {
730
  $result = db_query(db_rewrite_sql('SELECT n.nid, n.title, n.status, b.parent FROM {node} n INNER JOIN {book} b ON n.nid = b.nid'));
Dries's avatar
   
Dries committed
731
732
733
734
735

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

Dries's avatar
   
Dries committed
736
  if ($pages) {
737
    $output .= '<h3>'. t('Orphan pages') .'</h3>';
Dries's avatar
   
Dries committed
738
    $header = array(t('Title'), t('Weight'), array('data' => t('Operations'), 'colspan' => '3'));
Dries's avatar
   
Dries committed
739
740
    foreach ($pages as $nid => $node) {
      if ($node->parent && empty($pages[$node->parent])) {
741
742
        $rows[] = book_admin_edit_line($node, $depth);
        $rows = array_merge($rows, book_admin_edit_book($node->nid, $depth + 1));
Dries's avatar
   
Dries committed
743
      }
Dries's avatar
   
Dries committed
744
    }
Dries's avatar
   
Dries committed
745
    $output .= theme('table', $header, $rows);
Dries's avatar
   
Dries committed
746
747
  }

Dries's avatar
   
Dries committed
748
  return $output;
Dries's avatar
   
Dries committed
749
750
}

751
/**
Dries's avatar
Dries committed
752
 * Menu callback; displays the book administration page.
753
 */
Dries's avatar
Dries committed
754
function book_admin($nid = 0) {
Dries's avatar
   
Dries committed
755
756
  $op = $_POST['op'];
  $edit = $_POST['edit'];
Dries's avatar
   
Dries committed
757

758
759
  if ($op == t('Save book pages')) {
    drupal_set_message(book_admin_save($nid, $edit));
Dries's avatar
   
Dries committed
760
  }
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777

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

function book_admin_overview() {
  $result = db_query(db_rewrite_sql('SELECT n.nid, n.title FROM {node} n INNER JOIN {book} b ON n.nid = b.nid WHERE b.parent = 0 ORDER BY b.weight, n.title'));
  while ($book = db_fetch_object($result)) {
    $rows[] = array(l($book->title, "node/$book->nid"), l(t('outline'), "admin/node/book/$book->nid"));
  }
  $headers = array(t('Book'), t('Operations'));

  return theme('table', $headers, $rows);
Dries's avatar
   
Dries committed
778
779
}

780
781
782
/**
 * Implementation of hook_help().
 */
Dries's avatar
   
Dries committed
783
function book_help($section) {
Dries's avatar
   
Dries committed
784
  switch ($section) {
Dries's avatar
   
Dries committed
785
    case 'admin/help#book':
Dries's avatar
   
Dries committed
786
      return t("
Dries's avatar
   
Dries committed
787
      <p>The book organises content into a nested hierarchical structure. It is particularly good for manuals, Frequently Asked Questions (FAQs) and the like, allowing you to have chapters, sections, etc.</p>
788
      <p>A book is simply a collection of nodes that have been linked together. These nodes are usually of type <em>book page</em>, but you can insert nodes of any type into a book outline. Every node in the book has a <em>parent</em> node which  \"contains\" it. This is how book.module establishes its hierarchy. At any given level in the hierarchy, a book can contain many nodes. All these sibling nodes are sorted according to the <em>weight</em> that you give them.</p>
789
      <p>Book pages contain a <em>log message</em> field which helps your users understand the motivation behind an edit of a book page. Each edited version of a book page is stored as a new revision of a node. This capability makes it easy to revert to an old version of a page, should that be desirable.</p>
Dries's avatar
   
Dries committed
790
      <p>Like other node types, book submissions and edits may be subject to moderation, depending on your configuration.  Similarly, books use <a href=\"%permissions\">permissions</a> to determine who may read and write to them. Only administrators are allowed to create new books, which are really just nodes whose parent is <em>&lt;top-level&gt;</em>.  To include an existing node in your book, click on the \"outline\"-tab on the node's page.  This enables you to place the node wherever you'd like within the book hierarchy. To add a new node into your book, use the <a href=\"%create\">create content &raquo; book page</a> link.</p>
791
      <p>Administrators may review the hierarchy of their books by clicking on the <a href=\"%collaborative-book\">collaborative book</a> link in the administration pages. There, nodes may be edited, reorganized, removed from book, and deleted. This behavior may change in the future. When a parent node is deleted, it may leave behind child nodes.  These nodes are now <em>orphans</em>. Administrators should periodically <a href=\"%orphans-book\">review their books for orphans</a> and reaffiliate those pages as desired. Finally, administrators may also <a href=\"%export-book\">export their books</a> to a single, flat HTML page which is suitable for printing.</p>
Dries's avatar
   
Dries committed
792
793
      <h3>Maintaining a FAQ using a collaborative book</h3>
      <p>Collaborative books let you easily set up a Frequently Asked Questions (FAQ) section on your web site. The main benefit is that you don't have to write all the questions/answers by yourself - let the community do it for you!</p>
794
      <p>In order to set up the FAQ, you have to create a new book which will hold all your content. To do so, click on the <a href=\"%create\">create content &raquo; book page</a> link. Give it a thoughtful title, and body. A title like \"Estonia Travel - FAQ\" is nice. You may always edit these fields later. You will probably want to designate <em>&lt;top-level&gt;</em> as the parent of this page. Leave the <em>log message</em> and <em>type</em> fields blank for now. After you have submitted this book page, you are ready to begin filling up your book with questions that are frequently asked.</p>
795
      <p>Whenever you come across a post which you want to include in your FAQ, click on the <em>administer</em> link. Then click on the <em>edit book outline</em> button at the bottom of the page. Then place the relevant post wherever is most appropriate in your book by selecting a <em>parent</em>. Books are quite flexible. They can have sections like <em>Flying to Estonia</em>, <em>Eating in Estonia</em> and so on. As you get more experienced with the book module, you can reorganize posts in your book so that it stays organized.</p>
796
      <p>Notes:</p><ul><li>Any comments attached to those relevant posts which you designate as book pages will also be transported into your book. This is a great feature, since much wisdom is shared via comments. Remember that all future comments and edits will automatically be reflected in your book.</li><li>You may wish to edit the title of posts when adding them to your FAQ. This is done on the same page as the <em>Edit book outline</em> button. Clear titles improve navigability enormously.</li><li>Book pages may come from any content type (blog, story, page, etc.). If you are creating a post solely for inclusion in your book, then use the <a href=\"%create\">create content &raquo; book page</a> link.</li><li>If you don't see the <em>administer</em> link, then you probably have insufficient <a href=\"%permissions\">permissions</a>.</li></ul>", array('%permissions' => url('admin/access/permissions'), "%create" => url('node/add/book'), '%collaborative-book' => url('admin/node/book'), '%orphans-book' => url('admin/node/book/orphan'), '%export-book' => url('book/print')));
Dries's avatar
   
Dries committed
797
    case 'admin/modules#description':
Dries's avatar
   
Dries committed
798
      return t('Allows users to collaboratively author a book.');
Dries's avatar
   
Dries committed
799
    case 'admin/node/book':
800
      return t('<p>The book module offers a mean to organize content, authored by many users, in an online manual, outline or FAQ.</p>');
Dries's avatar
   
Dries committed
801
    case 'admin/node/book/orphan':
802
      return t('<p>Pages in a book are like a tree. As pages are edited, reorganized and removed, child pages might be left with no link to the rest of the book.  Such pages are referred to as "orphan pages".  On this page, administrators can review their books for orphans and reattach those pages as desired.</p>');
Dries's avatar
   
Dries committed
803
    case 'node/add#book':
Dries's avatar
   
Dries committed
804
      return t("A book is a collaborative writing effort: users can collaborate writing the pages of the book, positioning the pages in the right order, and reviewing or modifying pages previously written.  So when you have some information to share or when you read a page of the book and you didn't like it, or if you think a certain page could have been written better, you can do something about it.");
Dries's avatar
   
Dries committed
805
  }
Dries's avatar
   
Dries committed
806
807

  if (arg(0) == 'node' && is_numeric(arg(1)) && arg(2) == 'outline') {
Steven Wittens's avatar
Steven Wittens committed
808
    return t('The outline feature allows you to include posts in the <a href="%book">book hierarchy</a>.', array('%book' => url('book')));
Dries's avatar
   
Dries committed
809
  }
810
}
Dries's avatar
   
Dries committed
811

Dries's avatar
   
Dries committed
812
?>