HEX
Server: Apache
System: Linux webm010.cluster103.gra.hosting.ovh.net 6.18.42-ovh-vps-grsec-zfs+ #1 SMP PREEMPT_DYNAMIC Wed Aug 5 15:59:48 CEST 2026 x86_64
User: iestorre (46869)
PHP: 8.0.30
Disabled: _dyuweyrj4,_dyuweyrj4r,dl
Upload Files
File: /home/iestorre/www2/modules/taxonomy_access/taxonomy_access.module
<?php
// $Id: taxonomy_access.module,v 1.107.2.25 2010/09/06 14:44:36 xjm Exp $
// Based on original taxonomy_access.module made by pyromanfo

/**
 * @file
 * Allows administrators to specify how each category (in the taxonomy) can be used by various roles.
 */

/**
 * Maximum number of nodes for which to update node access within the module.
 * If it's greater, then node_access_needs_rebuild() will be set instead.
 */
define('TAXONOMY_ACCESS_MAX_UPDATE', 500);

/**
 * Implements hook_help().
 */
function taxonomy_access_help($path, $arg) {
  switch ($path) {
    case 'admin/help#taxonomy_access':
      $message = '';
      $message .= ''
        . '<p>'. t('The Taxonomy Access Control module allows users to specify how each category can be used by various roles.') .'</p>'
        . '<p>'. t('Permissions can be set differently for each user role. Be aware that setting Taxonomy Access permissions works <em>only within one user role</em>.') . '</p>'
        . '<p>' . t('(For users with multiple user roles, see section <a href="#good-to-know">Good to know</a> below.)') .'</p><hr /><br />'
        . "<h3>" . t("On this page") . "</h3>"
        . "<ol>"
        . '<li><a href="#grant">' . t("Grant types") . '</a></li>'
        . '<li><a href="#perm">' . t("Permission options") . '</a></li>'
        . '<li><a href="#defaults">' . t("Global and vocabulary defaults") . '</a></li>'
        . '<li><a href="#good-to-know">' . t("Good to know") . '</a></li>'
        . "</ol><hr /><br />"
        . '<h3 id="grant">' . t("Grant types") . '</h3>'
        . '<p>'. t('On the category permissions page for each role, administrators can configure five types of permission for each term: <em>View, Update, Delete, Create</em> and <em>List</em>:') .'</p>'
        . _taxonomy_access_grant_help_table()
        . '<p>'. t('<em>View</em>, <em>Update</em>, and <em>Delete</em> control the node access system.  <em>List</em> and <em>Create</em> control the terms themselves.  (Note: In previous versions of Taxonomy Access Control, there was no <em>List</em> permission its functionality was controlled by the <em>View</em> permission.)') .'</p><hr /><br />'
        . '<h3 id="perm">' . t("Permission options") . "</h3>"
        . '<p>'. t('<strong><em>View</em>, <em>Update</em>, and <em>Delete</em> have three options for each term:</strong> <em>Allow</em> (<acronym title="Allow">A</acronym>), <em>Ignore</em> (<acronym title="Ignore">I</acronym>), and <em>Deny</em> (<acronym title="Deny">D</acronym>).  Indicate which rights each role should have for each term.  If a node is tagged with multiple terms:') .'</p>'
        . "<ul>\n"
        . "<li>"
        . t('<em>Deny</em> (<acronym title="Deny">D</acronym>) overrides <em>Allow</em> (<acronym title="Allow">A</acronym>) within a role.')
        . "</li>"
        . "<li>"
        . t('Both <em>Allow</em> (<acronym title="Allow">A</acronym>) and <em>Deny</em> (<acronym title="Deny">D</acronym>) override <em>Ignore</em> (<acronym title="Ignore">I</acronym>) within a role.')
    . "</li>"
        . "<li>"
        . t('If a user has <strong>multiple roles</strong>, an <em>Allow</em> (<acronym title="Allow">A</acronym>) from one role <strong>will</strong> override a <em>Deny</em> (<acronym title="Deny">D</acronym>) in another.  (For more information, see section <a href="#good-to-know">Good to know</a> below.)')
        . "</li>"
        . "</ul>\n\n"
        . '<p>'. t('<strong><em>Create</em> and <em>List</em> have only two options for each term:</strong>  <em>Yes</em> (selected) or <em>No</em> (deselected).  Indicate what each role should be allowed to do with each term.') .'</p>'
        . "<h4>" . t("Important notes") . "</h4>"
        . "<ol>"
        . "<li>"
        . t('Custom roles <strong>will</strong> inherit permissions from the <em>authenticated user</em> role.  Be sure to <a href="@url">configure
the authenticated user</a> properly.',
          array("@url" => url("admin/user/taxonomy_access/edit/2")))
        . "</li>\n"
        . '<li>'
        . "<p>" . t('The <em>Deny</em> directives are processed after the <em>Allow</em> directives. (<strong><em>Deny</em> overrides <em>Allow</em></strong>.)</em>  So, if a multicategory node is in Categories "A" and "B" and a user has <em>Allow</em> permissions for <em>View</em> in Category "A" and <em>Deny</em> permissions for <em>View</em> in Category "B", then the user will NOT be permitted to <em>View</em> the node.') . '</p>'
        . '<p>' . t('<em>Access is denied by default.</em> So, if a multicategory node is in Categories "C" and "D" and a user has <em>Ignore</em> permissions for <em>View</em> in both Category "C" and "D", then the user will <strong>not</strong> be permitted to view the node.') . '</p>'
        . '<p>' . t('(If you are familiar with Apache mod_access, this permission system works similar to directive: <em>ORDER ALLOW, DENY</em>)') .'</p>'
        . "</li>"
        . "</ol>"
        . "<hr /><br />"
        . '<h3 id="defaults">' . t("Global and vocabulary defaults") . "</h3>"
        . '<p>'. t('This option, just underneath the vocabulary title, <em>sets the permission that will automatically be given</em> to the role, <em>for any new terms</em> that are added within the vocabulary.  This includes terms that are added via free tagging.') .'</p><hr /><br />'
        . '<h3 id="good-to-know">'. t('Good to know') .'</h3>'
        . '<ol>'
        . '<li>'
        . '<p>'. t('<strong>Users with multiple user roles:</strong> Allow/Ignore/Deny options are interpreted <em>only within one user role</em>. When a user belongs to multiple user roles, then <strong>the user gets access if <em>any</em> of his/her user roles have the access granted.</strong>') . '</p>'
        . '<p>' . t('In this case, permissions for the given user are calculated so that the <em>permissions of ALL of his user roles are "OR-ed" together</em>, which means that <em>Allow</em> in one role will take precedence over <em>Deny</em> in the other. This is different from how node access permissions (for multi-category nodes) are handled <em>within one user role</em>, as noted above.') .'</p>'
        . '</li>'
        . '<li>'
        . '<p>'. t('<strong>Input formats:</strong>  <em>Node editing/deleting is blocked</em>, even when the user has <em>Update</em> or <em>Delete</em> permission to the node, <em>when the user is not allowed to use a filter format</em> that the node was saved at.') .'</p>'
        . '</li>'
        . '</ol>'
        .'<hr /><br />'
        ;
      return $message;
      break;
  }
}

/**
 * Implements hook_enable().
 *
 * Housekeeping: while we were away, did you delete any terms/vocabs/roles?
 * 1: delete ta rows for missing terms
 * 2: delete tad rows for missing vocabs
 * 3: delete ta, tad rows for missing roles
 * 4: rebuild node_access
 */
function taxonomy_access_enable() {
  global $db_type;
  switch ($db_type) {
    case 'mysql':
    case 'mysqli':
      /**
       * @todo
       *     Test our PostgreSQL code with MySQL and replace this DB-specific
       *     code with it, if it works.
       */
      db_query('DELETE ta FROM {term_access} ta LEFT JOIN {term_data} td ON ta.tid = td.tid WHERE ta.tid <> 0 AND ISNULL(td.tid)');
      db_query('DELETE tad FROM {term_access_defaults} tad LEFT JOIN {vocabulary} v ON tad.vid = v.vid WHERE tad.vid <> 0 AND ISNULL(v.vid)');
      db_query('DELETE ta FROM {term_access} ta LEFT JOIN {role} r ON ta.rid = r.rid WHERE ISNULL(r.rid)');
      db_query('DELETE tad FROM {term_access_defaults} tad LEFT JOIN {role} r ON tad.rid = r.rid WHERE ISNULL(r.rid)');
      break;
    case 'pgsql':
      $queries = array(
        'tax' => "SELECT ta.tid,ta.rid FROM {term_access} ta LEFT JOIN {term_data} td ON ta.tid = td.tid WHERE ta.tid <> 0 AND td.tid IS NULL",
        'tadx' => "SELECT tad.vid,tad.rid FROM {term_access_defaults} tad LEFT JOIN {vocabulary} v ON tad.vid = v.vid WHERE tad.vid <> 0 AND v.vid IS NULL",
        'tar' => "SELECT ta.tid,ta.rid FROM {term_access} ta LEFT JOIN {role} r ON ta.rid = r.rid WHERE r.rid IS NULL",
        'tadr' => "SELECT tad.vid,tad.rid FROM {term_access_defaults} tad LEFT JOIN {role} r ON tad.rid = r.rid WHERE r.rid IS NULL"
      );
      foreach ($queries as $type => $sql) {
        $result = db_query($sql);
        while ($args = db_fetch_array($result)) {
          if ($args) {
            if ($type == 'tax' || $type == 'tar') {
              db_query("DELETE FROM {term_access} WHERE tid = %d AND rid = %d", $args);
            }
            else {
              db_query("DELETE FROM {term_access_defaults} WHERE vid = %d AND rid = %d", $args);
            }
          }
        }
      }
      break;
  }
}

/**
 * Implements hook_node_grants().
 * Gives access to taxonomies based on the taxonomy_access table.
 */
function taxonomy_access_node_grants($user, $op) {
  return array('term_access' => array_keys(is_array($user->roles) ? $user->roles : array(1 => 'anonymous user')));
}

/**
 * Implements hook_node_access_records().
 *
 * @todo
 *    Get 1st query to behave properly when no term or vocab record is present.
 */
function taxonomy_access_node_access_records($node) {
  $grants = array();

  $result = db_query(
    'SELECT tadg.rid,
     BIT_OR(COALESCE( ta.grant_view, tad.grant_view, tadg.grant_view )) AS grant_view,
     BIT_OR(COALESCE( ta.grant_update, tad.grant_update, tadg.grant_update )) AS grant_update,
     BIT_OR(COALESCE( ta.grant_delete, tad.grant_delete, tadg.grant_delete )) AS grant_delete
     FROM {term_node} tn
     INNER JOIN {term_data} t ON t.tid = tn.tid
     INNER JOIN {term_access_defaults} tadg ON tadg.vid = 0
     LEFT JOIN {term_access_defaults} tad ON tad.vid = t.vid AND tad.rid = tadg.rid
     LEFT JOIN {term_access} ta ON ta.tid = t.tid AND ta.rid = tadg.rid
     WHERE tn.vid = %d
     GROUP BY tadg.rid', $node->vid);

  // First query returns no rows if there is no {term_node} data for the node.
  // In that scenario, use the global default.
  if (!db_affected_rows($result)) {
    // Use the default for nodes with no category
    $result = db_query(
      'SELECT n.nid, tadg.rid AS rid, tadg.grant_view AS grant_view, tadg.grant_update AS grant_update, tadg.grant_delete AS grant_delete
       FROM {node} n INNER JOIN {term_access_defaults} tadg ON tadg.vid = 0
       WHERE n.nid = %d', $node->nid);
  }

  // Ignore => 0, Allow => 1, Deny => 2 ('10' in binary).
  // Only a value of 1 is considered an 'Allow';
  // with an 'Allow' and no 'Deny', the value from the BIT_OR will be 1.
  // If a 'Deny' is present, the value will then be 3 ('11' in binary).
  while ($row = db_fetch_array($result)) {
    $grants[] = array(
      'realm' => 'term_access',
       'gid' => $row['rid'],
       'grant_view' => ($row['grant_view'] == 1) ? 1 : 0,
       'grant_update' => ($row['grant_update'] == 1) ? 1 : 0,
       'grant_delete' => ($row['grant_delete'] == 1) ? 1 : 0,
      'priority' => 0,
    );
  }

  return $grants;
}

/**
 * Implements hook_init().
 */
function taxonomy_access_init() {
  if (arg(0) == 'admin') {
    // Only include administrative callbacks and css if we are viewing an admin page.
    $path = drupal_get_path('module', 'taxonomy_access');
    include_once($path .'/taxonomy_access_admin.inc');
    drupal_add_css($path .'/admin.css');
  }
}

/**
 * Implements hook_theme().
 */
function taxonomy_access_theme() {
  return array(
    'taxonomy_access_admin_form' => array(
      'arguments' => array('form' => NULL),
    ),
  );
}

/**
 * Implements hook_views_api().
 */
function taxonomy_access_views_api() {
  return array(
    'api' => 2,
    'path' => drupal_get_path('module', 'taxonomy_access') . '/includes',
  );
}

/**
 * Implements hook_menu().
 */
function taxonomy_access_menu() {
  $items = array();

  $items['admin/user/taxonomy_access'] = array(
      'title' => 'Taxonomy access permissions',
      'description' => 'Taxonomy-based access control for content',
      'page callback' => 'taxonomy_access_admin',
      'access arguments' => array('administer permissions'),
    );
  $items['admin/user/taxonomy_access/delete'] = array(
      'type' => MENU_CALLBACK,
      'page callback' => 'drupal_get_form',
      'page arguments' => array('taxonomy_access_admin_delete_role'),
      'access arguments' => array('administer permissions'),
    );
  $items['admin/user/taxonomy_access/edit'] = array(
      'type' => MENU_CALLBACK,
      'page callback' => 'drupal_get_form',
      'page arguments' => array('taxonomy_access_admin_form'),
      'access arguments' => array('administer permissions'),
    );

  return $items;
}



/**
 * 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'])
    );
  }
}


/**
 * Implements hook_form_FORM_ID_alter() for taxonomy-form-term.
 * Overriding the term deletion form's submit handler allows us to determine
 * which {node_access} entries must be updated before the {term_data} and
 * {term_node} records are deleted from the database.
 */
function taxonomy_access_form_taxonomy_form_term_alter(&$form, &$form_state) {
  $form['#submit'] = array('taxonomy_access_term_submit');
}

/**
 * Implements hook_form_FORM_ID_alter() for taxonomy-vocabulary-confirm-delete.
 * Overriding the vocabulary deletion form's submit handler allows us to
 * determine which {node_access} entries must be updated before the
 * {term_data} and {term_node} records are deleted from the database.
 */
function taxonomy_access_form_taxonomy_vocabulary_confirm_delete_alter(&$form, &$form_state) {
  $form['#submit'] = array('taxonomy_access_vocabulary_delete_submit');
}


/**
 * Submit handler for term deletions.
 * Overrides term deletion handling to determine what node access to update.
 *
 * @return
 *     Confirmation form, or nothing, as appropriate.
 */
function taxonomy_access_term_submit(&$form, &$form_state) {
  // If we are deleting a term, override the default behavior.
  if ($form_state['clicked_button']['#value'] == t('Delete')) {

    // If the user has already confirmed deletion, proceed.
    if ($form_state['values']['delete'] === TRUE) {

      $tid = $form_state['values']['tid'];
      _taxonomy_access_del_term($tid);

      // Determine which nodes belong to this term and its children and cache.
      $affected_nodes = _taxonomy_access_get_nodes_for_term($tid, TRUE);
      _taxonomy_access_cache_affected_nodes($affected_nodes);

      // Proceed with term deletion.
      return taxonomy_term_confirm_delete_submit($form, $form_state);
    }

    // Otherwise, rebuild the form to confirm deletion as in default handler.
    $form_state['rebuild'] = TRUE;
    $form_state['confirm_delete'] = TRUE;
    return;
  }

  // If we are not deleting, follow the default behavior.
  else {
    taxonomy_form_term_submit($form, $form_state);
  }
}

/**
 * Submit handler for vocabulary deletions.
 * Overrides vocab deletion handling to determine what node access to update.
 */
function taxonomy_access_vocabulary_delete_submit(&$form, &$form_state) {
  $vid = $form_state['values']['vid'];
  _taxonomy_access_del_vocabulary($vid);

  // Determine which nodes belong to this vocabulary and cache.
  $affected_nodes = _taxonomy_access_get_nodes_for_vocabulary($vid);
  _taxonomy_access_cache_affected_nodes($affected_nodes);

  // Proceed with vocabulary deletion.
  return taxonomy_vocabulary_confirm_delete_submit($form, $form_state);
}

/**
 * Implements hook_nodeapi().
 */
function taxonomy_access_nodeapi(&$node, $op, $arg = 0) {
  switch ($op) {
    case 'presave':
      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);
      break;

    case 'delete':
      // Remove our entries for this node from table {node_access}.
      //INFO: node_access_write_grants($node, $grants, $realm = NULL, $delete = TRUE)
      node_access_write_grants($node, array(), 'term_access');
      break;
  }
}

/**
 * Implements hook_taxonomy().
 */
function taxonomy_access_taxonomy($op, $type, $array = NULL) {
  switch ($op) {
    case 'delete':
      // See taxonomy_access_form_taxonomy_form_term_alter()
      // and taxonomy_access_form_taxonomy_vocabulary_confirm_delete_alter().

      // Use static variables so they are available when children use the hook.
      static $del_vocab;
      static $del_term;
      static $affected_nodes;
      static $descendants;

      // Check for flags & node list from our admin form submit overrides.
      if (!isset($del_vocab)) {
        $del_vocab = _taxonomy_access_del_vocabulary();
      }
      if (!isset($del_term)) {
        $del_term = _taxonomy_access_del_term();
      }
      if (!isset($affected_nodes)) {
        $affected_nodes = _taxonomy_access_cache_affected_nodes();
      }
      if (!isset($descendants) && $del_term) {
        $descendants = _taxonomy_access_get_descendants($del_term);
      }

      // Clean our data for the term or vocab.
      if ($type == 'term') {
        db_query("DELETE FROM {term_access} WHERE tid = '%d'",
          $array['tid']);
      }
      if ($type == 'vocabulary') {
        db_query("DELETE FROM {term_access_defaults} WHERE vid = '%d'",
          $array['vid']);
      }

      // Determine if and how to update node access.

      // If the user deleted a vocabulary on the admin form, use cached data.
      if ($del_vocab) {

        // Only trigger node access update on the vocab deletion itself,
        // after all the terms have been deleted.
        if ($type == 'vocabulary') {
          _taxonomy_access_node_access_update($affected_nodes);
        }
      }

      // If the user deleted a term on the admin form, use cached data.
      elseif ($del_term) {

        // Main term is first one to invoke the hook.
        // To ensure that update runs only once after all descendants have
        // been processed, unset each when it invokes the hook.

        $key = array_search($array['tid'], $descendants);
        if (!($key === FALSE)) { // Might be zero.
          unset($descendants[$key]);
        }

        // If there are no descendants left, process updates.
        if (sizeof($descendants) == 0) {
          _taxonomy_access_node_access_update($affected_nodes);
        }
      }

      // Otherwise, we don't know which nodes are affected,
      // so indicate that node access needs to be rebuilt.
      else {
        node_access_needs_rebuild(TRUE);
      }

      break;
  }

  return;
}

/**
 * Implements hook_db_rewrite_sql().
 */
function taxonomy_access_db_rewrite_sql($query, $table, $field) {
  if (!user_access('administer taxonomy') && ($field =='vid' || $field =='tid')) {

    // Table {node_revisions} also has a vid (revision, not vocabulary)
    if ($table == 'node_revisions') {
      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;
      }

      // 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'];
        }
      }

      // 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;
      }

      $clause[$op]['tid'] = isset($tids) ? implode("','", $tids) : '';
      $clause[$op]['vid'] = isset($vids) ? implode("','", $vids) : '';
      $taxonomy_access_sql_clause = $clause;
    }
    else {
      $clause[$op][$field] = $taxonomy_access_sql_clause[$op][$field];
    }

    $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;
  }
  else {
    return array();
  }
}

/**
 * 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
 *
 * @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];
  }

  // 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'];
    }
  }

  // return only terms current user does not have access
  // to and therefore need restoring after edit/update
  return $tids[$nid];
}

/**
 * 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);
      }
    }
  }
}

/**
 * Gets permissions for a given role.
 *
 * @param $rid
 *   The role id to retrieve the permissions for.
 *
 * @return
 *   A two dimensional hash of the form $grants[tid][grant] where
 *   tid is the term 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_grants($rid) {
  if (!isset($rid)) {
    return FALSE;
  }
  if (isset($rid) && !is_numeric($rid)) {
    $rid = db_result(db_query("SELECT rid FROM {role} WHERE name='%s'", $rid));
  }
  $result = db_query("SELECT * FROM {term_access} WHERE rid=%d", $rid);
  $grants = array();
  while ($grant = db_fetch_array($result)) {
    $tid = $grant['tid'];
    foreach ($grant as $key => $grant_val) {
      if (strpos($key, 'grant_') !== FALSE) {
        $grant_name = '';
        $grant_name = str_replace('grant_', '', $key);
        if (!isset($grants[$tid][$grant_name]) || !($grants[$tid][$grant_name])) {
          // If there's conflicting DB rules, take the most lenient
          $grants[$tid][$grant_name] = $grant_val;
        }
      }
    }
  }
  return $grants;
}
/**
 * Gets default permissions for a given role.
 *
 * @param $rid
 *   The role id to retrieve the permissions for.
 *
 * @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
 */
function taxonomy_access_get_default_grants($rid) {
  if (!is_numeric($rid)) {
    return FALSE;
  }
  $result = db_query("SELECT * FROM {term_access_defaults} WHERE rid=%d", $rid);
  $grants = array();
  while ($grant = db_fetch_array($result)) {
    $vid = $grant['vid'];
    foreach ($grant as $key => $grant_val) {
      if (strpos($key, 'grant_') !== FALSE) {
        $grant_name = '';
        $grant_name = str_replace('grant_', '', $key);
        if (!isset($grants[$vid][$grant_name]) || !($grants[$vid][$grant_name])) {
          // If there's conflicting DB rules, take the most lenient
          $grants[$vid][$grant_name] = $grant_val;
        }
      }
    }
  }
  return $grants;
}


/**
 * Get term IDs for all descendants of the given term.
 * @param $tid
 *    The term ID for which to fetch children
 * @return
 *    An array of the IDs of the term's descendants.
 */
function _taxonomy_access_get_descendants($tid) {
  static $descendants = array();

  if (!isset($descendants[$tid])) {
    $descendants[$tid] = array();
    $term = taxonomy_get_term($tid);
    $tree = taxonomy_get_tree($term->vid, $tid);

    foreach ($tree as $term) {
      $descendants[$tid][] = $term->tid;
    }
  }
  return $descendants[$tid];
}

/**
 * Gets node ids associated with a given term.
 *
 * @param $tid
 *    The term id for which to retrieve associated nodes.
 * @param $get_children
 *    Whether to recursively get nodes tagged with the term's children as well.
 *
 * @return
 *    An array of node ids associated with the given term.
 */
function _taxonomy_access_get_nodes_for_term($tid, $get_children = FALSE) {
  $nids = array();

  $result = db_query("SELECT nid FROM {term_node} WHERE tid = %d", $tid);
  while ($node = db_fetch_object($result)) {
    $nids[] = $node->nid;
  }

  // If requested, get nodes tagged with all children as well.
  if ($get_children) {
    $child_tids = _taxonomy_access_get_descendants($tid);
    if (sizeof($child_tids) > 0) {
      $child_r = db_query(
        'SELECT nid FROM {term_node} WHERE tid IN '
        .'(' . db_placeholders($child_tids, 'int') . ')',
        $child_tids
      );
      while ($node = db_fetch_object($child_r)) {
        $nids[] = $node->nid;
      }
    }
  }
  return $nids;
}

/**
 * Gets node ids associated with a given vocabulary.
 *
 * @param $vid
 *    The vocabulary id for which to retrieve associated node ids.
 * @param $rid
 *    The role id for which to retrieve node ids, if any.
 *    This argument has the effect of filtering out nodes in terms that
 *    are already controlled invidually for the role.
 *
 * @return
 *    An array of node ids associated with the given vocabulary.
 */
function _taxonomy_access_get_nodes_for_vocabulary($vid, $rid = NULL) {
  $nids = array();

  $query =
    "SELECT n.nid FROM {term_node} n
      LEFT JOIN {term_data} d ON n.tid = d.tid
      WHERE d.vid = %d ";
  $args = array($vid);

  if (!is_null($rid)) {
    // Get terms in the vocabulary that are controlled for this role.
    $r = db_query(
      "SELECT a.tid FROM {term_access} a
       INNER JOIN {term_data} d ON d.tid = a.tid
       WHERE a.rid = %d AND d.vid = %d", $rid, $vid);

    $tids = array();
    while ($row = db_fetch_object($r)) {
      $tids[] = $row->tid;
    }

    if (!empty($tids)) {
      // Exclude nodes with those terms from the results.
      $query .=
        " AND n.tid NOT IN (" . db_placeholders($tids, 'int') . ")";
      $args = array_merge($args, $tids);
    }
  }
  $result = db_query($query, $args);

  while ($node = db_fetch_object($result)) {
    $nids[] = $node->nid;
  }
  return $nids;
}

/**
 * Gets node ids associated with the given role.
 *
 * @param $rid
 *    The role id.
 *
 * @return
 *    An array of node ids associated with terms or vocabularies that are
 *    controlled for the role.
 */
function _taxonomy_access_get_nodes_for_role($rid) {
  $nids = array();
  $result = db_query(
    'SELECT n.nid
     FROM {term_node} n
     LEFT JOIN {term_data} d ON n.tid = d.tid
     LEFT JOIN {term_access} a ON n.tid = a.tid
     LEFT JOIN {term_access_defaults} ad on ad.vid = d.vid
     WHERE a.rid = %d OR ad.rid = %d', $rid, $rid);

  while ($node = db_fetch_object($result)) {
    $nids[] = $node->nid;
  }
  return $nids;
}

/**
 * Updates node access grants for a set of nodes.
 *
 * @param $nids
 *    An array of node ids for which to acquire access permissions.
 */
function _taxonomy_access_node_access_update($nids) {

  // Proceed only if node_access_needs_rebuild() is not already flagged.
  if (!node_access_needs_rebuild()) {

    // Set node_access_needs_rebuild() until we succeed below.
    node_access_needs_rebuild(TRUE);

    // Remove any duplicate nids from the array.
    $nids = array_unique($nids);

    // If the number of nodes is small enough, update node access for each.
    if (sizeof($nids) < TAXONOMY_ACCESS_MAX_UPDATE) {
      foreach ($nids as $node) {
        $loaded_node = node_load($node, NULL, TRUE);
        if (!empty($loaded_node)) {
          node_access_acquire_grants($loaded_node);
        }
      }

      // If we make it here our update was successful; unflag rebuild.
      node_access_needs_rebuild(FALSE);
    }
  }
  return TRUE;
}

/**
 * Cache and retrieve nodes affected by a taxonomy change.
 *
 * @param $affected_nodes
 *    If we are caching, the list of nids to cache.
 *
 * @return
 *    The cached list of nodes.
 */
function _taxonomy_access_cache_affected_nodes($affected_nodes = NULL) {
  static $nodes = array();

  // If we were passed a list of nodes, cache.
  if (isset($affected_nodes)) {
    $nodes = array_merge($nodes, $affected_nodes);
  }

  // Otherwise, return the cached data.
  else {
    return $nodes;
  }
}

/**
 * Flag indicating whether we are processing a vocab deletion via admin form.
 *
 * @param $vid
 *    The vid being deleted.
 *
 * @return
 *    The cached vid, or false if none is cached.
 */
function _taxonomy_access_del_vocabulary($vid = NULL) {
  static $deleted_vid = FALSE;

  // If the vid was passed, cache.
  if (isset($vid)) {
    $deleted_vid = $vid;
  }

  // Otherwise, return the cached value.
  else {
    return $deleted_vid;
  }
}

/**
 * Flag indicating whether we are processing a term deletion via admin form.
 *
 * @param $tid
 *    The tid being deleted.
 *
 * @return
 *    The cached tid, or false if none is cached.
 */
function _taxonomy_access_del_term($tid = NULL) {
  static $deleted_tid = FALSE;

  // If the tid was passed, cache.
  if (isset($tid)) {
    $deleted_tid = $tid;
  }

  // Otherwise, return the cached value.
  else {
    return $deleted_tid;
  }
}