You are here

securesite.module in Secure Site 5

Secure Site contrib module

Secure Site allows the use of HTTP Auth to secure Drupal sites

File

securesite.module
View source
<?php

/**
 * @file
 * Secure Site contrib module
 *
 * Secure Site allows the use of HTTP Auth to secure Drupal sites
 */

/**
 * Secure Site status: Disabled
 */
define('SECURESITE_DISABLED', 0);

/**
 * Secure Site status: Enabled with Web browser HTTP Auth security
 */
define('SECURESITE_AUTH', 1);

/**
 * Secure Site status: Enabled with Web browser HTTP Auth security using the
 * alterative method (deprecated)
 */
define('SECURESITE_AUTH_ALT', 2);

/**
 * Secure Site status: Enabled with HTML login form
 */
define('SECURESITE_FORM', 3);

/**
 * Secure Site bypass: Only the listed pages don't require authentication
 *
 * @see _securesite_filter_check()
 */
define('SECURESITE_WHITELIST', 0);

/**
 * Secure Site bypass: Only the listed pages do require authentication
 *
 * @see _securesite_filter_check()
 */
define('SECURESITE_BLACKLIST', 1);

/**
 * Implementation of hook_perm()
 */
function securesite_perm() {
  return array(
    'access secured pages',
  );
}

/**
 * Implementation of hook_menu()
 */
function securesite_menu($may_cache) {
  $items = array();
  if ($may_cache) {
    $items[] = array(
      'path' => 'admin/settings/securesite',
      'title' => t('Secure Site'),
      'callback' => 'drupal_get_form',
      'callback arguments' => 'securesite_admin_settings',
      'description' => t('Enables HTTP Auth security or an HTML form to restrict site access.'),
      'access' => user_access('administer site configuration'),
    );
  }
  return $items;
}

/**
 * FAPI definition for Secure Site admin settings form
 *
 * @see system_settings_form()
 */
function securesite_admin_settings() {
  global $base_url;

  // Authentication settings
  $form['authentication'] = array(
    '#type' => 'fieldset',
    '#title' => t('Authentication'),
    '#description' => t('Enable Secure Site below.  Users must have the <a href="!access">"access secured pages" permission</a> in order to access the site once the following setting is enabled.', array(
      '!access' => url('admin/user/access', NULL, 'module-securesite'),
    )),
  );
  $form['authentication']['securesite_enabled'] = array(
    '#type' => 'radios',
    '#title' => t('Forced authentication'),
    '#default_value' => variable_get('securesite_enabled', SECURESITE_DISABLED),
    '#options' => array(
      SECURESITE_DISABLED => t('Disabled'),
      SECURESITE_AUTH => t('Use HTTP Auth'),
      SECURESITE_FORM => t('Use HTML login form'),
    ),
    '#description' => t('HTTP Auth requires extra configuration if PHP is not installed as an Apache module.  See the Known Issues section of the README.txt included with Secure Site for details.'),
  );
  $form['authentication']['securesite_realm'] = array(
    '#type' => 'textfield',
    '#title' => t('Authentication realm'),
    '#default_value' => variable_get('securesite_realm', variable_get('site_name', 'Drupal')),
    '#length' => 30,
    '#maxlength' => 40,
    '#description' => t('Name to identify login area in HTTP Auth dialog'),
  );

  // Guest access
  $form['guest'] = array(
    '#type' => 'fieldset',
    '#title' => t('Guest access'),
    '#description' => t('Guest access allows unregistered (anonymous) users to view secure pages, though a username and password are still required.  You can set the username and password for all anonymous users below. If left blank, guest user access will be disabled.'),
  );
  $form['guest']['securesite_guest_name'] = array(
    '#type' => 'textfield',
    '#title' => t('Guest user'),
    '#default_value' => variable_get('securesite_guest_name', ''),
    '#length' => 30,
    '#maxlength' => 40,
    '#description' => t('Leave empty to disable guest access'),
  );
  $form['guest']['securesite_guest_pass'] = array(
    '#type' => 'textfield',
    '#title' => t('Guest password'),
    '#default_value' => variable_get('securesite_guest_pass', ''),
    '#length' => 30,
    '#maxlength' => 40,
    '#description' => t('Leave empty to disable guest access'),
  );

  // HTML login form settings
  $form['login_form'] = array(
    '#type' => 'fieldset',
    '#title' => t('Customize HTML forms'),
    '#description' => t('Configure the message displayed on the HTML login form (if enabled) and password reset form below.'),
  );
  $form['login_form']['securesite_login_form'] = array(
    '#type' => 'textarea',
    '#title' => t('Custom message for HTML login form'),
    '#default_value' => variable_get('securesite_login_form', t('<p>Enter your username and password:</p>')),
    '#length' => 60,
    '#height' => 3,
  );
  $form['login_form']['securesite_request_form'] = array(
    '#type' => 'textarea',
    '#title' => t('Custom message for password reset form'),
    '#default_value' => variable_get('securesite_request_form', t('<p>Enter your username or e-mail address:</p>')),
    '#length' => 60,
    '#height' => 3,
    '#description' => t('Leave empty to disable Secure Site\'s password reset form.'),
  );

  // Bypass login settings
  $form['filter_pages'] = array(
    '#type' => 'fieldset',
    '#title' => t('Bypass login'),
    '#description' => t('Some sites may wish to only use Secure Site for certain pages.  Use the settings below to configure which pages force authentication via Secure Site.  Select "On every page except the listed pages" and leave the "Pages" list empty to force authentication for all pages (this is the default configuration).  The cron page is always accessible.'),
  );
  $form['filter_pages']['securesite_filter_pages_type'] = array(
    '#type' => 'radios',
    '#title' => t('Force authentication'),
    '#default_value' => variable_get('securesite_filter_pages_type', SECURESITE_WHITELIST),
    '#options' => array(
      SECURESITE_WHITELIST => t('On every page except the listed pages.  Use this setting to restrict access of your entire site.'),
      SECURESITE_BLACKLIST => t('Only on the listed pages.  Use this setting only if you want to restrict access to certain pages, not your entire site.'),
    ),
  );
  $form['filter_pages']['securesite_filter_pages'] = array(
    '#type' => 'textarea',
    '#title' => t('Pages'),
    '#default_value' => variable_get('securesite_filter_pages', ''),
    '#length' => 60,
    '#height' => 3,
    '#description' => t("Enter one path per line.  Enter <em>&lt;front&gt;</em> for your site's front page.  \"*\" acts as a wildcard.  For example, enter <em>blog</em> for the blog page and <em>blog/*</em> for every personal blog."),
  );
  return system_settings_form($form);
}

/**
 * Implementation of hook_init()
 *
 * This is where Secure Site does most of its processing
 *
 * Note: When a user is logged in, but doesn't have the 'access secured pages'
 * permission, they get a normal Access Denied message
 */
function securesite_init() {
  global $user, $base_path;
  $securesite_enabled = variable_get('securesite_enabled', SECURESITE_DISABLED);
  $guest_name = variable_get('securesite_guest_name', '');
  $guest_pass = variable_get('securesite_guest_pass', '');

  // Step #1: Process conditions that bypass Secure Site authentication
  if (!$securesite_enabled || $securesite_enabled == SECURESITE_AUTH_ALT || php_sapi_name() == 'cli' || request_uri() == $base_path . 'cron.php' || $user->uid == 1 || $user->uid && user_access('access secured pages') || !empty($guest_name) && (isset($_SESSION['securesite_guest']) ? $_SESSION['securesite_guest'] : '')) {
    return;
  }

  // Do a partial bootstrap when caching is enabled since Secure Site uses
  // stuff in path.inc for the following bypass check
  if (variable_get('cache', CACHE_DISABLED) != CACHE_DISABLED) {
    drupal_bootstrap(DRUPAL_BOOTSTRAP_PATH);
  }

  // User accessing a page in the bypass list. This has to be after a partial
  // bootstrap so path.inc functions are available and $_GET['q'] is set.
  if (_securesite_filter_check(isset($_GET['q']) ? $_GET['q'] : '')) {
    return;
  }

  // Prevent a login/logout loop by redirecting off the logout page
  if (strpos(request_uri(), $base_path . 'logout') === 0) {
    drupal_goto('<front>');
  }

  // Finish bootstrap since Secure Site uses many functions throughout Core,
  // such as user.module functions, t(), and theme()
  if (variable_get('cache', CACHE_DISABLED) != CACHE_DISABLED) {
    drupal_bootstrap(DRUPAL_BOOTSTRAP_FULL);
  }

  // Step #2: Process password resets
  if (strpos($_GET['q'], 'user/reset/') === 0 || module_exists('i18n') && i18n_selection_mode() != 'off' && strpos($_GET['q'], i18n_selection_mode('params') . '/user/reset/') === 0) {
    $args = explode('/', $_GET['q']);
    if (module_exists('i18n') && i18n_selection_mode() != 'off' && i18n_selection_mode('params') != '') {

      // Remove the language argument.
      array_shift($args);
    }

    // The password reset function doesn't work well if it doesn't have all the
    // required parameters or if the UID parameter isn't valid
    if (count($args) >= 5 && user_load(array(
      'uid' => $args[2],
      'status' => 1,
    ))) {
      user_pass_reset($args[2], $args[3], $args[4], 'login');
    }
    else {
      drupal_set_message(t('You have tried to use an invalid one-time login link. Please request a new one using the form below.'), 'error');
    }
  }

  // Step #3: Set up variables
  if ($securesite_enabled == SECURESITE_FORM && !empty($_POST)) {
    $edit = $_POST['edit'];
  }
  elseif ($securesite_enabled == SECURESITE_AUTH) {

    // PHP in CGI mode work-arounds.  Sometimes, "REDIRECT_" prefixes $_SERVER
    // variables.  See http://www.php.net/reserved.variables
    if (!empty($_SERVER['REDIRECT_HTTP_AUTHORIZATION']) && empty($_SERVER['HTTP_AUTHORIZATION'])) {
      $_SERVER['HTTP_AUTHORIZATION'] = $_SERVER['REDIRECT_HTTP_AUTHORIZATION'];
    }

    // Auth variables set via Rewrite rules need to be decoded
    // See http://www.php.net/manual/en/features.http-auth.php#76708
    if (!empty($_SERVER['HTTP_AUTHORIZATION'])) {
      list($_SERVER['PHP_AUTH_USER'], $_SERVER['PHP_AUTH_PW']) = explode(':', base64_decode(substr($_SERVER['HTTP_AUTHORIZATION'], 6)));
    }

    // Process username and password normally.  The correct $_SERVER variables
    // are now set if PHP is run in CGI mode
    if (isset($_SERVER['PHP_AUTH_USER'])) {
      $edit['name'] = $_SERVER['PHP_AUTH_USER'];
    }
    if (isset($_SERVER['PHP_AUTH_PW'])) {
      $edit['pass'] = $_SERVER['PHP_AUTH_PW'];
    }
  }

  // Step #4: If needed, ask user for credentials
  if ((empty($edit['name']) || empty($edit['pass'])) && $user->uid == 0) {
    _securesite_user_auth();
  }

  // Step #5: Check if user is a guest and log them in if they are
  if (!empty($guest_name) && !empty($guest_pass) && $guest_name == $edit['name'] && $guest_pass == $edit['pass']) {

    // Mark this session to prevent re-login (note: guests can't logout)
    $_SESSION['securesite_guest'] = TRUE;
    $_SESSION['securesite_login'] = TRUE;

    // Redirect to prevent some caching problems
    drupal_goto($_GET['q']);
  }
  unset($_SESSION['securesite_guest']);

  // If not a guest, make sure to clear the guest session
  // Step #6: Check user's credentials
  // The LDAP auth module can't use the regular external user login system, so
  // we have to call its login function directly
  if (function_exists('_ldapauth_user_authenticate')) {
    $account = _ldapauth_user_authenticate($edit['name'], $edit['pass']);
  }
  else {
    $account = user_authenticate($edit['name'], $edit['pass']);
  }

  // Step #7: Process login attempt
  if ((isset($account->uid) ? $account->uid : FALSE) && user_access('access secured pages', $account)) {

    // Login successful
    $user = $account;

    // Mark the session so Secure Site will be triggered on logout
    $_SESSION['securesite_login'] = TRUE;
    watchdog('user', t('Session opened for %name.', array(
      '%name' => $user->name,
    )));
    db_query("UPDATE {users} SET login = '%d' WHERE uid = '%s'", time(), $user->uid);
    user_module_invoke('login', $edit, $user);

    // When caching is enabled, redirect to prevent some caching problems.  If
    // caching is enabled, some feed readers (that don't support cookies) will
    // not be able to authenticate due to this redirect
    if (variable_get('cache', CACHE_DISABLED) != CACHE_DISABLED) {
      drupal_goto($_GET['q']);
    }
  }
  else {

    // Login failed
    if (!empty($edit['name'])) {
      watchdog('user', t('Login attempt failed for %user.', array(
        '%user' => $edit['name'],
      )));
    }
    else {
      watchdog('user', t('Login attempt failed for <em>anonymous</em> user.', array(
        '%user' => $edit['name'],
      )));
    }
    _securesite_user_auth();
  }
}

/**
 * Implementation of hook_user()
 *
 * When users logout, show the HTTP Auth dialog to make sure the HTTP Auth
 * credentials are cleared
 *
 * @see _securesite_user_auth()
 */
function securesite_user($op, &$edit, &$user) {
  if ($op == 'logout' && variable_get('securesite_enabled', SECURESITE_DISABLED) == SECURESITE_AUTH && (isset($_SESSION['securesite_login']) ? $_SESSION['securesite_login'] : FALSE)) {

    // Load the anonymous user
    $user = drupal_anonymous_user();

    // Clear stored credentials
    _securesite_user_auth();
  }
}

/**
 * Display authentication dialog and send password reset mails
 */
function _securesite_user_auth() {
  global $base_url;
  include_once 'securesite.inc';
  $securesite_enabled = variable_get('securesite_enabled', SECURESITE_DISABLED);
  $content = '';

  // Step #1: Check if the user attempted to submit the login form. If so,
  // getting here means they didn't enter their info correctly
  if (isset($_POST['securesite_login_form'])) {
    drupal_set_message(t('Unrecognized username and/or password.'), 'error');
  }

  // Step #2: Check if the user attempted to submit the password request form.
  // If so, check if we have information for the name/mail they entered and
  // send it if we do
  if (isset($_POST['securesite_request_form']) && isset($_POST['edit'])) {
    _securesite_password_reset($_POST['edit']);
  }

  // Get content for dialog
  if ($securesite_enabled == SECURESITE_FORM) {
    $content .= _securesite_login_form();
  }
  $content .= _securesite_request_form();

  // Step #3: If using HTTP Auth, send the appropriate headers, but only if the
  // user isn't logged in and they haven't just submitted the password reset or
  // login forms
  if ($securesite_enabled == SECURESITE_AUTH && empty($_POST['securesite_request_form']) && empty($_POST['securesite_login_form'])) {
    $realm = variable_get('securesite_realm', variable_get('site_name', 'Drupal'));

    // If not on the home page of the site, Opera will not show the auth dialog
    // the first time after logout.  It will show the page displayed before
    // logging out.  Reloading will cause the dialog to display.  Safari
    // doesn't seem show the login/password request form when cancelling the
    // auth dialog no matter what
    $browsers = array(
      'msie',
      'opera',
      'safari',
    );
    $user_agent = strtolower($_SERVER['HTTP_USER_AGENT']);
    foreach ($browsers as $browser) {
      if (strpos($user_agent, $browser) !== FALSE) {
        $realm .= ' - ' . mt_rand(10, 999);
        break;
      }
    }
    header('WWW-Authenticate: Basic realm="' . $realm . '"');
    header('HTTP/1.0 401 Unauthorized');
  }

  // Step #4: Show the login form and/or password request form if user cancels
  // HTTP Auth dialog
  _securesite_dialog_page($content);
  module_invoke_all('exit', request_uri());
  session_write_close();
  exit;
}

/**
 * Check if pages should bypass Secure Site
 *
 * @param $path
 *   String containing the path to be filtered
 *
 * @return
 *   TRUE if Secure Site should be bypassed
 */
function _securesite_filter_check($path) {

  // Don't allow empty paths
  if (empty($path)) {
    return FALSE;
  }

  // Fetch paths to ignore
  $pages = variable_get('securesite_filter_pages', '');

  // Check if the current path matches the list defined by the admin
  $alias = drupal_get_path_alias($path);
  $regexp = '/^(' . preg_replace(array(
    '/(\\r\\n?|\\n)/',
    '/\\\\\\*/',
    '/(^|\\|)\\\\<front\\\\>($|\\|)/',
  ), array(
    '|',
    '.*',
    '\\1' . preg_quote(variable_get('site_frontpage', 'node'), '/') . '\\2',
  ), preg_quote($pages, '/')) . ')$/';
  $page_match = preg_match($regexp, $alias);

  // Check normal paths if the alias lookup fails to match
  if (!$page_match) {
    $alias = drupal_get_normal_path($path);
    $page_match = preg_match($regexp, $alias);
  }

  // Whitelist or blacklist?
  if (variable_get('securesite_filter_pages_type', SECURESITE_WHITELIST) == !$page_match) {
    return TRUE;
  }
  return FALSE;
}

Functions

Namesort descending Description
securesite_admin_settings FAPI definition for Secure Site admin settings form
securesite_init Implementation of hook_init()
securesite_menu Implementation of hook_menu()
securesite_perm Implementation of hook_perm()
securesite_user Implementation of hook_user()
_securesite_filter_check Check if pages should bypass Secure Site
_securesite_user_auth Display authentication dialog and send password reset mails

Constants

Namesort descending Description
SECURESITE_AUTH Secure Site status: Enabled with Web browser HTTP Auth security
SECURESITE_AUTH_ALT Secure Site status: Enabled with Web browser HTTP Auth security using the alterative method (deprecated)
SECURESITE_BLACKLIST Secure Site bypass: Only the listed pages do require authentication
SECURESITE_DISABLED Secure Site status: Disabled
SECURESITE_FORM Secure Site status: Enabled with HTML login form
SECURESITE_WHITELIST Secure Site bypass: Only the listed pages don't require authentication