Skip to content
book.module 35.8 KiB
Newer Older
Dries Buytaert's avatar
Dries Buytaert committed
<?php
Dries Buytaert's avatar
Dries Buytaert committed

Dries Buytaert's avatar
 
Dries Buytaert committed
/**
 * @file
 * Allows users to collaboratively author a book.
 */

 * Implementation of hook_node_info().
  return array('book' => array('name' => t('book page'), 'base' => 'book'));
Dries Buytaert's avatar
 
Dries Buytaert committed
}

Dries Buytaert's avatar
 
Dries Buytaert committed
function book_perm() {
    return array('outline posts in books', 'create book pages', 'create new books', 'edit book pages', 'edit own book pages', 'see printer-friendly version');
Dries Buytaert's avatar
 
Dries Buytaert committed
}

Dries Buytaert's avatar
 
Dries Buytaert committed
function book_access($op, $node) {
Dries Buytaert's avatar
 
Dries Buytaert committed
  global $user;
Dries Buytaert's avatar
 
Dries Buytaert committed

    // Only registered users can create book pages. Given the nature
Dries Buytaert's avatar
 
Dries Buytaert committed
    // of the book module this is considered to be a good/safe idea.
    return user_access('create book pages');
Dries Buytaert's avatar
 
Dries Buytaert committed
  }

    // Only registered users can update book pages. Given the nature
Dries Buytaert's avatar
 
Dries Buytaert committed
    // 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
    // of that page waiting for approval. That is, only updates that
    // don't overwrite the current or pending information are allowed.
    if ((user_access('edit book pages') && !$node->moderate) || ($node->uid == $user->uid && user_access('edit own book pages'))) {
      return TRUE;
    }
    else {
       // do nothing. node-access() will determine further access
    }
Dries Buytaert's avatar
 
Dries Buytaert committed
  }
Dries Buytaert's avatar
 
Dries Buytaert committed
}

Dries Buytaert's avatar
 
Dries Buytaert committed
/**
 * Implementation of hook_link().
 */
Dries Buytaert's avatar
 
Dries Buytaert committed
function book_link($type, $node = 0, $main = 0) {
Dries Buytaert's avatar
 
Dries Buytaert committed

  $links = array();

  if ($type == 'node' && isset($node->parent)) {
Dries Buytaert's avatar
 
Dries Buytaert committed
    if (!$main) {
Dries Buytaert's avatar
 
Dries Buytaert committed
      if (book_access('create', $node)) {
          'title' => t('add child page'),
          'href' => "node/add/book/parent/$node->nid"
Dries Buytaert's avatar
 
Dries Buytaert committed
      }
      if (user_access('see printer-friendly version')) {
          'title' => t('printer-friendly version'),
          'href' => 'book/export/html/'. $node->nid,
          'attributes' => array('title' => t('Show a printer-friendly version of this book page and its sub-pages.'))
Dries Buytaert's avatar
 
Dries Buytaert committed
    }
Dries Buytaert's avatar
 
Dries Buytaert committed
  }

Dries Buytaert's avatar
 
Dries Buytaert committed
  return $links;
Dries Buytaert's avatar
 
Dries Buytaert committed
}

Dries Buytaert's avatar
 
Dries Buytaert committed
/**
 * Implementation of hook_menu().
 */
Dries Buytaert's avatar
 
Dries Buytaert committed
function book_menu($may_cache) {
Dries Buytaert's avatar
 
Dries Buytaert committed
  $items = array();

Dries Buytaert's avatar
 
Dries Buytaert committed
  if ($may_cache) {
    $items[] = array(
      'path' => 'node/add/book',
      'title' => t('book page'),
      'access' => user_access('create book pages'));
    $items[] = array(
      'path' => 'admin/node/book',
      'title' => t('books'),
Dries Buytaert's avatar
 
Dries Buytaert committed
      'callback' => 'book_admin',
      'access' => user_access('administer nodes'),
      'type' => MENU_LOCAL_TASK,
      'weight' => -1);
    $items[] = array(
      'path' => 'admin/node/book/list',
      'title' => t('list'),
      'type' => MENU_DEFAULT_LOCAL_TASK);
    $items[] = array(
      'path' => 'admin/node/book/orphan',
      'title' => t('orphan pages'),
Dries Buytaert's avatar
 
Dries Buytaert committed
      'callback' => 'book_admin_orphan',
      'type' => MENU_LOCAL_TASK,
Dries Buytaert's avatar
 
Dries Buytaert committed
      'weight' => 8);
    $items[] = array(
      'path' => 'book',
      'title' => t('books'),
Dries Buytaert's avatar
 
Dries Buytaert committed
      'callback' => 'book_render',
      'access' => user_access('access content'),
      'type' => MENU_SUGGESTED_ITEM);
    $items[] = array(
      'path' => 'book/export',
      'callback' => 'book_export',
      'access' => user_access('access content'),
Dries Buytaert's avatar
 
Dries Buytaert committed
      'type' => MENU_CALLBACK);
Dries Buytaert's avatar
 
Dries Buytaert committed
  }
Dries Buytaert's avatar
 
Dries Buytaert committed
  else {
    // To avoid SQL overhead, check whether we are on a node page and whether the
    // user is allowed to outline posts in books.
    if (arg(0) == 'node' && is_numeric(arg(1)) && user_access('outline posts in books')) {
Dries Buytaert's avatar
 
Dries Buytaert committed
      // Only add the outline-tab for non-book pages:
      $result = db_query(db_rewrite_sql("SELECT n.nid FROM {node} n WHERE n.nid = %d AND n.type != 'book'"), arg(1));
Dries Buytaert's avatar
 
Dries Buytaert committed
      if (db_num_rows($result) > 0) {
        $items[] = array(
          'path' => 'node/'. arg(1) .'/outline',
          'title' => t('outline'),
          'callback' => 'book_outline',
          'access' => user_access('outline posts in books'),
          'type' => MENU_LOCAL_TASK,
          'weight' => 2);
Dries Buytaert's avatar
 
Dries Buytaert committed
      }
    }
  }
Dries Buytaert's avatar
 
Dries Buytaert committed

  return $items;
}

Dries Buytaert's avatar
 
Dries Buytaert committed
 * Displays the book table of contents in a block when the current page is a
 * single-node view of a book node.
Dries Buytaert's avatar
 
Dries Buytaert committed
function book_block($op = 'list', $delta = 0) {
Dries Buytaert's avatar
 
Dries Buytaert committed
  $block = array();
Dries Buytaert's avatar
 
Dries Buytaert committed
  if ($op == 'list') {
    $block[0]['info'] = t('Book navigation');
Dries Buytaert's avatar
 
Dries Buytaert committed
  }
Dries Buytaert's avatar
 
Dries Buytaert committed
    // Only display this block when the user is browsing a book:
    if (arg(0) == 'node' && is_numeric(arg(1))) {
      $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 Buytaert's avatar
 
Dries Buytaert committed
      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;
        }

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

Dries Buytaert's avatar
 
Dries Buytaert committed
}

Dries Buytaert's avatar
 
Dries Buytaert committed
function book_load($node) {
  return db_fetch_object(db_query('SELECT * FROM {book} WHERE vid = %d', $node->vid));
Dries Buytaert's avatar
 
Dries Buytaert committed
}

Dries Buytaert's avatar
 
Dries Buytaert committed
function book_insert($node) {
  db_query("INSERT INTO {book} (nid, vid, parent, weight) VALUES (%d, %d, %d, %d)", $node->nid, $node->vid, $node->parent, $node->weight);
Dries Buytaert's avatar
 
Dries Buytaert committed
}
Dries Buytaert's avatar
 
Dries Buytaert committed

Dries Buytaert's avatar
 
Dries Buytaert committed
function book_update($node) {
  if ($node->revision) {
    db_query("INSERT INTO {book} (nid, vid, parent, weight) VALUES (%d, %d, %d, %d)", $node->nid, $node->vid, $node->parent, $node->weight);
  }
  else {
    db_query("UPDATE {book} SET parent = %d, weight = %d WHERE vid = %d", $node->parent, $node->weight, $node->vid);
  }
Dries Buytaert's avatar
 
Dries Buytaert committed
}
Dries Buytaert's avatar
 
Dries Buytaert committed

  db_query('DELETE FROM {book} WHERE nid = %d', $node->nid);
Dries Buytaert's avatar
 
Dries Buytaert committed
}

  // Set default values for non-administrators.
  if (!user_access('administer nodes')) {
    $node->weight = 0;
    $node->revision = 1;
    $book->uid = $user->uid;
    $book->name = $user->uid ? $user->name : '';
Dries Buytaert's avatar
 
Dries Buytaert committed
function book_form(&$node) {
  if ($node->nid && !$node->parent && !user_access('create new books')) {
    $form['parent'] = array('#type' => 'value', '#value' => $node->parent);
  }
  else {
    $form['parent'] = array('#type' => 'select',
      '#title' => t('Parent'),
      '#default_value' => ($node->parent ? $node->parent : arg(4)),
      '#options' => book_toc($node->nid),
      '#weight' => -4,
      '#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.'),

  $form['title'] = array('#type' => 'textfield',
    '#title' => t('Title'),
    '#required' => TRUE,
    '#default_value' => $node->title,
    '#weight' => -5,
  );
  $form['body_filter']['body'] = array('#type' => 'textarea',
    '#title' => t('Body'),
    '#default_value' => $node->body,
    '#rows' => 20,
    '#required' => TRUE,
  );
  $form['body_filter']['format'] = filter_form($node->format);
  $form['log'] = array(
    '#type' => 'textarea',
    '#title' => t('Log message'),
    '#default_value' => $node->log,
    '#weight' => 5,
    '#description' => t('An explanation of the additions or updates being made to help other authors understand your motivations.'),
Dries Buytaert's avatar
 
Dries Buytaert committed

    $form['weight'] = array('#type' => 'weight',
      '#title' => t('Weight'),
      '#default_value' => $node->weight,
      '#delta' => 15,
      '#weight' => 5,
      '#description' => t('Pages at a given level are ordered first by weight and then by title.'),
Dries Buytaert's avatar
 
Dries Buytaert committed
  }
  else {
Dries Buytaert's avatar
 
Dries Buytaert committed
    // If a regular user updates a book page, we create a new revision
    // authored by that user:
    $form['revision'] = array('#type' => 'hidden', '#value' => 1);
Dries Buytaert's avatar
 
Dries Buytaert committed
  }

Dries Buytaert's avatar
 
Dries Buytaert committed
}

Dries Buytaert's avatar
 
Dries Buytaert committed
 * Implementation of function book_outline()
 * Handles all book outline operations.
function book_outline($nid) {
  $node = node_load($nid);
Dries Buytaert's avatar
 
Dries Buytaert committed

  $form['parent'] = array('#type' => 'select',
    '#title' => t('Parent'),
    '#default_value' => $page->parent,
    '#description' => t('The parent page in the book.'),
  );
  $form['weight'] = array('#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.'),
  );
  $form['log'] = array('#type' => 'textarea',
    '#title' => t('Log message'),
    '#default_value' => $node->log,
    '#description' => t('An explanation to help other authors understand your motivations to put this post into the book.'),
  );
Dries Buytaert's avatar
 
Dries Buytaert committed

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

  drupal_set_title(check_plain($node->title));
  return drupal_get_form('book_outline', $form);
}

/**
 * Handles book outline form submissions.
 */
function book_outline_submit($form_id, $form_values) {
  $op = $_POST['op'];
  $node = node_load($form_values['nid']);

  switch ($op) {
    case t('Add to book outline'):
      db_query('INSERT INTO {book} (nid, vid, parent, weight) VALUES (%d, %d, %d, %d)', $node->nid, $node->vid, $form_values['parent'], $form_values['weight']);
      db_query("UPDATE {node_revisions} SET log = '%s' WHERE vid = %d", $form_values['log'], $node->vid);
      drupal_set_message(t('The post has been added to the book.'));
      break;
    case t('Update book outline'):
      db_query('UPDATE {book} SET parent = %d, weight = %d WHERE vid = %d', $form_values['parent'], $form_values['weight'], $node->vid);
      db_query("UPDATE {node_revisions} SET log = '%s' WHERE vid = %d", $form_values['log'], $node->vid);
      drupal_set_message(t('The book outline has been updated.'));
      break;
    case t('Remove from book outline'):
      db_query('DELETE FROM {book} WHERE nid = %d', $node->nid);
      drupal_set_message(t('The post has been removed from the book.'));
      break;
Dries Buytaert's avatar
 
Dries Buytaert committed
  }
Dries Buytaert's avatar
 
Dries Buytaert committed
}

 * Given a node, this function returns an array of 'book node' objects
 * representing the path in the book tree from the root to the
 * @param node - a book node object for which to compute the path
 *
 * @return - an array of book node objects representing the path of
 * nodes root to parent of the given node. Returns an empty array if
 * the node does not exist or is not part of a book hierarchy.
 *
Dries Buytaert's avatar
 
Dries Buytaert committed
function book_location($node, $nodes = array()) {
  $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));
  if (isset($parent->title)) {
Dries Buytaert's avatar
 
Dries Buytaert committed
    $nodes = book_location($parent, $nodes);
    $nodes[] = $parent;
Dries Buytaert's avatar
 
Dries Buytaert committed
  }
  return $nodes;
}

/**
 * Accumulates the nodes up to the root of the book from the given node in the $nodes array.
 */
Dries Buytaert's avatar
 
Dries Buytaert committed
function book_location_down($node, $nodes = array()) {
  $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));
Dries Buytaert's avatar
 
Dries Buytaert committed
  if ($last_direct_child) {
    $nodes[] = $last_direct_child;
Dries Buytaert's avatar
 
Dries Buytaert committed
    $nodes = book_location_down($last_direct_child, $nodes);
  }
  return $nodes;
}

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

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

    return $path ? (count($path) > 0 ? array_pop($path) : NULL) : $direct_above;
Dries Buytaert's avatar
 
Dries Buytaert committed
  }
  else {
Dries Buytaert's avatar
 
Dries Buytaert committed
    // Direct parent:
    $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 Buytaert's avatar
 
Dries Buytaert committed
    return $prev;
  }
}

 * Fetches the node object of the next page of the book.
Dries Buytaert's avatar
 
Dries Buytaert committed
function book_next($node) {
  // get first direct child
  $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 Buytaert's avatar
 
Dries Buytaert committed
  if ($child) {
    return $child;
  }

Dries Buytaert's avatar
 
Dries Buytaert committed
  // No direct child: get next for this level or any parent in this book.
  $path = book_location($node); // Path to top-level node including this one.
  $path[] = $node;
Dries Buytaert's avatar
 
Dries Buytaert committed
  while (($leaf = array_pop($path)) && count($path)) {
    $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 Buytaert's avatar
 
Dries Buytaert committed
    if ($next) {
      return $next;
    }
  }
}

 * 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 Buytaert's avatar
 
Dries Buytaert committed
function book_content($node, $teaser = FALSE) {
  // Return the page body.
  return node_prepare($node, $teaser);
Dries Buytaert's avatar
 
Dries Buytaert committed
}

/**
 * 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 Buytaert's avatar
 
Dries Buytaert committed
function book_view(&$node, $teaser = FALSE, $page = FALSE) {
  $node = node_prepare($node, $teaser);
Dries Buytaert's avatar
 
Dries Buytaert committed
}

Dries Buytaert's avatar
 
Dries Buytaert committed
 * Implementation of hook_nodeapi().
 *
 * Appends book navigation to all nodes in the book.
Dries Buytaert's avatar
 
Dries Buytaert committed
function book_nodeapi(&$node, $op, $teaser, $page) {
  switch ($op) {
    case 'view':
      if (!$teaser) {
        $book = db_fetch_array(db_query('SELECT * FROM {book} WHERE vid = %d', $node->vid));
Dries Buytaert's avatar
 
Dries Buytaert committed
        if ($book) {
          if ($node->moderate && user_access('administer nodes')) {
            drupal_set_message(t("The post has been submitted for moderation and won't be accessible until it has been approved."));
Dries Buytaert's avatar
 
Dries Buytaert committed
          foreach ($book as $key => $value) {
            $node->$key = $value;
          }

          $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);

          if ($page) {
            menu_set_location($node->breadcrumb);
          }
Dries Buytaert's avatar
 
Dries Buytaert committed
        }
Dries Buytaert's avatar
 
Dries Buytaert committed
      }
Dries Buytaert's avatar
 
Dries Buytaert committed
      break;
      db_query('DELETE FROM {book} WHERE vid = %d', $node->vid);
      break;
    case 'delete':
      db_query('DELETE FROM {book} WHERE nid = %d', $node->nid);
      break;
Dries Buytaert's avatar
 
Dries Buytaert committed
  }
Dries Buytaert's avatar
 
Dries Buytaert committed
}
Dries Buytaert's avatar
 
Dries Buytaert committed

 * Prepares the links to children (TOC) and forward/backward
 * navigation for a node presented as a book page.
function theme_book_navigation($node) {
Dries Buytaert's avatar
 
Dries Buytaert committed
  if ($node->nid) {
Dries Buytaert's avatar
 
Dries Buytaert committed

Dries Buytaert's avatar
 
Dries Buytaert committed
    if ($prev = book_prev($node)) {
      drupal_add_link(array('rel' => 'prev', 'href' => url('node/'. $prev->nid)));
      $links .= l(t('‹ ') . $prev->title, 'node/'. $prev->nid, array('class' => 'page-previous', 'title' => t('Go to previous page')));
Dries Buytaert's avatar
 
Dries Buytaert committed
    }
      drupal_add_link(array('rel' => 'up', 'href' => url('node/'. $node->parent)));
      $links .= l(t('up'), 'node/'. $node->parent, array('class' => 'page-up', 'title' => t('Go to parent page')));
Dries Buytaert's avatar
 
Dries Buytaert committed
    }
    if ($next = book_next($node)) {
      drupal_add_link(array('rel' => 'next', 'href' => url('node/'. $next->nid)));
      $links .= l($next->title . t(' ›'), 'node/'. $next->nid, array('class' => 'page-next', 'title' => t('Go to next page')));
Dries Buytaert's avatar
 
Dries Buytaert committed
    }
Dries Buytaert's avatar
 
Dries Buytaert committed

    if (isset($tree) || isset($links)) {
      $output = '<div class="book-navigation">';
      if (isset($tree)) {
        $output .= $tree;
      }
      if (isset($links)) {
        $output .= '<div class="page-links">'. $links .'</div>';
      }
      $output .= '</div>';
    }
Dries Buytaert's avatar
 
Dries Buytaert committed
  }
Dries Buytaert's avatar
 
Dries Buytaert committed

Dries Buytaert's avatar
 
Dries Buytaert committed
}
Dries Buytaert's avatar
Dries Buytaert committed

/**
 * This is a helper function for book_toc().
 */
function book_toc_recurse($nid, $indent, $toc, $children, $exclude) {
Dries Buytaert's avatar
 
Dries Buytaert committed
  if ($children[$nid]) {
    foreach ($children[$nid] as $foo => $node) {
      if (!$exclude || $exclude != $node->nid) {
        $toc[$node->nid] = $indent .' '. $node->title;
        $toc = book_toc_recurse($node->nid, $indent .'--', $toc, $children, $exclude);
      }
Dries Buytaert's avatar
 
Dries Buytaert committed
    }
  }

  return $toc;
}

/**
 * Returns an array of titles and nid entries of book pages in table of contents order.
 */
  $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 Buytaert's avatar
 
Dries Buytaert committed

Dries Buytaert's avatar
 
Dries Buytaert committed
  while ($node = db_fetch_object($result)) {
Dries Buytaert's avatar
 
Dries Buytaert committed
    if (!$children[$node->parent]) {
      $children[$node->parent] = array();
    }
    $children[$node->parent][] = $node;
Dries Buytaert's avatar
 
Dries Buytaert committed
  }
Dries Buytaert's avatar
 
Dries Buytaert committed

  // If the user has permission to create new books, add the top-level book page to the menu;
Dries Buytaert's avatar
 
Dries Buytaert committed
  }

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

Dries Buytaert's avatar
 
Dries Buytaert committed
  return $toc;
}

/**
 * This is a helper function for book_tree()
 */
Dries Buytaert's avatar
 
Dries Buytaert committed
function book_tree_recurse($nid, $depth, $children, $unfold = array()) {
Dries Buytaert's avatar
 
Dries Buytaert committed
  if ($depth > 0) {
    if (isset($children[$nid])) {
Dries Buytaert's avatar
 
Dries Buytaert committed
      foreach ($children[$nid] as $foo => $node) {
Dries Buytaert's avatar
 
Dries Buytaert committed
        if (in_array($node->nid, $unfold)) {
          if ($tree = book_tree_recurse($node->nid, $depth - 1, $children, $unfold)) {
            $output .= '<li class="expanded">';
Dries Buytaert's avatar
 
Dries Buytaert committed
            $output .= l($node->title, 'node/'. $node->nid);
            $output .= '<ul class="menu">'. $tree .'</ul>';
Dries Buytaert's avatar
 
Dries Buytaert committed
            $output .= '</li>';
          }
          else {
Dries Buytaert's avatar
 
Dries Buytaert committed
            $output .= '<li class="leaf">'. l($node->title, 'node/'. $node->nid) .'</li>';
Dries Buytaert's avatar
 
Dries Buytaert committed
          }
        }
        else {
          if ($tree = book_tree_recurse($node->nid, 1, $children)) {
Dries Buytaert's avatar
 
Dries Buytaert committed
            $output .= '<li class="collapsed">'. l($node->title, 'node/'. $node->nid) .'</li>';
Dries Buytaert's avatar
 
Dries Buytaert committed
          }
          else {
Dries Buytaert's avatar
 
Dries Buytaert committed
            $output .= '<li class="leaf">'. l($node->title, 'node/'. $node->nid) .'</li>';
Dries Buytaert's avatar
 
Dries Buytaert committed
          }
Dries Buytaert's avatar
 
Dries Buytaert committed
        }
Dries Buytaert's avatar
 
Dries Buytaert committed
      }
    }
Dries Buytaert's avatar
 
Dries Buytaert committed
  }

  return $output;
}

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

Dries Buytaert's avatar
 
Dries Buytaert committed
  while ($node = db_fetch_object($result)) {
    $list = isset($children[$node->parent]) ? $children[$node->parent] : array();
    $list[] = $node;
Dries Buytaert's avatar
 
Dries Buytaert committed
    $children[$node->parent] = $list;
Dries Buytaert's avatar
 
Dries Buytaert committed
  }
Dries Buytaert's avatar
 
Dries Buytaert committed

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

 * Menu callback; prints a listing of all books.
Dries Buytaert's avatar
 
Dries Buytaert committed
function book_render() {
  $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 Buytaert's avatar
 
Dries Buytaert committed

  $books = array();
  while ($node = db_fetch_object($result)) {
    $books[] = l($node->title, 'node/'. $node->nid);
  }

  return theme('item_list', $books);
Dries Buytaert's avatar
 
Dries Buytaert committed
}

 * Menu callback; Generates various representation of a book page with
 * all descendants and prints the requested representation to output.
 *
 * 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
 *
 * @param type
 *   - a string encoding the type of output requested.
 *       The following types are currently supported in book module
 *          html: HTML (printer friendly output)
 *       Other types are supported in contributed modules.
 * @param nid
 *   - an integer representing the node id (nid) of the node to export
 *
function book_export($type = 'html', $nid = 0) {
  $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);
  if (db_num_rows($node_result) > 0) {
      $node = db_fetch_object($node_result);
  }
  $depth = count(book_location($node)) + 1;
  $export_function = 'book_export_' . $type;

  if (function_exists($export_function)) {
    print call_user_func($export_function, $nid, $depth);
    drupal_set_message(t('Unknown export format.'));
Dries Buytaert's avatar
 
Dries Buytaert committed

/**
 * 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
 *
 *   - 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')) {
      $content .= "<div class=\"section-$i\">\n";
    $content .= book_recurse($nid, $depth, 'book_node_visitor_html_pre', 'book_node_visitor_html_post');
    return theme('book_export_html', check_plain($node->title), $content);
/**
 * 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;
}

 * 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) {
  $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 Buytaert's avatar
 
Dries Buytaert committed
  while ($page = db_fetch_object($result)) {
Dries Buytaert's avatar
 
Dries Buytaert committed
    // Load the node:
    $node = node_load($page->nid);
Dries Buytaert's avatar
 
Dries Buytaert committed

Dries Buytaert's avatar
 
Dries Buytaert committed
    if ($node) {
      if (function_exists($visit_pre)) {
        $output .= call_user_func($visit_pre, $node, $depth, $nid);
Dries Buytaert's avatar
 
Dries Buytaert committed
      }
      else {
        $output .= book_node_visitor_html_pre($node, $depth, $nid);
Dries Buytaert's avatar
 
Dries Buytaert committed
      }
Dries Buytaert's avatar
 
Dries Buytaert committed

      $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);
      while ($childpage = db_fetch_object($children)) {
          $childnode = node_load($childpage->nid);
          if ($childnode->nid != $node->nid) {
              $output .= book_recurse($childnode->nid, $depth + 1, $visit_pre, $visit_post);
          }
      }
      if (function_exists($visit_post)) {
        $output .= call_user_func($visit_post, $node, $depth);
        $output .= book_node_visitor_html_post($node, $depth);
Dries Buytaert's avatar
 
Dries Buytaert committed
    }
Dries Buytaert's avatar
 
Dries Buytaert committed
  }
Dries Buytaert's avatar
 
Dries Buytaert committed

Dries Buytaert's avatar
 
Dries Buytaert committed
  return $output;
}
Dries Buytaert's avatar
 
Dries Buytaert committed

 * 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.
 */
function book_node_visitor_html_pre($node, $depth, $nid) {
  // Output the content:
  if (node_hook($node, 'content')) {
    $node = node_invoke($node, 'content');
  }
  // Allow modules to change $node->body before viewing.
  node_invoke_nodeapi($node, 'print', $node->body, FALSE);
  $output .= "<div id=\"node-". $node->nid ."\" class=\"section-$depth\">\n";
  $output .= "<h1 class=\"book-heading\">". check_plain($node->title) ."</h1>\n";

  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().
 */
function book_node_visitor_html_post($node, $depth) {
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;
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,
    ),

  $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 Buytaert's avatar
 
Dries Buytaert committed
}

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

  $rows = array();
  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', NULL, 'destination=admin/node/book'. (arg(3) == 'orphan' ? '/orphan' : '') . ($pid != $nid ? '/'.$pid : ''))
Dries Buytaert's avatar
 
Dries Buytaert committed
  }

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

/**
 * Display an administrative view of the hierarchy of a book.
 */
function book_admin_edit($nid) {
  $node = node_load($nid);
    drupal_set_title(check_plain($node->title));
    $form['table'] = _book_admin_table(array($node));
    $form['save'] = array(
      '#type' => 'submit',
      '#value' => t('Save book pages'),
    );
Dries Buytaert's avatar
 
Dries Buytaert committed

    return drupal_get_form('book_admin_edit', $form);
Dries Buytaert's avatar
 
Dries Buytaert committed
  }
Dries Buytaert's avatar
 
Dries Buytaert committed
}

 * Menu callback; displays a listing of all orphaned book pages.
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 Buytaert's avatar
 
Dries Buytaert committed

  $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 Buytaert's avatar
 
Dries Buytaert committed
      }
Dries Buytaert's avatar
 
Dries Buytaert committed
    }
Dries Buytaert's avatar
 
Dries Buytaert committed

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

    $form['table'] = _book_admin_table($orphans);
    $form['save'] = array(
      '#type' => 'submit',
      '#value' => t('Save book pages'),
    );

    return drupal_get_form('book_admin_edit', $form);
  }
  else {
    return '<p>'. t('There are no orphan pages.') .'</p>';
Dries Buytaert's avatar
 
Dries Buytaert committed
  }
Dries Buytaert's avatar
 
Dries Buytaert committed
}

function book_admin_edit_submit($form_id, $form_values) {
  foreach ($form_values['table'] as $row) {
    $node = node_load($row['nid']);
Dries Buytaert's avatar
 
Dries Buytaert committed

    if ($row['title'] != $node->title || $row['weight'] != $node->weight) {
      $node->title = $row['title'];
      $node->weight = $row['weight'];
Dries Buytaert's avatar
 
Dries Buytaert committed

      node_save($node);
      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));
Dries Buytaert's avatar
 
Dries Buytaert committed
    }
  }

  if (is_numeric(arg(3))) {
    // Updating pages in a single book.
    $book = node_load(arg(3));
    drupal_set_message(t('Updated book %title.', array('%title' => theme('placeholder', $book->title))));
  }
  else {
    // Updating the orphan pages.
    drupal_set_message(t('Updated orphan book pages.'));
  }
Dries Buytaert's avatar
 
Dries Buytaert committed
}

 * Menu callback; displays the book administration page.
function book_admin($nid = 0) {
  if ($nid) {
    return book_admin_edit($nid);
  }
  else {
    return book_admin_overview();
  }
}

/**
 * Returns an administrative overview of all books.
 */
  $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'));
  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 Buytaert's avatar
 
Dries Buytaert committed
}

Dries Buytaert's avatar
 
Dries Buytaert committed
function book_help($section) {
Dries Buytaert's avatar
 
Dries Buytaert committed
  switch ($section) {
Dries Buytaert's avatar
 
Dries Buytaert committed
    case 'admin/help#book':
      $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>';
      $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>';
      $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>';
      $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>';