Index: book_access.install
===================================================================
RCS file: /cvs/drupal-contrib/contributions/modules/book_access/book_access.install,v
retrieving revision 1.4.2.4
diff -u -p -r1.4.2.4 book_access.install
--- book_access.install	13 Dec 2009 03:13:25 -0000	1.4.2.4
+++ book_access.install	13 Dec 2009 04:42:00 -0000
@@ -28,6 +28,8 @@ function book_access_install() {
  * Implementation of hook_schema().
  */
 function book_access_schema() {
+  $schema = array();
+
   $schema['book_access'] = array(
     'description' => 'Table for tracking book access.',
     'fields' => array(
@@ -44,7 +46,7 @@ function book_access_schema() {
         'default' => 0,
       ),
       'grant_view' => array(
-        'description' => 'View book is allowed.',
+        'description' => 'View book is permission.',
         'type' => 'int',
         'size' => 'tiny',
         'insigned' => TRUE,
@@ -52,7 +54,7 @@ function book_access_schema() {
         'default' => 0,
       ),
       'grant_update' => array(
-        'description' => 'Edit book pages is allowed.',
+        'description' => 'Edit book pages permission.',
         'type' => 'int',
         'size' => 'tiny',
         'insigned' => TRUE,
@@ -60,7 +62,7 @@ function book_access_schema() {
         'default' => 0,
       ),
       'grant_delete' => array(
-        'description' => 'Delete book pages is allowed.',
+        'description' => 'Delete book pages permission.',
         'type' => 'int',
         'size' => 'tiny',
         'insigned' => TRUE,
@@ -71,6 +73,49 @@ function book_access_schema() {
     'primary key' => array('nid', 'rid'),
   );
 
+  $schema['book_access_user'] = array(
+    'description' => 'Table for tracking book access by user.',
+    'fields' => array(
+      'nid' => array(
+        'description' => 'Primary key: The node ID of the book.',
+        'type' => 'int',
+        'not null' => TRUE,
+        'default' => 0,
+      ),
+      'uid' => array(
+        'description' => 'Primary key: The user ID associated with a book node ID.',
+        'type' => 'int',
+        'not null' => TRUE,
+        'default' => 0,
+      ),
+      'grant_view' => array(
+        'description' => 'View book is permission.',
+        'type' => 'int',
+        'size' => 'tiny',
+        'insigned' => TRUE,
+        'not null' => TRUE,
+        'default' => 0,
+      ),
+      'grant_update' => array(
+        'description' => 'Edit book pages permission.',
+        'type' => 'int',
+        'size' => 'tiny',
+        'insigned' => TRUE,
+        'not null' => TRUE,
+        'default' => 0,
+      ),
+      'grant_delete' => array(
+        'description' => 'Delete book pages permission.',
+        'type' => 'int',
+        'size' => 'tiny',
+        'insigned' => TRUE,
+        'not null' => TRUE,
+        'default' => 0,
+      ),
+    ),
+    'primary key' => array('nid', 'uid'),
+  );
+
   return $schema;
 }
 
@@ -83,7 +128,62 @@ function book_access_update_6100() {
   return $ret;
 }
 
- /**
+/**
+ * Implementation of hook_update_N().
+ */
+function book_access_update_6101() {
+  $ret = array();
+  $schema = array();
+
+  $schema['book_access_user'] = array(
+    'fields' => array(
+      'nid' => array(
+        'type' => 'int',
+        'not null' => TRUE,
+        'default' => 0,
+      ),
+      'uid' => array(
+        'type' => 'int',
+        'not null' => TRUE,
+        'default' => 0,
+      ),
+      'grant_view' => array(
+        'type' => 'int',
+        'size' => 'tiny',
+        'insigned' => TRUE,
+        'not null' => TRUE,
+        'default' => 0,
+      ),
+      'grant_update' => array(
+        'type' => 'int',
+        'size' => 'tiny',
+        'insigned' => TRUE,
+        'not null' => TRUE,
+        'default' => 0,
+      ),
+      'grant_delete' => array(
+        'type' => 'int',
+        'size' => 'tiny',
+        'insigned' => TRUE,
+        'not null' => TRUE,
+        'default' => 0,
+      ),
+    ),
+    'primary key' => array('nid', 'uid'),
+  );
+
+  if (!db_table_exists('book_access_user')) {
+    db_create_table($ret, 'book_access_user', $schema['book_access_user']);
+  }
+
+  if (!db_table_exists('book_access_role')) {
+    db_rename_table($ret, 'book_access', 'book_access_role');
+  }
+
+  return $ret;
+}
+
+/**
  * Implementation of hook_uninstall().
  */
 function book_access_uninstall() {
Index: book_access.module
===================================================================
RCS file: /cvs/drupal-contrib/contributions/modules/book_access/book_access.module,v
retrieving revision 1.15.2.11
diff -u -p -r1.15.2.11 book_access.module
--- book_access.module	13 Dec 2009 02:59:41 -0000	1.15.2.11
+++ book_access.module	13 Dec 2009 04:42:01 -0000
@@ -44,10 +44,10 @@ function book_access_form_alter(&$form, 
     _book_access_restrict_options($form['book']['bid']['#options']);
   }
 
-  // when an outline is modified, taxonomy is changed, but the node is not
-  // saved, so node grants can become broken if a book page is moved into
-  // another book. so we fix that by adding an additional #submit callback
-  // that rebuilds the grants when the book outline is modified.
+  // When an outline is modified, taxonomy is changed but the node is not
+  // saved; node grants can become broken if a book page is moved into
+  // another book. We fix that by adding an additional #submit callback
+  // to rebuild the grants when the book outline is modified.
   if ($form_id == 'book_outline_form') {
     // TODO: diff settings before/after and only rebuild if changes were made
     $form['#submit'][] = 'book_access_build_node_grants';
@@ -61,7 +61,7 @@ function book_access_help($path, $arg) {
   switch ($path) {
     case 'admin/content/book/access':
       $help = t(
-        '<p>Configure access control per book based on user roles. Settings
+        '<p>Configure access control per book based on users or roles. Settings
         affect all pages within the given book. If a page is moved into a
         different book, it will assume that book access control settings.</p>
         <p><em>Important:</em> If you are going to manage access control here,
@@ -148,18 +148,21 @@ function book_access_node_access_explain
     }
 
     if (isset($roles[$row->gid])) {
-      return array(
+      $result = array(
         t('grants for users of role: %role',
           array('%role' => $roles[$row->gid])
         )
       );
     }
+    else {
+      $result = array(
+        t('Unknown group id %gid: It is recommended to rebuild node access',
+          array('%gid' => $row->gid)
+        )
+      );
+    }
 
-    return array(
-      t('Unknown group id %gid: It is recommended to rebuild node access',
-        array('%gid' => $row->gid)
-      )
-    );
+    return $result;
   }
 }
 
@@ -175,7 +178,6 @@ function book_access_node_access_explain
 
   if (isset($node->book['bid'])) {
     $result = db_query('SELECT * FROM {book_access} WHERE nid = %d', $node->book['bid']);
-
     while ($grant = db_fetch_object($result)) {
       $grants[] = array(
         'realm'        => 'book_access',
@@ -187,6 +189,19 @@ function book_access_node_access_explain
       );
     }
 
+    // Add the per-user grants.
+    $result = db_query("SELECT * FROM {book_access_user} WHERE nid = %d", $node->book['bid']);
+    while ($grant = db_fetch_object($result)) {
+      $grants[] = array(
+        'realm'        => 'book_access_user',
+        'gid'          => $grant->uid,
+        'grant_view'   => $grant->grant_view,
+        'grant_update' => $grant->grant_update,
+        'grant_delete' => $grant->grant_delete,
+        'priority'     => BOOK_ACCESS_GRANT_PRIORITY,
+      );
+    }
+
     return $grants;
   }
 }
@@ -244,6 +259,8 @@ function book_access_perm() {
  */
 function book_access_node_grants($user, $op) {
   $grants['book_access'] = array_keys($user->roles);
+  $grants['book_access_user'] = array($user->uid);
+
   return $grants;
 }
 
@@ -263,6 +280,11 @@ function book_access_admin_form() {
   );
   $books = array();
   $form = array();
+  $grants = array(
+    'grant_view' => 'view',
+    'grant_update' => 'update',
+    'grant_delete' => 'delete',
+  );
   $rids = user_roles();
 
   if ($bool) {
@@ -281,38 +303,33 @@ function book_access_admin_form() {
     foreach ($books as $book_nid => $book_name) {
 
       // Variables used to store existing grants for this book.
-
-      $view   = array();
-      $update = array();
-      $delete = array();
+      $book_grants = array(
+        'view' => array(),
+        'update' => array(),
+        'delete' => array(),
+      );
 
       $result = db_result(
         db_query(
-          "SELECT COUNT(*) FROM {book_access} where nid = %d", $book_nid
+          "SELECT COUNT(*) FROM {book_access_role} where nid = %d", $book_nid
         )
       );
 
-      // If  there are no grants, then use some safe defaults; it only makes
+      // If there are no grants, then use some safe defaults; it only makes
       // sense when a new book is created.
       if (!$result) {
-        $view   = array(1, 2);
-        $update = array();
-        $delete = array();
+        $book_grants['view'] = array(1, 2);
       }
       else {
         $result = db_query(
-          "SELECT * FROM {book_access} where nid = %d", $book_nid
+          "SELECT * FROM {book_access_role} where nid = %d", $book_nid
         );
 
         while ($book_access = db_fetch_object($result)) {
-          if ($book_access->grant_view) {
-            $view[] = $book_access->rid;
-          }
-          if ($book_access->grant_update) {
-            $update[] = $book_access->rid;
-          }
-          if ($book_access->grant_delete) {
-            $delete[] = $book_access->rid;
+          foreach ($grants as $grant => $var) {
+            if ($book_access->$grant) {
+              $book_grants[$var][] = $book_access->rid;
+            }
           }
         }
       }
@@ -329,7 +346,7 @@ function book_access_admin_form() {
         '#type' => 'checkboxes',
         '#title' => t('View this book'),
         '#options' => $rids,
-        '#default_value' => $view,
+        '#default_value' => $book_grants['view'],
         '#prefix' => '<div class="book-access-div">',
         '#suffix' => '</div>',
       );
@@ -337,17 +354,75 @@ function book_access_admin_form() {
       $form['access'][$book_nid]['update'] = array(
         '#type' => 'checkboxes',
         '#title' => t('Edit pages in this book'),
+        '#options' => $rids,
+        '#default_value' => $book_grants['update'],
         '#prefix' => '<div class="book-access-div">',
         '#suffix' => '</div>',
-        '#options' => $rids,
-        '#default_value' => $update,
       );
 
       $form['access'][$book_nid]['delete'] = array(
         '#type' => 'checkboxes',
         '#title' => t('Delete pages in this book'),
         '#options' => $rids,
-        '#default_value' => $delete,
+        '#default_value' => $book_grants['delete'],
+        '#prefix' => '<div class="book-access-div">',
+        '#suffix' => '</div>',
+      );
+    }
+
+    // Note that the autocomplete widget will only enable for users with the
+    // 'access profiles' permission. Other users will have to specify the name
+    // manually.
+    $form['access'][$book_nid]['search_user'] = array(
+      '#type' => 'textfield',
+      '#title' => t('Add a User'),
+      '#description' => t('Enter a user name to set permissions for that user on this book.'),
+      '#size' => 40,
+      '#autocomplete_path' => 'user/autocomplete',
+      '#prefix' => '<div class="book-access-clearer"></div><div class="book-user-div">',
+      '#suffix' => '</div>',
+    );
+
+    $form['access'][$book_nid]['user_view'] = array(
+      '#type' => 'checkbox',
+      '#title' => t('View this book'),
+      '#default_value' => TRUE,
+    );
+
+    $form['access'][$book_nid]['user_update'] = array(
+      '#type' => 'checkbox',
+      '#title' => t('Edit pages in this book'),
+    );
+
+    $form['access'][$book_nid]['user_delete'] = array(
+      '#type' => 'checkbox',
+      '#title' => t('Delete pages in this book'),
+    );
+
+    // Show a table of existing user grants.
+    $form['access'][$book_nid]['users'] = array();
+    $result = db_query("SELECT * FROM {book_access_user} where nid = %d", $book_nid);
+
+    while ($book_access = db_fetch_object($result)) {
+      $defaults = array();
+      $user = user_load($book_access->uid);
+
+      foreach ($grants as $grant => $var) {
+        if (!empty($book_access->$grant)) {
+          $defaults[] = $var;
+        }
+      }
+
+      $form['access'][$book_nid]['users'][$user->uid] = array(
+        '#type' => 'checkboxes',
+        '#title' => t('Permissions for %user', array('%user' => $user->name)),
+        '#options' => array(
+          'view' => t('View pages in this book'),
+          'update' => t('Edit pages in this book'),
+          'delete' => t('Delete pages in this book'),
+          'remove' => t('Reset permissions for this user to defaults'),
+        ),
+        '#default_value' => $defaults,
         '#prefix' => '<div class="book-access-div">',
         '#suffix' => '</div>',
       );
@@ -372,25 +447,80 @@ function book_access_admin_form() {
 }
 
 /**
+ * Validation function for the administration settings.
+ */
+function book_access_admin_form_validate($form, &$form_state) {
+  foreach ($form_state['values']['access'] as $book_nid => $access) {
+    if (!empty($access['search_user']) && !user_load(array('name' => $access['search_user']))) {
+      form_set_error("access][$book_nid][search_user", t('%user is not a valid user name.', array('%user' => $access['search_user'])));
+    }
+  }
+}
+
+/**
  * Submission callback for the administration settings.
  */
 function book_access_admin_form_submit($form, &$form_state) {
+  $grants = array(
+    'grant_view' => 'view',
+    'grant_update' => 'update',
+    'grant_delete' => 'delete',
+  );
   $row = new stdClass();
 
-  // Remove previous book access entries, and keep table clean.
-  db_query("DELETE FROM {book_access}");
-
   if (isset($form_state['values']['access'])) {
     foreach ($form_state['values']['access'] as $nid => $access) {
       $row->nid = $nid;
 
+      // Remove previous book access entries.
+      db_query("DELETE FROM {book_access_role} WHERE nid = %d", $nid);
+
       foreach ($access['view'] as $rid => $checked) {
         $row->rid = $rid;
-        $row->grant_view = (bool) $checked;
-        $row->grant_update = ($access['update'][$rid] > 0);
-        $row->grant_delete = ($access['delete'][$rid] > 0);
 
-        drupal_write_record('book_access', $row);
+        foreach ($grants as $grant => $var) {
+          $row->$grant = (!empty($access[$var][$rid]));
+        }
+
+        drupal_write_record('book_access_role', $row);
+
+        // If a user has been specified, update the permissions for that user. Also
+        // update a user if it currently has existing permissions.
+        if (!empty($access['search_user'])) {
+          if ($user = user_load(array('name' => $access['search_user']))) {
+            $row->uid = $user->uid;
+
+            db_query("DELETE FROM {book_access_user} WHERE nid = %d AND uid = %d", $nid, $user->uid);
+
+            foreach ($grants as $grant => $var) {
+              $row->$grant = (!empty($access["user_$var"]));
+            }
+
+            drupal_write_record('book_access_user', $row);
+
+            // If an existing user has been changed, update that user.
+            if (!empty($access['users'])) {
+              foreach($access['users'] as $uid => $checked) {
+                if ($user = user_load($uid)) {
+                  $row->uid = $user->uid;
+
+                  db_query(
+                    "DELETE FROM {book_access_user} WHERE nid = %d AND uid = %d",
+                    $book_nid, $user->uid
+                  );
+
+                  if (!$checked['remove']) {
+                    foreach ($grants as $grant => $var) {
+                      $row->$grant = !empty($checked[$var]);
+                    }
+
+                    drupal_write_record('book_access_user', $row);
+                  }
+                }
+              }
+            }
+          }
+        }
       }
     }
   }
@@ -436,6 +566,16 @@ function _book_access_restrict_options(&
     $permitted_bids[$result->nid] = $result->nid;
   }
 
+  // Add in the per-user grants.
+  $results = db_query(
+    "SELECT nid FROM {node_access} WHERE realm = 'book_access_user' AND gid = %d AND grant_update > 0",
+    $user->uid
+  );
+
+  while ($result = db_fetch_object($results)) {
+    $permitted_bids[$result->nid] = $result->nid;
+  }
+
   if (isset($options)) {
     foreach ($options as $bid => $value) {
       if ($bid > 0 && !isset($permitted_bids[$bid])) {
