--- taxonomy_access.module	2010-09-04 16:42:13.000000000 -0500
+++ taxonomy_access.module.new	2010-09-04 17:10:31.000000000 -0500
@@ -1,5 +1,5 @@
 <?php
-// $Id: taxonomy_access.module,v 1.107.2.24 2010/09/04 21:42:13 xjm Exp $
+// $Id: taxonomy_access.module,v 1.107.2.23 2010/09/03 21:25:55 xjm Exp $
 // Based on original taxonomy_access.module made by pyromanfo
 
 /**
@@ -254,23 +254,106 @@
 
 /**
  * Implements hook_form_alter().
- *
- * @todo
- *     Move control of "create" op here
- * @todo
- *     Look at feasability to eliminate _restore_terms and _preserve_terms
- *     by simply setting the '#access' attribute for those terms.
  */
 function taxonomy_access_form_alter(&$form, $form_state, $form_id) {
-  if ($form['#id'] == 'node-form' && is_numeric($form['nid']['#value'])) {
-    $form['tac_protected_terms'] = array(
-      '#type' => 'value',
-      '#value' => taxonomy_access_preserve_terms($form['#node'])
-    );
+  if ($form['#id'] == 'node-form') {
+
+    // Do not alter the form for taxonomy administrators.
+    if (user_access('administer taxonomy')) {
+      return;
+    }
+
+    // Hide any terms the user does not have access to change.
+    global $user;
+    $allowed = taxonomy_access_get_user_allowed_terms($user, 'create');
+
+    if (is_array($form['taxonomy'])) {
+
+      // Check all available vocabularies.
+      foreach ($form['taxonomy'] as $vid => $el) {
+        if (is_numeric($vid) && is_array($el)) {
+
+          // If the user does not have access to any term in the vocab, hide.
+          if (!in_array($vid, $allowed['vids'])) {
+            $form['taxonomy'][$vid]['#access'] = FALSE;
+          }
+
+          // Else, check individual terms.
+          elseif (is_array($el['#options'])) {
+
+            // Search for terms that the user doesn't have create privs for.
+            $restricted = array();
+            foreach ($el['#options'] as $key => $term) {
+              if (is_array($term->option)) {
+                $tid_array = array_keys($term->option);
+                $tid = $tid_array[0];
+                 if (!in_array($tid, $allowed['tids'])) {
+                   $restricted[] = $tid;
+                 }
+              }
+            }
+
+            /* If there are any terms the user does not have create privs for,
+             * set the access to false and create two new form elements named
+             * tac_new_N and tac_orig_N, where N is the vocab id.
+             * The first is editable by the user; 
+             * the second is hidden to preserve the original values so it is 
+             * easy to check for the changes the user wants to make.
+             */
+            if (!empty($restricted)) {
+
+              // Hide the field.
+              $form['taxonomy'][$vid]['#access'] = FALSE;
+
+              // Use normal blank text.
+              if (!$el['#multiple']) {
+                $blank = ($el['#required']) 
+                  ? t('- Please choose -') : t('- None selected -');
+              }
+              else {
+                $blank = ($el['#required']) ? 0 : t('- None -');
+              }
+
+              // Use the same defaults, but exclude restricted terms.
+              if (is_array($el['#default_value'])) {
+                $default = array_diff($el['#default_value'], $restricted);
+              }
+              else {
+                $default = in_array($el['#default_value'], $restricted)
+                  ? '' : $el['#default_value'];
+              }
+
+              // @see http://api.drupal.org/api/function/_taxonomy_term_select
+              $form['taxonomy']["tac_new_$vid"] = 
+                _taxonomy_term_select(
+                  $el['#title'],
+                  $el['#title'],
+                  $default,
+                  $vid,
+                  $el['#description'],
+                  $el['#multiple'],
+                  $blank,
+                  $restricted
+                );
+
+              // Additionally add a hidden element to preserve the orig. state.
+              $form['taxonomy']["tac_orig_$vid"] = 
+                $form['taxonomy']["tac_new_$vid"];
+              $form['taxonomy']["tac_orig_$vid"]['#access'] = FALSE;
+
+              // Make the vocab required if it was originally marked so.
+              /**
+               * @todo Allow it to be not required if there's a hidden term?
+               */
+              $form['taxonomy']["tac_new_$vid"]['#required'] = $el['#required'];
+            }
+          }
+        }
+      }
+    }
   }
 }
 
-
 /**
  * Implements hook_form_FORM_ID_alter() for taxonomy-form-term.
  * Overriding the term deletion form's submit handler allows us to determine
@@ -351,11 +434,59 @@
 function taxonomy_access_nodeapi(&$node, $op, $arg = 0) {
   switch ($op) {
     case 'presave':
+
+      // Merge in user's changes for vocabs restricted in hook_form_alter().
+      foreach ($node->taxonomy as $vocab => $tids) {
+
+        /* There are two fields added in hook_form_alter() for each vocabulary:
+         * tac_new_N and tac_orig_N, where N is the vocab id.
+         * The first is editable by the user; the second is hidden to preserve
+         * the original values so it is easy to check for the changes the user 
+         * wants to make.
+         */
+        if (strpos($vocab, "tac_new_") !== FALSE) {
+          // The vid is whatever comes after "tac_new_".
+          $vid = substr($vocab, 8);
+
+          // Check for terms that were tagged or untagged by comparing
+          // the submitted values with the hidden original ones.
+          $orig_tids = $node->taxonomy["tac_orig_$vid"];
+
+          $tagged = array();
+          $untagged = array();
+
+          if (is_array($tids) && is_array($orig_tids)) {
+            $tagged = array_diff($tids, $orig_tids);
+            $untagged = array_diff($orig_tids, $tids);
+          }
+
+          // Add newly tagged terms to the real taxonomy list for the vocab.
+          foreach ($tagged as $tid) {
+            if (!in_array($tid, $node->taxonomy[$vid])) {
+              $node->taxonomy[$vid][] = $tid;
+            }
+          }
+
+          // Remove terms that were untagged from the list.
+          foreach ($untagged as $tid) {
+            $key = array_search($tid, $node->taxonomy[$vid]);
+            if ($key !== FALSE) {
+              unset($node->taxonomy[$vid][$key]);
+            }
+          }
+          
+          // Now, unset this temporary field and the hidden original.
+          // (If we don't, their values will also be submitted.)
+          unset($node->taxonomy[$vocab]);
+          unset($node->taxonomy["tac_orig_$vid"]);
+        }
+      }
       break;
 
     case 'update':
-      // restore terms that the user shouldn't have access to delete
-      taxonomy_access_restore_terms($node->nid, $node->vid, $node->tac_protected_terms);
+      /**
+       * @todo Add access check for create grant?
+       */
       break;
 
     case 'delete':
@@ -449,172 +580,140 @@
 
 /**
  * Implements hook_db_rewrite_sql().
+ *
+ * This hook controls the list grant.  
+ * Create is now controlled in hook_form_alter() and hook_nodeapi().
+ *
+ * @todo
+ *     Better way of excluding node edit pages?
+ *     Does this method include pages it shouldn't?
+ *     Does create without list even make sense?
  */
 function taxonomy_access_db_rewrite_sql($query, $table, $field) {
-  if (!user_access('administer taxonomy') && ($field =='vid' || $field =='tid')) {
+  /* Do not take action if:
+   * 1. The user has "administer taxonomy" permissions.
+   * 2. The table is {node_revisions} which also has a vid field (revision id).
+   * 3. The field is not tid or vid.
+   */
+  if 
+    (user_access('administer taxonomy')
+      || $table == 'node_revisions'
+      || !($field =='vid' || $field =='tid')
+    ) {
+    return array();
+  }
 
-    // Table {node_revisions} also has a vid (revision, not vocabulary)
-    if ($table == 'node_revisions') {
+  /* Do not take action if we are on any sort of node add/edit page.
+   * Those forms (create op) are now controlled in hook_form_alter() and 
+   * hook_nodeapi().
+   */
+  $arg = arg();
+  if ($arg[0] == 'node' || ($arg[0] == 'admin' && $arg[1] == 'node')) {
+    // The add/edit argument will be the last.
+    $last_arg = array_pop($arg);
+    if ($last_arg == 'add' || $last_arg == 'edit') {
       return array();
     }
+  }
 
-    global $user;
-
-    if (arg(0) == "admin") {
-      $op = (arg(1) == 'node' && (arg(2) == 'add' || arg(3) == 'edit')) ? 'create' : 'list';
-    }
-    else {
-      $op = (arg(0) == 'node' && (arg(1) == 'add' || arg(2) == 'edit')) ? 'create' : 'list';
-    }
-
-    // let's cache
-    static $taxonomy_access_sql_clause;
-    $clause = array();
-
-    if (!isset($taxonomy_access_sql_clause)) {
-      $taxonomy_access_sql_clause = array();
-    }
-    if (!isset($taxonomy_access_sql_clause[$op][$field]))  {
-      if (isset($user) && is_array($user->roles)) {
-        $rids = array_keys($user->roles);
-      }
-      else {
-        $rids[] = 1;
-      }
-
-      $sql = db_query(
-        'SELECT t.tid AS tid, t.vid AS vid FROM {term_data} t
-         INNER JOIN {term_access_defaults} tdg ON tdg.vid=0
-         LEFT JOIN {term_access_defaults} td ON td.vid=t.vid AND td.rid=tdg.rid
-         LEFT JOIN {term_access} ta ON ta.tid=t.tid AND ta.rid=tdg.rid
-         WHERE tdg.rid IN (' . db_placeholders($rids, 'int') .')
-         GROUP BY t.tid, t.vid
-         HAVING BIT_OR(COALESCE(
-                                ta.' . db_escape_table("grant_$op") . ',
-                                td.' . db_escape_table("grant_$op") . ',
-                                tdg.' . db_escape_table("grant_$op") . '
-                               )) > 0', $rids);
-
-      $tids = array();
-      $vids = array();
-
-      while ($result = db_fetch_object($sql)) {
-        $tids[]= $result->tid;
-        $vids[$result->vid]= $result->vid;
-      }
+  global $user;
 
-      // Insert required vocabularies to avoid skipping of validation at node submission
-      if ($op == 'create') {
-        $sql = db_query('SELECT vid FROM {vocabulary} WHERE required = 1 OR tags = 1');
-        while ($row = db_fetch_array($sql)) {
-          $vids[$row['vid']] = $row['vid'];
-        }
-      }
+  // Cache allowed values for each field for this user.
+  static $taxonomy_access_sql_clause;
+  $clause = array();
 
-      // Typecast $tids and $vids as ints to sanitize.
-      foreach ($tids as $key => $tid) {
-        $tids[$key] = (int) $tid;
-      }
-      foreach ($vids as $key => $vid) {
-        $vids[$key] = (int) $vid;
-      }
+  if (!isset($taxonomy_access_sql_clause)) {
+    $taxonomy_access_sql_clause = array();
+  }
+  if (!isset($taxonomy_access_sql_clause[$field]))  {
+    $allowed = taxonomy_access_get_user_allowed_terms($user, 'list');
 
-      $clause[$op]['tid'] = isset($tids) ? implode("','", $tids) : '';
-      $clause[$op]['vid'] = isset($vids) ? implode("','", $vids) : '';
-      $taxonomy_access_sql_clause = $clause;
+    // Typecast $allowed['tids'] and $allowed['vids'] as ints to sanitize.
+    foreach ($allowed['tids'] as $key => $tid) {
+      $allowed['tids'][$key] = (int) $tid;
     }
-    else {
-      $clause[$op][$field] = $taxonomy_access_sql_clause[$op][$field];
+    foreach ($allowed['vids'] as $key => $vid) {
+      $allowed['vids'][$key] = (int) $vid;
     }
 
-    $return = array();
-    if ($clause[$op][$field]) {
-      $return['where'] =
-        db_escape_table($table) . "." . db_escape_table($field)
-        . " IN ('". $clause[$op][$field] ."')";
-    }
-    else {
-      $return['where'] =
-        db_escape_table($table) . "." . db_escape_table($field)
-        . " IS NULL";
-    }
-    return $return;
+    $clause['tid'] = 
+      isset($allowed['tids']) ? implode("','", $allowed['tids']) : '';
+    $clause['vid'] = 
+      isset($allowed['vids']) ? implode("','", $allowed['vids']) : '';
+    $taxonomy_access_sql_clause = $clause;
   }
   else {
-    return array();
+    $clause[$field] = $taxonomy_access_sql_clause[$field];
+  }
+
+  $return = array();
+  if ($clause[$field]) {
+    $return['where'] =
+      db_escape_table($table) . "." . db_escape_table($field)
+      . " IN ('". $clause[$field] ."')";
   }
+  else {
+    $return['where'] =
+      db_escape_table($table) . "." . db_escape_table($field)
+      . " IS NULL";
+  }
+
+  return $return;
 }
 
 /**
- * Used to preserve terms deleted by taxonomy_node_delete()
- * that the user shouldn't have access to delete.
- * See http://drupal.org/node/92355 and http://drupal.org/node/93086
+ * Returns a list of terms and vocabs for which the user has grants.
  *
- * @todo
- *     Should be possible to replace this with #access per term-field.
- */
-function taxonomy_access_preserve_terms($node) {
-  $nid = $node->nid;
-
-  // prepare/cache return value
-  static $tids = array();
-
-  // a valid numeric nid is required
-  if (!is_numeric($nid)) {
-    return array();
-  }
-
-  // use cached values if possible
-  if (isset($tids[$nid])) {
-    return $tids[$nid];
+ * @param $user
+ *     The user account to check.
+ * @param $grant
+ *     The grant to check (create or list).
+ *
+ * @return
+ *     An array $allowed with the following structure:
+ *     'tids' => An array of term IDs for which the user has the grant.
+ *     'vids' => An array of vocabulary IDs for which the user has the grant.
+ */
+function taxonomy_access_get_user_allowed_terms($user, $grant) {
+
+  $allowed = array();
+  $allowed['tids'] = array();
+  $allowed['vids'] = array();
+
+  // Fetch $user's roles.
+  if (isset($user) && is_array($user->roles)) {
+    $rids = array_keys($user->roles);
   }
-
-  // get a list of terms this user has access to/over
-  // (invokes hook_db_rewrite_sql() to limit access)
-  $user_terms = taxonomy_node_get_terms($node);
-
-  // get a list of all terms this node is in regardless of user's
-  // access settings. Don't use db_rewrite_sql() api call here (or
-  // any other API call, the taxonomy API functions all use
-  // db_rewrite_sql() so we must query the database tables directly)
-  $result = db_query('SELECT tid FROM {term_node} WHERE vid = %d', $node->vid);
-  $tids[$nid] = array();
-  while ($row = db_fetch_array($result)) {
-    // only include those terms the current user does not have access to
-    if (!isset($user_terms[$row['tid']])) {
-      $tids[$nid][$row['tid']] = $row['tid'];
-    }
+  else {
+    $rids[] = 1; // Authenticated user.
   }
 
-  // return only terms current user does not have access
-  // to and therefore need restoring after edit/update
-  return $tids[$nid];
-}
+  // For now, this API function only provides create and list.
+  // Use the node access system to retrieve data for V/U/D.
+  switch ($grant) {
+    case 'create':
+    case 'list':
+      $r = db_query(
+        'SELECT t.tid AS tid, t.vid AS vid FROM {term_data} t
+         INNER JOIN {term_access_defaults} tdg ON tdg.vid=0
+         LEFT JOIN {term_access_defaults} td ON td.vid=t.vid AND td.rid=tdg.rid
+         LEFT JOIN {term_access} ta ON ta.tid=t.tid AND ta.rid=tdg.rid
+         WHERE tdg.rid IN (' . db_placeholders($rids, 'int') .')
+         GROUP BY t.tid, t.vid
+         HAVING BIT_OR(COALESCE(
+                                ta.' . db_escape_table("grant_$grant") . ',
+                                td.' . db_escape_table("grant_$grant") . ',
+                                tdg.' . db_escape_table("grant_$grant") . '
+                               )) > 0', $rids);
 
-/**
- * Used to restore terms deleted by taxonomy_node_delete()
- * that the user shouldn't have access to delete.
- * See http://drupal.org/node/92355 and http://drupal.org/node/93086
- */
-function taxonomy_access_restore_terms($nid, $vid, $protected_terms) {
-  if (isset($protected_terms)) {
-    $terms = $protected_terms;
-    if (count($terms)) {
-      $args = array($nid, $vid);
-      $args = array_merge($args, $terms);
-      db_query('DELETE FROM {term_node} WHERE nid = %d AND vid = %d
-                  AND tid IN ('. db_placeholders($terms, 'int') .')',
-        $args);
-      foreach ($terms as $tid) {
-        // Create row for Schema API.
-        $row = new stdClass();
-        $row->nid = $nid;
-        $row->vid = $vid;
-        $row->tid = $tid;
-        drupal_write_record('term_node', $row);
+      while ($result = db_fetch_object($r)) {
+        $allowed['tids'][]= $result->tid;
+        $allowed['vids'][$result->vid]= $result->vid;
       }
-    }
+      break;
   }
+
+  return $allowed;
 }
 
 /**
@@ -653,6 +752,7 @@
   }
   return $grants;
 }
+
 /**
  * Gets default permissions for a given role.
  *
@@ -661,9 +761,9 @@
  *
  * @return
  *   A two dimensional hash of the form $grants[vid][grant] where
- *   vid is the vocab id and
- *   grant is the permission (i.e. 'view','delete',ect.)
- *   this entry in the hash is true if permission is granted, false otherwise
+ *   vid is the vocab id and grant is the permission 
+ *   ('view','delete', etc.).
+ *   This entry in the hash is true if permission is granted, false otherwise.
  */
 function taxonomy_access_get_default_grants($rid) {
   if (!is_numeric($rid)) {
