You are here

node_access_example.module in Examples for Developers 7

Same filename and directory in other branches
  1. 6 node_access_example/node_access_example.module

Module file illustrating API-based node access.

File

node_access_example/node_access_example.module
View source
<?php

/**
 * @file
 * Module file illustrating API-based node access.
 */

/**
 * @defgroup node_access_example Example: Node Access
 * @ingroup examples
 * @{
 * Demonstrates node access.
 *
 * This is an example demonstrating how to grant or deny access to nodes
 * using the Drupal core API node access system.
 *
 * This module will add a 'private' flag for each node, which the node's
 * author can manage. Nodes marked private can only be viewed, edited,
 * or deleted by the author. However, not everything is as private as it
 * seems on the internet, and so we need to implement some ways to allow
 * other users to manage this 'private' content.
 *
 * We will use the node grant system to specify which users
 * are allowed to view, edit, or delete 'private' content. We will also
 * allow a user named 'foobar' to have edit privileges on private content
 * as well.
 *
 * In addition, we will provide a page which will show some minimal
 * instructions, and statistics on private nodes on the site.
 *
 * We use NodeAPI hooks to put a single marker on a node, called
 * 'private'. The marker is implemented by a database table which has one
 * row per node simply indicating that the node is private. If the "private"
 * marker is set, users other than the owner and privileged users are denied
 * access.
 *
 * Standard permissions are defined which allow users with
 * 'access any private content' or 'edit any private content' to override
 * the 'private' node access restrictions.
 *
 * A separate access realm grants privileges to each node's author, so that
 * they can always view, edit, and delete their own private nodes.
 *
 * The only page provided by this module gives a rundown of how many nodes
 * are marked private, and how many of those are accessible to the current
 * user. This demonstrates the use of the 'node_access' tag in node queries,
 * preventing disclosure of information which should not be shown to users
 * who don't have the proper permissions.
 *
 * Most relevant functions:
 * - node_access_example_permission()
 * - node_access_example_node_access()
 * - node_access_example_node_access_records()
 * - node_access_example_node_grants()
 *
 * Drupal's node access system has three layers.
 * - Overall override permissions. User 1 and any user with 'bypass node
 *   access' permission are automatically granted access.
 * - hook_node_access() gives each module the opportunity to approve or deny
 *   access. Any module that returns NODE_ACCESS_DENY from hook_node_access()
 *   will result in denial of access. If no module denies access and one or
 *   more modules allow access, then access is granted. hook_node_access() was
 *   introduced in Drupal 7.
 * - If no resolution has yet been reached, then the node_access table is used
 *   along with hook_node_grants(). (Drupal updates the node_access table when
 *   nodes are saved, by calling hook_node_access_records().)
 *
 * Note that the hook_node_grants()/hook_node_access_records() layer is a
 * first-grant-wins system, which means a module using it can't deny access to
 * a node. Contributed modules have been developed to overcome this
 * shortcoming, with their own APIs, such as
 * @link http://drupal.org/project/acl ACL. @endlink ACL, in fact, has emerged
 * as the more-or-less standard solution for fine-grained access control, and
 * you really should be reading up on it. Many modules use it, and if your
 * module implements another node access system, there could be chaos.
 *
 * A list of the many node access modules (and differing APIs) is here:
 * @link http://drupal.org/node/270000 Overview of Node Access Modules. @endlink
 * Note that this is Drupal 6 documentation, linked here so the reader can
 * understand the historical reasons for using contributed node access modules
 * such as ACL.
 *
 * @see node_access()
 * @see hook_node_access()
 * @see hook_node_grants()
 * @see hook_node_access_records()
 */

/**
 * Implements hook_menu().
 *
 * This path provides a page, with some instructions for the user, and some
 * statistics about node access changes implemented by this module.
 *
 * @see hook_menu()
 */
function node_access_example_menu() {
  $items['examples/node_access'] = array(
    'title' => 'Node Access Example',
    'page callback' => 'node_access_example_private_node_listing',
    'access callback' => TRUE,
  );
  return $items;
}

/**
 * Our hook_menu() page callback function.
 *
 * Information for the user about what nodes are marked private on the system
 * and which of those the user has access to.
 *
 * The queries showing what is accessible to the current user demonstrate the
 * use of the 'node_access' tag to make sure that we don't show inappropriate
 * information to unprivileged users.
 *
 * @return string
 *   Page content.
 *
 * @see page_example
 */
function node_access_example_private_node_listing() {
  $content = '<div>' . t('This example shows how a module can use the Drupal node access system to allow access to specific nodes. You will need to look at the code and then experiment with it by creating nodes, marking them private, and accessing them as various users.') . '</div>';

  // Find out how many nodes are marked private.
  $query = db_select('node', 'n');
  $query
    ->addExpression('COUNT(n.nid)', 'private_count');
  $query
    ->join('node_access_example', 'nae', 'nae.nid = n.nid');
  $num_private = $query
    ->condition('nae.private', 1)
    ->execute()
    ->fetchField();

  // Find out how many nodes owned by this user are marked private.
  $query = db_select('node', 'n');
  $query
    ->addExpression('COUNT(n.nid)', 'private_count');
  $query
    ->join('node_access_example', 'nae', 'nae.nid = n.nid');
  $num_personal = $query
    ->condition('n.uid', $GLOBALS['user']->uid)
    ->condition('nae.private', 1)
    ->execute()
    ->fetchfield();
  $content .= '<div>' . t('There are currently @num private nodes in the system @num_personal are yours.', array(
    '@num' => $num_private,
    '@num_personal' => $num_personal,
  )) . '</div>';

  // Use a 'node_access' tag with a query to find out how many this user has
  // access to. This will be the standard way to make lists while respecting
  // node access restrictions.
  $query = db_select('node', 'n');
  $query
    ->addExpression('COUNT(n.nid)', 'private_count');
  $query
    ->addTag('node_access');
  $query
    ->join('node_access_example', 'nae', 'nae.nid = n.nid');
  $num_private_accessible = $query
    ->condition('nae.private', 1)
    ->execute()
    ->fetchField();
  $content .= '<div>' . t('You have access to @num private nodes.', array(
    '@num' => $num_private_accessible,
  )) . '</div>';

  // Use the key 'node_access' tag to get the key data from the nodes this
  // has access to.
  $query = db_select('node', 'n', array(
    'fetch' => PDO::FETCH_ASSOC,
  ));
  $query
    ->addTag('node_access');
  $query
    ->join('node_access_example', 'nae', 'nae.nid = n.nid');
  $query
    ->join('users', 'u', 'u.uid = n.uid');
  $result = $query
    ->fields('n', array(
    'nid',
    'title',
    'uid',
  ))
    ->fields('u', array(
    'name',
  ))
    ->condition('nae.private', 1)
    ->execute();
  $rows = array();
  foreach ($result as $node) {
    $node['nid'] = l($node['nid'], 'node/' . $node['nid']);
    $rows[] = array(
      'data' => $node,
      'class' => array(
        'accessible',
      ),
    );
  }
  $content .= '<div>' . t('Accessible rows:') . theme('table', array(
    'header' => array(
      'nid',
      'title',
      'uid',
      'username',
    ),
    'rows' => $rows,
  )) . '</div>';
  return array(
    '#markup' => $content,
  );
}

/**
 * Implements hook_permission().
 *
 * We create two permissions, which we can use as a base for our grant/deny
 * decision:
 *
 * - 'access any private content' allows global access to content marked
 *   private by other users.
 * - 'edit any private content' allows global edit
 *   privileges, basically overriding the node access system.
 *
 * Note that the 'edit any * content' and 'delete any * content' permissions
 * will allow edit or delete permissions to the holder, regardless of what
 * this module does.
 *
 * @see hook_permissions()
 */
function node_access_example_permission() {
  return array(
    'access any private content' => array(
      'title' => t('Access any private content'),
      'description' => t('May view posts of other users even though they are marked private.'),
    ),
    'edit any private content' => array(
      'title' => t('Edit any private content'),
      'description' => t('May edit posts of other users even though they are marked private.'),
    ),
  );
}

/**
 * Implements hook_node_access().
 *
 * Allows view and edit access to private nodes, when the account requesting
 * access has the username 'foobar'.
 *
 * hook_node_access() was introduced in Drupal 7. We use it here to demonstrate
 * allowing certain privileges to an arbitrary user.
 *
 * @see hook_node_access()
 */
function node_access_example_node_access($node, $op, $account) {

  // If $node is a string, the node has not yet been created. We don't care
  // about that case.
  if (is_string($node)) {
    return NODE_ACCESS_IGNORE;
  }
  if (($op == 'view' || $op == 'update') && (!empty($account->name) && $account->name == 'foobar') && !empty($node->private)) {
    drupal_set_message(t('Access to node @nid allowed because requester name (@name) is specifically allowed', array(
      '@name' => $node->name,
      '@uid' => $account->uid,
    )));
    return NODE_ACCESS_ALLOW;
  }
  return NODE_ACCESS_IGNORE;
}

/**
 * Here we define a constant for our node access grant ID, for the
 * node_access_example_view and node_access_example_edit realms. This ID could
 * be any integer, but here we choose 23, because it is this author's favorite
 * number.
 */
define('NODE_ACCESS_EXAMPLE_GRANT_ALL', 23);

/**
 * Implements hook_node_grants().
 *
 * Tell the node access system what grant IDs the user belongs to for each
 * realm, based on the operation being performed.
 *
 * When the user tries to perform an operation on the node, Drupal calls
 * hook_node_grants() to determine grant ID and realm for the user. Drupal
 * looks up the grant ID and realm for the node, and compares them to the
 * grant ID and realm provided here. If grant ID and realm match for both
 * user and node, then the operation is allowed.
 *
 * Grant ID and realm are both determined per node, by your module in
 * hook_node_access_records().
 *
 * In our example, we've created three access realms: One for authorship, and
 * two that track with the permission system.
 *
 * We always add node_access_example_author to the list of grants, with a grant
 * ID equal to their user ID. We do this because in our model, authorship
 * always gives you permission to edit or delete your nodes, even if they're
 * marked private.
 *
 * Then we compare the user's permissions to the operation to determine whether
 * the user falls into the other two realms: node_access_example_view, and/or
 * node_access_example_edit. If the user has the 'access any private content'
 * permission we defined in hook_permission(), they're declared as belonging to
 * the node_access_example_realm. Similarly, if they have the 'edit any private
 * content' permission, we add the node_access_example_edit realm to the list
 * of grants they have.
 *
 * @see node_access_example_permission()
 * @see node_access_example_node_access_records()
 */
function node_access_example_node_grants($account, $op) {
  $grants = array();

  // First grant a grant to the author for own content.
  // Do not grant to anonymous user else all anonymous users would be author.
  if ($account->uid) {
    $grants['node_access_example_author'] = array(
      $account->uid,
    );
  }

  // Then, if "access any private content" is allowed to the account,
  // grant view, update, or delete as necessary.
  if ($op == 'view' && user_access('access any private content', $account)) {
    $grants['node_access_example_view'] = array(
      NODE_ACCESS_EXAMPLE_GRANT_ALL,
    );
  }
  if (($op == 'update' || $op == 'delete') && user_access('edit any private content', $account)) {
    $grants['node_access_example_edit'] = array(
      NODE_ACCESS_EXAMPLE_GRANT_ALL,
    );
  }
  return $grants;
}

/**
 * Implements hook_node_access_records().
 *
 * All node access modules must implement this hook. If the module is
 * interested in the privacy of the node passed in, return a list
 * of node access values for each grant ID we offer.
 *
 * In this example, for each node which is marked 'private,' we define
 * three realms:
 *
 * The first and second are realms are 'node_access_example_view' and
 * 'node_access_example_edit,' which have a single grant ID, 1. The
 * user is either a member of these realms or not, depending upon the
 * operation and the access permission set.
 *
 * The third is node_access_example_author. It gives the node
 * author special privileges. node_access_example_author has one grant ID for
 * every UID, and each user is automatically a member of the group where
 * GID == UID. This has the effect of giving each user their own grant ID
 * for nodes they authored, within this realm.
 *
 * Drupal calls this hook when a node is saved, or when access permissions
 * change in order to rebuild the node access database table(s).
 *
 * The array you return will define the realm and the grant ID for the
 * given node. This is stored in the {node_access} table for subsequent
 * comparison against the user's realm and grant IDs, which you'll
 * supply in hook_node_grants().
 *
 * Realm names and grant IDs are arbitrary. Official drupal naming
 * conventions do not cover access realms, but since all realms are
 * stored in the same database table, it's probably a good idea to
 * use descriptive names which follow the module name, such as
 * 'mymodule_realmname'.
 *
 * @see node_access_example_node_grants()
 */
function node_access_example_node_access_records($node) {

  // We only care about the node if it's been marked private. If not, it is
  // treated just like any other node and we completely ignore it.
  if (!empty($node->private)) {
    $grants = array();
    $grants[] = array(
      'realm' => 'node_access_example_view',
      'gid' => NODE_ACCESS_EXAMPLE_GRANT_ALL,
      'grant_view' => 1,
      'grant_update' => 0,
      'grant_delete' => 0,
      'priority' => 0,
    );
    $grants[] = array(
      'realm' => 'node_access_example_edit',
      'gid' => NODE_ACCESS_EXAMPLE_GRANT_ALL,
      'grant_view' => 1,
      'grant_update' => 1,
      'grant_delete' => 1,
      'priority' => 0,
    );

    // For the node_access_example_author realm, the grant ID (gid) is
    // equivalent to the node author's user ID (UID).
    // We check the node UID so that we don't grant author privileges for
    // anonymous nodes to anonymous users.
    if ($node->uid) {
      $grants[] = array(
        'realm' => 'node_access_example_author',
        'gid' => $node->uid,
        'grant_view' => 1,
        'grant_update' => 1,
        'grant_delete' => 1,
        'priority' => 0,
      );
    }
    return $grants;
  }

  // Return nothing if the node has not been marked private.
}

/**
 * Implements hook_form_alter().
 *
 * This module adds a simple checkbox to the node form labeled private. If the
 * checkbox is checked, only the node author and users with
 * 'access any private content' privileges may see it.
 */
function node_access_example_form_alter(&$form, $form_state) {
  if (!empty($form['#node_edit_form'])) {
    $form['node_access_example'] = array(
      '#type' => 'fieldset',
      '#title' => t('Node Access Example'),
      '#collapsible' => TRUE,
      '#collapsed' => FALSE,
      '#weight' => 8,
    );
    $form['node_access_example']['private'] = array(
      '#type' => 'checkbox',
      '#title' => t('Private'),
      '#description' => t('Check here if this content should be set private and only shown to privileged users.'),
      '#default_value' => isset($form['#node']->private) ? $form['#node']->private : FALSE,
    );
  }
}

/**
 * Implements hook_node_load().
 *
 * Gather and add the private setting for the nodes Drupal is loading.
 * @see nodeapi_example.module
 */
function node_access_example_node_load($nodes, $types) {
  $result = db_query('SELECT nid, private FROM {node_access_example} WHERE nid IN(:nids)', array(
    ':nids' => array_keys($nodes),
  ));
  foreach ($result as $record) {
    $nodes[$record->nid]->private = $record->private;
  }
}

/**
 * Implements hook_node_delete().
 *
 * Delete the node_access_example record when the node is deleted.
 * @see nodeapi_example.module
 */
function node_access_example_node_delete($node) {
  db_delete('node_access_example')
    ->condition('nid', $node->nid)
    ->execute();
}

/**
 * Implements hook_node_insert().
 *
 * Insert a new access record when a node is created.
 * @see nodeapi_example.module
 */
function node_access_example_node_insert($node) {
  if (isset($node->private)) {
    db_insert('node_access_example')
      ->fields(array(
      'nid' => $node->nid,
      'private' => (int) $node->private,
    ))
      ->execute();
  }
  drupal_set_message(t('New node @nid was created and private=@private', array(
    '@nid' => $node->nid,
    '@private' => !empty($node->private) ? 1 : 0,
  )));
}

/**
 * Implements hook_node_update().
 *
 * If the record in the node_access_example table already exists, we must
 * update it. If it doesn't exist, we create it.
 * @see nodeapi_example.module
 */
function node_access_example_node_update($node) {

  // Find out if there is already a node_access_example record.
  $exists = db_query('SELECT nid FROM {node_access_example} WHERE nid = :nid', array(
    ':nid' => $node->nid,
  ))
    ->fetchField();

  // If there is already a record, update it with the new private value.
  if ($exists) {
    $num_updated = db_update('node_access_example')
      ->fields(array(
      'nid' => $node->nid,
      'private' => !empty($node->private) ? 1 : 0,
    ))
      ->condition('nid', $node->nid)
      ->execute();
    drupal_set_message(t("Updated node @nid to set private=@private (@num nodes actually updated)", array(
      '@private' => $node->private,
      '@num' => $num_updated,
      '@nid' => $node->nid,
    )));
  }
  else {
    node_access_example_node_insert($node);
    drupal_set_message(t('Inserted new node_access nid=@nid, private=@private', array(
      '@nid' => $node->nid,
      '@private' => $node->private,
    )));
  }
}

/**
 * @} End of "defgroup node_access_example".
 */

Functions

Constants

Namesort descending Description
NODE_ACCESS_EXAMPLE_GRANT_ALL Here we define a constant for our node access grant ID, for the node_access_example_view and node_access_example_edit realms. This ID could be any integer, but here we choose 23, because it is this author's favorite number.