? drupal7
? sites/default/files
? sites/default/settings.php
Index: modules/book/book.admin.inc
===================================================================
RCS file: /cvs/drupal/drupal/modules/book/book.admin.inc,v
retrieving revision 1.19
diff -u -p -r1.19 book.admin.inc
--- modules/book/book.admin.inc	14 Mar 2009 20:13:26 -0000	1.19
+++ modules/book/book.admin.inc	17 May 2009 01:23:52 -0000
@@ -13,15 +13,22 @@ function book_admin_overview() {
   $rows = array();
 
   $headers = array(t('Book'), t('Operations'));
+  if (user_access('bypass node access')) {
+    $headers[] = t('Delete');
+  }
 
   // Add any recognized books to the table list.
   foreach (book_get_books() as $book) {
-    $rows[] = array(l($book['title'], $book['href'], $book['options']), l(t('edit order and titles'), 'admin/content/book/' . $book['nid']));
+    $row = array(l($book['title'], $book['href'], $book['options']), l(t('edit order and titles'), 'admin/content/book/' . $book['nid']));
+    if (user_access('bypass node access')) {
+      $row[] = l(t('delete book'), 'admin/content/book/delete/' . $book['nid']);
+    }
+    $rows[] = $row;
   }
 
   // If no books were found, let the user know.
   if (empty($rows)) {
-    $rows[] = array(array('data' => t('No books available.'), 'colspan' => 2));
+    $rows[] = array(array('data' => t('No books available.'), 'colspan' => count($headers)));
   }
 
   return theme('table', $headers, $rows);
@@ -259,3 +266,95 @@ function theme_book_admin_table($form) {
   return theme('table', $header, $rows, array('id' => 'book-outline'));
 }
 
+/**
+ * Menu callback. Ask for confirmation of book deletion.
+ */
+function book_delete_confirm(&$form_state, $node) {
+  $form['bid'] = array(
+    '#type' => 'value',
+    '#value' => $node->nid,
+  );
+  $form['book_title'] = array(
+    '#type' => 'value',
+    '#value' => $node->title,
+  );
+
+  return confirm_form($form,
+    t('Are you sure you want to delete the entire book %title?', array('%title' => $node->title)), 'admin/content/book',
+    t('This action cannot be undone.'),
+    t('Delete'),
+    t('Cancel')
+  );
+}
+
+/**
+ * Execute full book deletion using batch processing.
+ */
+function book_delete_confirm_submit($form, &$form_state) {
+  if ($form_state['values']['confirm']) {
+    $bid = $form_state['values']['bid'];
+    $batch = array(
+     'title' => t('Deleting book %title', array('%title' => $form_state['values']['book_title'])),
+     'operations' => array(
+        array('book_delete', array($bid)),
+      ),
+     'finished' => '_book_delete_finished',
+     'file' => drupal_get_path('module', 'book') . '/book.admin.inc',
+    );
+    batch_set($batch);
+    batch_process();
+  }
+}
+
+/**
+ * Batch processing callback to delete an entire book.
+ */
+function book_delete($bid, &$context) {
+  if (!isset($context['sandbox']['progress'])) {
+    $context['sandbox']['max'] = db_query("SELECT COUNT(nid) FROM {book} WHERE bid = :bid", array(':bid' => $bid))->fetchField();
+    $context['sandbox']['progress'] = 0;
+    $context['sandbox']['highest_nid'] = 0;
+    $context['sandbox']['bid'] = $bid;
+  }
+
+  // Delete 5 nodes at a time. This needs to be set fairly low as there can be
+  // many search index deletions for each node.
+  $limit = 5;
+  $result = db_query_range("SELECT nid FROM {book} WHERE bid = :bid AND nid > :nid AND nid <> bid ORDER BY nid ASC", array(
+    ':bid' => $context['sandbox']['bid'], 
+    ':nid' => $context['sandbox']['highest_nid'], 
+    ), 0, $limit);
+  while ($nid = $result->fetchField()) {
+    node_delete($nid);
+    // Update our progress information.
+    $context['sandbox']['progress']++;
+    $context['sandbox']['highest_nid'] = $nid;
+  }
+
+  // Delete the top book node last.
+  if ($context['sandbox']['progress'] == $context['sandbox']['max'] - 1) {
+    node_delete($context['sandbox']['bid']);
+    $context['sandbox']['progress']++;
+  }
+
+  // Multistep processing : report progress.
+  if ($context['sandbox']['progress'] != $context['sandbox']['max']) {
+    $context['finished'] = $context['sandbox']['progress'] / $context['sandbox']['max'];
+  }
+}
+
+/**
+ * Book delete batch 'finished' callback.
+ */
+function _book_delete_finished($success, $results, $operations) {
+  if ($success) {
+    drupal_set_message(t('The book has been deleted.'));
+  }
+  else {
+    drupal_set_message(t('An error occurred and processing did not complete.'), 'error');
+    $message = format_plural(count($results), '1 item successfully deleted:', '@count items successfully deleted:');
+    $message .= theme('item_list', $results);
+    drupal_set_message($message);
+  }
+  drupal_goto('admin/content/book');
+}
\ No newline at end of file
Index: modules/book/book.module
===================================================================
RCS file: /cvs/drupal/drupal/modules/book/book.module,v
retrieving revision 1.493
diff -u -p -r1.493 book.module
--- modules/book/book.module	9 May 2009 18:28:11 -0000	1.493
+++ modules/book/book.module	17 May 2009 01:23:53 -0000
@@ -125,6 +125,14 @@ function book_menu() {
     'access arguments' => array(3),
     'type' => MENU_CALLBACK,
   );
+  $items['admin/content/book/delete/%node'] = array(
+    'title' => 'Delete Book',
+    'page callback' => 'drupal_get_form',
+    'page arguments' => array('book_delete_confirm', 4),
+    'access callback' => '_book_delete_access',
+    'access arguments' => array(4),
+    'type' => MENU_CALLBACK,
+  );
   $items['book'] = array(
     'title' => 'Books',
     'page callback' => 'book_render',
@@ -178,6 +186,13 @@ function _book_outline_remove_access($no
 }
 
 /**
+ * Menu item access callback - determine if the user can delete entire books.
+ */
+function _book_delete_access($node) {
+  return user_access('administer book outlines') && user_access('bypass node access') && isset($node->book);
+}
+
+/**
  * Implementation of hook_init().
  */
 function book_init() {
Index: modules/book/book.test
===================================================================
RCS file: /cvs/drupal/drupal/modules/book/book.test,v
retrieving revision 1.9
diff -u -p -r1.9 book.test
--- modules/book/book.test	25 Apr 2009 23:01:43 -0000	1.9
+++ modules/book/book.test	17 May 2009 01:23:55 -0000
@@ -7,7 +7,7 @@ class BookTestCase extends DrupalWebTest
   public static function getInfo() {
     return array(
       'name' => t('Book functionality'),
-      'description' => t('Create a book, add pages, and test book interface.'),
+      'description' => t('Create a book, add pages, test book interface, and then delete.'),
       'group' => t('Book'),
     );
   }
@@ -23,6 +23,8 @@ class BookTestCase extends DrupalWebTest
     // Create users.
     $book_author = $this->drupalCreateUser(array('create new books', 'create book content', 'edit own book content', 'add content to books'));
     $web_user = $this->drupalCreateUser(array('access printer-friendly version'));
+    $book_outliner = $this->drupalCreateUser(array('administer book outlines'));
+    $admin_user = $this->drupalCreateUser(array('administer book outlines', 'bypass node access'));
 
     // Create new book.
     $this->drupalLogin($book_author);
@@ -76,6 +78,19 @@ class BookTestCase extends DrupalWebTest
     $this->book = $other_book;
     $this->checkBookNode($other_book, array($node), FALSE, FALSE, $node, array());
     $this->checkBookNode($node, NULL, $other_book, $other_book, FALSE, array($other_book));
+
+    // Check that the book can be deleted.
+    $this->drupalLogout();
+    $this->drupalLogin($book_outliner);
+    // The book outliner user should not be able to delete books.
+    $this->deleteBook($book, FALSE);
+    $this->drupalLogout();
+    $this->drupalLogin($admin_user);
+    // The admin user should be able to delete books.
+    $this->deleteBook($book, TRUE);
+    // Users should only be able to delete books, not nodes of other types.
+    $page = $this->drupalCreateNode();
+    $this->deleteBook($page, TRUE);
   }
 
   /**
@@ -190,6 +205,46 @@ class BookTestCase extends DrupalWebTest
 
     return $node;
   }
+
+  /**
+   * Delete the entire book.
+   *
+   * @param $book Book node object.
+   * @param $access If the user has access to delete the book.
+   */
+  function deleteBook($book, $access) {
+    if (!isset($book->book['bid'])) {
+      $this->drupalGet('admin/content/book/list');
+      $this->assertNoRaw(l(t('delete book'), 'admin/content/book/delete/' . $book->nid), t("The delete link does not show up on the book list for nodes which are't books."));
+      $this->drupalGet('admin/content/book/delete/' . $book->nid);
+      $this->assertNoRaw(t('Are you sure you want to delete the entire book %title?', array('%title' => $book->title)), t("Confirm that a user can't delete a node which isn't a book."));
+    }
+    // Make sure users without access don't see the delete link or title and can't go there.
+    else if (!$access) {
+      $this->drupalGet('admin/content/book/list');
+      $this->assertNoRaw(l(t('delete book'), 'admin/content/book/delete/' . $book->nid), t('The delete link does not show up on the book list.'));
+      $this->assertNoRaw(t('Delete'), t('The Delete header does not show on the page.'));
+      $this->drupalGet('admin/content/book/delete/' . $book->nid);
+      $this->assertNoRaw(t('Are you sure you want to delete the entire book %title?', array('%title' => $book->title)), t('Confirm that a user can\'t delete a book.'));
+    }
+    else {
+      // Make sure the delete link shows up on the book list page.
+      $this->drupalGet('admin/content/book/list');
+      $this->assertRaw(l(t('delete book'), 'admin/content/book/delete/' . $book->nid), t('The delete link shows up on the book list.'));
+
+      // Delete the book.
+      $edit = array();
+      $this->drupalGet('admin/content/book/delete/' . $book->nid);
+      $this->assertRaw(t('Are you sure you want to delete the entire book %title?', array('%title' => $book->title)), t('Confirm that the user wants to delete book.'));
+      $this->drupalPost(NULL, $edit, t('Delete'));
+
+      // Confirm deletion.
+      $this->assertRaw(t('The book has been deleted.'), t('Message confirms deletion'));
+      $this->drupalGet('admin/content/book/list');
+      $this->assertNoRaw(l(t('delete book'), 'admin/content/book/delete/' . $book->nid), t('The delete link does not show up on the book list.'));
+      $this->assertTrue(db_query('SELECT COUNT(nid) FROM {book} WHERE bid = :bid', array(':bid' => $book->nid))->fetchField() == 0, t('The book pages are not in the database.'));
+    }
+  }
 }
 
 class BookBlockTestCase extends DrupalWebTestCase {
