You are here

ip_login.module in IP Login 7.3

Allow user login by IP addresses, ranges or wildcards.

Originally by David W H Thomas Major enhancements and 2.x branch by Jim Kirkpatrick Other big contributions from:

Picked up again by David Thomas for 7.3.x

File

ip_login.module
View source
<?php

/**
 * @file
 * Allow user login by IP addresses, ranges or wildcards.
 *
 * Originally by David W H Thomas
 * Major enhancements and 2.x branch by Jim Kirkpatrick
 * Other big contributions from:
 *  - JohnV: Code tidy/refactor & D7 version - http://drupal.org/node/1151458
 *  - PeterX: Many bug fixes and tweaks
 * Picked up again by David Thomas for 7.3.x
 */

/*
 * @todo for security, IP addresses and ranges should really be checked for collisions between existing accounts users
 */
define('ATTEMPT_IP_LOGIN', 'user/login_by_ip');

// path for ip login
define('IP_LOGOUT', 'user/logout');

// path for user logout (different between D6/D7)
define('IP_CHECKED', 'ip_login_checked');

// TRUE when IP check has happened
define('IP_UID_MATCH', 'ip_login_uid');

// TRUE when a user account matches the request IP
define('LOGIN_AS_DIFFERENT_USER', 'ip_login_as_different_user');

// TRUE when user wants alternate account
define('CACHE_DISABLED', 0);

// D7 TODO: this is removed from bootstrap.inc, and now in?? --> avoid double definition

/**
 * Implementation of hook_menu().
 */
function ip_login_menu() {
  $items[ATTEMPT_IP_LOGIN] = array(
    'title' => 'Automatically log me in by IP',
    'access callback' => 'ip_login_is_possible',
    'page callback' => 'ip_login_attempt_login',
    'type' => MENU_CALLBACK,
  );
  $items['admin/config/people/ip_login'] = array(
    'title' => 'IP Login',
    'description' => 'Configure IP Login settings',
    'access callback' => 'user_access',
    'access arguments' => array(
      'administer ip login',
    ),
    'page callback' => 'drupal_get_form',
    'page arguments' => array(
      'ip_login_admin_settings',
    ),
    'file' => 'ip_login.admin.inc',
    'type' => MENU_NORMAL_ITEM,
  );
  return $items;
}

/**
 * Implementation of hook_help().
 */
function ip_login_help($path, $arg) {
  switch ($path) {
    case 'admin/config/people/ip_login':
      $help = '<p>';
      $help .= t("This module allows this site to automatically authenticate and login users arriving with a chosen IP address - optionally at certain pages only.") . '</p> <p>';
      $help .= t('It also allows users with the <code>administer ip login</code> and <code>can log in as another user</code> <a href="@permissions-link">permissions</a> to log out and log in as another user, with other users being forced to stay logged in.', array(
        '@permissions-link' => '/admin/people/permissions#module-ip_login',
      ));
      $help .= '</p>';
      $help .= ip_login_help_ranges();
      return $help;
  }
}

/**
 * Provides help about accepted values of IP ranges etc.
 */
function ip_login_help_ranges($intro = '') {
  $help = '<p id="ip_login">' . $intro . ' ' . t('Accepted IP Login match values are:') . '</p>';
  $help .= '<ul><li>';
  $help .= t("Single IP matches like <code>123.123.123.123</code>");
  $help .= '</li><li>';
  $help .= t("Wildcards using an asterisk (<code>*</code>) in any quadrant except the first one, for example <code>123.123.123.*</code> or <code>100.*.*.*</code> etc.");
  $help .= '</li><li>';
  $help .= t("Ranges using a hyphen (<code>-</code>) in any quadrant except the first one, for example <code>123.123.123.100-200</code> etc.");
  $help .= '</li><li>';
  $help .= t("Any number of comma-separated IP addresses or ranges like <code>10.11.12.13, 123.123.123.100-200, 123.123.124-125.*</code> etc.");
  $help .= '</li></ul>';
  return $help;
}

/**
 * Implementation of hook_permission().
 */
function ip_login_permission() {

  // @todo add perms checks to correct places in module
  return array(
    'administer ip login' => array(
      'title' => t('Administer IP Login module'),
      'description' => t('Perform administration tasks for IP Login.'),
    ),
    'view any field_ip_login' => array(
      'title' => t('View any field_ip_login'),
      'description' => t('View the display output of field_ip_login on any user account.'),
    ),
    'view own field_ip_login' => array(
      'title' => t('View own field_ip_login'),
      'description' => t('View the display output of field_ip_login on own user account.'),
    ),
    'edit any field_ip_login' => array(
      'title' => t('Edit any field_ip_login'),
      'description' => t('Edit the value of field_ip_login on any user account.'),
    ),
    'edit own field_ip_login' => array(
      'title' => t('Edit own field_ip_login'),
      'description' => t('Edit the value of field_ip_login on own user account.'),
    ),
    'can log in as another user' => array(
      'title' => t('Can login as other user'),
      'description' => t('Allow user to logout and login as another user.'),
    ),
  );
}

/**
 * Implements hook_field_access
 * Restricts view and edit access to field_ip_login
 */
function ip_login_field_access($op, $field, $entity_type, $entity, $account) {
  if ($field['field_name'] == 'field_ip_login') {
    global $user;
    $account = $account ? $account : $user;

    // Use logged in user if account not passed
    switch ($op) {
      case 'view':

        // User has admin access?
        if (user_access('administer ip login', $account) || user_access('administer users', $account)) {
          return TRUE;

          // User has access to view any field_ip_login instance?
        }
        elseif (user_access('view any field_ip_login', $account)) {
          return TRUE;

          // User has access to view own field_ip_login instance
          // and logged in user uid matches entity uid being checked
        }
        elseif (user_access('view own field_ip_login', $account) && isset($entity->uid) && $entity->uid == $user->uid) {
          return TRUE;
        }
        break;
      case 'edit':

        // User has admin access?
        if (user_access('administer ip login', $account) || user_access('administer users', $account)) {
          return TRUE;

          // User has access to edit any field_ip_login instance?
        }
        elseif (user_access('edit any field_ip_login', $account)) {
          return TRUE;

          // User has access to view own field_ip_login instance
          // and logged in user uid matches entity uid being checked
        }
        elseif (user_access('edit own field_ip_login', $account) && isset($entity->uid) && $entity->uid == $user->uid) {
          return TRUE;
        }
        break;
    }
    return FALSE;

    // Default access deny
  }
}

/**
 * Implementation of hook_boot().
 *
 *   see http://drupal.org/node/509028
 */
function ip_login_boot() {

  // skip rest of this if user is logged in
  global $user;
  if ($user->uid != 0 || drupal_is_cli()) {
    return;
  }

  // skip rest of this if the admin has disabled IP login
  if (!variable_get('ip_login_enabled', 1)) {
    return;
  }

  // Avoid settings cookies if not on an IP Login-enabled page to improve
  // external caching support - http://drupal.org/node/1263234 thanks Vacilando
  if (ip_login_check_path() === FALSE) {
    return;
  }

  // check the user IP
  $matched_uid = ip_login_check(ip_login_ip_address());
  if ($matched_uid > 0) {
    $can_login_as_another_user = isset($_COOKIE[LOGIN_AS_DIFFERENT_USER]) ? $_COOKIE[LOGIN_AS_DIFFERENT_USER] : NULL;

    // for clarity about every scenario, use extensive logic
    if (is_null($can_login_as_another_user)) {

      // first time login for user, so log in automatically.
      ip_login_login($matched_uid);
      drupal_goto(ltrim(request_uri(), '/'));
    }
    elseif ($can_login_as_another_user == FALSE) {

      // user logged out, but is not allowed to use another user, so log in again.
      ip_login_login($matched_uid);
      drupal_goto(ltrim(request_uri(), '/'));
    }
    elseif ($can_login_as_another_user == TRUE) {

      // user logged out, and is allowed to login as another user,
      // so do nothing, just stay on this page and wait for user action.
    }
    else {

      // do automatic login.
      ip_login_login($matched_uid);
      drupal_goto(ltrim(request_uri(), '/'));
    }
  }
}

/**
 * Implementation of hook_block_info
 *
 * Adds the simple 'Automatic login' link block
 */
function ip_login_block_info() {
  $blocks = array();
  $blocks['ip-login-link'] = array(
    'info' => t('Log in by IP link'),
    'cache' => DRUPAL_NO_CACHE,
  );
  return $blocks;
}

/**
 * Implementation of hook_block_view
 *
 * Makes simple a 'Automatic login' link available for those not wanting to use the
 * overridden 'User Login' block.
 */
function ip_login_block_view($delta = '') {

  // only show for anonymous users who can log in
  global $user;
  if ($user->uid > 0 || !ip_login_is_possible()) {
    return;
  }
  if ($delta != 'ip-login-link') {
    return;
  }

  // build simple block
  // @todo should be a hook_theme call
  $link_text = t(variable_get('ip_login_link_login_block', 'Log in automatically'));
  $markup = '<div class="ip-login-available"><span class="ip-login-link">';
  $markup .= l($link_text, ATTEMPT_IP_LOGIN, array(
    'query' => array(
      'ip_login_override_pages' => 'yes',
    ),
  ));
  $markup .= '</span></div>';
  $block = array(
    'subject' => t('Automatic login'),
    'content' => $markup,
  );
  return $block;
}

/**
 * Implementation of hook_form_alter().
 */
function ip_login_form_alter(&$form, &$form_state, $form_id) {

  // @todo should call hook_theme ideally
  switch ($form_id) {
    case 'user_login':
      $matched_uid = ip_login_check(ip_login_ip_address());
      if ($matched_uid > 0) {
        $link_text = t(variable_get('ip_login_link_login_page', 'Log in automatically'));
        if (strlen($link_text)) {

          // hide if no link text
          $link_help = t(variable_get('ip_login_link_login_page_help', "Your computer's IP address has been matched and validated."));
          $markup = '<ul class="item-list">';
          $markup .= '<li><strong>' . l($link_text, ATTEMPT_IP_LOGIN, array(
            'query' => array(
              'ip_login_override_pages' => 'yes',
            ),
          )) . '</strong>';
          if (strlen($link_help)) {
            $markup .= '<br/><small>' . filter_xss_admin($link_help) . '</small>';
          }
          $markup .= '</li></ul>';
          $form['ip_login'] = array(
            '#markup' => $markup,
            '#weight' => variable_get('ip_login_link_login_page_weight', -10),
          );
        }
      }
      break;
    case 'user_login_block':
      $matched_uid = ip_login_check(ip_login_ip_address());
      if ($matched_uid > 0) {
        $link_text = t(variable_get('ip_login_link_login_block', 'Log in automatically'));
        if (strlen($link_text)) {

          // hide if no link text
          $markup = '<ul class="item-list">';
          $markup .= '<li><strong>' . l($link_text, ATTEMPT_IP_LOGIN, array(
            'query' => array(
              'ip_login_override_pages' => 'yes',
            ),
          )) . '</strong>';
          $markup .= '</li></ul>';
          $form['ip_login'] = array(
            '#markup' => $markup,
            '#weight' => variable_get('ip_login_link_login_page_weight', -10),
          );
        }
      }
      break;
  }
}

/**
 * Helper function to return IP address
 * for easy test/debug
 */
function ip_login_ip_address() {
  return ip_address();
}

/**
 * Callback function for hook_menu (menu access)
 *
 * @return boolean
 *    TRUE if login by IP can happen because a user match has happened
 */
function ip_login_is_possible() {

  // Return TRUE if a matching uid is found.
  return !empty($_SESSION[IP_UID_MATCH]);
}

/**
 * Checks path of current page matches any set as options on the admin page to
 * see if IP login should occur. Adapted from core Block module's block_list().
 *
 * @return $uid_matched
 *    The uid of the matching user account
 */
function ip_login_check_path() {
  if (!isset($_GET['ip_login_override_pages'])) {
    $pages = variable_get('ip_login_active_pages', '');
    if ($pages) {
      $page_match = FALSE;

      // first char, if variable set, is 'check_mode' - remainder is paths to match or PHP
      $check_mode = substr($pages, 0, 1);
      $pages = substr($pages, 1);
      if ($check_mode < 2) {

        // Compare with the path with allowed pages.
        // Since this happens in hook_boot, we cannot call drupal_get_path_alias
        // so call our own path matcher code and avoid a DB/alias check
        $path = isset($_GET['q']) ? $_GET['q'] : '';
        $page_match = ip_login_match_path($path, $pages);

        // When $check_mode has a value of 0, the IP check happens on
        // all pages except those listed in $pages. When set to 1, IPs
        // are checked only on those pages listed in $pages.
        $page_match = !($check_mode xor $page_match);
      }
      else {

        // evaluate PHP
        $page_match = drupal_eval($pages);
      }

      // if we don't have a path/PHP match, don't log in
      if (!$page_match) {
        return FALSE;
      }
    }
  }

  // all is well, continue with login.
  return TRUE;
}

/**
 * Check if a path matches any pattern in a set of patterns. This is a clone of
 * drupal_match_path() found in path.inc because the bootstrap hasn't occurred,
 * so path.inc isn't available.
 *
 * See: http://api.drupal.org/api/drupal/includes--path.inc/function/drupal_match_path/6
 */
function ip_login_match_path($path, $patterns) {
  static $regexps;
  if (!isset($regexps[$patterns])) {
    $regexps[$patterns] = '/^(' . preg_replace(array(
      '/(\\r\\n?|\\n)/',
      '/\\\\\\*/',
      '/(^|\\|)\\\\<front\\\\>($|\\|)/',
    ), array(
      '|',
      '.*',
      '\\1' . preg_quote(variable_get('site_frontpage', 'node'), '/') . '\\2',
    ), preg_quote($patterns, '/')) . ')$/';
  }
  return preg_match($regexps[$patterns], $path);
}

/**
 * Checks the request IP and logs user in there's a match by calling
 * ip_login_check then ip_login_attempt_login
 *
 * Callback function for hook_menu
 */
function ip_login_attempt_login() {
  drupal_page_is_cacheable(FALSE);
  $matched_uid = ip_login_check(ip_login_ip_address());
  if ($matched_uid > 0) {
    ip_login_login($matched_uid);
  }
  drupal_goto(variable_get('ip_login_destination', 'user'));
}

/**
 * Compares the current request's IP address to the ip_login_user table
 * and then does a proper match for each match on exact, ranges and wildcards
 *
 * @param $ip
 *    An ip address string, usually from the current user's request
 * @return $uid_matched
 *    The uid of the matching user account
 */
function ip_login_check($ip, $diagnostics = FALSE) {

  // have we checked user IP already this session?
  if (!empty($_SESSION[IP_CHECKED])) {
    return $_SESSION[IP_UID_MATCH];
  }
  $uid_matched = ip_login_match_user($ip, $diagnostics);

  // if not diagnostic test, set processed session flag, store matching user (if there is one)
  if (!$diagnostics) {
    $_SESSION[IP_CHECKED] = TRUE;
    $_SESSION[IP_UID_MATCH] = $uid_matched;
  }
  return $uid_matched;
}

/**
 * Checks for a user with matching IP address
 */
function ip_login_match_user($ip, $diagnostics = FALSE) {

  // Check for ip address in field_data_field_ip_login data table
  // We can utilize a range search via
  // field_ipaddress long numeric range query
  // Currently IPv4 only
  $matching_uid = db_select('field_data_field_ip_login', 'ip')
    ->fields('ip', array(
    'entity_id',
  ))
    ->condition('entity_type', 'user')
    ->condition('field_ip_login_start', ip2long($ip), '<=')
    ->condition('field_ip_login_end', ip2long($ip), '>=')
    ->execute()
    ->fetchField();
  return $matching_uid;
}

/**
 * Performs a login for user with $uid and stores IP Login variables for later
 *
 * @param $uid
 *    The UID of the account to be logged in
 */
function ip_login_login($uid) {
  if ($uid) {

    // if a uid is passed in
    // check this page's path is ok to login automatically from
    if (ip_login_check_path() === FALSE) {
      return;
    }

    // get user module and include some handy functions
    drupal_bootstrap(DRUPAL_BOOTSTRAP_FULL);

    // get account (reload from db) , bail if no loaded active user
    $account = user_load($uid, TRUE);
    if (!$account || $account->status != 1) {
      return;
    }

    // login by assigning account to global $user object
    global $user;
    $user = $account;
    if (!variable_get('ip_login_suppress_messages', 0)) {

      // notify user - if messages not suppressed
      $message = t('Welcome %name. You have been automatically logged into %sitename.', array(
        '%name' => $user->name,
        '%sitename' => variable_get('site_name', 'this website'),
      ));
      drupal_set_message($message);

      // add handy message for those who can log out and then back in as another user
      if (_ip_login_can_login_as_another_user($user)) {
        $message = t('You may also <a href="@other_user_link">log in as another user</a> if required.', array(
          '@other_user_link' => url(IP_LOGOUT),
        ));
        drupal_set_message($message);
      }
    }

    // following borrowed from user_authenticate_finalize(), but with slightly different message
    watchdog('user', 'Session opened for %name by IP Login.', array(
      '%name' => $user->name,
    ));

    // This is also used to invalidate one-time login links.
    $user->login = time();
    db_update('users')
      ->fields(array(
      'login' => $user->login,
    ))
      ->condition('uid', $user->uid)
      ->execute();

    // Regenerate the session ID to prevent against session fixation attacks.
    // This is called before hook_user in case one of those functions fails
    // or incorrectly does a redirect which would leave the old session in place.
    $edit = NULL;
    drupal_session_regenerate();
    user_module_invoke('login', $edit, $user);

    // following borrowed from ipAuthenticator's login and avoids caching issues
    if (variable_get('cache', CACHE_DISABLED) != CACHE_DISABLED && !isset($_GET['ip_login_no_cache'])) {

      // make a url to reload page, remove newlines from the URL to avoid header injection attacks.
      // use admin settings for destination if set.
      $url = variable_get('ip_login_destination', '');
      if (strlen($url) == 0) {
        $url = str_replace(array(
          "\n",
          "\r",
        ), '', $_GET["q"]);
      }
      if ($url == 'logout') {
        $url = '<front>';
      }
      $url = url($url, array(
        'query' => array(
          'ip_login_no_cache' => drupal_random_bytes(8),
        ),
        'absolute' => TRUE,
      ));

      // Before the redirect, allow modules to react to the end of the page request.
      module_invoke_all('exit', $url);

      // Even though session_write_close() is registered as a shutdown function, we
      // need all session data written to the database before redirecting.
      session_write_close();
      header('Location: ' . $url, TRUE, 302);
      exit;
    }
  }
}

/**
 * Implementation of hook_user_logout
 *
 * Called from hook_user on logout, most of the code taken from user_logout()
 * and _drupal_bootstrap(DRUPAL_BOOTSTRAP_SESSION).
 * D7-changes: this is now an implementation of a hook, so:
 *  - Only do a logout, leave the automatic login to ip_login_boot;
 *  - prevent logging out if needed, just by calling drupal_goto()
 *  - N.B. check user_logout() in user.pages.inc; this is the calling function;
 *  - N.B. check devel_switch_user() in devel.module; here users are switched, too;
 */
function ip_login_user_logout() {

  // prevent recursive call via user_module_invoke() / module_invoke_all() in user.pages.inc
  if (!ip_login_is_possible()) {
    return;
  }
  else {
    $_SESSION[IP_CHECKED] = FALSE;
    $_SESSION[IP_UID_MATCH] = 0;
  }
  global $user;

  // store whether this user can log back in automatically
  $can_login_as_another_user = _ip_login_can_login_as_another_user($user);

  // sets indicator to behaviour in hook_boot().
  $expire = 0;

  // Cookie expires at the end of the session (when the browser closes).
  setcookie(LOGIN_AS_DIFFERENT_USER, $can_login_as_another_user, $expire, '/');
  if (!$can_login_as_another_user) {

    // @todo: it is possible that some other hook_user_logout() has been called already
    // does this generate an undetermined state?
    $message = t(variable_get('ip_login_logged_back_in', 'This account does not have permission to log out once logged in automatically. You have been logged back in.'));
    $message = token_replace($message, array(
      'user' => $user,
    ), array(
      'clear' => TRUE,
    ));
    drupal_set_message($message, 'warning');

    // show the login page
    drupal_goto(variable_get('ip_login_destination', 'user'));
  }
}

/*
 * Returns TRUE if a user has permission to log out and back in as another user
 */
function _ip_login_can_login_as_another_user($user) {

  // super user can log in as another user
  if ($user->uid == 1) {
    return TRUE;
  }

  // people who can administer this module can
  if (user_access('administer ip login', $user)) {
    return TRUE;
  }

  // If the user doesn't have a matching IP, then we let them log in normally
  if (!ip_login_check(ip_login_ip_address())) {
    return TRUE;
  }

  // all others check correct permission, making sure only TRUE, FALSE is returned
  return user_access('can log in as another user', $user) ? TRUE : FALSE;
}

Functions

Namesort descending Description
ip_login_attempt_login Checks the request IP and logs user in there's a match by calling ip_login_check then ip_login_attempt_login
ip_login_block_info Implementation of hook_block_info
ip_login_block_view Implementation of hook_block_view
ip_login_boot Implementation of hook_boot().
ip_login_check Compares the current request's IP address to the ip_login_user table and then does a proper match for each match on exact, ranges and wildcards
ip_login_check_path Checks path of current page matches any set as options on the admin page to see if IP login should occur. Adapted from core Block module's block_list().
ip_login_field_access Implements hook_field_access Restricts view and edit access to field_ip_login
ip_login_form_alter Implementation of hook_form_alter().
ip_login_help Implementation of hook_help().
ip_login_help_ranges Provides help about accepted values of IP ranges etc.
ip_login_ip_address Helper function to return IP address for easy test/debug
ip_login_is_possible Callback function for hook_menu (menu access)
ip_login_login Performs a login for user with $uid and stores IP Login variables for later
ip_login_match_path Check if a path matches any pattern in a set of patterns. This is a clone of drupal_match_path() found in path.inc because the bootstrap hasn't occurred, so path.inc isn't available.
ip_login_match_user Checks for a user with matching IP address
ip_login_menu Implementation of hook_menu().
ip_login_permission Implementation of hook_permission().
ip_login_user_logout Implementation of hook_user_logout
_ip_login_can_login_as_another_user

Constants