<?php
// $Id: forum_access.module,v 1.61 2010/02/20 10:32:12 salvis Exp $

/**
 * @file forum_access.module
 *
 * This module uses form_alter to add permissions and moderator settings to
 * forums.
 *
 */

/**
 * Implementation of hook_node_grants().
 *
 * This function supplies the forum access grants. forum_access simply uses
 * roles as ACLs, so rids translate directly to gids.
 */
function forum_access_node_grants($user, $op) {
  $grants['forum_access'] = array_keys($user->roles);
  return $grants;
}

/**
 * Implementation of hook_node_access_records().
 *
 * Returns a list of grant records for the passed in node object.
 * Checks to see if maybe we're being disabled.
 */
function forum_access_node_access_records($node) {
  if (!forum_access_enabled()) {
    return;
  }

  static $grants = array();
  $tid = _forum_access_get_tid($node);

  // Set proper grants for nodecomment comment nodes.
  if (isset($node->comment_target_nid)) {
    if ($changed_tid = _forum_access_changed_tid()) {
      $tid = $changed_tid;  // the topic node hasn't been saved yet!
    }
    else {
      $node = node_load($node->comment_target_nid);
      $tid = _forum_access_get_tid($node);
    }
  }

  if ($tid) {
    if (!isset($grants[$tid])) {
      $result = db_query('SELECT * FROM {forum_access} WHERE tid = %d', $tid);
      while ($grant = db_fetch_object($result)) {
        $grants[$tid][] = array(
          'realm' => 'forum_access',
          'gid' => $grant->rid,
          'grant_view' => $grant->grant_view,
          'grant_update' => $grant->grant_update,
          'grant_delete' => $grant->grant_delete,
          'priority' => $grant->priority,
        );
      }
      //drupal_set_message("forum_access_node_access_records($node->nid) (tid=$tid) returns ". var_export($grants[$tid], TRUE), 'status');
    }
    if (isset($grants[$tid])) {
      return $grants[$tid];
    }
  }
}

/**
 * Implementation of hook_init().
 *
 * Deny access to forum if the user does not have access to it.
 * Enable moderator access on node/%, node/%/edit, comment/edit/%,
 * and comment/delete/% where needed.
 */
function forum_access_init() {
  global $user;
  if ($user->uid == 1) {
    return;
  }
  
  switch (arg(0)) {
    case 'forum':
      if (is_numeric($tid = arg(1))) {
        if (!forum_access_access($tid, 'view', NULL, FALSE)) {
          drupal_access_denied();
          module_invoke_all('exit');
          exit;
        }
      }
      break;
      
    case 'comment':
      if (variable_get('forum_access_D5_legacy_mode', FALSE)) {
        return;  // disable comment access control
      }
      if ((arg(1) == 'edit' || arg(1) == 'delete') && !user_access('administer comments')) {
        if (is_numeric($cid = arg(2))) {  // comment/edit/%, comment/delete/%
          $access[] = (arg(1) == 'edit' ? 'update' : 'delete'); 
          $comment = _comment_load($cid);
          $nid = $comment->nid;
          // If the node turns out to be in a forum where we have update/delete
          // access, then we need Moderator permissions now, so we can moderate
          // this comment.
          // We won't provide full Administrator access, though: we'll remove
          // author and timestamp, for example.
        }
      }
      break;
      
    case 'node':
      if (is_numeric(arg(1))) {
        $access[] = 'update';
        if (arg(2) == 'edit' && !user_access('administer nodes')) {  // node/%/edit
          $nid = arg(1);
          // If the node turns out to be in a forum where we have update/delete
          // access, then we already get limited edit capabilities from NA, but
          // we need some more, e.g. publish/unpublish and comment status.
          // In order to get these controls on the form, we need Moderator
          // permissions now.
          // We won't provide full Administrator access, though: we'll remove
          // author and timestamp, for example.
        }
        if (arg(2) == NULL && !user_access('administer comments')) {  // node/%
          $nid = arg(1);
          // If the node turns out to be in a forum where we have update/delete
          // access, then we'll get the 'Edit' link automatically from NA, but
          // we'll need Moderator permissions, so that we can add the edit/delete
          // comment links (*after* we've identified the other comment links). 
        }
      }
      break;
  }
  if (isset($nid)) {
    $node = node_load($nid);
    if ($tid = _forum_access_get_tid($node)) {
      foreach ($access as $a) {
        if (forum_access_access($tid, $a) > 1) {
          $user->_forum_access_moderator = TRUE;
          if (arg(0) == 'comment' || arg(0) == 'node' && arg(2) == 'edit') {
            module_load_include('node.inc', 'forum_access');
            _forum_access_enable_moderator();
            break;
          }
        }
      }
    }
  }
}

/**
 * Implementation of hook_form_alter().
 *
 * Alter the node/comment create/edit forms and various admin forms.
 */
function forum_access_form_alter(&$form, &$form_state, $form_id) {
  //dpm($form, "form_id($form_id)");
  if (isset($form['type']) && $form['type']['#value'] .'_node_form' == $form_id) {
    module_load_include('node.inc', 'forum_access');
    _forum_access_node_form($form, $form_state);
  }
  else if ($form_id == 'comment_form' && !variable_get('forum_access_D5_legacy_mode', FALSE)) {
    module_load_include('node.inc', 'forum_access');
    _forum_access_comment_form($form, $form_state);
  }
  else if ($form_id == 'forum_overview') {
    module_load_include('admin.inc', 'forum_access');
    _forum_access_forum_overview($form, $form_state);
  }
  else if ($form_id == 'forum_form_container') {
    module_load_include('admin.inc', 'forum_access');
    _forum_access_forum_form($form, $form_state, TRUE);
  }
  else if ($form_id == 'forum_form_forum') {
    module_load_include('admin.inc', 'forum_access');
    _forum_access_forum_form($form, $form_state, FALSE);
  }
  else if ($form_id == 'forum_admin_settings') {
    module_load_include('admin.inc', 'forum_access');
    _forum_access_forum_admin_settings_form($form, $form_state);
  }
  else if ($form_id == 'user_admin_role') {
    module_load_include('admin.inc', 'forum_access');
    _forum_access_user_admin_role_form($form, $form_state);
  }
  else if ($form_id == 'content_access_admin_settings' && empty($_POST)) {
    module_load_include('admin.inc', 'forum_access');
    _forum_access_content_access_admin_form();
  }
  else if ($form_id == 'user_admin_perm') {
    module_load_include('admin.inc', 'forum_access');
    _forum_access_user_admin_perm_form($form, $form_state);
  }
  else if ($form_id == 'user_admin_account') {
    module_load_include('admin.inc', 'forum_access');
    _forum_access_user_admin_account_form($form, $form_state);
  }
  else if ($form_id == 'user_profile_form') {
    module_load_include('admin.inc', 'forum_access');
    _forum_access_user_profile_form($form, $form_state);
  }
}

/**
 * Implementation of hook_db_rewrite_sql().
 *
 * Because in order to restrict the visible forums, we have to rewrite
 * the sql. This is because there isn't a node_access equivalent for
 * taxonomy. There should be.
 */
function forum_access_db_rewrite_sql($query, $primary_table, $primary_field, $args) {
  global $user;
  //dpm($query, "hook_db_rewrite_sql($primary_table.$primary_field)");
  $sql = NULL;
  switch ($primary_field) {
    case 'tid':
      if ($user->uid == 1
        || user_access('administer nodes') && arg(0) != 'forum' && $_GET['q'] != 'node/add/forum'
        || user_access('administer forums') && $_GET['q'] == 'admin/content/forum') {
        break;
      }
      $roles = _forum_access_get_roles($user);
      $sql['join'] = "LEFT JOIN {forum_access} fa ON $primary_table.tid = fa.tid
                      LEFT JOIN {acl} acl_fa ON acl_fa.name = ". ($GLOBALS['db_type'] == 'pgsql' ? 'CAST(' : '')
                                                            ."$primary_table.tid". ($GLOBALS['db_type'] == 'pgsql' ? ' AS VARCHAR)' : '')
                                                                              ." AND acl_fa.module = 'forum_access'
                      LEFT JOIN {acl_user} aclu_fa ON aclu_fa.acl_id = acl_fa.acl_id AND aclu_fa.uid = $user->uid";
      $sql['where'] = "(fa.grant_view >= 1 AND fa.rid IN ($roles)) OR fa.tid IS NULL OR aclu_fa.uid = $user->uid";
      $sql['distinct'] = 1;
      break;

    case 'rid':
      if (strpos($query, 'FROM {role}') === FALSE
        || strpos($_GET['q'] .'/', 'admin/content/forum/') === 0) {
        break;
      }
      $moderator_rid = forum_access_query_moderator_rid();
      if (!empty($moderator_rid)) {
        $sql['where'] = "$primary_table.rid <> $moderator_rid";
      }
      break;
  }
  //dpm($sql, "rewritten:");
  return $sql;
}

/**
 * Implementation of hook_nodeapi().
 *
 * Add ACL data to fresh forum posts.
 */
function forum_access_nodeapi(&$node, $op, $teaser = NULL, $page = NULL) {
  static $old_tid = NULL;
  // This is modeled after forum_nodeapi():
  $vid = variable_get('forum_nav_vocabulary', '');
  $vocabulary = taxonomy_vocabulary_load($vid);
  if (empty($vocabulary) || !in_array($node->type, $vocabulary->nodes)) {
    if ($op == 'insert' && isset($node->comment_target_nid)) {
      // Set moderator on nodecomment.
      $topic_node = node_load($node->comment_target_nid);
      if ($topic_tid = _forum_access_get_tid($topic_node)) {
        $acl_id = acl_get_id_by_name('forum_access', $topic_tid);
        acl_node_add_acl($node->nid, $acl_id, 1, 1, 1);
      }
    }
    return;
  }

  switch ($op) {
    case 'presave':
      $old_tid = db_result(db_query('SELECT tid FROM {forum} WHERE nid = %d', $node->nid));
      break;

    case 'update':
      if (!empty($old_tid)) {
        if (!empty($node->tid) && $node->tid == $old_tid) {
          return;
        }
        acl_node_clear_acls($node->nid, 'forum_access');

        if (module_exists('nodecomment')) {
          _forum_access_changed_tid($node->tid);
          $result = db_query('SELECT cid FROM {node_comments} WHERE nid = %d', $node->nid);
          while ($row = db_fetch_object($result)) {
            acl_node_clear_acls($row->cid, 'forum_access');
          }
        }
      }
      // Deliberate no break -- for changed and for previously unassigned terms we need an insert.

    case 'insert':
      if (!empty($node->tid)) {
        $acl_id = acl_get_id_by_name('forum_access', $node->tid);
        acl_node_add_acl($node->nid, $acl_id, 1, 1, 1);

        if (isset($old_tid) && module_exists('nodecomment')) {
          $result = db_query('SELECT cid FROM {node_comments} WHERE nid = %d', $node->nid);
          while ($row = db_fetch_object($result)) {
            acl_node_add_acl($row->cid, $acl_id, 1, 1, 1);
            node_access_acquire_grants(node_load($row->cid));
          }
        }
      }
      $old_tid = NULL;
      break;
  }
}

/**
 * Get an array of moderator UIDs or NULL.
 */
function forum_access_get_moderator_uids($tid) {
  if ($acl_id = acl_get_id_by_name('forum_access', $tid)) {
    if ($uids = acl_get_uids($acl_id)) {
      return $uids;
    }
  }
}

/**
 * Implementation of $modulename_preprocess_$hook() for forum_list.
 *
 * Add forum_access_moderators to each forum,
 * containing a list of user objects.
 *
 * Note: On a site with many moderators, this function is expensive,
 * and thus it is disabled by default. Set the variable to TRUE to enable.
 */
function forum_access_preprocess_forum_list(&$variables) {
  if (variable_get('forum_access_provide_moderators_template_variable', FALSE)) {
    static $users;
    foreach ($variables['forums'] as $tid => $forum)
    {
      $moderators = array();
      if ($uids = forum_access_get_moderator_uids($tid)) {
        foreach ($uids as $uid) {
          if (!isset($users[$uid])) {
            $users[$uid] = user_load(array('uid' => $uid));
          }
          $moderators[$uid] = $users[$uid];
        }
      }
      $forum->forum_access_moderators = (empty($moderators) ? NULL : $moderators);
    }
  }
}

/**
 * Implementation of $modulename_preprocess_$hook() for forums.
 *
 * Remove 'post' links from forum page, if the user does not have the
 * 'create' permission.
 */
function forum_access_preprocess_forums(&$variables) {
  if (!forum_access_access($variables['tid'], 'create') && !empty($variables['links'])) {
    foreach ($variables['links'] as $key => $link) {
      if (substr($link['href'], 0, 9) == 'node/add/') {
        unset($variables['links'][$key]);
      }
    }
  }
}

if (!variable_get('forum_access_D5_legacy_mode', FALSE))
{  // LEGACY-MODE disables these methods

/**
 * Implementation of hook_link_alter().
 *
 * Remove the 'Add new comment' link from nodes, if the user does not have the
 * 'create' permission.
 */
function forum_access_link_alter(&$links, $node)
{
  global $user;
  if ($user->uid != 1 && ($tid = _forum_access_get_tid($node)) && isset($links['comment_add']) && !forum_access_access($tid, 'create')) {
    unset($links['comment_add']);
  }
}

/**
 * Implementation of $modulename_preprocess_$hook() for box.
 *
 * Remove the in-line 'Post new comment' box, if it's empty
 * (after _forum_access_comment_form()).
 */
function forum_access_preprocess_box(&$variables) {
  $tr = 't';
  if (empty($variables['content']) && ($variables['title'] == $tr('Post new comment') || $variables['title'] == $tr('Reply'))) {
    $variables['title'] = '';
  }
}

/**
 * Implementation of $modulename_preprocess_$hook() for comment.
 *
 * Recreate comment links (they've already been themed), and
 * remove those that aren't accessible to the user.
 */
function forum_access_preprocess_comment(&$variables) {
  if (isset($variables['node']->tid)) {
    module_load_include('node.inc', 'forum_access');
    _forum_access_preprocess_comment($variables);
  }
}

}  // End of !LEGACY-MODE

/**
 * This is also required by ACL module.
 */
function forum_access_enabled($set = NULL) {
  static $enabled = true;
  if ($set !== NULL) {
    $enabled = $set;
  }
  return $enabled;
}

/**
 * Implementation of hook_enable().
 */
function forum_access_enable() {
  node_access_needs_rebuild();
  variable_del('forum_access_rids');  // clear cache
}

/**
 * Implementation of hook_disable().
 */
function forum_access_disable() {
  forum_access_enabled(FALSE);
  node_access_needs_rebuild();
}

/**
 * See if a given user has access to a forum.
 *
 * $tid -- the tid of the forum
 * $type -- view, update, delete or create
 * $account -- the account to test for. If NULL use current user.
 * $administer_nodes_sees_everything -- pass FALSE to ignore the 'administer nodes' permission
 * 
 * Return:
 *   FALSE - access not granted
 *   1     - access granted
 *   2     - access granted for forum moderator
 */
function forum_access_access($tid, $type, $account = NULL, $administer_nodes_sees_everything = TRUE) {
  static $cache = array();
  if (!$account) {
    global $user;
    $account = $user;
  }

  if ($account->uid == 1 ||
      $administer_nodes_sees_everything && user_access('administer nodes', $account) && array_search($type, array('view', 'update', 'delete')) !== FALSE) {
    return 1;
  }

  if (!isset($cache[$account->uid][$tid][$type])) {
    $roles = _forum_access_get_roles($account);
    $result = db_result(db_query("SELECT tid FROM {forum_access} WHERE rid IN (%s) AND grant_%s = 1 AND tid = %d", $roles, $type, $tid));

    if ($result) {
      $cache[$account->uid][$tid][$type] = 1;
    }
    else {
      // check our moderators too
      $acl_id = acl_get_id_by_name('forum_access', $tid);
      $result = db_result(db_query("SELECT uid FROM {acl_user} WHERE acl_id = %d AND uid = %d", $acl_id, $account->uid));
      if ($result) {
        $cache[$account->uid][$tid][$type] = 2;
      }
      else {
        $cache[$account->uid][$tid][$type] = FALSE;
      }
    }
  }
  return $cache[$account->uid][$tid][$type];
}

/**
 * Implementation of hook_user().
 */
function forum_access_user($op, &$edit, &$account, $category = NULL) {
  switch ($op) {
    case 'validate':
      $rid = forum_access_query_moderator_rid();
      if (!empty($rid)) {
        if (isset($edit['roles'][$rid]) && $edit['roles'][$rid]) {
          $roles = user_roles();
          $variables = array(
            '@Forum_Access' => 'Forum Access',
            '%Role' => $roles[$rid],
          );
          drupal_set_message(t('The %Role role is reserved for internal use by the @Forum_Access module! It was not assigned.', $variables), 'warning');
          unset($edit['roles'][$rid]);
        }
      }
      break;
  }
}

/**
 * Implementation of hook_menu_alter().
 *
 * Remove the 'Forum' menu item if no forums are visible.
 */
function forum_access_menu_alter(&$items) {
  if (!empty($items['forum'])) {
    //dpm($items['forum'], 'hook_menu_alter($items[\'forum\'])');
    if (!empty($items['forum']['access callback']) || $items['forum']['access arguments'][0] != 'access content') {
      drupal_set_message(t('Unexpected access specification for the %forum menu path; @Forum_Access cannot control its access.', array('%forum' => 'forum', '@Forum_Access' => 'Forum Access')), 'error');
      return;
    }
    $items['forum']['access callback'] = '_forum_access_access_a_forum';
    unset($items['forum']['access arguments']);
  }
}

/**
 * Access callback for the 'forum' menu item.
 *
 * Returns TRUE if the user has at least one role that can access
 * at least one forum.
 */
function _forum_access_access_a_forum() {
  global $user;
  static $return;

  if (!isset($return)) {
    if ($user->uid == 1) {
      return $return = TRUE;
    }
    if (!user_access('access content')) {
      return $return = FALSE;
    }
    $rids = variable_get('forum_access_rids', NULL);
    if (!isset($rids)) {
      $rids = array();
      $result = db_query("SELECT fa.rid FROM {forum_access} fa WHERE fa.grant_view > 0 GROUP BY fa.rid");
      while ($role = db_fetch_object($result)) {
        $rids[] = $role->rid;
      }
      variable_set('forum_access_rids', $rids);
    }
    foreach ($rids as $rid) {
      if (isset($user->roles[$rid])) {
        return $return = TRUE;
      }
    }
    $return = FALSE;
  }
  return $return;
}

/**
 * Implementation of hook_taxonomy().
 * 
 * Delete {forum_access} records when forums are deleted.
 */
function forum_access_taxonomy($op, $type, $array = NULL) {
  //dpm($array, "hook_taxonomy($op, $type)");
  if ($type = 'term' && $op == 'delete' && $array['vid'] == variable_get('forum_nav_vocabulary', '')) {
    db_query("DELETE FROM {forum_access} WHERE tid = %d", $array['tid']);
    variable_del('forum_access_rids');  // clear cache
  }
}

/**
 * Return the rid of the Forum Moderator role or NULL if the role does not
 * exist.
 */
function forum_access_query_moderator_rid()
{
  return variable_get('forum_access_moderator_rid', NULL);
}

/**
 * Get the roles of a user.
 */
function _forum_access_get_roles($user) {
  return implode(', ', array_keys($user->roles));
}

/**
 * Return the forum tid or FALSE.
 */
function _forum_access_get_tid($node) {
  return (isset($node->forum_tid) ? $node->forum_tid : (isset($node->tid) ? $node->tid : FALSE));
}

/**
 * Save and return the $tid.
 */
function _forum_access_changed_tid($tid = NULL) {
  static $saved_tid = NULL;
  if (!empty($tid)) {
    $saved_tid = $tid;
  }
  return $saved_tid;
}

/**
 * Implementation of hook_node_access_explain().
 */
function forum_access_node_access_explain($row) {
  static $roles = NULL;
  if ($row->realm == 'forum_access') {
    if (!isset($roles)) {
      module_load_include('node.inc', 'forum_access');
      $roles = _forum_access_get_all_roles();
    }
    if (isset($roles[$row->gid])) {
      return array($roles[$row->gid]);
    }
    return array('(unknown gid)');
  }
}
