You are here

protected_node.module in Protected Node 7

Protected Node module.

File

protected_node.module
View source
<?php

/**
 * @file
 * Protected Node module.
 */

/**
 * Per node password.
 *
 * The password is required on all nodes unless the node type defines
 * a default password.
 *
 * Defined in: protected_node_use_global_password.
 */
define('PROTECTED_NODE_PER_NODE_PASSWORD', 0);

/**
 * Per node or global password.
 *
 * The password is not required. The system uses the global password
 * if the node does not define a password.
 *
 * Defined in: protected_node_use_global_password.
 */
define('PROTECTED_NODE_PER_NODE_AND_GLOBAL_PASSWORD', 1);

/**
 * Global password only.
 *
 * Use the global password only. Ignore the node specific password
 * and don't ask for one when editing the node.
 *
 * Defined in: protected_node_use_global_password.
 */
define('PROTECTED_NODE_GLOBAL_PASSWORD', 2);

/**
 * Never protect these types of nodes.
 *
 * Defined in: protected_node_protection_<node type name>.
 */
define('PROTECTED_NODE_PROTECTION_NEVER', 0);

/**
 * The author can choose whether the node is protected or not.
 *
 * By default, the node is not protected.
 *
 * Defined in: protected_node_protection_<node type name>.
 */
define('PROTECTED_NODE_PROTECTION_PROTECTABLE', 1);

/**
 * The author can choose whether the node is protected or not.
 *
 * By default, the node is protected.
 *
 * Defined in: protected_node_protection_<node type name>.
 */
define('PROTECTED_NODE_PROTECTION_PROTECTED', 2);

/**
 * The nodes of this type will always be protected.
 *
 * Defined in: protected_node_protection_<node type name>.
 */
define('PROTECTED_NODE_PROTECTION_ALWAYS', 3);

/**
 * Implements hook_help().
 */
function protected_node_help($path, $arg) {
  switch ($path) {
    case 'admin/modules#description':
      return t('With this module anybody who has edit protected node permission can password protect his or her own node.');
  }
}

/**
 * Implements hook_permission().
 */
function protected_node_permission() {
  $permissions = array(
    'access protected node overview page' => array(
      'title' => t('Access protected node overview page'),
    ),
    'access protected node password form' => array(
      'title' => t('Access protected node password form'),
      'description' => t('Access protected node password form page. Without this permission user will be denied access completely.'),
    ),
    'edit any protected node password' => array(
      'title' => t('Edit any protected node password'),
      'description' => t('Edit the password of any protected node. Grants access to the password fieldset in the node form.'),
    ),
    'view protected content' => array(
      'title' => t('View protected content (bypass password)'),
      'description' => t('Allow to view any protected node by bypassing the password protection.'),
    ),
    'edit protected content' => array(
      'title' => t('Edit protected content (bypass password)'),
      'description' => t('Allow to edit any protected node by bypassing the password protection. Do not allow to edit the password. Note: the user will also be able to view the content without entering the password.'),
    ),
  );
  foreach (node_type_get_types() as $key => $type) {
    $permissions['edit ' . $key . ' password'] = array(
      'title' => t('Edit %type_name password', array(
        '%type_name' => $key,
      )),
      'description' => t('Edit password for %type_name nodes', array(
        '%type_name' => $key,
      )),
    );
  }
  return $permissions;
}

/**
 * Implements hook_menu().
 */
function protected_node_menu() {
  module_load_include('settings.inc', 'protected_node');
  return protected_node_menu_array();
}

/**
 * Callback function to determine who can enter a password.
 */
function protected_node_access_callback() {
  global $user;

  // Super user?
  if ($user->uid == 1) {
    return TRUE;
  }
  if (!user_access('access protected node password form')) {
    return FALSE;
  }

  // Is $nid properly defined?
  if (empty($_GET['protected_page']) || !is_numeric($_GET['protected_page'])) {
    return FALSE;
  }

  // Valid node?
  $node = node_load($_GET['protected_page']);
  if (!$node) {
    return FALSE;
  }

  // Editing/deleting? user has edit right?
  if (substr($_GET['destination'], 0, 5) == 'node/') {
    if (substr($_GET['destination'], -5) == '/edit') {
      if (!node_access('update', $node)) {
        return FALSE;
      }
    }
    elseif (substr($_GET['destination'], -7) == '/delete') {
      if (!node_access('delete', $node)) {
        return FALSE;
      }
    }
  }
  return TRUE;
}

/**
 * Implements hook_init().
 */
function protected_node_init() {

  // Let Drush bypass password protection.
  if (function_exists('drush_main')) {
    return;
  }

  // Are we about to display a node?
  // Can user see all nodes anyway?
  if (user_access('edit protected content')) {
    return;
  }
  if (variable_get('protected_node_use_global_password', PROTECTED_NODE_PER_NODE_PASSWORD) == PROTECTED_NODE_GLOBAL_PASSWORD && isset($_SESSION['has_entered_global_password'])) {
    return;
  }
  $nid = FALSE;
  $param2 = arg(2);
  if (arg(0) == 'node' && is_numeric(arg(1))) {
    if ($param2 === NULL) {
      $nid = protected_node_is_locked(arg(1), 'view');
      if ($nid === -1) {
        return;
      }
    }
    elseif ($param2 == 'edit' || $param2 == 'delete') {
      $nid = protected_node_is_locked(arg(1), $param2);
    }
    else {

      // Any other sub-path.
      $nid = protected_node_is_locked(arg(1), 'view');
    }
    if ($nid === TRUE || $nid === -1) {
      drupal_access_denied();
      exit;
    }
  }
  elseif (arg(0) == 'system' && arg(1) == 'files') {
    $requested_url = drupal_parse_url(request_uri());
    $path = urldecode(str_replace('system/files', '', $requested_url['path']));
    if (!empty($path)) {
      $nid = protected_node_and_attachment($path);
    }
  }
  if ($nid) {
    $query = drupal_get_destination();
    if (!empty($_SERVER['HTTP_REFERER'])) {
      $query['back'] = urlencode($_SERVER['HTTP_REFERER']);
    }
    $query['protected_page'] = $nid;
    drupal_goto('protected-node', array(
      'query' => $query,
    ));
  }
}

/**
 * Check whether a node is protected and a password is required.
 *
 * @param int $nid
 *   The node identifier.
 * @param string $op
 *   Operation: 'access', 'view', 'edit', or 'delete'.
 *
 * @return false
 *   if the node is not protected for the current user.
 *   Return TRUE if it is protected and cannot be viewed by the current user.
 *   Return $nid if the user has a chance to unlock this protected node by
 *   entering the password.
 *   Return -1 if the user is trying to view the node and has both access to
 *   view nodes of that type and the 'view protected content' permission.
 */
function protected_node_is_locked($nid, $op = 'access') {
  global $user;

  // Get the node.
  $node = node_load($nid);

  // Is the node protected?
  if (!isset($node->protected_node_is_protected) || !$node->protected_node_is_protected) {
    return FALSE;
  }

  // Anonymous user?
  if (!$user->uid) {

    // Do not cache anything for anonymous users as that could make
    // the content of the page available to people who never enter
    // the password (especially with aggressive caching.).
    if (variable_get('cache', 1)) {

      // Prevent caching (do NOT use variable_set() since this is temporary
      // for this session.).
      $GLOBALS['conf']['cache'] = 0;
    }
  }
  else {

    // Author looking at his work (if not anonymous)?
    if ($node->uid === $user->uid) {
      return FALSE;
    }
  }

  // User cannot access any protected node. This check avoids the rather
  // useless drupal_goto() and thus does not change the URL on the user.
  if (!user_access('access protected node password form')) {
    return TRUE;
  }

  // If the user is only trying to view this node, accept.
  if ($op == 'view') {
    if (user_access('view protected content') && node_access('view', $node)) {

      // User's got view permission without password
      // (password for edit/delete rights).
      return -1;
    }
  }
  elseif ($op == 'edit') {
    if (!node_access('update', $node)) {

      // No rights to edit.
      return TRUE;
    }
    elseif (user_access('edit protected content') && node_access('update', $node)) {

      // User's got edit permission without password
      // (password for edit/delete rights).
      return -1;
    }

    // Rights to edit, but password is still required in this case!
  }
  elseif ($op == 'delete') {
    if (!node_access('delete', $node)) {

      // No rights to delete.
      return TRUE;
    }

    // Rights to delete, but password is still required in this case!
  }
  else {
    return TRUE;
  }

  // User already entered the global password?
  if (isset($_SESSION['_protected_node']['passwords']['global'])) {
    $when = $_SESSION['_protected_node']['passwords']['global'];
    if ($when > variable_get('protected_node_session_timelimit', 0) && $when > $node->protected_node_passwd_changed) {
      return FALSE;
    }

    // The session is out of date, we can as well get rid of it now.
    unset($_SESSION['_protected_node']['passwords']['global']);
  }
  else {

    // User already entered the password?
    if (isset($_SESSION['_protected_node']['passwords'][$nid])) {
      $when = $_SESSION['_protected_node']['passwords'][$nid];
      if ($when > variable_get('protected_node_session_timelimit', 0) && $when > $node->protected_node_passwd_changed) {
        return FALSE;
      }

      // The session is out of date, we can as well get rid of it now.
      unset($_SESSION['_protected_node']['passwords'][$nid]);
    }
  }
  return $nid;
}

/**
 * Helper function.
 *
 * If gathering an attachment, verify that it is accessible and if
 * not ask for the password.
 *
 * @param string $path
 *   The path to the attachment file.
 *
 * @return mixed
 *   File nid if user has access. FALSE otherwise.
 */
function protected_node_and_attachment($path) {
  global $user;
  if (user_access('edit protected content')) {
    return FALSE;
  }

  // Check whether the node linked to this file attachment is protected.
  $query = db_select('node', 'n');
  $query
    ->join('file_usage', 'fu', 'n.nid = fu.id');
  $query
    ->join('file_managed', 'fm', 'fm.fid = fu.fid');
  $query
    ->join('protected_nodes', 'pn', 'n.nid = pn.nid');
  $query
    ->fields('n', array(
    'nid',
    'uid',
  ));
  $query
    ->fields('pn', array(
    'protected_node_passwd_changed',
  ));
  $query
    ->condition('fu.type', 'node');
  $query
    ->condition('fm.uri', '%' . db_like($path), 'LIKE');
  $query
    ->condition('pn.protected_node_is_protected', '1');
  $number_of_results = $query
    ->countQuery()
    ->execute()
    ->fetchField();

  // If number is 0, node is not protected, or file is in a field collection.
  if (0 == $number_of_results) {
    if (module_exists('field_collection')) {

      // Check if file is attached to protected node via field collection.
      $query = db_select('file_usage', 'fu');
      $query
        ->join('file_managed', 'fm', 'fu.fid = fm.fid');
      $query
        ->fields('fu', array(
        'id',
      ));
      $query
        ->condition('fu.type', 'field_collection_item');
      $query
        ->condition('fm.uri', '%' . db_like($path), 'LIKE');
      $in_field_collection = $query
        ->countQuery()
        ->execute()
        ->fetchField();

      // The file is attached to a field collection item.
      if ($in_field_collection != '0') {
        $field_collection_ids = $query
          ->execute()
          ->fetchCol();
        $field_collection_items = entity_load('field_collection_item', $field_collection_ids);

        // Get the nids.
        $protected_node_nids = array();
        foreach ($field_collection_items as $field_collection_item) {
          $protected_node_nids[] = $field_collection_item
            ->hostEntity()->nid;
        }

        // Query the node table again with the nid the field collection belongs
        // to.
        $query = db_select('node', 'n');
        $query
          ->join('protected_nodes', 'pn', 'n.nid = pn.nid');
        $query
          ->fields('n', array(
          'nid',
          'uid',
        ));
        $query
          ->fields('pn', array(
          'protected_node_passwd_changed',
        ));
        $query
          ->condition('n.nid', $protected_node_nids, 'IN');
        $query
          ->condition('pn.protected_node_is_protected', '1');
        $number_of_results = $query
          ->countQuery()
          ->execute()
          ->fetchField();
        if (0 == $number_of_results) {
          return FALSE;
        }
      }
      else {
        return FALSE;
      }
    }
    elseif (module_exists('paragraphs')) {

      // Check if file is attached to protected node via paragraphs.
      $query = db_select('file_usage', 'fu');
      $query
        ->join('file_managed', 'fm', 'fu.fid = fm.fid');
      $query
        ->fields('fu', array(
        'id',
      ));
      $query
        ->condition('fu.type', 'paragraphs_item');
      $query
        ->condition('fm.uri', '%' . db_like($path), 'LIKE');
      $in_paragraphs = $query
        ->countQuery()
        ->execute()
        ->fetchField();

      // The file is attached to a paragraphs item.
      if ($in_paragraphs != '0') {
        $paragraphs_ids = $query
          ->execute()
          ->fetchCol();

        /** @var \ParagraphsItemEntity[] $paragraphs_items */
        $paragraphs_items = entity_load('paragraphs_item', $paragraphs_ids);

        // Get the nids.
        $protected_node_nids = array();
        foreach ($paragraphs_items as $paragraphs_item) {
          $nid = _protected_node_get_paragraph_node_host_entity_id($paragraphs_item);
          if ($nid) {
            $protected_node_nids[] = $nid;
          }
        }

        // Query the node table again with the nid the paragraph belongs
        // to.
        if (!empty($protected_node_nids)) {
          $query = db_select('node', 'n');
          $query
            ->join('protected_nodes', 'pn', 'n.nid = pn.nid');
          $query
            ->fields('n', array(
            'nid',
            'uid',
          ));
          $query
            ->fields('pn', array(
            'protected_node_passwd_changed',
          ));
          $query
            ->condition('n.nid', $protected_node_nids, 'IN');
          $query
            ->condition('pn.protected_node_is_protected', '1');
          $number_of_results = $query
            ->countQuery()
            ->execute()
            ->fetchField();
          if (0 == $number_of_results) {
            return FALSE;
          }
        }
        else {
          return FALSE;
        }
      }
      else {
        return FALSE;
      }
    }
    else {

      // If not in node, nor in field_collection or paragraphs, return FALSE
      return FALSE;

      /* Row doesn't exist, it's not protected */
    }
  }
  $result = $query
    ->execute();
  foreach ($result as $file_info) {

    // Row doesn't exist, it's not protected || $user is the author.
    if ($file_info === FALSE || $user->uid && $user->uid == $file_info->uid) {
      return FALSE;
    }

    // The user has the bypass password for view.
    if (user_access('view protected content', $user)) {
      return FALSE;
    }

    // Got the global password?
    if (isset($_SESSION['_protected_node']['passwords']['global'])) {
      $when = $_SESSION['_protected_node']['passwords']['global'];

      // This page reset time && global reset time.
      if ($when > $file_info->protected_node_passwd_changed && $when > variable_get('protected_node_session_timelimit', 0)) {
        return FALSE;
      }

      // The session is out of date, we can as well get rid of it now.
      unset($_SESSION['_protected_node']['passwords']['global']);
    }
    else {

      // Got the password?
      if (isset($_SESSION['_protected_node']['passwords'][$file_info->nid])) {
        $when = $_SESSION['_protected_node']['passwords'][$file_info->nid];

        // This page reset time && global reset time.
        if ($when > $file_info->protected_node_passwd_changed && $when > variable_get('protected_node_session_timelimit', 0)) {
          return FALSE;
        }

        // The session is out of date, we can as well get rid of it now.
        unset($_SESSION['_protected_node']['passwords'][$file_info->nid]);
      }
    }

    // No password, access denied.
    return $file_info->nid;
  }
}

/**
 * Module invoke.
 *
 * Call module implemented functions with a parameter passed as reference
 * instead of copy.
 *
 * For calls that require multiple parameters, use an array or object.
 *
 * @param[in] $hook
 *   The name of the hook to call.
 *
 * @param[in,out] $param
 *   The one parameter to pass to the hook functions.
 */
function protected_node_invoke($hook, &$param) {
  foreach (module_implements($hook) as $module) {
    call_user_func_array($module . '_' . $hook, array(
      &$param,
    ));
  }
}

/**
 * Implements hook_form_FORM_ID_alter().
 *
 * Add the protected node form fieldset if the user editing has permission to
 * edit this node type password.
 */
function protected_node_form_node_type_form_alter(&$form, &$form_state, $form_id) {
  if (user_access('edit any protected node password') || user_access('edit ' . $form['type']['#value'] . ' password')) {
    form_load_include($form_state, 'settings.inc', 'protected_node');
    protected_node_node_type_form_alter($form);
  }
}

/**
 * Implements hook_form_FORM_ID_alter().
 *
 * Add the protected node form fieldset if the user editing has permission to
 * edit this node type password.
 */
function protected_node_form_node_form_alter(&$form, &$form_state, $form_id) {
  if (user_access('edit any protected node password') || user_access('edit ' . $form['type']['#value'] . ' password')) {
    form_load_include($form_state, 'settings.inc', 'protected_node');
    protected_node_node_form_alter($form);
  }
}

/**
 * Implements hook_node_load().
 */
function protected_node_node_load($nodes, $types) {
  return protected_node_load($nodes);
}

/**
 * Implements hook_node_validate().
 */
function protected_node_node_validate($node, $form, &$form_state) {

  // The node type is never protected if $node->protected_node_is_protected is
  // not set.
  if (!isset($node->protected_node_is_protected)) {
    return;
  }
  global $_protected_node_emails;
  global $_protected_node_random_passwd;
  $_protected_node_emails = '';
  $_protected_node_random_passwd = '';
  if ($node->protected_node_is_protected && (user_access('edit any protected node password') || user_access('edit ' . $node->type . ' password'))) {
    $missing_password = FALSE;
    if (empty($node->protected_node_passwd)) {

      // Global content type password exists ?
      $global_content_type_password = variable_get('protected_node_node_type_password_' . $node->type, '');
      if ($global_content_type_password != '') {

        // it's ok.
      }
      else {
        $result = db_select('protected_nodes')
          ->fields('protected_nodes', array(
          'protected_node_passwd',
        ))
          ->condition('nid', $node->nid)
          ->execute()
          ->fetchField();

        // Getting "    " (40 spaces) when empty.
        $result = trim($result);
        if (empty($result)) {
          $missing_password = TRUE;
        }
      }
    }
    if (!empty($node->protected_node_emails)) {
      if ($node->status) {

        // Verify each email address.
        $emails = explode(',', str_replace(array(
          "\r",
          "\n",
        ), ',', $node->protected_node_emails));
        foreach ($emails as $k => $m) {
          $m = trim($m);
          if ($m) {
            if (!valid_email_address($m)) {
              form_error($form['protected_node']['protected_node_emails'], t('Invalid email address: @m. Please correct this mistake and try again.', array(
                '@m' => $m,
              )));

              // Unset just in case; should be useless though.
              unset($emails[$k]);
            }
            else {
              $emails[$k] = $m;
            }
          }
          else {

            // Ignore empty entries.
            unset($emails[$k]);
          }
        }
        $_protected_node_emails = implode(', ', $emails);
        if ($_protected_node_emails && $missing_password && variable_get('protected_node_random_password', FALSE)) {

          // Automatically generate a password for the email. Note that means
          // the author won't know the password!
          $_protected_node_random_passwd = user_password();

          // Not missing anymore.
          $missing_password = FALSE;
          drupal_set_message(t('A random password was generated in order to send the email about this page. Remember that changing the password will prevent users you just emailed from accessing this page.'), 'warning');
        }
      }
      else {

        // The node is not published, forget about emails!
        form_error($form['protected_node']['protected_node_emails'], t('The node is not published. Therefore no email will be sent.'));
      }
    }
    if ($missing_password) {
      global $user;
      if ($user->uid == 0) {

        // If anonymous user, then global password is not an option otherwise
        // all the nodes could be edited by all the anonymous users!
        $global_password = PROTECTED_NODE_PER_NODE_PASSWORD;
      }
      else {
        $global_password = variable_get('protected_node_use_global_password', PROTECTED_NODE_PER_NODE_PASSWORD);
      }
      switch ($global_password) {
        case PROTECTED_NODE_PER_NODE_PASSWORD:
          form_error($form['protected_node']['protected_node_passwd'], t('To protect this page, please enter a password.'));
          break;
      }
    }
  }
  elseif (isset($node->protected_node_emails) && trim($node->protected_node_emails)) {
    form_error($form['protected_node']['protected_node_emails'], t('No email can be sent by the protected node module when the node is not protected or you do not have permission to set a password.'));
  }
}

/**
 * Implements hook_node_insert().
 */
function protected_node_node_insert($node) {
  _protected_node_node_create_or_update($node);
}

/**
 * Implements hook_node_update().
 */
function protected_node_node_update($node) {
  _protected_node_node_create_or_update($node);
}

/**
 * Helper function.
 *
 * Do protected node actions when creating or updating a node.
 *
 * @param object $node
 *   A node object.
 */
function _protected_node_node_create_or_update($node) {

  // The node type is never protected if $node->protected_node_is_protected
  // is not set.
  if (!isset($node->protected_node_is_protected)) {
    return;
  }

  // Ugly but we want to keep some variables between the validation and
  // insert/update.
  global $_protected_node_emails;
  global $_protected_node_random_passwd;
  if (!empty($_protected_node_random_passwd)) {
    $node->protected_node_passwd = $_protected_node_random_passwd;
  }
  if (!empty($_protected_node_emails)) {
    $node->protected_node_emails = $_protected_node_emails;
  }
  _protected_node_save($node);

  // Send notifications if there is at least one email.
  if ($node->protected_node_is_protected && !empty($node->protected_node_emails) && $node->status == 1 && isset($node->protected_node_clear_passwd)) {
    module_load_include('mail.inc', 'protected_node');
    protected_node_send_mail($node);
  }
}

/**
 * Implements hook_node_view().
 */
function protected_node_node_view($node, $view_mode, $langcode) {
  global $user;
  if (!empty($node->protected_node_is_protected)) {

    // Accessed for search indexing? (usually by cron.php).
    if ($view_mode == 'search_index') {

      // "user" could see the node, but at this time, not its contents
      // (the current user is Anonymous, so that statement is not exactly true,
      // but at the time of the search index building we cannot know who will
      // be searching so we let go without the access denied error).
      protected_node_invoke('protected_node_hide', $node);
    }
    elseif (!user_access('view protected content') && _protected_node_check_view_mode($view_mode)) {
      if (!$user->uid && variable_get('cache', 1)) {

        // Prevent caching (do NOT use variable_set() since this is temporary
        // for this session).
        $GLOBALS['conf']['cache'] = 0;
      }
      $global_reset_time = variable_get('protected_node_session_timelimit', 0);
      $this_page_reset_time = $node->protected_node_passwd_changed;
      if ($node->uid !== $user->uid) {

        // Is there a global password?
        if (isset($_SESSION['_protected_node']['passwords']['global'])) {

          // Is password out of date?
          $when = $_SESSION['_protected_node']['passwords']['global'];
          if ($when <= $global_reset_time || $when <= $this_page_reset_time) {
            unset($_SESSION['_protected_node']['passwords']['global']);
          }
        }

        // Is there a password?
        if (isset($_SESSION['_protected_node']['passwords'][$node->nid])) {

          // Is password out of date?
          $when = $_SESSION['_protected_node']['passwords'][$node->nid];
          if ($when <= $global_reset_time || $when <= $this_page_reset_time) {
            unset($_SESSION['_protected_node']['passwords'][$node->nid]);
          }
        }
        if (!isset($_SESSION['_protected_node']['passwords'][$node->nid]) && !isset($_SESSION['_protected_node']['passwords']['global'])) {
          if (!user_access('access protected node password form')) {

            // User will never be given access (no drupal_goto() call
            // necessary).
            drupal_access_denied();
            exit;
          }

          // User could see the node, but at this time, not its contents.
          protected_node_invoke('protected_node_hide', $node);
        }
      }
    }
  }
}

/**
 * Implements hook_node_delete().
 */
function protected_node_node_delete($node) {
  db_delete('protected_nodes')
    ->condition('nid', $node->nid)
    ->execute();
}

/**
 * Implements hook_protected_node_hide().
 *
 * We implement this callback since it makes sense (I think) although
 * it makes the module a bit slower.
 *
 * This function hides the body, and if requested on that node we hide the title
 * as well.
 *
 * @param[in,out] $node
 *   The affected node.
 */
function protected_node_protected_node_hide(&$node) {

  // Core module fields.
  if (!$node->protected_node_show_title) {
    $node->title = t('Password protected page');
  }
  $node->body = '';

  // Remove $node->content children to avoid the user see content he/she should
  // not see.
  $content_children = element_children($node->content);
  foreach ($content_children as $content_key) {
    unset($node->content[$content_key]);
  }
}

/**
 * Implements hook_file_download().
 */
function protected_node_file_download($uri) {
  global $user;
  $path = file_uri_target($uri);

  // Private file access for image style derivatives.
  if (strpos($path, 'styles/') === 0) {

    // Check that the file exists and is an image.
    if (image_get_info($uri)) {
      $original_uri = _protected_node_get_original_uri($path, $uri);

      // Check the permissions of the original to grant access to this image.
      $headers = module_invoke_all('file_download', $original_uri);

      // Confirm there's at least one module granting access and none denying
      // access.
      if (!empty($headers) && !in_array(-1, $headers)) {
        return array();
      }
    }
    return array();
  }

  // Private file access for the original files.
  $files = file_load_multiple(array(), array(
    'uri' => $uri,
  ));
  if (count($files)) {
    $file = reset($files);
    if ($file->status) {

      // Is it a file submitted with a webform?
      if (strpos($file->uri, '://webform/') !== FALSE) {

        // Pass through Webform submissions to get the nid given the fid.
        $query = db_select('file_usage', 'fu');
        $query
          ->join('webform_submissions', 'ws', 'ws.sid = fu.id');
        $query
          ->join('node', 'n', 'n.nid = ws.nid');
        $query
          ->join('protected_nodes', 'pn', 'n.nid = pn.nid');
        $query
          ->fields('n', array(
          'nid',
          'uid',
        ));
        $query
          ->fields('pn', array(
          'protected_node_passwd_changed',
        ));
        $query
          ->condition('fu.module', 'webform');
        $query
          ->condition('fu.type', 'submission');
        $query
          ->condition('fu.fid', $file->fid);
        $query
          ->condition('pn.protected_node_is_protected', '1');
      }
      else {
        $query = db_select('node', 'n');
        $query
          ->join('file_usage', 'fu', 'n.nid = fu.id');
        $query
          ->join('protected_nodes', 'pn', 'n.nid = pn.nid');
        $query
          ->fields('n', array(
          'nid',
          'uid',
        ));
        $query
          ->fields('pn', array(
          'protected_node_passwd_changed',
        ));
        $query
          ->condition('fu.fid', $file->fid);
        $query
          ->condition('fu.type', 'node');
        $query
          ->condition('pn.protected_node_is_protected', '1');
      }
      $number_of_results = $query
        ->countQuery()
        ->execute()
        ->fetchField();
      if (0 == $number_of_results) {
        return array();

        /* Row doesn't exist, it's not protected */
      }
      $result = $query
        ->execute();
      foreach ($result as $file_info) {

        // If the file belongs to the current user let them see it.
        if ($file_info === FALSE || $user->uid && $user->uid == $file_info->uid) {
          return array();
        }

        // The user has the bypass password for view.
        if (user_access('view protected content', $user)) {
          return array();
        }

        // Got the global password?
        if (isset($_SESSION['_protected_node']['passwords']['global'])) {
          $when = $_SESSION['_protected_node']['passwords']['global'];

          // This page reset time && global reset time.
          if ($when > $file_info->protected_node_passwd_changed && $when > variable_get('protected_node_session_timelimit', 0)) {
            return array();
          }
        }
        elseif (isset($_SESSION['_protected_node']['passwords'][$file_info->nid])) {
          $when = $_SESSION['_protected_node']['passwords'][$file_info->nid];

          // This page reset time && global reset time.
          if ($when > $file_info->protected_node_passwd_changed && $when > variable_get('protected_node_session_timelimit', 0)) {
            return array();
          }
        }
      }

      // No password, access denied.
      return -1;
    }
    elseif (strpos($file->uri, '://webform/') !== FALSE) {
      return array();
    }
    else {
      return array();
    }
  }

  // Not a file managed by a protected node.
  return array();
}

/**
 * Helper function used to return the original uri from a path and an uri.
 *
 * @see protected_node_file_download()
 */
function _protected_node_get_original_uri($path, $uri) {
  $args = explode('/', $path);

  // Discard the first part of the path (styles).
  array_shift($args);

  // Discard the second part of the path (style_name).
  array_shift($args);

  // Discard the third part of the path (scheme).
  array_shift($args);

  // Then the remaining parts are the path to the image.
  $original_uri = file_uri_scheme($uri) . '://' . implode('/', $args);
  return $original_uri;
}

/**
 * Sets the given node to protected with the provided password.
 *
 * The password cannot be empty.
 *
 * If the node already password protected this method changes the password
 * to the one you provided as $password parameter.
 *
 * @param[in,out] object $node
 *   The node to be saved.
 */
function _protected_node_save(&$node) {

  // The node type is never protected if $node->protected_node_is_protected
  // is not set.
  if (!isset($node->protected_node_is_protected)) {
    return;
  }

  // We first test whether a protected_nodes entry exist so we can use UPDATE
  // or INSERT accordingly (UPDATE does not always properly report working
  // with MySQL).
  // We also retrieve nid because protected_node_passwd may exist and be empty.
  $result = db_select('protected_nodes')
    ->fields('protected_nodes', array(
    'nid',
    'protected_node_passwd',
    'protected_node_emails',
  ))
    ->condition('nid', $node->nid)
    ->execute()
    ->fetchAssoc();
  if (!empty($result)) {

    // Note: the following test prevents the user from using "0" as a password.
    if (isset($node->protected_node_passwd)) {
      $changed = $node->protected_node_passwd != $result['protected_node_passwd'];
      if ($changed) {
        if (empty($node->protected_node_passwd)) {

          // Keep result if it's empty ...
          $node->protected_node_passwd = $result['protected_node_passwd'];
          $changed = FALSE;
        }
        else {
          $node->protected_node_clear_passwd = $node->protected_node_passwd;
          $node->protected_node_passwd = hash('sha256', $node->protected_node_passwd);
        }
      }
    }
    else {
      $changed = FALSE;
      $node->protected_node_passwd = $result['protected_node_passwd'];
    }

    // Check if the email addresses is empty.
    if (empty($node->protected_node_emails)) {
      if (!empty($result['protected_node_emails'])) {

        // Keep the addresses.
        $saved_emails = $result['protected_node_emails'];
      }
      else {
        $saved_emails = '';
      }
    }
    else {
      $saved_emails = $node->protected_node_emails;
    }
    $args = array(
      'protected_node_is_protected' => (int) $node->protected_node_is_protected,
      'protected_node_passwd' => $node->protected_node_passwd,
      'protected_node_show_title' => (int) $node->protected_node_show_title,
      'protected_node_emails' => $saved_emails,
      'protected_node_hint' => isset($node->protected_node_hint) ? $node->protected_node_hint : '',
    );
    if ($changed) {
      $args['protected_node_passwd_changed'] = REQUEST_TIME;
    }
    db_update('protected_nodes')
      ->fields($args)
      ->condition('nid', $node->nid)
      ->execute();
  }
  else {
    if (!isset($node->protected_node_passwd)) {

      // This happens when the global password is to be used.
      $node->protected_node_passwd = '';
    }
    elseif ($node->protected_node_passwd) {
      $node->protected_node_clear_passwd = $node->protected_node_passwd;
      $node->protected_node_passwd = hash('sha256', $node->protected_node_passwd);
    }

    // We don't need to set the protected_node_passwd_changed since no
    // one has ever entered a password for this node.
    db_insert('protected_nodes')
      ->fields(array(
      'protected_node_is_protected' => (int) $node->protected_node_is_protected,
      'protected_node_passwd' => $node->protected_node_passwd,
      'protected_node_show_title' => (int) $node->protected_node_show_title,
      'nid' => $node->nid,
      'protected_node_emails' => isset($node->protected_node_emails) ? $node->protected_node_emails : '',
      'protected_node_hint' => isset($node->protected_node_hint) ? $node->protected_node_hint : '',
    ))
      ->execute();
  }
}

/**
 * Load the node extension fields.
 *
 * @param[in] object $node
 *   The node to complement with the protected node parameters.
 *
 * @return array
 *   An array with the node extended fields or FALSE.
 */
function protected_node_load($nodes) {
  foreach ($nodes as &$node) {

    // Valid input parameters?
    if (!is_object($node) || !is_numeric($node->nid)) {
      return FALSE;
    }

    // Default fields for protected nodes.
    static $default_fields = array(
      'protected_node_is_protected' => 0,
      'protected_node_passwd' => '',
      'protected_node_passwd_changed' => 0,
      'protected_node_show_title' => 0,
      'protected_node_emails' => '',
      'protected_node_hint' => '',
    );

    // Can the node be protected at all?
    $protection = variable_get('protected_node_protection_' . $node->type, PROTECTED_NODE_PROTECTION_PROTECTABLE);
    if ($protection == PROTECTED_NODE_PROTECTION_NEVER) {

      // By default the node is not protected, return that.
      return $default_fields;
    }
    $result = db_select('protected_nodes')
      ->fields('protected_nodes', array(
      'protected_node_is_protected',
      'protected_node_passwd',
      'protected_node_passwd_changed',
      'protected_node_show_title',
      'protected_node_emails',
      'protected_node_hint',
    ))
      ->condition('nid', $node->nid)
      ->execute()
      ->fetchAssoc();
    if (!is_array($result)) {

      // The SELECT failed, use the defaults.
      $result = $default_fields;
    }
    else {

      // Define any missing field.
      $result += $default_fields;
    }

    // The password is a CHAR(40) and when empty it's all spaces
    // (this is possible when the global password is used).
    $result['protected_node_passwd'] = trim($result['protected_node_passwd']);

    // If the user changed the mode to "always protected" then we force that
    // here it means the node may not be accessible to people without
    // administration privileges since it may not have a default password.
    if ($protection == PROTECTED_NODE_PROTECTION_ALWAYS) {
      $result['protected_node_is_protected'] = TRUE;
    }
    foreach ($result as $property => &$value) {
      $node->{$property} = $value;
    }
  }
}

/**
 * Implements hook_token_info().
 *
 * This function defines some extras for the protected node (i.e. whether a
 * node is protected, title flag, last time the password was changed, etc.)
 */
function protected_node_token_info() {
  $info['tokens']['node'] = array(
    'is-protected' => array(
      'name' => t('Node protected status'),
      'description' => t("Whether the node is protected (yes/no)."),
    ),
    'password' => array(
      'name' => t('Node protected password'),
      'description' => t("The password in clear (only if available, empty otherwise)."),
    ),
    'protected-title' => array(
      'name' => t('Node protected show title'),
      'description' => t("Whether the title node of the node is protected (yes/no)."),
    ),
    'password-hint' => array(
      'name' => t('Node protected password hint'),
      'description' => t("The password hint as entered in this node."),
    ),
  );
  return $info;
}

/**
 * Implements hook_node_type_delete().
 *
 * This function deletes the variables corresponding to the fields added
 * to the node type form.
 *
 * @param[in] $op
 *   The operation performed on the node type.
 * @param[in] $type
 *   The type object concerned.
 */
function protected_node_node_type_delete($info) {
  variable_del('protected_node_fieldset_' . $info->type);
  variable_del('protected_node_protection_' . $info->type);
  variable_del('protected_node_node_type_password_' . $info->type);

  // Should already be deleted by the submit().
  variable_del('protected_node_node_type_password_field_' . $info->type);
}

/**
 * Implements hook_tokens().
 */
function protected_node_tokens($type, $tokens, array $data = array(), array $options = array()) {
  $replacements = array();
  if ($type == 'node' && !empty($data['node'])) {
    $node = $data['node'];
    if (!empty($node->protected_node_is_protected)) {
      foreach ($tokens as $name => $original) {
        switch ($name) {
          case 'is-protected':
            $replacements[$original] = t('yes');
            break;
          case 'password':
            $replacements[$original] = empty($node->protected_node_clear_passwd) ? '' : $node->protected_node_clear_passwd;
            break;
          case 'protected-title':
            $replacements[$original] = empty($node->protected_node_show_title) ? t('yes') : t('no');
            break;
          case 'password-hint':
            $replacements[$original] = $node->protected_node_hint;
            break;
        }
      }
    }
  }
  return $replacements;
}

/**
 * After_build function to disable autocomplete for the password fields.
 *
 * Without this FF >= 3 will attempt to autocomplete the fields with the user's
 * login info.
 */
function protected_node_autocomplete_off($form_element, &$form_state) {
  $form_element['pass1']['#attributes']['autocomplete'] = 'new-password';
  $form_element['pass2']['#attributes']['autocomplete'] = 'new-password';
  return $form_element;
}

/**
 * Implements hook_boost_is_cacheable().
 *
 * Prevent boost from caching protected nodes.
 *
 * @todo
 * We also need to make sure the cache gets cleared whenever
 * the protection is turned on.
 */
function protected_node_boost_is_cacheable($path) {
  if (arg(0) == 'node' && is_numeric(arg(1))) {

    // If protected, do not cache (i.e. not caching == return FALSE).
    return array(
      'is_cacheable' => !protected_node_isset_protected(arg(1)),
    );
  }
  return array(
    'is_cacheable' => TRUE,
  );
}

/**
 * This method marks the specified node as protected.
 *
 * The method accepts a password. It is legal to not pass a password in
 * which case the previously defined password is used or the global password.
 * If no password is available, then the node gets locked until edited by
 * the author or the administrator (UID=1) and a password is added.
 *
 * If the \p $passwd parameter is set, then the change is marked in the
 * database. In other words, all users who had previously enter a password
 * will be kicked out.
 *
 * @param[in] $param
 *   The node identifier or whatever valid $param passed to node_load.
 * @param[in] $passwd
 *   The node password.
 *
 * @return bool
 *   TRUE if the node is protection on return.
 */
function protected_node_set_protected($param, $passwd = NULL) {

  // Get the existing node.
  $node = node_load($param);
  if ($node == FALSE) {

    // Not even a valid node identifier?!
    return FALSE;
  }
  if (empty($node->protected_node_is_protected)) {

    // Node exists in our table?
    $select = db_select('protected_nodes')
      ->fields('protected_nodes', array(
      'nid',
    ))
      ->condition('nid', $node->nid)
      ->execute()
      ->fetchField();
    if ($select) {
      if (empty($passwd)) {

        // In this case, an empty password is fine.
        $result = db_update('protected_nodes')
          ->fields(array(
          'protected_node_is_protected' => 1,
        ))
          ->condition('nid', $node->nid)
          ->execute() !== FALSE;
      }
      else {

        // We have to also update the password in this case.
        $result = db_update('protected_nodes')
          ->fields(array(
          'protected_node_is_protected' => 1,
          'protected_node_passwd' => hash('sha256', $passwd),
          'protected_node_passwd_changed' => REQUEST_TIME,
        ))
          ->condition('nid', $node->nid)
          ->execute() !== FALSE;
      }
    }
    else {

      // No entry in the database yet, add it now.
      if (empty($passwd)) {
        $passwd = '';
      }
      else {
        $passwd = hash('sha256', $passwd);
      }
      $result = db_insert('protected_nodes')
        ->fields(array(
        'nid' => $node->nid,
        'protected_node_is_protected' => 1,
        'protected_node_passwd' => $passwd,
        'protected_node_show_title' => variable_get('protected_node_show_node_titles', FALSE),
      ))
        ->execute() !== FALSE;
    }
  }
  else {

    // The node is already protected, change the password if necessary.
    if (empty($passwd)) {

      // It is protected; we're done (the password is not to be changed).
      return TRUE;
    }
    $result = db_update('protected_nodes')
      ->fields(array(
      'protected_node_passwd' => hash('sha256', $passwd),
      'protected_node_passwd_changed' => REQUEST_TIME,
    ))
      ->condition('nid', $node->nid)
      ->execute() !== FALSE;
  }
  return $result;
}

/**
 * This method marks the specified node as unprotected.
 *
 * This function ensures that the specified node is not protected anymore.
 * It does not delete the row from the database which means calling the
 * protected_node_set_protected() function with the same $nid parameter
 * will restore the previous state (assuming the node was protected before.)
 *
 * When the node was previously protected and this call succeeds, the method
 * returns TRUE.
 *
 * If an invalid $nid is passed FALSE is returned.
 *
 * @param[in] int $nid
 *   The node identifier.
 *
 * @return bool
 *   TRUE if the node was protected before the call, FALSE otherwise.
 */
function protected_node_unset_protected($nid) {
  $result = db_select('protected_nodes')
    ->fields('protected_nodes', array(
    'protected_node_is_protected',
  ))
    ->condition('nid', $nid)
    ->execute()
    ->fetchField() == 1;
  db_update('protected_nodes')
    ->fields(array(
    'protected_node_is_protected' => 0,
  ))
    ->condition('nid', $nid)
    ->execute();
  return $result;
}

/**
 * This method determines the protected flag status for the given node id.
 *
 * Note that doesn't mean the node is protected for the current user
 * (i.e. the current user may have entered the password successfully.)
 *
 * @param[in] int $nid
 *   The node id to check.
 *
 * @return bool
 *   TRUE if the node identified by the nid you provided is protected, FALSE
 *   otherwise.
 */
function protected_node_isset_protected($nid) {
  if (!is_numeric($nid)) {
    return FALSE;
  }
  $result = db_select('protected_nodes')
    ->fields('protected_nodes', array(
    'protected_node_is_protected',
  ))
    ->condition('nid', $nid)
    ->execute()
    ->fetchField() == 1;
  return $result;
}

/**
 * Revoke access to the current used from the specified protected node.
 *
 * The effect is immediate.
 *
 * Note that the date is not checked so it is possible that the node was
 * already locked and this function still returns TRUE (i.e. the lock
 * release was out of date and thus the node was anyway not accessible.)
 *
 * @param[in] $nid
 *   The node to lock.
 *
 * @return bool
 *   TRUE if the node gets unlocked.
 */
function protected_node_lock($nid) {
  if (is_numeric($nid) && isset($_SESSION['_protected_node']['passwords']['global'])) {
    unset($_SESSION['_protected_node']['passwords']['global']);
    return TRUE;
  }
  if (is_numeric($nid) && isset($_SESSION['_protected_node']['passwords'][$nid])) {
    unset($_SESSION['_protected_node']['passwords'][$nid]);
    return TRUE;
  }
  return FALSE;
}

/**
 * Give access to the current user to the specified protected node.
 *
 * The duration of the lock is as expected starting now.
 *
 * @param[in] $nid
 *   The node to unlock.
 *
 * @return bool
 *   TRUE if the node gets unlocked.
 */
function protected_node_unlock($nid) {
  if (is_numeric($nid)) {

    // Make sure the node exists.
    $node = node_load($nid);
    if ($node->protected_node_is_protected) {
      if (isset($_SESSION['has_entered_global_password'])) {
        $_SESSION['_protected_node']['passwords']['global'] = REQUEST_TIME;
      }
      else {
        $_SESSION['_protected_node']['passwords'][$nid] = REQUEST_TIME;
      }
      return TRUE;
    }
  }
  return FALSE;
}

/**
 * Helper function.
 *
 * Evaluate whether the current view mode is one to check the password for.
 *
 * @param string $view_mode
 *   Eg: full, teaser, or custom (eg: ds view modes).
 *
 * @return bool
 *   TRUE if we are checking passwords, FALSE if not.
 */
function _protected_node_check_view_mode($view_mode) {
  $permitted_view_modes = variable_get('protected_node_checked_view_modes', _protected_node_get_default_checked_view_modes());
  return in_array($view_mode, $permitted_view_modes);
}

/**
 * Helper function.
 *
 * Used to get the default checked view modes.
 */
function _protected_node_get_default_checked_view_modes() {
  $default_view_modes =& drupal_static(__FUNCTION__);
  if (!isset($default_view_modes)) {
    $node_info = entity_get_info('node');
    $default_view_modes = array();
    foreach ($node_info['view modes'] as $id => $item) {
      $default_view_modes[] = $id;
    }
  }
  return $default_view_modes;
}

/**
 * Helper function.
 *
 * Used to get the options for the protected_node_failed_password_ip_limit
 * variable.
 */
function _protected_node_get_failed_password_ip_limit_options() {
  return drupal_map_assoc(array(
    1,
    2,
    3,
    4,
    5,
    6,
    7,
    8,
    9,
    10,
    20,
    30,
    40,
    50,
    75,
    100,
    125,
    150,
    200,
    250,
    500,
  ));
}

/**
 * Helper function.
 *
 * Used to get the options for the protected_node_failed_password_ip_window
 * variable.
 */
function _protected_node_get_failed_password_ip_window_options() {
  return array(
    0 => t('None (flood control disabled)'),
  ) + drupal_map_assoc(array(
    60,
    180,
    300,
    600,
    900,
    1800,
    2700,
    3600,
    10800,
    21600,
    32400,
    43200,
    86400,
  ), 'format_interval');
}

/**
 * Helper function to get recursively the host entity nid of the paragraph.
 *
 * @param \ParagraphsItemEntity $paragraphs_item
 *   A paragraph item.
 *
 * @return int|false
 *   Return FALSE if the paragraph or the parent paragraphs are not attached to
 *   a node
 */
function _protected_node_get_paragraph_node_host_entity_id(ParagraphsItemEntity $paragraphs_item) {

  // Need to load the host entity otherwise host entity date are null.
  paragraphs_item_get_host_entity($paragraphs_item);
  $host_entity_type = $paragraphs_item
    ->hostEntityType();
  if ($host_entity_type == 'node') {
    return $paragraphs_item
      ->hostEntity()->nid;
  }
  elseif ($host_entity_type == 'paragraphs_item') {
    $host_id = array(
      $paragraphs_item
        ->hostEntityId(),
    );
    $host_paragraphs_items = entity_load('paragraphs_item', $host_id);
    return _protected_node_get_paragraph_node_host_entity_id(array_shift($host_paragraphs_items));
  }
  return FALSE;
}

Functions

Namesort descending Description
protected_node_access_callback Callback function to determine who can enter a password.
protected_node_and_attachment Helper function.
protected_node_autocomplete_off After_build function to disable autocomplete for the password fields.
protected_node_boost_is_cacheable Implements hook_boost_is_cacheable().
protected_node_file_download Implements hook_file_download().
protected_node_form_node_form_alter Implements hook_form_FORM_ID_alter().
protected_node_form_node_type_form_alter Implements hook_form_FORM_ID_alter().
protected_node_help Implements hook_help().
protected_node_init Implements hook_init().
protected_node_invoke Module invoke.
protected_node_isset_protected This method determines the protected flag status for the given node id.
protected_node_is_locked Check whether a node is protected and a password is required.
protected_node_load Load the node extension fields.
protected_node_lock Revoke access to the current used from the specified protected node.
protected_node_menu Implements hook_menu().
protected_node_node_delete Implements hook_node_delete().
protected_node_node_insert Implements hook_node_insert().
protected_node_node_load Implements hook_node_load().
protected_node_node_type_delete Implements hook_node_type_delete().
protected_node_node_update Implements hook_node_update().
protected_node_node_validate Implements hook_node_validate().
protected_node_node_view Implements hook_node_view().
protected_node_permission Implements hook_permission().
protected_node_protected_node_hide Implements hook_protected_node_hide().
protected_node_set_protected This method marks the specified node as protected.
protected_node_tokens Implements hook_tokens().
protected_node_token_info Implements hook_token_info().
protected_node_unlock Give access to the current user to the specified protected node.
protected_node_unset_protected This method marks the specified node as unprotected.
_protected_node_check_view_mode Helper function.
_protected_node_get_default_checked_view_modes Helper function.
_protected_node_get_failed_password_ip_limit_options Helper function.
_protected_node_get_failed_password_ip_window_options Helper function.
_protected_node_get_original_uri Helper function used to return the original uri from a path and an uri.
_protected_node_get_paragraph_node_host_entity_id Helper function to get recursively the host entity nid of the paragraph.
_protected_node_node_create_or_update Helper function.
_protected_node_save Sets the given node to protected with the provided password.

Constants

Namesort descending Description
PROTECTED_NODE_GLOBAL_PASSWORD Global password only.
PROTECTED_NODE_PER_NODE_AND_GLOBAL_PASSWORD Per node or global password.
PROTECTED_NODE_PER_NODE_PASSWORD Per node password.
PROTECTED_NODE_PROTECTION_ALWAYS The nodes of this type will always be protected.
PROTECTED_NODE_PROTECTION_NEVER Never protect these types of nodes.
PROTECTED_NODE_PROTECTION_PROTECTABLE The author can choose whether the node is protected or not.
PROTECTED_NODE_PROTECTION_PROTECTED The author can choose whether the node is protected or not.