1667b92596a267840ce2f6184e0d82dc8d09aa6b
[plewww.git] / modules / book.module
1 <?php
2 // $Id: book.module 144 2007-03-28 07:52:20Z thierry $
3
4 /**
5  * @file
6  * Allows users to collaboratively author a book.
7  */
8
9 /**
10  * Implementation of hook_node_info().
11  */
12 function book_node_info() {
13   return array('book' => array('name' => t('book page'), 'base' => 'book'));
14 }
15
16 /**
17  * Implementation of hook_perm().
18  */
19 function book_perm() {
20     return array('outline posts in books', 'create book pages', 'create new books', 'edit book pages', 'edit own book pages', 'see printer-friendly version');
21 }
22
23 /**
24  * Implementation of hook_access().
25  */
26 function book_access($op, $node) {
27   global $user;
28
29   if ($op == 'create') {
30     // Only registered users can create book pages.  Given the nature
31     // of the book module this is considered to be a good/safe idea.
32     return user_access('create book pages');
33   }
34
35   if ($op == 'update') {
36     // Only registered users can update book pages.  Given the nature
37     // of the book module this is considered to be a good/safe idea.
38     // One can only update a book page if there are no suggested updates
39     // of that page waiting for approval.  That is, only updates that
40     // don't overwrite the current or pending information are allowed.
41
42     if ((user_access('edit book pages') && !$node->moderate) || ($node->uid == $user->uid && user_access('edit own book pages'))) {
43       return TRUE;
44     }
45     else {
46        // do nothing. node-access() will determine further access
47     }
48   }
49 }
50
51 /**
52  * Implementation of hook_link().
53  */
54 function book_link($type, $node = 0, $main = 0) {
55
56   $links = array();
57
58   if ($type == 'node' && isset($node->parent)) {
59     if (!$main) {
60       if (book_access('create', $node)) {
61         $links[] = l(t('add child page'), "node/add/book/parent/$node->nid");
62       }
63       if (user_access('see printer-friendly version')) {
64         $links[] = l(t('printer-friendly version'),
65                      'book/export/html/'. $node->nid,
66                      array('title' => t('Show a printer-friendly version of this book page and its sub-pages.')));
67       }
68     }
69   }
70
71   return $links;
72 }
73
74 /**
75  * Implementation of hook_menu().
76  */
77 function book_menu($may_cache) {
78   $items = array();
79
80   if ($may_cache) {
81     $items[] = array(
82       'path' => 'node/add/book',
83       'title' => t('book page'),
84       'access' => user_access('create book pages'));
85     $items[] = array(
86       'path' => 'admin/node/book',
87       'title' => t('books'),
88       'callback' => 'book_admin',
89       'access' => user_access('administer nodes'),
90       'type' => MENU_LOCAL_TASK,
91       'weight' => -1);
92     $items[] = array(
93       'path' => 'admin/node/book/list',
94       'title' => t('list'),
95       'type' => MENU_DEFAULT_LOCAL_TASK);
96     $items[] = array(
97       'path' => 'admin/node/book/orphan',
98       'title' => t('orphan pages'),
99       'callback' => 'book_admin_orphan',
100       'type' => MENU_LOCAL_TASK,
101       'weight' => 8);
102     $items[] = array(
103       'path' => 'book',
104       'title' => t('books'),
105       'callback' => 'book_render',
106       'access' => user_access('access content'),
107       'type' => MENU_SUGGESTED_ITEM);
108     $items[] = array(
109       'path' => 'book/export',
110       'callback' => 'book_export',
111       'access' => user_access('access content'),
112       'type' => MENU_CALLBACK);
113   }
114   else {
115     // To avoid SQL overhead, check whether we are on a node page and whether the
116     // user is allowed to outline posts in books.
117     if (arg(0) == 'node' && is_numeric(arg(1)) && user_access('outline posts in books')) {
118       // Only add the outline-tab for non-book pages:
119       $result = db_query(db_rewrite_sql("SELECT n.nid FROM {node} n WHERE n.nid = %d AND n.type != 'book'"), arg(1));
120       if (db_num_rows($result) > 0) {
121         $items[] = array(
122           'path' => 'node/'. arg(1) .'/outline',
123           'title' => t('outline'),
124           'callback' => 'book_outline',
125           'callback arguments' => array(arg(1)),
126           'access' => user_access('outline posts in books'),
127           'type' => MENU_LOCAL_TASK,
128           'weight' => 2);
129       }
130     }
131   }
132
133   return $items;
134 }
135
136 /**
137  * Implementation of hook_block().
138  *
139  * Displays the book table of contents in a block when the current page is a
140  * single-node view of a book node.
141  */
142 function book_block($op = 'list', $delta = 0) {
143   $block = array();
144   if ($op == 'list') {
145     $block[0]['info'] = t('Book navigation');
146     return $block;
147   }
148   else if ($op == 'view') {
149     // Only display this block when the user is browsing a book:
150     if (arg(0) == 'node' && is_numeric(arg(1))) {
151       $result = db_query(db_rewrite_sql('SELECT n.nid, n.title, b.parent FROM {node} n INNER JOIN {book} b ON n.vid = b.vid WHERE n.nid = %d'), arg(1));
152       if (db_num_rows($result) > 0) {
153         $node = db_fetch_object($result);
154
155         $path = book_location($node);
156         $path[] = $node;
157
158         $expand = array();
159         foreach ($path as $key => $node) {
160           $expand[] = $node->nid;
161         }
162
163         $block['subject'] = check_plain($path[0]->title);
164         $block['content'] = book_tree($expand[0], 5, $expand);
165       }
166     }
167
168     return $block;
169   }
170 }
171
172 /**
173  * Implementation of hook_load().
174  */
175 function book_load($node) {
176   $book = db_fetch_object(db_query('SELECT * FROM {book} WHERE vid = %d', $node->vid));
177   return $book;
178 }
179
180 /**
181  * Implementation of hook_insert().
182  */
183 function book_insert($node) {
184   db_query("INSERT INTO {book} (nid, vid, parent, weight) VALUES (%d, %d, %d, %d)", $node->nid, $node->vid, $node->parent, $node->weight);
185 }
186
187 /**
188  * Implementation of hook_update().
189  */
190 function book_update($node) {
191   if ($node->revision) {
192     db_query("INSERT INTO {book} (nid, vid, parent, weight) VALUES (%d, %d, %d, %d)", $node->nid, $node->vid, $node->parent, $node->weight);
193   }
194   else {
195     db_query("UPDATE {book} SET parent = %d, weight = %d WHERE vid = %d", $node->parent, $node->weight, $node->vid);
196   }
197 }
198
199 /**
200  * Implementation of hook_delete().
201  */
202 function book_delete(&$node) {
203   db_query('DELETE FROM {book} WHERE nid = %d', $node->nid);
204 }
205
206 /**
207  * Implementation of hook_submit().
208  */
209 function book_submit(&$node) {
210   global $user;
211   // Set default values for non-administrators.
212   if (!user_access('administer nodes')) {
213     $node->weight = 0;
214     $node->revision = 1;
215     $book->uid = $user->uid;
216     $book->name = $user->uid ? $user->name : '';
217   }
218 }
219
220 /**
221  * Implementation of hook_form().
222  */
223 function book_form(&$node) {
224   if ($node->nid && !$node->parent && !user_access('create new books')) {
225     $form['parent'] = array('#type' => 'value', '#value' => $node->parent);
226   }
227   else {
228     $form['parent'] = array('#type' => 'select',
229       '#title' => t('Parent'),
230       '#default_value' => ($node->parent ? $node->parent : arg(4)),
231       '#options' => book_toc($node->nid),
232       '#weight' => -4,
233       '#description' => user_access('create new books') ? t('The parent section in which to place this page.  Note that each page whose parent is &lt;top-level&gt; is an independent, top-level book.') : t('The parent that this page belongs in.'),
234     );
235   }
236
237   $form['title'] = array('#type' => 'textfield',
238     '#title' => t('Title'),
239     '#required' => TRUE,
240     '#default_value' => $node->title,
241     '#weight' => -5,
242   );
243   $form['body_filter']['body'] = array('#type' => 'textarea',
244     '#title' => t('Body'),
245     '#default_value' => $node->body,
246     '#rows' => 20,
247     '#required' => TRUE,
248   );
249   $form['body_filter']['format'] = filter_form($node->format);
250
251   $form['log'] = array(
252     '#type' => 'textarea',
253     '#title' => t('Log message'),
254     '#default_value' => $node->log,
255     '#weight' => 5,
256     '#description' => t('An explanation of the additions or updates being made to help other authors understand your motivations.'),
257   );
258
259   if (user_access('administer nodes')) {
260     $form['weight'] = array('#type' => 'weight',
261       '#title' => t('Weight'),
262       '#default_value' => $node->weight,
263       '#delta' => 15,
264       '#weight' => 5,
265       '#description' => t('Pages at a given level are ordered first by weight and then by title.'),
266     );
267   }
268   else {
269     // If a regular user updates a book page, we create a new revision
270     // authored by that user:
271     $form['revision'] = array('#type' => 'hidden', '#value' => 1);
272   }
273
274   return $form;
275 }
276
277 /**
278  * Implementation of function book_outline()
279  * Handles all book outline operations.
280  */
281 function book_outline($nid) {
282   $node = node_load($nid);
283   $page = book_load($node);
284
285   $form['parent'] = array('#type' => 'select',
286     '#title' => t('Parent'),
287     '#default_value' => $page->parent,
288     '#options' => book_toc($node->nid),
289     '#description' => t('The parent page in the book.'),
290   );
291   $form['weight'] = array('#type' => 'weight',
292     '#title' => t('Weight'),
293     '#default_value' => $page->weight,
294     '#delta' => 15,
295     '#description' => t('Pages at a given level are ordered first by weight and then by title.'),
296   );
297   $form['log'] = array('#type' => 'textarea',
298     '#title' => t('Log message'),
299     '#default_value' => $node->log,
300     '#description' => t('An explanation to help other authors understand your motivations to put this post into the book.'),
301   );
302
303   $form['nid'] = array('#type' => 'value', '#value' => $nid);
304   if ($page->nid) {
305     $form['update'] = array('#type' => 'submit',
306       '#value' => t('Update book outline'),
307     );
308     $form['remove'] = array('#type' => 'submit',
309       '#value' => t('Remove from book outline'),
310     );
311   }
312   else {
313     $form['add'] = array('#type' => 'submit', '#value' => t('Add to book outline'));
314   }
315
316   drupal_set_title(check_plain($node->title));
317   return drupal_get_form('book_outline', $form);
318 }
319
320 /**
321  * Handles book outline form submissions.
322  */
323 function book_outline_submit($form_id, $form_values) {
324   $op = $_POST['op'];
325   $node = node_load($form_values['nid']);
326
327   switch ($op) {
328     case t('Add to book outline'):
329       db_query('INSERT INTO {book} (nid, vid, parent, weight) VALUES (%d, %d, %d, %d)', $node->nid, $node->vid, $form_values['parent'], $form_values['weight']);
330       db_query("UPDATE {node_revisions} SET log = '%s' WHERE vid = %d", $form_values['log'], $node->vid);
331       drupal_set_message(t('The post has been added to the book.'));
332       break;
333     case t('Update book outline'):
334       db_query('UPDATE {book} SET parent = %d, weight = %d WHERE vid = %d', $form_values['parent'], $form_values['weight'], $node->vid);
335       db_query("UPDATE {node_revisions} SET log = '%s' WHERE vid = %d", $form_values['log'], $node->vid);
336       drupal_set_message(t('The book outline has been updated.'));
337       break;
338     case t('Remove from book outline'):
339       db_query('DELETE FROM {book} WHERE nid = %d', $node->nid);
340       drupal_set_message(t('The post has been removed from the book.'));
341       break;
342   }
343   return "node/$node->nid";
344 }
345
346 /**
347  * Given a node, this function returns an array of 'book node' objects
348  * representing the path in the book tree from the root to the
349  * parent of the given node.
350  *
351  * @param node - a book node object for which to compute the path
352  *
353  * @return - an array of book node objects representing the path of
354  * nodes root to parent of the given node. Returns an empty array if
355  * the node does not exist or is not part of a book hierarchy.
356  *
357  */
358 function book_location($node, $nodes = array()) {
359   $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));
360   if (isset($parent->title)) {
361     $nodes = book_location($parent, $nodes);
362     $nodes[] = $parent;
363   }
364   return $nodes;
365 }
366
367 /**
368  * Accumulates the nodes up to the root of the book from the given node in the $nodes array.
369  */
370 function book_location_down($node, $nodes = array()) {
371   $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.vid = b.vid WHERE n.status = 1 AND b.parent = %d ORDER BY b.weight DESC, n.title DESC'), $node->nid));
372   if ($last_direct_child) {
373     $nodes[] = $last_direct_child;
374     $nodes = book_location_down($last_direct_child, $nodes);
375   }
376   return $nodes;
377 }
378
379 /**
380  * Fetches the node object of the previous page of the book.
381  */
382 function book_prev($node) {
383   // If the parent is zero, we are at the start of a book so there is no previous.
384   if ($node->parent == 0) {
385     return NULL;
386   }
387
388   // Previous on the same level:
389   $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));
390   if ($direct_above) {
391     // Get last leaf of $above.
392     $path = book_location_down($direct_above);
393
394     return $path ? (count($path) > 0 ? array_pop($path) : NULL) : $direct_above;
395   }
396   else {
397     // Direct parent:
398     $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));
399     return $prev;
400   }
401 }
402
403 /**
404  * Fetches the node object of the next page of the book.
405  */
406 function book_next($node) {
407   // get first direct child
408   $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));
409   if ($child) {
410     return $child;
411   }
412
413   // No direct child: get next for this level or any parent in this book.
414   $path = book_location($node); // Path to top-level node including this one.
415   $path[] = $node;
416
417   while (($leaf = array_pop($path)) && count($path)) {
418     $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));
419     if ($next) {
420       return $next;
421     }
422   }
423 }
424
425 /**
426  * Returns the content of a given node.  If $teaser if true, returns
427  * the teaser rather than full content.  Displays the most recently
428  * approved revision of a node (if any) unless we have to display this
429  * page in the context of the moderation queue.
430  */
431 function book_content($node, $teaser = FALSE) {
432   // Return the page body.
433   return node_prepare($node, $teaser);
434 }
435
436 /**
437  * Implementation of hook_view().
438  *
439  * If not displayed on the main page, we render the node as a page in the
440  * book with extra links to the previous and next pages.
441  */
442 function book_view(&$node, $teaser = FALSE, $page = FALSE) {
443   $node = node_prepare($node, $teaser);
444 }
445
446 /**
447  * Implementation of hook_nodeapi().
448  *
449  * Appends book navigation to all nodes in the book.
450  */
451 function book_nodeapi(&$node, $op, $teaser, $page) {
452   switch ($op) {
453     case 'view':
454       if (!$teaser) {
455         $book = db_fetch_array(db_query('SELECT * FROM {book} WHERE vid = %d', $node->vid));
456         if ($book) {
457           if ($node->moderate && user_access('administer nodes')) {
458             drupal_set_message(t("The post has been submitted for moderation and won't be accessible until it has been approved."));
459           }
460
461           foreach ($book as $key => $value) {
462             $node->$key = $value;
463           }
464
465           $path = book_location($node);
466           // Construct the breadcrumb:
467           $node->breadcrumb = array(); // Overwrite the trail with a book trail.
468           foreach ($path as $level) {
469             $node->breadcrumb[] = array('path' => 'node/'. $level->nid, 'title' =>  $level->title);
470           }
471           $node->breadcrumb[] = array('path' => 'node/'. $node->nid);
472
473           $node->body .= theme('book_navigation', $node);
474
475           if ($page) {
476             menu_set_location($node->breadcrumb);
477           }
478         }
479       }
480       break;
481     case 'delete revision':
482       db_query('DELETE FROM {book} WHERE vid = %d', $node->vid);
483       break;
484     case 'delete':
485       db_query('DELETE FROM {book} WHERE nid = %d', $node->nid);
486       break;
487   }
488 }
489
490 /**
491  * Prepares the links to children (TOC) and forward/backward
492  * navigation for a node presented as a book page.
493  *
494  * @ingroup themeable
495  */
496 function theme_book_navigation($node) {
497   $output = '';
498   $links = '';
499
500   if ($node->nid) {
501     $tree = book_tree($node->nid);
502
503     if ($prev = book_prev($node)) {
504       drupal_add_link(array('rel' => 'prev', 'href' => url('node/'. $prev->nid)));
505       $links .= l(t('‹ ') . $prev->title, 'node/'. $prev->nid, array('class' => 'page-previous', 'title' => t('Go to previous page')));
506     }
507     if ($node->parent) {
508       drupal_add_link(array('rel' => 'up', 'href' => url('node/'. $node->parent)));
509       $links .= l(t('up'), 'node/'. $node->parent, array('class' => 'page-up', 'title' => t('Go to parent page')));
510     }
511     if ($next = book_next($node)) {
512       drupal_add_link(array('rel' => 'next', 'href' => url('node/'. $next->nid)));
513       $links .= l($next->title . t(' â€º'), 'node/'. $next->nid, array('class' => 'page-next', 'title' => t('Go to next page')));
514     }
515
516     if (isset($tree) || isset($links)) {
517       $output = '<div class="book-navigation">';
518       if (isset($tree)) {
519         $output .= $tree;
520       }
521       if (isset($links)) {
522         $output .= '<div class="page-links">'. $links .'</div>';
523       }
524       $output .= '</div>';
525     }
526   }
527
528   return $output;
529 }
530
531 /**
532  * This is a helper function for book_toc().
533  */
534 function book_toc_recurse($nid, $indent, $toc, $children, $exclude) {
535   if ($children[$nid]) {
536     foreach ($children[$nid] as $foo => $node) {
537       if (!$exclude || $exclude != $node->nid) {
538         $toc[$node->nid] = $indent .' '. $node->title;
539         $toc = book_toc_recurse($node->nid, $indent .'--', $toc, $children, $exclude);
540       }
541     }
542   }
543
544   return $toc;
545 }
546
547 /**
548  * Returns an array of titles and nid entries of book pages in table of contents order.
549  */
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'));
552
553   while ($node = db_fetch_object($result)) {
554     if (!$children[$node->parent]) {
555       $children[$node->parent] = array();
556     }
557     $children[$node->parent][] = $node;
558   }
559
560   $toc = array();
561   // If the user has permission to create new books, add the top-level book page to the menu;
562   if (user_access('create new books')) {
563     $toc[0] = '<'. t('top-level') .'>';
564   }
565
566   $toc = book_toc_recurse(0, '', $toc, $children, $exclude);
567
568   return $toc;
569 }
570
571 /**
572  * This is a helper function for book_tree()
573  */
574 function book_tree_recurse($nid, $depth, $children, $unfold = array()) {
575   $output = '';
576   if ($depth > 0) {
577     if (isset($children[$nid])) {
578       foreach ($children[$nid] as $foo => $node) {
579         if (in_array($node->nid, $unfold)) {
580           if ($tree = book_tree_recurse($node->nid, $depth - 1, $children, $unfold)) {
581             $output .= '<li class="expanded">';
582             $output .= l($node->title, 'node/'. $node->nid);
583             $output .= '<ul class="menu">'. $tree .'</ul>';
584             $output .= '</li>';
585           }
586           else {
587             $output .= '<li class="leaf">'. l($node->title, 'node/'. $node->nid) .'</li>';
588           }
589         }
590         else {
591           if ($tree = book_tree_recurse($node->nid, 1, $children)) {
592             $output .= '<li class="collapsed">'. l($node->title, 'node/'. $node->nid) .'</li>';
593           }
594           else {
595             $output .= '<li class="leaf">'. l($node->title, 'node/'. $node->nid) .'</li>';
596           }
597         }
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
607  * as a tree.
608  */
609 function book_tree($parent = 0, $depth = 3, $unfold = array()) {
610   $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'));
611
612   while ($node = db_fetch_object($result)) {
613     $list = isset($children[$node->parent]) ? $children[$node->parent] : array();
614     $list[] = $node;
615     $children[$node->parent] = $list;
616   }
617
618   if ($tree = book_tree_recurse($parent, $depth, $children, $unfold)) {
619     return '<ul class="menu">'. $tree .'</ul>';
620   }
621 }
622
623 /**
624  * Menu callback; prints a listing of all books.
625  */
626 function book_render() {
627   $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'));
628
629   $books = array();
630   while ($node = db_fetch_object($result)) {
631     $books[] = l($node->title, 'node/'. $node->nid);
632   }
633
634   return theme('item_list', $books);
635 }
636
637 /**
638  * Menu callback; Generates various representation of a book page with
639  * all descendants and prints the requested representation to output.
640  *
641  * The function delegates the generation of output to helper functions.
642  * The function name is derived by prepending 'book_export_' to the
643  * given output type.  So, e.g., a type of 'html' results in a call to
644  * the function book_export_html().
645  *
646  * @param type
647  *   - a string encoding the type of output requested.
648  *       The following types are currently supported in book module
649  *          html: HTML (printer friendly output)
650  *       Other types are supported in contributed modules.
651  * @param nid
652  *   - an integer representing the node id (nid) of the node to export
653  *
654  */
655 function book_export($type = 'html', $nid = 0) {
656   $type = drupal_strtolower($type);
657   $node_result = db_query(db_rewrite_sql('SELECT n.nid, n.title, b.parent FROM {node} n INNER JOIN {book} b ON n.vid = b.vid WHERE n.nid = %d'), $nid);
658   if (db_num_rows($node_result) > 0) {
659       $node = db_fetch_object($node_result);
660   }
661   $depth = count(book_location($node)) + 1;
662   $export_function = 'book_export_' . $type;
663
664   if (function_exists($export_function)) {
665     print call_user_func($export_function, $nid, $depth);
666   }
667   else {
668     drupal_set_message(t('Unknown export format.'));
669     drupal_not_found();
670   }
671 }
672
673 /**
674  * This function is called by book_export() to generate HTML for export.
675  *
676  * The given node is /embedded to its absolute depth in a top level
677  * section/.  For example, a child node with depth 2 in the hierarchy
678  * is contained in (otherwise empty) &lt;div&gt; elements
679  * corresponding to depth 0 and depth 1.  This is intended to support
680  * WYSIWYG output - e.g., level 3 sections always look like level 3
681  * sections, no matter their depth relative to the node selected to be
682  * exported as printer-friendly HTML.
683  *
684  * @param nid
685  *   - an integer representing the node id (nid) of the node to export
686  * @param depth
687  *   - an integer giving the depth in the book hierarchy of the node
688  *     which is to be exported
689  *
690  * @return
691  *   - string containing HTML representing the node and its children in
692  *     the book hierarchy
693 */
694 function book_export_html($nid, $depth) {
695   if (user_access('see printer-friendly version')) {
696     $node = node_load($nid);
697     for ($i = 1; $i < $depth; $i++) {
698       $content .= "<div class=\"section-$i\">\n";
699     }
700     $content .= book_recurse($nid, $depth, 'book_node_visitor_html_pre', 'book_node_visitor_html_post');
701     for ($i = 1; $i < $depth; $i++) {
702       $content .= "</div>\n";
703     }
704     return theme('book_export_html', check_plain($node->title), $content);
705   }
706   else {
707     drupal_access_denied();
708   }
709 }
710
711 /**
712  * How the book's HTML export should be themed
713  *
714  * @ingroup themeable
715  */
716 function theme_book_export_html($title, $content) {
717   global $base_url;
718   $html = "<!DOCTYPE html PUBLIC \"-//W3C//DTD XHTML 1.0 Transitional//EN\" \"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd\">\n";
719   $html .= '<html xmlns="http://www.w3.org/1999/xhtml" lang="en" xml:lang="en">';
720   $html .= "<head>\n<title>". $title ."</title>\n";
721   $html .= '<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />';
722   $html .= '<base href="'. $base_url .'/" />' . "\n";
723   $html .= "<style type=\"text/css\">\n@import url(misc/print.css);\n</style>\n";
724   $html .= "</head>\n<body>\n". $content . "\n</body>\n</html>\n";
725   return $html;
726 }
727
728 /**
729  * Traverses the book tree.  Applies the $visit_pre() callback to each
730  * node, is called recursively for each child of the node (in weight,
731  * title order).  Finally appends the output of the $visit_post()
732  * callback to the output before returning the generated output.
733  *
734  * @param nid
735  *  - the node id (nid) of the root node of the book hierarchy.
736  * @param depth
737  *  - the depth of the given node in the book hierarchy.
738  * @param visit_pre
739  *  - a function callback to be called upon visiting a node in the tree
740  * @param visit_post
741  *  - a function callback to be called after visiting a node in the tree,
742  *    but before recursively visiting children.
743  * @return
744  *  - the output generated in visiting each node
745  */
746 function book_recurse($nid = 0, $depth = 1, $visit_pre, $visit_post) {
747   $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);
748   while ($page = db_fetch_object($result)) {
749     // Load the node:
750     $node = node_load($page->nid);
751
752     if ($node) {
753       if (function_exists($visit_pre)) {
754         $output .= call_user_func($visit_pre, $node, $depth, $nid);
755       }
756       else {
757         $output .= book_node_visitor_html_pre($node, $depth, $nid);
758       }
759
760       $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);
761       while ($childpage = db_fetch_object($children)) {
762           $childnode = node_load($childpage->nid);
763           if ($childnode->nid != $node->nid) {
764               $output .= book_recurse($childnode->nid, $depth + 1, $visit_pre, $visit_post);
765           }
766       }
767       if (function_exists($visit_post)) {
768         $output .= call_user_func($visit_post, $node, $depth);
769       }
770       else {
771         # default
772         $output .= book_node_visitor_html_post($node, $depth);
773       }
774     }
775   }
776
777   return $output;
778 }
779
780 /**
781  * Generates printer-friendly HTML for a node.  This function
782  * is a 'pre-node' visitor function for book_recurse().
783  *
784  * @param $node
785  *   - the node to generate output for.
786  * @param $depth
787  *   - the depth of the given node in the hierarchy. This
788  *   is used only for generating output.
789  * @param $nid
790  *   - the node id (nid) of the given node. This
791  *   is used only for generating output.
792  * @return
793  *   - the HTML generated for the given node.
794  */
795 function book_node_visitor_html_pre($node, $depth, $nid) {
796   // Output the content:
797   if (node_hook($node, 'content')) {
798     $node = node_invoke($node, 'content');
799   }
800   // Allow modules to change $node->body before viewing.
801   node_invoke_nodeapi($node, 'print', $node->body, false);
802
803   $output .= "<div id=\"node-". $node->nid ."\" class=\"section-$depth\">\n";
804   $output .= "<h1 class=\"book-heading\">". check_plain($node->title) ."</h1>\n";
805
806   if ($node->body) {
807     $output .= $node->body;
808   }
809   return $output;
810 }
811
812 /**
813  * Finishes up generation of printer-friendly HTML after visiting a
814  * node. This function is a 'post-node' visitor function for
815  * book_recurse().
816  */
817 function book_node_visitor_html_post($node, $depth) {
818   return "</div>\n";
819 }
820
821 function _book_admin_table($nodes = array()) {
822   $form = array(
823     '#theme' => 'book_admin_table',
824     '#tree' => TRUE,
825   );
826
827   foreach ($nodes as $node) {
828     $form = array_merge($form, _book_admin_table_tree($node, 0));
829   }
830
831   return $form;
832 }
833
834 function _book_admin_table_tree($node, $depth) {
835   $form = array();
836
837   $form[] = array(
838     'nid' => array('#type' => 'value', '#value' => $node->nid),
839     'depth' => array('#type' => 'value', '#value' => $depth),
840     'title' => array(
841       '#type' => 'textfield',
842       '#default_value' => $node->title,
843       '#maxlength' => 255,
844     ),
845     'weight' => array(
846       '#type' => 'weight',
847       '#default_value' => $node->weight,
848       '#delta' => 15,
849     ),
850   );
851
852   $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);
853   while ($child = db_fetch_object($children)) {
854     $form = array_merge($form, _book_admin_table_tree(node_load($child->nid), $depth + 1));
855   }
856
857   return $form;
858 }
859
860 function theme_book_admin_table($form) {
861   $header = array(t('Title'), t('Weight'), array('data' => t('Operations'), 'colspan' => '3'));
862
863   $rows = array();
864   foreach (element_children($form) as $key) {
865     $nid = $form[$key]['nid']['#value'];
866     $pid = $form[0]['nid']['#value'];
867     if ($pid == $nid) {
868       // Don't return to the parent book page if it is deleted.
869       $pid = '';
870     }
871     $rows[] = array(
872       '<div style="padding-left: '. (25 * $form[$key]['depth']['#value']) .'px;">'. form_render($form[$key]['title']) .'</div>',
873       form_render($form[$key]['weight']),
874       l(t('view'), 'node/'. $nid),
875       l(t('edit'), 'node/'. $nid .'/edit'),
876       l(t('delete'), 'node/'. $nid .'/delete', NULL, 'destination=admin/node/book/'. (arg(3) == 'orphan' ? 'orphan' : $pid)),
877     );
878   }
879
880   return theme('table', $header, $rows);
881 }
882
883 /**
884  * Display an administrative view of the hierarchy of a book.
885  */
886 function book_admin_edit($nid) {
887   $node = node_load($nid);
888   if ($node->nid) {
889     drupal_set_title(check_plain($node->title));
890     $form = array();
891
892     $form['table'] = _book_admin_table(array($node));
893     $form['save'] = array(
894       '#type' => 'submit',
895       '#value' => t('Save book pages'),
896     );
897
898     return drupal_get_form('book_admin_edit', $form);
899   }
900   else {
901     drupal_not_found();
902   }
903 }
904
905 /**
906  * Menu callback; displays a listing of all orphaned book pages.
907  */
908 function book_admin_orphan() {
909   $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'));
910
911   $pages = array();
912   while ($page = db_fetch_object($result)) {
913     $pages[$page->nid] = $page;
914   }
915
916   $orphans = array();
917   if (count($pages)) {
918     foreach ($pages as $page) {
919       if ($page->parent && empty($pages[$page->parent])) {
920         $orphans[] = node_load($page->nid);
921       }
922     }
923   }
924
925   if (count($orphans)) {
926     $form = array();
927
928     $form['table'] = _book_admin_table($orphans);
929     $form['save'] = array(
930       '#type' => 'submit',
931       '#value' => t('Save book pages'),
932     );
933
934     return drupal_get_form('book_admin_edit', $form);
935   }
936   else {
937     return '<p>'. t('There are no orphan pages.') .'</p>';
938   }
939 }
940
941 function book_admin_edit_submit($form_id, $form_values) {
942   foreach ($form_values['table'] as $row) {
943     $node = node_load($row['nid']);
944
945     if ($row['title'] != $node->title || $row['weight'] != $node->weight) {
946       $node->title = $row['title'];
947       $node->weight = $row['weight'];
948
949       node_save($node);
950       watchdog('content', t('%type: updated %title.', array('%type' => theme('placeholder', t('book')), '%title' => theme('placeholder', $node->title))), WATCHDOG_NOTICE, l(t('view'), 'node/'. $node->nid));
951     }
952   }
953
954   if (is_numeric(arg(3))) {
955     // Updating pages in a single book.
956     $book = node_load(arg(3));
957     drupal_set_message(t('Updated book %title.', array('%title' => theme('placeholder', $book->title))));
958   }
959   else {
960     // Updating the orphan pages.
961     drupal_set_message(t('Updated orphan book pages.'));
962   }
963 }
964
965 /**
966  * Menu callback; displays the book administration page.
967  */
968 function book_admin($nid = 0) {
969   if ($nid) {
970     return book_admin_edit($nid);
971   }
972   else {
973     return book_admin_overview();
974   }
975 }
976
977 /**
978  * Returns an administrative overview of all books.
979  */
980 function book_admin_overview() {
981   $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 ORDER BY b.weight, n.title'));
982   while ($book = db_fetch_object($result)) {
983     $rows[] = array(l($book->title, "node/$book->nid"), l(t('outline'), "admin/node/book/$book->nid"));
984   }
985   $headers = array(t('Book'), t('Operations'));
986
987   return theme('table', $headers, $rows);
988 }
989
990 /**
991  * Implementation of hook_help().
992  */
993 function book_help($section) {
994   switch ($section) {
995     case 'admin/help#book':
996       $output = '<p>'. t('The <em>book</em> content type is suited for creating structured, multi-page hypertexts such as site resource guides, manuals, and Frequently Asked Questions (FAQs).  It permits a document to have chapters, sections, subsections, etc.  Authors with suitable permissions can add pages to a collaborative book,  placing them into the existing document by adding them to a table of contents menu. ') .'</p>';
997       $output .= '<p>'. t('Books have additional <em>previous</em>, <em>up</em>, and <em>next</em> navigation elements at the bottom of each page for moving through the text.  Additional navigation may be provided by enabling the <em>book navigation block</em> on the <a href="%admin-block">block administration page</a>.', array('%admin-block' => url('admin/block'))) .'</p>';
998       $output .= '<p>'. t('Users can select the <em>printer-friendly version</em> link visible at the bottom of a book page to generate a printer-friendly display of the page and all of its subsections. ') .'</p>';
999       $output .= '<p>'. t('Administrators can view a book outline, from which is it possible to change the titles of sections, and their <i>weight</i> (thus reordering sections).   From this outline, it is also possible to edit and/or delete book pages.   Many content types besides pages (for example, blog entries, stories, and polls) can be added to a collaborative book by choosing the <em>outline</em> tab when viewing the post.') .'</p>';
1000       $output .= t('<p>You can</p>
1001 <ul>
1002 <li>create new book pages: <a href="%node-add-book">create content &gt;&gt; book page</a>.</li>
1003 <li>administer individual books (choose a book from list): <a href="%admin-node-book">administer &gt;&gt; content &gt;&gt; books</a>.</li>
1004 <li>set workflow and other global book settings on the book configuration page: <a href="%admin-settings-content-types-book-page" title="book page content type">administer &gt;&gt; settings &gt;&gt; content types &gt;&gt; configure book page</a>.</li>
1005 <li>enable the book navigation block: <a href="%admin-block">administer &gt;&gt; blocks</a>.</li>
1006 <li>control who can create, edit, and outline posts in books by setting access permissions: <a href="%admin-access">administer &gt;&gt; access control</a>.</li>
1007 </ul>
1008 ', array('%node-add-book' => url('node/add/book'), '%admin-node-book' => url('admin/node/book'), '%admin-settings-content-types-book-page' => url('admin/settings/content-types/book'), '%admin-block' => url('admin/block'), '%admin-access' => url('admin/access')));
1009       $output .= '<p>'. t('For more information please read the configuration and customization handbook <a href="%book">Book page</a>.', array('%book' => 'http://drupal.org/handbook/modules/book/')) .'</p>';
1010       return $output;
1011     case 'admin/modules#description':
1012       return t('Allows users to collaboratively author a book.');
1013     case 'admin/node/book':
1014       return t('<p>The book module offers a means to organize content, authored by many users, in an online manual, outline or FAQ.</p>');
1015     case 'admin/node/book/orphan':
1016       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>');
1017     case 'node/add#book':
1018       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.");
1019   }
1020
1021   if (arg(0) == 'node' && is_numeric(arg(1)) && arg(2) == 'outline') {
1022     return t('The outline feature allows you to include posts in the <a href="%book">book hierarchy</a>.', array('%book' => url('book')));
1023   }
1024 }
1025
1026