You are here

bakery.module in Bakery Single Sign-On System 7.3

File

bakery.module
View source
<?php

/**
 * Implements hook_menu().
 */
function bakery_menu() {
  $items = array();
  $items['admin/config/system/bakery'] = array(
    'title' => 'Bakery',
    'access arguments' => array(
      'administer bakery',
    ),
    'page callback' => 'drupal_get_form',
    'page arguments' => array(
      'bakery_settings',
    ),
    'file' => 'bakery.pages.inc',
    'description' => 'Infrastructure-wide single-sign-on system options.',
  );
  if (variable_get('bakery_is_master', 0)) {
    $items['bakery/register'] = array(
      'title' => 'Register',
      'access callback' => 'user_is_anonymous',
      'page callback' => 'bakery_register_handler',
      'type' => MENU_CALLBACK,
    );
    $items['bakery/login'] = array(
      'title' => 'Login',
      'access callback' => 'user_is_anonymous',
      'page callback' => 'bakery_login_handler',
      'type' => MENU_CALLBACK,
    );
  }
  else {
    $items['bakery/register'] = array(
      'title' => 'Register',
      'access callback' => TRUE,
      'page callback' => 'bakery_register_return',
      'type' => MENU_CALLBACK,
    );
    $items['bakery/login'] = array(
      'title' => 'Login',
      'access callback' => TRUE,
      'page callback' => 'bakery_login_return',
      'type' => MENU_CALLBACK,
    );

    // Admin feature.
    $items['admin/config/people/bakery'] = array(
      'title' => 'Pull Bakery user',
      'description' => 'Request an account from the master site',
      'access arguments' => array(
        'administer users',
      ),
      'page callback' => 'drupal_get_form',
      'page arguments' => array(
        'bakery_pull_form',
      ),
      'type' => MENU_NORMAL_ITEM,
    );
  }
  return $items;
}

/**
 * Implements hook_translated_menu_link_alter().
 */
function bakery_translated_menu_link_alter(&$item, $map) {
  if ($item['href'] == 'bakery') {
    $destination = drupal_get_destination();
    $item['localized_options']['query'] = $destination;
  }
}

/**
 * Implements hook_permission().
 */
function bakery_permission() {
  return array(
    'administer bakery' => array(
      'title' => t('Administer Bakery'),
    ),
    'bypass bakery' => array(
      'title' => t('Bypass Bakery'),
      'description' => t('Bypass SSO enforcement policy and allow a user to log in without a valid SSO cookie'),
      'restrict access' => TRUE,
    ),
  );
}

/**
 * Helper function to retrieve the instance of Bakery.
 *
 * @return Bakery Instansiated Bakery object.
 */
function bakery_get_bakery() {
  $context_set =& drupal_static(__FUNCTION__, FALSE);
  if (!$context_set) {
    $config = array(
      'is_master' => variable_get('bakery_is_master', 0),
      'domain' => variable_get('bakery_domain', 'http://example.com'),
      'key' => variable_get('bakery_key', ''),
      'sso_cookie' => _bakery_cookie_name('SSO'),
      'sub_cookie' => _bakery_cookie_name('SUB'),
      'lifetime' => variable_get('bakery_freshness', 3600),
      'expiration' => variable_get('bakery_expiration', 3600 * 24 * 7),
      'debug' => variable_get('bakery_debug', FALSE),
    );
    $context_set = TRUE;
    return Bakery::instance($config);
  }
  return Bakery::instance();
}

/**
 * Implements hook_user_login().
 */
function bakery_user_login(&$edit, $account) {
  if (variable_get('bakery_is_master', 0) && isset($account->uid)) {
    $bakery = bakery_get_bakery();

    // Create SSO cookie.
    $params = array(
      'name' => $account->name,
      'mail' => $account->mail,
      'init' => _bakery_init_field($account->uid),
      'uid' => $account->uid,
    );
    $bakery
      ->setSsoCookie($params);
  }
}

/**
 * Implements hook_user_logout().
 */
function bakery_user_logout($account) {
  global $user;
  $bakery = bakery_get_bakery();

  // Only delete the SSO cookie if the name is the same in case there was an
  // existing session that's being logged out and SSO cookie is for new session.
  $cookie = $bakery
    ->validateSsoCookie();
  if ($user->uid && !is_null($cookie) && $cookie !== FALSE) {
    $bakery
      ->deleteSsoCookie();
  }
}

/**
 * Implements hook_user_update().
 */
function bakery_user_update(&$edit, $account, $category) {
  global $user;

  // Need to push changes.
  if (variable_get('bakery_is_master', 0) && isset($_SESSION['bakery'])) {
    if ($user->uid === $account->uid) {
      $bakery = bakery_get_bakery();

      // Rebake SSO cookie in case any account properties changed.
      $params = array(
        'name' => $account->name,
        'mail' => $account->mail,
        'init' => _bakery_init_field($account->uid),
        'uid' => $account->uid,
      );
      $bakery
        ->setSsoCookie($params);
    }
  }
}

/**
 * Implements hook_boot().
 */
function bakery_boot() {
  if (drupal_is_cli()) {
    return;
  }
  global $user;
  $bakery = bakery_get_bakery();
  $cookie = $bakery
    ->validateSsoCookie($user);
  if ($cookie) {
    _bakery_handle_sso($cookie);
  }
  else {

    // Invalid SSO cookie so delete and force logout if existing session.
    if ($cookie === FALSE) {
      $bakery
        ->deleteSsoCookie();
    }

    // No cookie or invalid cookie
    if (!$cookie && $user->uid > 1) {

      // Log out users that have lost their SSO cookie, with the exception of
      // UID 1 and any applied roles with permission to bypass.
      // This runs for logged in users whom are getting a full bootstrap
      // anyways.
      drupal_bootstrap(DRUPAL_BOOTSTRAP_FULL);
      if (!user_access('bypass bakery')) {
        watchdog('bakery', 'Logging out the user with the bad cookie.');
        _bakery_user_logout();
      }
    }
  }
}

/**
 * Implements hook_form_alter().
 *
 */
function bakery_form_alter(&$form, $form_state, $form_id) {
  switch ($form_id) {
    case 'user_profile_form':
    case 'user_edit_form':
      if (!variable_get('bakery_is_master', 0) && !user_access('administer users')) {
        $init_url = _bakery_init_field_url($form['#user']->init);
        $index = key($form);
        if (isset($form['account'])) {
          drupal_set_message(t('You can change the name, mail, and password <a href="!url">at the master site</a>.', array(
            '!url' => check_url($init_url),
          )), 'status', FALSE);
          $form['account']['#access'] = FALSE;
          $form['account']['name']['#access'] = FALSE;
          $form['account']['pass']['#access'] = FALSE;
          $form['account']['mail']['#access'] = FALSE;
        }
        foreach (variable_get('bakery_supported_fields', array(
          'mail' => 'mail',
          'name' => 'name',
        )) as $type => $value) {
          if ($value) {
            switch ($type) {
              case 'mail':
              case 'name':
                break;
              case 'picture':
                if (isset($form['picture'])) {
                  $form['picture']['picture_delete']['#access'] = FALSE;
                  $form['picture']['picture_upload']['#access'] = FALSE;
                  $form['picture']['#description'] = t('You can change the image <a href="!url">at the master site</a>.', array(
                    '!url' => check_url($init_url),
                  ));
                }
                break;
              case 'language':
                if (isset($form['locale'][$type])) {
                  $form['locale'][$type]['#disabled'] = TRUE;
                  $form['locale'][$type]['#description'] .= ' ' . t('You can change the language setting <a href="!url">at the master site</a>.', array(
                    '!url' => check_url($init_url),
                  ));
                }
                break;
              case 'signature':
                if (isset($form['signature_settings'][$type])) {
                  $form['signature_settings'][$type]['#disabled'] = TRUE;
                  $form['signature_settings'][$type]['#description'] .= ' ' . t('You can change the signature <a href="!url">at the master site</a>.', array(
                    '!url' => check_url($init_url),
                  ));
                }
                break;
              default:
                if (isset($form[$type])) {
                  $form[$type]['#disabled'] = TRUE;
                }
                if (isset($form[$type][$type])) {
                  $form[$type][$type]['#disabled'] = TRUE;
                  $form[$type][$type]['#description'] .= ' ' . t('You can change this setting <a href="!url">at the master site</a>.', array(
                    '!url' => check_url($init_url),
                  ));
                }
                break;
            }
          }
        }
      }
      break;
    case 'user_register_form':

      // Provide register ability on the slave sites.
      if (!variable_get('bakery_is_master', FALSE)) {
        if (arg(0) == 'admin') {

          // Admin create user form. Add a note about account synchronization.
          $form['account']['bakery_help'] = array(
            '#value' => t('<strong>Note:</strong> Only use this form to create accounts for users who exist on <a href="!url">@master</a> and not on this site. Be sure to use the exact same username and e-mail for the account here that they have on @master.', array(
              '!url' => variable_get('bakery_master', 'http://drupal.org'),
              '@master' => variable_get('bakery_master', 'http://drupal.org'),
            )),
            '#weight' => -100,
          );
        }
        else {

          // Anonymous user registration form.
          // Populate fields if set from previous attempt.
          if (isset($_SESSION['bakery']['register'])) {
            $form['account']['name']['#default_value'] = $_SESSION['bakery']['register']['name'];
            $form['account']['mail']['#default_value'] = $_SESSION['bakery']['register']['mail'];
            unset($_SESSION['bakery']['register']);
          }

          // Replace the submit handler Bakerys that will redirect to master.
          $form['#submit'] = array(
            '_bakery_register_submit',
          );
        }
      }
      break;
    case 'user_pass':

      // Slave sites need to make sure the local account exists, if the master
      // account exists.
      if (!variable_get('bakery_is_master', FALSE)) {
        array_unshift($form['#validate'], '_bakery_pass_validate');
      }
      break;
    case 'user_pass_reset':

      // As part of the slave site registration we need to handle email
      // validation and password reset.
      if (!variable_get('bakery_is_master', FALSE)) {

        // Set a submit handler for the psuedo-reset form.
        $form['#submit'] = array(
          '_bakery_reset_submit',
        );

        // Unset its custom action.
        unset($form['#action']);
      }
      break;
    case 'user_login_block':
    case 'user_login':

      // Provide login ability on the slave sites.
      if (!variable_get('bakery_is_master', FALSE)) {

        // Replace two validators from user module because they log the user in
        // and test if account exists. Bakery will instead check if the account
        // exists on the master with custom submit handler.
        $form['#validate'] = array_diff($form['#validate'], array(
          'user_login_authenticate_validate',
          'user_login_final_validate',
        ));
        $form['#submit'] = array(
          '_bakery_login_submit',
        );
      }
      break;
    default:
      break;
  }
}

/**
 * Check if a form destination is set and save it in $data array.
 *
 * Used to preserve destination in Bakery redirection to master and slave
 * during login and registration.
 *
 * @see drupal_goto()
 *
 * @param $form
 *   Form definition to check.
 * @param $data
 *   Array to store the detected destination value, if any.
 */
function _bakery_save_destination_param($form, &$data) {

  // Hold on to destination if set.
  if (strpos($form['#action'], 'destination=') !== FALSE) {

    // If an absolute URL is in destination parse_url() will issue a warning
    // and not populate $url_args so no further protection is needed.
    parse_str(parse_url($form['#action'], PHP_URL_QUERY), $url_args);
    if (!empty($url_args['destination'])) {
      $data['destination'] = $url_args['destination'];
    }
  }
}

/**
 * Handle registration by redirecting to master.
 */
function _bakery_register_submit($form, &$form_state) {
  global $base_url;

  // Create an array of fields to send to the master. We need these four fields.
  $allowed = array(
    'name',
    'mail',
    'pass',
    'timezone',
  );
  foreach ($form_state['values'] as $key => $value) {
    if (!in_array($key, $allowed)) {
      unset($form_state['values'][$key]);
    }
  }

  // Remove unneeded values.
  form_state_values_clean($form_state);

  // Save values to cookie.
  $data = $form_state['values'];
  _bakery_save_destination_param($form, $data);
  unset($_GET['destination']);

  // Store name and email in case of error and return from master.
  $_SESSION['bakery']['register'] = array(
    'name' => $data['name'],
    'mail' => $data['mail'],
  );

  // Create cookie and redirect to master.
  $bakery = bakery_get_bakery();
  $bakery
    ->setSubCookie($data['name'], $data, $base_url . '/');
  drupal_goto(variable_get('bakery_master', 'http://drupal.org/') . 'bakery/register');
}

/**
 * Handle login by redirecting to master.
 */
function _bakery_login_submit($form, &$form_state) {
  global $base_url;

  // Get rid of all the values we don't explicitly know we want. While this may
  // break some modules it ensures we don't send sensitive data between sites.
  $allowed = array(
    'name',
    'pass',
    'op',
  );
  foreach ($form_state['values'] as $key => $value) {
    if (!in_array($key, $allowed)) {
      unset($form_state['values'][$key]);
    }
  }

  //form_state_values_clean($form_state); @todo ?
  $data = $form_state['values'];
  _bakery_save_destination_param($form, $data);
  unset($_GET['destination']);

  // Save query parameters to be available when user returns from master.
  $data['query'] = drupal_get_query_parameters();

  // Create cookie and redirect to master.
  $bakery = bakery_get_bakery();
  $bakery
    ->setSubCookie($data['name'], $data, $base_url . '/');
  drupal_goto(variable_get('bakery_master', 'http://drupal.org/') . 'bakery/login');
}

/**
 * Special Bakery register callback registers the user and returns to slave.
 */
function bakery_register_handler() {
  $bakery = bakery_get_bakery();
  $cookie = $bakery
    ->validateSubCookie();
  if (!$cookie) {
    return MENU_ACCESS_DENIED;
  }
  if (variable_get('user_register', 1)) {

    // Users are allowed to register.
    $data = array();

    // Save errors.
    $errors = array();
    $name = trim($cookie['data']['name']);
    $mail = trim($cookie['data']['mail']);

    // Check if user exists with same email.
    $account = user_load_by_mail($mail);
    if ($account) {
      $errors['mail'] = 1;
    }
    else {

      // Check username.
      $account = user_load_by_name($name);
      if ($account) {
        $errors['name'] = 1;
      }
    }
  }
  else {
    watchdog('bakery', 'Master Bakery site user registration is disabled but users are trying to register from a subsite.', array(), WATCHDOG_ERROR);
    $errors['register'] = 1;
  }
  if (empty($errors)) {

    // Create user.
    $userinfo = $cookie['data'];
    if (!$cookie['data']['pass']) {
      $pass = user_password();
    }
    else {
      $pass = $cookie['data']['pass'];
    }

    // Set additional properties.
    $userinfo['name'] = $name;
    $userinfo['mail'] = $mail;
    $userinfo['pass'] = $pass;
    $userinfo['init'] = $mail;
    $userinfo['status'] = 1;
    $userinfo['authname_bakery'] = $name;
    $account = user_save('', $userinfo);

    // Set some info to return to the slave.
    $data['uid'] = $account->uid;
    $data['mail'] = $mail;
    watchdog('user', 'New external user: %name using module bakery from slave !slave.', array(
      '%name' => $account->name,
      '!slave' => $cookie['slave'],
    ), WATCHDOG_NOTICE, l(t('edit'), 'user/' . $account->uid . '/edit'));

    // Redirect to slave.
    if (!variable_get('user_email_verification', TRUE)) {

      // Create identification cookie and log user in.
      $params = array(
        'name' => $account->name,
        'mail' => $account->mail,
        'init' => _bakery_init_field($account->uid),
        'uid' => $account->uid,
      );
      $bakery
        ->setSsoCookie($params);

      // If any implementations of hook_user_login() do a redirect it will
      // break Bakery's registration flow.
      bakery_user_external_login($account);
    }
    else {

      // The user needs to validate their email, redirect back to slave to
      // inform them.
      $errors['validate'] = 1;
    }
  }
  else {

    // There were errors.
    session_destroy();
  }

  // Redirect back to custom Bakery callback on slave.
  $data['errors'] = $errors;
  $data['name'] = $name;

  // Carry destination through return.
  if (isset($cookie['data']['destination'])) {
    $data['destination'] = $cookie['data']['destination'];
  }

  // Bake a new cookie for validation on the slave.
  $bakery
    ->setSubCookie($name, $data, $cookie['slave']);
  drupal_goto($cookie['slave'] . 'bakery/register');
}

/**
 * Custom return for slave registration process.
 *
 * Redirects to the homepage on success or to the register page if there was a problem.
 */
function bakery_register_return() {
  $bakery = bakery_get_bakery();
  $cookie = $bakery
    ->validateSubCookie();
  if (!$cookie) {
    return MENU_ACCESS_DENIED;
  }

  // Cookie no longer needed.
  $bakery
    ->deleteSubCookie();

  // Destination in cookie was set before user left this site, extract it to
  // be sure destination workflow is followed.
  if (empty($cookie['data']['destination'])) {
    $destination = '<front>';
  }
  else {
    $destination = $cookie['data']['destination'];
  }
  $errors = $cookie['data']['errors'];
  if (empty($errors)) {
    drupal_set_message(t('Registration successful. You are now logged in.'));

    // Redirect to destination.
    drupal_goto($destination);
  }
  else {
    if (!empty($errors['register'])) {
      drupal_set_message(t('Registration is not enabled on @master. Please contact a site administrator.', array(
        '@master' => variable_get('bakery_master', 'http://drupal.org/'),
      )), 'error');
      watchdog('bakery', 'Master Bakery site user registration is disabled', array(), WATCHDOG_ERROR);
    }
    if (!empty($errors['validate'])) {

      // If the user must validate their email then create an account for them.
      $new = array(
        'name' => $cookie['name'],
        'mail' => $cookie['data']['mail'],
        'init' => _bakery_init_field($cookie['data']['uid']),
        'status' => 1,
        'pass' => user_password(),
      );
      $account = user_save(new stdClass(), $new);

      // Notify the user that they need to validate their email.
      _user_mail_notify('register_no_approval_required', $account);
      unset($_SESSION['bakery']['register']);
      drupal_set_message(t('A welcome message with further instructions has been sent to your e-mail address.'));
    }
    if (!empty($errors['name'])) {
      drupal_set_message(t('Name is already taken.'), 'error');
    }
    if (!empty($errors['mail'])) {
      drupal_set_message(t('E-mail address is already registered.'), 'error');
    }
    if (!empty($errors['mail_denied'])) {
      drupal_set_message(t('The e-mail address has been denied access..'), 'error');
    }
    if (!empty($errors['name_denied'])) {
      drupal_set_message(t('The name has been denied access..'), 'error');
    }

    // There are errors so keep user on registration page.
    drupal_goto('user/register');
  }
}

/**
 * Special Bakery login callback authenticates the user and returns to slave.
 */
function bakery_login_handler() {
  global $user;
  $bakery = bakery_get_bakery();
  $cookie = $bakery
    ->validateSubCookie();
  if (!$cookie) {
    return MENU_ACCESS_DENIED;
  }

  // Make sure there are query defaults.
  $cookie['data'] += array(
    'query' => array(),
  );
  $errors = array();

  // First see if the user_login form validation has any errors for them.
  $name = trim($cookie['data']['name']);
  $pass = trim($cookie['data']['pass']);

  // Execute the login form which checks username, password, status and flood.
  $form_state = array();
  $form_state['values'] = $cookie['data'];
  drupal_form_submit('user_login', $form_state);
  $errors = form_get_errors();
  if (empty($errors)) {

    // Check if account credentials are correct.
    $account = user_load_by_name($name);
    if (isset($account->uid)) {

      // Check if the mail is denied.
      if (drupal_is_denied('user', $account->mail)) {
        $errors['name'] = t('The name %name is registered using a reserved e-mail address and therefore could not be logged in.', array(
          '%name' => $name,
        ));
      }
      else {

        // Passed all checks, create identification cookie and log in.
        $params = array(
          'name' => $account->name,
          'mail' => $account->mail,
          'init' => _bakery_init_field($account->uid),
          'uid' => $account->uid,
        );
        $bakery
          ->setSsoCookie($params);
        $user = $account;
        $edit = array(
          'name' => $user->name,
        );
        bakery_user_authenticate_finalize($edit);

        // If any implementations of hook_user_login() do a redirect it will
        // break Bakery's registration flow.
        // @todo use bakery_user_external_login ? since it does auth_finalize and also checks username/password & flood
      }
    }
    else {
      $errors['incorrect-credentials'] = 1;
    }
  }
  if (!empty($errors)) {

    // Report failed login.
    watchdog('user', 'Login attempt failed for %user.', array(
      '%user' => $name,
    ));

    // Clear the messages on the master's session, since they were set during
    // drupal_form_submit() and will be displayed out of context.
    drupal_get_messages();
  }

  // Bake a new cookie for validation on the slave.
  $data = array(
    'errors' => $errors,
    'name' => $name,
  );

  // Carry destination through login.
  if (isset($cookie['data']['destination'])) {
    $data['destination'] = $cookie['data']['destination'];
  }

  // Carry other query parameters through login.
  $data['query'] = $cookie['data']['query'];
  $bakery
    ->setSubCookie($name, $data, $cookie['slave']);
  drupal_goto($cookie['slave'] . '/bakery/login');
}

/**
 * Custom return for errors during slave login process.
 */
function bakery_login_return() {
  global $user;
  $bakery = bakery_get_bakery();
  $cookie = $bakery
    ->validateSubCookie();
  if ($cookie) {

    // Make sure we always have a default query key.
    $cookie['data'] += array(
      'query' => array(),
    );

    // Cookie no longer needed.
    $bakery
      ->deleteSubCookie();
    if (!empty($cookie['data']['errors'])) {
      $errors = $cookie['data']['errors'];
      if (!empty($errors['incorrect-credentials'])) {
        drupal_set_message(t('Sorry, unrecognized username or password.'), 'error');
      }
      elseif (!empty($errors['name'])) {

        // In case an attacker got the hash we filter the argument here to avoid
        // exposing a XSS vector.
        drupal_set_message(filter_xss($errors['name']), 'error');
      }
    }

    // Prepare the url options array to pass to drupal_goto().
    $options = array(
      'query' => $cookie['data']['query'],
    );
    if (empty($cookie['data']['destination'])) {
      drupal_goto('user', $options);
    }
    else {
      $destination = $cookie['data']['destination'];
      if (($pos = strpos($cookie['data']['destination'], '?')) !== FALSE) {

        // Destination contains query arguments that must be extracted.
        $destination = substr($cookie['data']['destination'], 0, $pos);
        $options['query'] += drupal_get_query_array(substr($cookie['data']['destination'], $pos + 1));
      }
      drupal_goto($destination, $options);
    }
  }
  elseif (user_is_logged_in()) {
    drupal_goto();
  }
  drupal_access_denied();
}

/**
 * Name for cookie including session.cookie_secure and variable extension.
 *
 * @param string $type
 * @return string
 *   The cookie name for this environment.
 */
function _bakery_cookie_name($type = 'SSO') {

  // Use different names for HTTPS and HTTP to prevent a cookie collision.
  if (ini_get('session.cookie_secure')) {
    $type .= 'SSL';
  }

  // Allow site to modify the cookie name.
  $extension = variable_get('bakery_cookie_extension', '');
  $type .= $extension;
  return 'BAKERY' . $type;
}

/**
 * Authenticate from valid SSO cookie.
 *
 * @param array $cookie
 */
function _bakery_handle_sso($cookie) {
  global $user;

  // Detect SSO cookie mismatch if there is already a valid session for user.
  if ($user->uid && $cookie['name'] !== $user->name) {

    // The SSO cookie doesn't match the existing session so force a logout.
    drupal_bootstrap(DRUPAL_BOOTSTRAP_FULL);
    _bakery_user_logout();
  }
  elseif ($user->uid > 0) {

    // User is already authenticated as SSO user.
    return;
  }

  // Since this might happen in hook_boot we need to bootstrap first.
  // Note that this only runs if they have a valid session on the master
  // and do not have one on the slave so it only creates the extra load of
  // a bootstrap on one pageview per session on the site which is not much.
  drupal_bootstrap(DRUPAL_BOOTSTRAP_FULL);

  // User is anonymous. If they do not have an account we'll create one by
  // requesting their information from the master site. If they do have an
  // account we may need to correct some disparant information.
  $account = user_load_multiple(array(), array(
    'name' => $cookie['name'],
    'mail' => $cookie['mail'],
  ));
  $account = reset($account);

  // Fix out of sync users with valid init.
  if (!$account && !variable_get('bakery_is_master', 0) && $cookie['master']) {
    $count = db_select('users', 'u')
      ->fields('u', array(
      'uid',
    ))
      ->condition('init', $cookie['init'])
      ->countQuery()
      ->execute()
      ->fetchField();
    if ($count > 1) {

      // Uh oh.
      watchdog('bakery', 'Account uniqueness problem: Multiple users found with init %init.', array(
        '%init' => $cookie['init'],
      ), WATCHDOG_ERROR);
      drupal_set_message(t('Account uniqueness problem detected. <a href="@contact">Please contact the site administrator.</a>', array(
        '@contact' => variable_get('bakery_master', 'http://drupal.org/') . 'contact',
      )), 'error');
    }
    if ($count == 1) {
      $account = user_load_multiple(array(), array(
        'init' => $cookie['init'],
      ));
      if (is_array($account)) {
        $account = reset($account);
      }
      if ($account) {
        watchdog('bakery', 'Fixing out of sync uid %uid. Changed name %name_old to %name_new, mail %mail_old to %mail_new.', array(
          '%uid' => $account->uid,
          '%name_old' => $account->name,
          '%name_new' => $cookie['name'],
          '%mail_old' => $account->mail,
          '%mail_new' => $cookie['mail'],
        ));
        user_save($account, array(
          'name' => $cookie['name'],
          'mail' => $cookie['mail'],
        ));
        $account = user_load_multiple(array(), array(
          'name' => $cookie['name'],
          'mail' => $cookie['mail'],
        ));
        $account = reset($account);
      }
    }
  }

  // Create the account if it doesn't exist.
  if (!$account && !variable_get('bakery_is_master', 0) && $cookie['master']) {

    // Check if existing account with same name, mail or init.
    if (_bakery_check_account($user->uid, $cookie)) {
      $account = bakery_create_account($cookie);
    }
    else {
      drupal_set_message(t('Your user account on %site appears to have problems. Would you like to try to <a href="@url">repair it yourself</a>?', array(
        '%site' => variable_get('site_name', 'Drupal'),
        '@url' => url('bakery/repair'),
      )));
      drupal_set_message(filter_xss_admin(variable_get('bakery_help_text', 'Otherwise you can contact the site administrators.')));
      $_SESSION['BAKERY_CRUMBLED'] = TRUE;
    }
  }
  if ($account && $cookie['master'] && $account->uid && !variable_get('bakery_is_master', 0) && $account->init != $cookie['init']) {

    // User existed previously but init is wrong. Fix it to ensure account
    // remains in sync.
    // Make sure that there aren't any OTHER accounts with this init already.
    $count = db_select('users', 'u')
      ->fields('u', array(
      'uid',
    ))
      ->condition('init', $cookie['init'], '=')
      ->countQuery()
      ->execute()
      ->fetchField();
    if ($count == 0) {
      db_update('users')
        ->fields(array(
        'init' => $cookie['init'],
      ))
        ->condition('uid', $account->uid)
        ->execute();
      watchdog('bakery', 'uid %uid out of sync. Changed init field from %oldinit to %newinit', array(
        '%oldinit' => $account->init,
        '%newinit' => $cookie['init'],
        '%uid' => $account->uid,
      ));
    }
    else {

      // Username and email matched, but init belonged to a DIFFERENT account.
      // Something got seriously tangled up.
      watchdog('bakery', 'Accounts mixed up! Username %user and init %init disagree with each other!', array(
        '%user' => $account->name,
        '%init' => $cookie['init'],
      ), WATCHDOG_CRITICAL);
    }
  }
  if ($account && $user->uid == 0) {

    // If the login attempt fails destroy the cookie to prevent infinite
    // redirects (with infinite failed login messages).
    $login = bakery_user_external_login($account);
    if ($login) {

      // If an anonymous user has just been logged in, trigger a 'refresh'
      // of the current page, ensuring that drupal_goto() does not override
      // the current page with the destination query.
      $query = drupal_get_query_parameters();
      unset($_GET['destination']);
      drupal_goto(current_path(), array(
        'query' => $query,
      ));
    }
  }
}

/**
 * Check for a different account with same name, email or Bakery init field.
 *
 * @param  int $uid UID of current account.
 * @param  array $data Array of properties to find account on.
 * @return bool TRUE if another account with same name, email, or init field.
 */
function _bakery_check_account($uid, $data) {
  $checks = TRUE;
  $mail_count = db_select('users', 'u')
    ->fields('u', array(
    'uid',
  ))
    ->condition('uid', $uid, '!=')
    ->condition('mail', '', '!=')
    ->where('LOWER(mail) = LOWER(:mail)', array(
    ':mail' => $data['mail'],
  ))
    ->countQuery()
    ->execute()
    ->fetchField();
  if ($mail_count > 0) {
    $checks = FALSE;
  }
  $name_count = db_select('users', 'u')
    ->fields('u', array(
    'uid',
  ))
    ->condition('uid', $uid, '!=')
    ->where('LOWER(name) = LOWER(:name)', array(
    ':name' => $data['name'],
  ))
    ->countQuery()
    ->execute()
    ->fetchField();
  if ($name_count > 0) {
    $checks = FALSE;
  }
  $init_count = db_select('users', 'u')
    ->fields('u', array(
    'uid',
  ))
    ->condition('uid', $uid, '!=')
    ->condition('init', $data['init'], '=')
    ->where('LOWER(name) = LOWER(:name)', array(
    ':name' => $data['name'],
  ))
    ->countQuery()
    ->execute()
    ->fetchField();
  if ($init_count > 0) {
    $checks = FALSE;
  }
  return $checks;
}

/**
 * Hookable local user account creation from SSO cookie properties.
 *
 * @param  array Array of properties, often the SSO cookie array.
 * @return User account object.
 */
function bakery_create_account($data) {

  // @todo make hook
  $new_account = array(
    'name' => $data['name'],
    'pass' => user_password(),
    'mail' => $data['mail'],
    'status' => 1,
    'init' => _bakery_init_field($data['uid']),
  );
  $account = user_save(NULL, $new_account);
  return $account;
}

/**
 * Build internal init url (without scheme).
 */
function _bakery_init_field($uid) {
  $url = variable_get('bakery_master', 'http://drupal.org/');
  $scheme = parse_url($url, PHP_URL_SCHEME);
  return str_replace($scheme . '://', '', $url) . 'user/' . $uid . '/edit';
}

/**
 * Build full init url to master.
 */
function _bakery_init_field_url($init) {
  $scheme = parse_url(variable_get('bakery_master', 'http://drupal.org/'), PHP_URL_SCHEME);
  return $scheme . '://' . $init;
}

/**
 * Perform standard Drupal login operations for a user object.
 *
 * The user object must already be authenticated. This function verifies
 * that the user account is not blocked/denied and then performs the login,
 * updates the login timestamp in the database, invokes hook_user('login'),
 * and regenerates the session.
 *
 * @param $account
 *    An authenticated user object to be set as the currently logged
 *    in user.
 * @param $edit
 *    The array of form values submitted by the user, if any.
 *    This array is passed to hook_user op login.
 * @return boolean
 *    TRUE if the login succeeds, FALSE otherwise.
 */
function bakery_user_external_login($account, $edit = array()) {
  $form = drupal_get_form('user_login');
  $state['values'] = $edit;
  if (empty($state['values']['name'])) {
    $state['values']['name'] = $account->name;
  }

  // Check if user is blocked or denied by access rules.
  user_login_name_validate($form, $state, (array) $account);
  if (form_get_errors()) {

    // Invalid login.
    return FALSE;
  }

  // Valid login.
  global $user;
  $user = $account;
  bakery_user_authenticate_finalize($state['values']);
  return TRUE;
}

/**
 * Finalize the login process. Must be called when logging in a user.
 *
 * The function records a watchdog message about the new session, saves the
 * login timestamp, calls hook_user op 'login' and generates a new session.
 *
 * $param $edit
 *   This array is passed to hook_user op login.
 */
function bakery_user_authenticate_finalize(&$edit) {
  global $user;
  watchdog('user', 'Session opened for %name.', array(
    '%name' => $user->name,
  ));

  // Update the user table timestamp noting user has logged in.
  // 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.
  drupal_session_regenerate();
  user_module_invoke('login', $edit, $user);
}

/**
 * Custom logout function modified from user_logout.
 */
function _bakery_user_logout() {
  global $user;
  watchdog('user', 'Session closed for %name.', array(
    '%name' => $user->name,
  ));

  // Destroy the current session:
  session_destroy();
  module_invoke_all('user_logout', $user);

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

  // We want to redirect the user to his original destination.
  $get = $_GET;
  $destination = !empty($get['q']) ? $get['q'] : '';
  unset($get['q']);

  // We append a GET parameter so that the browser reloads the page.
  $get['no_cache'] = time();

  // Build the URL we'll redirect to. We set alias to TRUE so as not to try and
  // hit the unavailable database looking for an alias.
  $url = url($destination, array(
    'query' => $get,
    'absolute' => TRUE,
    'alias' => TRUE,
  ));

  // Remove newlines from the URL to avoid header injection attacks.
  $url = str_replace(array(
    "\n",
    "\r",
  ), '', $url);

  // We can't use drupal_goto because it assumes it's in a later boot phase. Set
  // the status code to be temporary redirect because of the no_cache time.
  header('Location: ' . $url, TRUE, 307);
  exit;
}

Functions

Namesort descending Description
bakery_boot Implements hook_boot().
bakery_create_account Hookable local user account creation from SSO cookie properties.
bakery_form_alter Implements hook_form_alter().
bakery_get_bakery Helper function to retrieve the instance of Bakery.
bakery_login_handler Special Bakery login callback authenticates the user and returns to slave.
bakery_login_return Custom return for errors during slave login process.
bakery_menu Implements hook_menu().
bakery_permission Implements hook_permission().
bakery_register_handler Special Bakery register callback registers the user and returns to slave.
bakery_register_return Custom return for slave registration process.
bakery_translated_menu_link_alter Implements hook_translated_menu_link_alter().
bakery_user_authenticate_finalize Finalize the login process. Must be called when logging in a user.
bakery_user_external_login Perform standard Drupal login operations for a user object.
bakery_user_login Implements hook_user_login().
bakery_user_logout Implements hook_user_logout().
bakery_user_update Implements hook_user_update().
_bakery_check_account Check for a different account with same name, email or Bakery init field.
_bakery_cookie_name Name for cookie including session.cookie_secure and variable extension.
_bakery_handle_sso Authenticate from valid SSO cookie.
_bakery_init_field Build internal init url (without scheme).
_bakery_init_field_url Build full init url to master.
_bakery_login_submit Handle login by redirecting to master.
_bakery_register_submit Handle registration by redirecting to master.
_bakery_save_destination_param Check if a form destination is set and save it in $data array.
_bakery_user_logout Custom logout function modified from user_logout.