You are here

module_grants.module in Module Grants 6

Module to enable access control for unpublished content. Also makes sure that modules that operate on access grants behave in the expected way when enabled together.

File

module_grants.module
View source
<?php

/**
 * @file
 *
 * Module to enable access control for unpublished content. Also makes
 * sure that modules that operate on access grants behave in the expected
 * way when enabled together.
 */

/**
 * Implementation of hook_perm().
 *
 * Revisioning permissions. Note that permissions to view, revert and delete
 * revisions already exist in node.module.
 */
function module_grants_perm() {
  return array(
    'access content summary',
  );
}

/**
 * Implementation of hook_menu().
 *
 * Define new menu items.
 * Existing menu items are modified through hook_menu_alter().
 */
function module_grants_menu() {
  $items = array();

  // Create a normal menu item in root Navigation menu
  $items['content'] = array(
    'title' => 'My content',
    'page callback' => 'module_grants_editable_nodes',
    'access arguments' => array(
      'access content summary',
    ),
  );

  // Add two tabs on page defined above
  $items['content/editable'] = array(
    'title' => 'Editable',
    'page callback' => 'module_grants_editable_nodes',
    'access arguments' => array(
      'access content summary',
    ),
    'type' => MENU_DEFAULT_LOCAL_TASK,
  );
  $items['content/viewable'] = array(
    'title' => 'Viewable',
    'page callback' => 'module_grants_viewable_nodes',
    'access arguments' => array(
      'access content summary',
    ),
    'type' => MENU_LOCAL_TASK,
  );
  return $items;
}

/**
 * Implementation of hook_menu_alter().
 *
 * Modify menu items defined in other modules (in particular the node module).
 */
function module_grants_menu_alter(&$items) {

  // As module_grants_node_access() fixes the problem of grants not being
  // checked when a node isn't published, all node access menu links are
  // altered to use this function.
  // For normal view/edit/delete operations module_grant_node_access() is
  // called directly, for the revision-specific operations the function is
  // called via mode_grants_node_revision_access().
  $items['node/%node']['access callback'] = 'module_grants_node_access';
  $items['node/%node']['access arguments'] = array(
    'view',
    1,
  );
  $items['node/%node/view']['access callback'] = 'module_grants_node_access';
  $items['node/%node/view']['access arguments'] = array(
    'view',
    1,
  );
  $items['node/%node/edit']['access callback'] = 'module_grants_node_access';
  $items['node/%node/delete']['access callback'] = 'module_grants_node_access';
  $items['node/%node/revisions']['access callback'] = 'module_grants_node_revision_access';
  $items['node/%node/revisions']['access arguments'] = array(
    'view revisions',
    1,
  );

  // Point /%node/revisions/%/view page to same callback as /%node/view for a
  // consistent view of current and other revisions
  $items['node/%node/revisions/%/view']['page callback'] = 'node_page_view';

  // as used by /%node/view
  $items['node/%node/revisions/%/view']['page arguments'] = array(
    1,
  );
  $items['node/%node/revisions/%/view']['access callback'] = 'module_grants_node_revision_access';
  $items['node/%node/revisions/%/view']['access arguments'] = array(
    'view revisions',
    1,
  );
  $items['node/%node/revisions/%/delete']['access callback'] = 'module_grants_node_revision_access';
  $items['node/%node/revisions/%/delete']['access arguments'] = array(
    'delete revisions',
    1,
  );
  $items['node/%node/revisions/%/revert']['access callback'] = 'module_grants_node_revision_access';
  $items['node/%node/revisions/%/revert']['access arguments'] = array(
    'revert revisions',
    1,
  );
}

/**
 * Menu options dealing with revisions have their revision-specific
 * permission checked before being tested for the associated node-specific
 * operation.
 * Return a boolean indicating whether the current user has the requested
 * permission AND access to the given node (regardless of its published
 * status).
 *
 * @param $node
 *   Node object for which access right is requested
 * @param $permission
 *   The requested permission, as a string eg 'delete revisions'
 * @return
 *   TRUE when the current user has access to the supplied node
 */
function module_grants_node_revision_access($revision_permission, $node) {

  // Map revision-permission to node access operation used in grants
  switch ($revision_permission) {
    case 'edit revisions':
      $op = 'update';
      break;
    case 'delete revisions':
      $op = 'delete';
      break;
    default:
      $op = 'view';
      break;
  }
  return user_access($revision_permission) && module_grants_node_access($op, $node);
}

/**
 * Similar to node_access() in node.module but ANDs rather than ORs grants
 * together on a per module base to create more natural behaviour.
 * Also makes sure that published and unpublished content are treated
 * in the same way, i.e. that grants are checked in either case.
 *
 * @param $op
 *   One of 'view', 'update' or 'delete'. 'create' isn't used.
 * @param $node
 *   The node for which the supplied operation is checked
 * @param $account
 *   user object, use NULL or omit for current user
 * @return
 *   FALSE if the supplied operation isn't permitted on the node
 */
function module_grants_node_access($op, $node, $account = NULL) {
  global $user;
  if (!$node) {
    return FALSE;
  }

  // If the node is in a restricted format, disallow editing.
  if ($op == 'update' && !filter_access($node->format)) {
    return FALSE;
  }

  // If no user object is supplied, the access check is for the current user.
  if (empty($account)) {
    $account = $user;
  }
  if (user_access('administer nodes', $account)) {
    return TRUE;
  }
  if (!user_access('access content', $account)) {
    return FALSE;
  }
  $module = node_get_types('module', $node);
  if ($module == 'node') {
    $module = 'node_content';
  }
  $access = module_invoke($module, 'access', $op, $node, $account);
  if (!is_null($access)) {

    //drupal_set_message("'$op' access=$access by $module: '$node->title'", 'warning');
    return $access;
  }

  // If the module neither allows nor denies access, then find grants
  // amongst modules that implement hook_node_grants()
  $all_grants = grants_by_module($op, $account);
  $base_sql = "SELECT COUNT(*) FROM {node_access} WHERE (nid=0 OR nid=%d) AND ((gid=0 AND realm='all')";
  if (count($all_grants) == 0) {

    // no module implements hook_node_grants()
    $sql = "{$base_sql}) AND grant_{$op} >=1";
    $result = db_result(db_query($sql, $node->nid));
  }
  foreach ($all_grants as $module => $module_grants) {
    $sql = "{$base_sql} OR ({$module_grants})) AND grant_{$op} >=1";

    // Effectively AND module_grants together by breaking loop as soon as one fails
    // A single SQL statement may be slightly quicker but won't tells us
    // which of the modules denied access. This is useful debug feedback.
    $result = db_result(db_query($sql, $node->nid));

    //drupal_set_message("'$op' access=$result by $module-grants: '$node->title'", 'warning');
    if ($result == 0) {
      break;
    }
  }
  return $result;
}

/**
 * Return a map keyed by module name of SQL clauses representing the
 * grants associated with the module, as returned by that module's
 * hook_node_grants().
 *
 * @param $op
 *   The operation, i.e 'view', 'update' or 'delete'
 * @param $account
 *   User account object
 * @return
 *   Array of module grants SQL
 */
function grants_by_module($op, $account) {
  $hook = 'node_grants';
  $all_grants = array();
  foreach (module_implements($hook) as $module) {
    $module_grants = array();
    foreach (module_invoke($module, $hook, $account, $op) as $realm => $gids) {
      foreach ($gids as $gid) {
        $module_grants[] = "(gid={$gid} AND realm='{$realm}')";
      }
    }

    // Within a module OR the gid/realm combinations together
    $module_sql = implode(' OR ', $module_grants);
    $all_grants[$module] = $module_sql;
  }
  return $all_grants;
}

/**
 *
 * Menu callback to list all content viewable to the logged-in user.
 */
function module_grants_viewable_nodes() {
  return theme('module_grants_viewable_nodes');
}

/**
 * Menu callback to list all content editable to the logged-in user
 */
function module_grants_editable_nodes() {
  return theme('module_grants_editable_nodes');
}

/**
 * Implementation of hook_theme().
 */
function module_grants_theme() {
  return array(
    'module_grants_viewable_nodes' => array(
      'arguments' => array(),
    ),
    'module_grants_editable_nodes' => array(
      'arguments' => array(),
    ),
  );
}

/**
 * Display in a table a summary of all content viewable to the logged-in user.
 *
 * @ingroup themeable
 */
function theme_module_grants_viewable_nodes() {
  $nodes = get_nodes('view');
  return _theme_nodes($nodes);
}

/**
 * Display in a table a summary of all content editable to the logged-in user.
 *
 * @ingroup themeable
 */
function theme_module_grants_editable_nodes() {
  $nodes = get_nodes('update');
  return _theme_nodes($nodes);
}

/**
 * Theme the passed-in nodes as a table.
 *
 * @param $nodes
 *   Array of nodes to display.
 * @return
 *   Themed table HTML or a paragraph saying 'No content found'.
 */
function _theme_nodes($nodes) {
  if (count($nodes) > 0) {
    $header = array(
      t('Title'),
      t('Type'),
      t('Last updated'),
      t('By'),
      t('Published?'),
    );
    $show_taxonomy_terms = module_exists('taxonomy');
    $show_workflow_state = module_exists('workflow');
    if ($show_taxonomy_terms) {
      $header[] = t('Term');
    }
    if ($show_workflow_state) {
      $header[] = t('Workflow state');
    }
    $rows = array();
    $page_link = user_access('view revisions') ? 'revisions' : 'view';
    foreach ($nodes as $node) {
      $row = array(
        l($node->title, "node/{$node->nid}/{$page_link}"),
        check_plain(node_get_types('name', $node)),
        format_date($node->timestamp),
        theme('username', user_load(array(
          'uid' => $node->uid,
        ))),
        $node->status ? t('Yes') : t('No'),
      );
      if ($show_taxonomy_terms) {
        $row[] = empty($node->term) ? '' : check_plain($node->term);
      }
      if ($show_workflow_state) {
        $row[] = empty($node->state) ? t('No state') : check_plain($node->state);
      }
      $rows[] = $row;
    }
    return theme('table', $header, $rows);
  }
  return '<p>' . t('No content found.') . '</p>';
}

/**
 * Retrieve a list of nodes or revisions accessible to the logged-in user via
 * the supplied operation.
 *
 * @param $op
 *   Operation, one of 'view', 'edit' or 'delete'
 * @param $pending
 *   Boolean indicating whether only pending or all nodes should be
 *   returned; a pending node is defined as a node that has a revision newer
 *   than the current.
 * @return
 *   An array of node objects each containing nid, content type, published flag,
 *   user id title+vid+user_id+timestamp of the current revision, plus taxonomy
 *   term(s) and workflow state, if these modules are installed and enabled.
 *
 * @todo
 *   Allow paging, improve performance
 */
function get_nodes($op, $pending = FALSE) {
  $sql_select = 'SELECT n.nid, n.type, n.status, r.title, r.uid, r.timestamp';
  $sql_from = ' FROM {node} n INNER JOIN {node_revisions} r ' . ($pending ? 'ON n.nid=r.nid' : 'ON n.vid=r.vid');
  $sql_where = $pending ? ' WHERE r.vid>n.vid OR (r.vid=n.vid AND n.status=0)' : '';
  $sql_order = ' ORDER BY r.timestamp DESC';
  $include_taxonomy_terms = module_exists('taxonomy');
  $include_workflow_state = module_exists('workflow');
  if ($include_taxonomy_terms) {
    $sql_select .= ', td.name AS term';
    $sql_from .= ' LEFT JOIN {term_node} tn ON n.vid=tn.vid LEFT JOIN {term_data} td ON tn.tid=td.tid';
  }
  if ($include_workflow_state) {
    $sql_select .= ', ws.state';
    $sql_from .= ' LEFT JOIN {workflow_node} wn ON wn.nid=n.nid LEFT JOIN {workflow_states} ws ON wn.sid=ws.sid';
  }
  $sql = $sql_select . $sql_from . $sql_where . $sql_order;
  $node_query_result = db_query_range($sql, 0, 1000);
  $nodes = array();
  while ($node = db_fetch_object($node_query_result)) {
    if (module_grants_node_access($op, $node)) {

      // @todo rework into a single query from hell?
      if (empty($nodes[$node->nid])) {
        $nodes[$node->nid] = $node;
      }
      elseif ($include_taxonomy_terms && !empty($node->term)) {

        // If a node has more than one taxonomy term, these will be returned by
        // the query as seperate objects differing only in their term.
        $existing_node = $nodes[$node->nid];
        $existing_node->term .= '/' . $node->term;
      }
    }
  }
  return $nodes;
}

Functions

Namesort descending Description
get_nodes Retrieve a list of nodes or revisions accessible to the logged-in user via the supplied operation.
grants_by_module Return a map keyed by module name of SQL clauses representing the grants associated with the module, as returned by that module's hook_node_grants().
module_grants_editable_nodes Menu callback to list all content editable to the logged-in user
module_grants_menu Implementation of hook_menu().
module_grants_menu_alter Implementation of hook_menu_alter().
module_grants_node_access Similar to node_access() in node.module but ANDs rather than ORs grants together on a per module base to create more natural behaviour. Also makes sure that published and unpublished content are treated in the same way, i.e. that grants are checked in…
module_grants_node_revision_access Menu options dealing with revisions have their revision-specific permission checked before being tested for the associated node-specific operation. Return a boolean indicating whether the current user has the requested permission AND access to the…
module_grants_perm Implementation of hook_perm().
module_grants_theme Implementation of hook_theme().
module_grants_viewable_nodes Menu callback to list all content viewable to the logged-in user.
theme_module_grants_editable_nodes Display in a table a summary of all content editable to the logged-in user.
theme_module_grants_viewable_nodes Display in a table a summary of all content viewable to the logged-in user.
_theme_nodes Theme the passed-in nodes as a table.