includes/clientside/static/login.js
author Dan
Fri, 18 Dec 2009 20:57:28 -0500
changeset 1179 ac861c01a764
parent 1176 5c98b9f181f0
child 1210 ad49fa34ff3c
permissions -rw-r--r--
Added an upgrade hook to populate the author_uid column in logs, pending test by Neal

/*
 * AJAX-based intelligent login interface
 */

/*
 * FRONTEND
 */

/**
 * Performs a logon as a regular member.
 */

window.ajaxLogonToMember = function()
{
  // IE <6 pseudo-compatibility
  if ( KILL_SWITCH )
    return true;
  if ( auth_level >= USER_LEVEL_MEMBER )
    return true;
  ajaxLoginInit(function(k)
    {
      if ( on_main_page && main_page_members != physical_title )
      {
        window.location = makeUrl(main_page_members);
      }
      else
      {
        window.location.reload();
      }
    }, USER_LEVEL_MEMBER);
}

/**
 * Authenticates to the highest level the current user is allowed to go to.
 */

window.ajaxLogonToElev = function()
{
  if ( auth_level == user_level )
    return true;
  
  ajaxLoginInit(function(k)
    {
      ENANO_SID = k;
      var url = String(' ' + window.location).substr(1);
      url = append_sid(url);
      window.location = url;
    }, user_level);
}

/*
 * BACKEND
 */

/**
 * Holding object for various AJAX authentication information.
 * @var object
 */

var logindata = {};

/**
 * Path to the image used to indicate loading progress
 * @var string
 */

if ( !ajax_login_loadimg_path )
  var ajax_login_loadimg_path = false;

if ( !ajax_login_successimg_path )
  var ajax_login_successimg_path = false;

if ( !ajax_login_lockimg_path )
  var ajax_login_lockimg_path = false;

/**
 * Status variables
 * @var int
 */

var AJAX_STATUS_LOADING_KEY = 1;
var AJAX_STATUS_GENERATING_KEY = 2;
var AJAX_STATUS_LOGGING_IN = 3;
var AJAX_STATUS_SUCCESS = 4;
var AJAX_STATUS_ERROR = 5;
var AJAX_STATUS_DESTROY = 65535;

/**
 * State constants
 * @var int
 */

var AJAX_STATE_EARLY_INIT = 1;
var AJAX_STATE_LOADING_KEY = 2;

/**
 * Switch to decide if DiffieHellman shows a "browser incompatible" error
 * @var bool
 */

var ajax_login_prevent_dh = ( IE && !IE_8 ) || ( is_iPhone && !is_iPhone_3 );

/**
 * Performs the AJAX request to get an encryption key and from there spawns the login form.
 * @param function The function that will be called once authentication completes successfully.
 * @param int The security level to authenticate at - see http://docs.enanocms.org/Help:Appendix_B
 */

window.ajaxLoginInit = function(call_on_finish, user_level)
{
  load_component(['messagebox', 'flyin', 'fadefilter', 'jquery', 'jquery-ui', 'l10n', 'crypto']);
  
  logindata = {};
  
  var title = ( user_level > USER_LEVEL_MEMBER ) ? $lang.get('user_login_ajax_prompt_title_elev') : $lang.get('user_login_ajax_prompt_title');
  logindata.mb_object = new MessageBox(MB_OKCANCEL | MB_ICONLOCK, title, '');
  
  //
  // Cancel function: called when the "Cancel" button is clicked
  //
  logindata.mb_object.onclick['Cancel'] = function()
  {
    // Hide the error message, if any
    $('#ajax_login_error_box').remove();
    // Hide the captcha, if any
    if ( document.getElementById('autoCaptcha') )
    {
      var to = fly_out_top(document.getElementById('autoCaptcha'), false, true);
      setTimeout(function() {
          var d = document.getElementById('autoCaptcha');
          d.parentNode.removeChild(d);
        }, to);
    }
    // Ask the server to delete the encryption key we're using
    ajaxLoginPerformRequest({
        mode: 'clean_key',
        key_aes: logindata.key_aes,
        key_dh: logindata.key_dh
    });
  };
  
  // Clicking OK will not cause the box to destroy, as this function returns true.
  logindata.mb_object.onbeforeclick['OK'] = function()
  {
    // Just call the submitter and let it take care of everything
    ajaxLoginSubmitForm();
    return true;
  }
  
  // Fetch the inner content area
  logindata.mb_inner = document.getElementById('messageBox').getElementsByTagName('div')[0];
  
  // Initialize state
  logindata.showing_status = false;
  logindata.user_level = user_level;
  logindata.successfunc = call_on_finish;
  
  // Build the "loading" window
  ajaxLoginSetStatus(AJAX_STATUS_LOADING_KEY);
  
  // Request the key
  ajaxLoginPerformRequest({ mode: 'getkey' });
}

/**
 * For compatibility only. Really, folks, it's ajaxLoginInit. If you need a
 * mnemonic device, use "two 'in's."
 */

window.ajaxLogonInit = function(call_on_finish, user_level)
{
  return ajaxLoginInit(call_on_finish, user_level);
}

/**
 * Sets the contents of the AJAX login window to the appropriate status message.
 * @param int One of AJAX_STATUS_* constants
 */

window.ajaxLoginSetStatus = function(status)
{
  if ( !logindata.mb_inner )
    return false;
  if ( logindata.showing_status )
  {
    var div = document.getElementById('ajax_login_status');
    if ( div )
      logindata.mb_inner.removeChild(div);
  }
  switch(status)
  {
    case AJAX_STATUS_LOADING_KEY:
      
      // Create the status div
      var div = document.createElement('div');
      div.id = 'ajax_login_status';
      div.style.marginTop = '10px';
      div.style.textAlign = 'center';
      
      // The circly ball ajaxy image + status message
      var status_msg = $lang.get('user_login_ajax_fetching_key');
      
      // Insert the status message
      div.appendChild(document.createTextNode(status_msg));
      
      // Append a br or two to space things properly
      div.appendChild(document.createElement('br'));
      div.appendChild(document.createElement('br'));
      
      var img = document.createElement('img');
      img.src = ( ajax_login_loadimg_path ) ? ajax_login_loadimg_path : scriptPath + '/images/loading-big.gif';
      div.appendChild(img);
      
      // Another coupla brs
      div.appendChild(document.createElement('br'));
      div.appendChild(document.createElement('br'));
      
      // The link to the full login form
      var small = document.createElement('small');
      small.innerHTML = $lang.get('user_login_ajax_link_fullform', { link_full_form: makeUrlNS('Special', 'Login/' + title) });
      div.appendChild(small);
      
      // Insert the entire message into the login window
      logindata.mb_inner.innerHTML = '';
      logindata.mb_inner.appendChild(div);
      
      break;
    case AJAX_STATUS_GENERATING_KEY:
      
      // Create the status div
      var div = document.createElement('div');
      div.id = 'ajax_login_status';
      div.style.marginTop = '10px';
      div.style.textAlign = 'center';
      
      // The circly ball ajaxy image + status message
      var status_msg = $lang.get('user_login_ajax_generating_key');
      
      // Insert the status message
      div.appendChild(document.createTextNode(status_msg));
      
      // Append a br or two to space things properly
      div.appendChild(document.createElement('br'));
      div.appendChild(document.createElement('br'));
      
      var img = document.createElement('img');
      img.src = ( ajax_login_lockimg_path ) ? ajax_login_lockimg_path : scriptPath + '/images/lock48.png';
      div.appendChild(img);
      
      // Another coupla brs
      div.appendChild(document.createElement('br'));
      div.appendChild(document.createElement('br'));
      
      // The link to the full login form
      var small = document.createElement('small');
      small.innerHTML = $lang.get('user_login_ajax_link_fullform_dh', { link_full_form: makeUrlNS('Special', 'Login/' + title) });
      div.appendChild(small);
      
      // Insert the entire message into the login window
      logindata.mb_inner.innerHTML = '';
      logindata.mb_inner.appendChild(div);
      
      break;
    case AJAX_STATUS_LOGGING_IN:
      
      // Create the status div
      var div = document.createElement('div');
      div.id = 'ajax_login_status';
      div.style.marginTop = '10px';
      div.style.textAlign = 'center';
      
      // The circly ball ajaxy image + status message
      var status_msg = $lang.get('user_login_ajax_loggingin');
      
      // Insert the status message
      div.appendChild(document.createTextNode(status_msg));
      
      // Append a br or two to space things properly
      div.appendChild(document.createElement('br'));
      div.appendChild(document.createElement('br'));
      
      var img = document.createElement('img');
      img.src = ( ajax_login_loadimg_path ) ? ajax_login_loadimg_path : scriptPath + '/images/loading-big.gif';
      div.appendChild(img);
      
      // Insert the entire message into the login window
      logindata.mb_inner.innerHTML = '';
      logindata.mb_inner.appendChild(div);
      
      break;
    case AJAX_STATUS_SUCCESS:
      
      // Create the status div
      var div = document.createElement('div');
      div.id = 'ajax_login_status';
      div.style.marginTop = '10px';
      div.style.textAlign = 'center';
      
      // The circly ball ajaxy image + status message
      var status_msg = $lang.get('user_login_success_short');
      
      // Insert the status message
      div.appendChild(document.createTextNode(status_msg));
      
      // Append a br or two to space things properly
      div.appendChild(document.createElement('br'));
      div.appendChild(document.createElement('br'));
      
      var img = document.createElement('img');
      img.src = ( ajax_login_successimg_path ) ? ajax_login_successimg_path : scriptPath + '/images/check.png';
      div.appendChild(img);
      
      // Insert the entire message into the login window
      logindata.mb_inner.innerHTML = '';
      logindata.mb_inner.appendChild(div);
      
      break;
      
    case AJAX_STATUS_ERROR:
      // Create the status div
      var div = document.createElement('div');
      div.id = 'ajax_login_status';
      div.style.marginTop = '10px';
      div.style.textAlign = 'center';
      
      // The circly ball ajaxy image + status message
      var status_msg = $lang.get('user_login_ajax_err_crypto');
      
      // Insert the status message
      div.appendChild(document.createTextNode(status_msg));
      
      // Append a br or two to space things properly
      div.appendChild(document.createElement('br'));
      div.appendChild(document.createElement('br'));
      
      var img = document.createElement('img');
      img.src = ( ajax_login_successimg_path ) ? ajax_login_successimg_path : scriptPath + '/images/checkbad.png';
      div.appendChild(img);
      
      // Append a br or two to space things properly
      div.appendChild(document.createElement('br'));
      div.appendChild(document.createElement('br'));
      
      // The circly ball ajaxy image + status message
      var detail_msg = $lang.get('user_login_ajax_err_crypto_details');
      var full_link = $lang.get('user_login_ajax_err_crypto_link');
      var link = document.createElement('a');
      link.href = makeUrlNS('Special', 'Login/' + title, 'level=' + logindata.user_level, true);
      link.appendChild(document.createTextNode(full_link));
      var span = document.createElement('span');
      span.style.fontSize = 'smaller';
      
      // Insert the message
      span.appendChild(document.createTextNode(detail_msg + ' '));
      span.appendChild(link);
      div.appendChild(span);
      
      // Insert the entire message into the login window
      logindata.mb_inner.innerHTML = '';
      logindata.mb_inner.appendChild(div);
      
      break;
      
    default:
      eval(setHook('login_set_status'));
      break;
      
    case AJAX_STATUS_DESTROY:
    case null:
    case undefined:
      logindata.showing_status = false;
      return;
      break;
  }
  logindata.showing_status = true;
}

/**
 * Performs an AJAX logon request to the server and calls ajaxLoginProcessResponse() on the result.
 * @param object JSON packet to send
 * @param function Optional function to call on the response as well.
 */

window.ajaxLoginPerformRequest = function(json, _hookfunc)
{
  json = toJSONString(json);
  json = ajaxEscape(json);
  var hookfunc = typeof(_hookfunc) == 'function' ? _hookfunc : false;
  ajaxPost(makeUrlNS('Special', 'Login/action.json'), 'r=' + json, function(ajax)
    {
      if ( ajax.readyState == 4 && ajax.status == 200 )
      {
        // parse response
        var response = String(ajax.responseText + '');
        if ( !check_json_response(response) )
        {
          handle_invalid_json(response);
          return false;
        }
        response = parseJSON(response);
        ajaxLoginProcessResponse(response, hookfunc);
      }
    }, true);
}

/**
 * Processes a response from the login server
 * @param object JSON response
 */

window.ajaxLoginProcessResponse = function(response, hookfunc)
{
  // Did the server send a plaintext error?
  if ( response.mode == 'error' )
  {
    if ( logindata.mb_object )
    {
      logindata.mb_object.destroy();
      var error_msg = $lang.get('user_' + ( response.error.toLowerCase() ));
      new MessageBox(MB_ICONSTOP | MB_OK, $lang.get('user_err_login_generic_title'), error_msg);
    }
    else
    {
      alert(response.error);
    }
    return false;
  }
  
  // Main mode switch
  switch ( response.mode )
  {
    case 'initial':
      // Rid ourselves of any loading windows
      ajaxLoginSetStatus(AJAX_STATUS_DESTROY);
      // show any errors
      ajaxLoginShowFriendlyError(response);
      // The server wants us to build the login form, all the information is there
      ajaxLoginBuildForm(response);
      break;
    case 'login_success':
      ajaxLoginSetStatus(AJAX_STATUS_SUCCESS);
      logindata.successfunc(response.key, response);
      break;
    case 'reset_pass_used':
      // We logged in with a temporary password. Prompt the user to go to the temp password page and
      // reset their real password. If they click no, treat it as a login failure, as no session key
      // is actually issued when this type of login is performed.
      
      var conf = confirm($lang.get('user_login_ajax_msg_used_temp_pass'));
      if ( conf )
      {
        var url = makeUrlNS('Special', 'PasswordReset/stage2/' + response.user_id + '/' + response.temp_password);
        window.location = url;
        break;
      }
      // else, treat as a failure
    default:
      // Rid ourselves of any loading windows
      ajaxLoginSetStatus(AJAX_STATUS_DESTROY);
      document.getElementById('messageBox').style.backgroundColor = '#C0C0C0';
      var mb_parent = document.getElementById('messageBox').parentNode;
      $(mb_parent).effect("shake", {}, 200);
      setTimeout(function()
        {
          document.getElementById('messageBox').style.backgroundColor = '#FFF';
          console.debug(response);
          ajaxLoginShowFriendlyError(response);
          ajaxLoginBuildForm(response);
        }, 2500);
      
      break;
    case 'logout_success':
      if ( ENANO_SID )
      {
        ajaxLoginReplaceSIDInline(false, ENANO_SID, USER_LEVEL_MEMBER);
      }
      break;
    case 'noop':
      break;
  }
  if ( hookfunc )
  {
    hookfunc(response);
  }
}

/*
 * RESPONSE HANDLERS
 */

/**
 * Builds the login form.
 * @param object Metadata to build off of
 */

window.ajaxLoginBuildForm = function(data)
{
  // let's hope this effectively preloads the image...
  var _1 = document.createElement('img');
  _1.src = ( ajax_login_successimg_path ) ? ajax_login_successimg_path : scriptPath + '/images/check.png';
  var _2 = document.createElement('img');
  _2.src = ( ajax_login_lockimg_path ) ? ajax_login_lockimg_path : scriptPath + '/images/lock48.png';
  
  var div = document.createElement('div');
  div.id = 'ajax_login_form';
  
  var show_captcha = ( data.lockout.active && data.lockout.policy == 'captcha' ) ? data.lockout.captcha : false;
  
  // text displayed on re-auth
  if ( logindata.user_level > USER_LEVEL_MEMBER )
  {
    div.innerHTML += $lang.get('user_login_ajax_prompt_body_elev') + '<br /><br />';
  }
  
  // Create the form
  var form = document.createElement('form');
  form.action = 'javascript:void(ajaxLoginSubmitForm());';
  form.onsubmit = function()
  {
    ajaxLoginSubmitForm();
    return false;
  }
  if ( IE )
  {
    form.style.marginTop = '-20px';
  }
  
  // Using tables to wrap form elements because it results in a
  // more visually appealing form. Yes, tables suck. I don't really
  // care - they make forms look good.
  
  var table = document.createElement('table');
  table.style.margin = '0 auto';
  
  // Field - username
  var tr1 = document.createElement('tr');
  var td1_1 = document.createElement('td');
  td1_1.appendChild(document.createTextNode($lang.get('user_login_field_username') + ':'));
  tr1.appendChild(td1_1);
  var td1_2 = document.createElement('td');
  var f_username = document.createElement('input');
  f_username.id = 'ajax_login_field_username';
  f_username.name = 'ajax_login_field_username';
  f_username.type = 'text';
  f_username.size = '25';
  if ( data.username )
    f_username.value = data.username;
  td1_2.appendChild(f_username);
  tr1.appendChild(td1_2);
  table.appendChild(tr1);
  
  // Field - password
  var tr2 = document.createElement('tr');
  var td2_1 = document.createElement('td');
  td2_1.appendChild(document.createTextNode($lang.get('user_login_field_password') + ':'));
  tr2.appendChild(td2_1);
  var td2_2 = document.createElement('td');
  var f_password = document.createElement('input');
  f_password.id = 'ajax_login_field_password';
  f_password.name = 'ajax_login_field_username';
  f_password.type = 'password';
  f_password.size = '25';
  if ( !show_captcha )
  {
    f_password.onkeyup = function(e)
    {
      if ( !e )
        e = window.event;
      if ( !e && IE )
        return true;
      if ( e.keyCode == 13 )
      {
        ajaxLoginSubmitForm();
      }
    }
  }
  td2_2.appendChild(f_password);
  tr2.appendChild(td2_2);
  table.appendChild(tr2);
  
  // Field - captcha
  if ( show_captcha )
  {
    var tr3 = document.createElement('tr');
    var td3_1 = document.createElement('td');
    td3_1.appendChild(document.createTextNode($lang.get('user_login_field_captcha') + ':'));
    tr3.appendChild(td3_1);
    var td3_2 = document.createElement('td');
    var f_captcha = document.createElement('input');
    f_captcha.id = 'ajax_login_field_captcha';
    f_captcha.name = 'ajax_login_field_username';
    f_captcha.type = 'text';
    f_captcha.size = '25';
    f_captcha.onkeyup = function(e)
    {
      if ( !e )
        e = window.event;
      if ( !e.keyCode )
        return true;
      if ( e.keyCode == 13 )
      {
        ajaxLoginSubmitForm();
      }
    }
    td3_2.appendChild(f_captcha);
    tr3.appendChild(td3_2);
    table.appendChild(tr3);
  }
  
  // ok, this is a compatibility hack
  data.locked_out = { locked_out: data.lockout.active };
  
  // hook for the login form
  eval(setHook('login_build_form'));
  
  delete(data.locked_out);
  
  // Done building the main part of the form
  form.appendChild(table);
  
  // Checkbox container
  var boxen = document.createElement('div');
  boxen.style.textAlign = 'center';
  boxen.style.padding = '7px 0';
  
  // Field: remember login
  if ( logindata.user_level <= USER_LEVEL_MEMBER )
  {
    var lbl_remember = document.createElement('label');
    lbl_remember.style.fontSize = 'smaller';
    lbl_remember.style.textAlign = 'center';
    
    // figure out what text to put in the "remember me" checkbox
    // infinite session length?
    if ( data.extended_time == 0 )
    {
      // yes, infinite
      var txt_remember = $lang.get('user_login_ajax_check_remember_infinite');
    }
    else
    {
      if ( data.extended_time % 7 == 0 )
      {
        // number of days is a multiple of 7
        // use weeks as our unit
        var sess_time = data.extended_time / 7;
        var unit = 'week';
      }
      else
      {
        // use days as our unit
        var sess_time = data.extended_time;
        var unit = 'day';
      }
      // more than one week or day?
      if ( sess_time != 1 )
        unit += 's';
      
      // assemble the string
      var txt_remember = $lang.get('user_login_ajax_check_remember', {
          session_length: sess_time,
          length_units: $lang.get('etc_unit_' + unit)
        });
    }
    var check_remember = document.createElement('input');
    check_remember.type = 'checkbox';
    // this onclick attribute changes the cookie whenever the checkbox or label is clicked
    check_remember.setAttribute('onclick', 'var ck = ( this.checked ) ? "enable" : "disable"; createCookie("login_remember", ck, 3650);');
    if ( readCookie('login_remember') != 'disable' )
      check_remember.setAttribute('checked', 'checked');
    check_remember.id = 'ajax_login_field_remember';
    lbl_remember.appendChild(check_remember);
    lbl_remember.innerHTML += ' ' + txt_remember;
    
    boxen.appendChild(lbl_remember);
  }
  
  var bullet = document.createElement('span');
  bullet.innerHTML = '&nbsp;';
  bullet.style.fontSize = '12pt';
  bullet.style.borderRight = '1px solid #aaa';
  bullet.style.margin = '0 6px 0 4px';
  
  // Field: enable Diffie Hellman
  if ( ajax_login_prevent_dh )
  {
    if ( logindata.user_level <= USER_LEVEL_MEMBER )
      // only show this if both checkboxes are visible
      boxen.appendChild(bullet);
      
    var lbl_dh = document.createElement('span');
    lbl_dh.style.fontSize = 'smaller';
    lbl_dh.style.textAlign = 'center';
    lbl_dh.innerHTML = $lang.get('user_login_ajax_check_dh_ie');
    boxen.appendChild(lbl_dh);
  }
  else if ( !data.crypto.dh_enable )
  {
    // create hidden control - server requested that DiffieHellman be disabled (usually means not supported)
    var check_dh = document.createElement('input');
    check_dh.type = 'hidden';
    check_dh.id = 'ajax_login_field_dh';
    boxen.appendChild(check_dh);
  }
  else
  {
    if ( logindata.user_level <= USER_LEVEL_MEMBER )
      // only show this if both checkboxes are visible
      boxen.appendChild(bullet);
    
    var lbl_dh = document.createElement('label');
    lbl_dh.style.fontSize = 'smaller';
    lbl_dh.style.textAlign = 'center';
    var check_dh = document.createElement('input');
    check_dh.type = 'checkbox';
    // this onclick attribute changes the cookie whenever the checkbox or label is clicked
    check_dh.setAttribute('onclick', 'var ck = ( this.checked ) ? "enable" : "disable"; createCookie("diffiehellman_login", ck, 3650);');
    if ( readCookie('diffiehellman_login') != 'disable' )
      check_dh.setAttribute('checked', 'checked');
    check_dh.id = 'ajax_login_field_dh';
    lbl_dh.appendChild(check_dh);
    lbl_dh.innerHTML += ' ' + $lang.get('user_login_ajax_check_dh');
    boxen.appendChild(lbl_dh);
  }
  
  form.appendChild(boxen);
  
  if ( IE )
  {
    div.innerHTML += form.outerHTML;
  }
  else
  {
    div.appendChild(form);
  }
  
  // Diagnostic / help links
  // (only displayed in login, not in re-auth)
  if ( logindata.user_level == USER_LEVEL_MEMBER )
  {
    var links = document.createElement('small');
    links.style.display = 'block';
    links.style.textAlign = 'center';
    links.innerHTML = '';
    if ( !show_captcha )
      links.innerHTML += $lang.get('user_login_ajax_link_fullform', { link_full_form: makeUrlNS('Special', 'Login/' + title) }) + ' &bull; ';
    // Always shown
    links.innerHTML += $lang.get('user_login_ajax_link_forgotpass', { forgotpass_link: makeUrlNS('Special', 'PasswordReset') }) + ' &bull; ';
    if ( !show_captcha )
      links.innerHTML += $lang.get('user_login_ajax_createaccount_blurb', { reg_link: makeUrlNS('Special', 'Register') });
    div.appendChild(links);
  }
  
  // Insert the entire form into the login window
  logindata.mb_inner.innerHTML = '';
  logindata.mb_inner.appendChild(div);
  
  // Post operations: field focus
  setTimeout(
    function()
    {
      if ( logindata.loggedin_username )
        document.getElementById('ajax_login_field_password').focus();
      else
        document.getElementById('ajax_login_field_username').focus();
    }, 750);        
  
  // Post operations: show captcha window
  if ( show_captcha )
  {
    ajaxShowCaptcha(show_captcha);
  }
  
  // Post operations: stash encryption keys and All That Jazz(TM)
  logindata.key_aes = data.crypto.aes_key;
  logindata.key_dh = data.crypto.dh_public_key;
  logindata.captcha_hash = show_captcha;
  logindata.loggedin_username = data.username;
  
  // If policy is lockout, also disable controls
  if ( data.lockout.policy == 'lockout' && data.lockout.active )
  {
    f_username.setAttribute('disabled', 'disabled');
    f_password.setAttribute('disabled', 'disabled');
  }
}

window.ajaxLoginSubmitForm = function(real, username, password, captcha, remember)
{
  // Perform AES test to make sure it's all working
  if ( !aes_self_test() )
  {
    alert('BUG: AES self-test failed');
    login_cache.mb_object.destroy();
    return false;
  }
  // Hide the error message and captcha
  if ( document.getElementById('ajax_login_error_box') )
  {
    document.getElementById('ajax_login_error_box').parentNode.removeChild(document.getElementById('ajax_login_error_box'));
  }
  if ( document.getElementById('autoCaptcha') )
  {
    var to = fly_out_top(document.getElementById('autoCaptcha'), false, true);
    setTimeout(function() {
        var d = document.getElementById('autoCaptcha');
        d.parentNode.removeChild(d);
      }, to);
  }
  // "Remember session" switch
  if ( typeof(remember) == 'boolean' )
  {
    var remember_session = remember;
  }
  else
  {
    if ( document.getElementById('ajax_login_field_remember') )
    {
      var remember_session = ( document.getElementById('ajax_login_field_remember').checked ) ? true : false;
    }
    else
    {
      var remember_session = false;
    }
  }
  // Encryption: preprocessor
  if ( real )
  {
    var do_dh = true;
  }
  else if ( document.getElementById('ajax_login_field_dh') )
  {
    var do_dh = document.getElementById('ajax_login_field_dh').checked;
  }
  else
  {
    if ( ajax_login_prevent_dh )
    {
      // IE/MobileSafari doesn't have this control, continue silently IF the rest
      // of the login form is there
      if ( !document.getElementById('ajax_login_field_username') )
      {
        return false;
      }
    }
    else
    {
      // The user probably clicked ok when the form wasn't in there.
      return false;
    }
  }
  
  if ( typeof(username) != 'string' )
  {
    var username = document.getElementById('ajax_login_field_username').value;
  }
  if ( typeof(password) != 'string' )
  {
    var password = document.getElementById('ajax_login_field_password').value;
  }
  if ( !captcha && document.getElementById('ajax_login_field_captcha') )
  {
    var captcha = document.getElementById('ajax_login_field_captcha').value;
  }
  
  // Only run early submit hook once
  if ( !window.logindata.early_submit_run )
    eval(setHook('login_submit_early'));
  
  window.logindata.early_submit_run = true;
  
  try
  {
  
  if ( do_dh )
  {
    ajaxLoginSetStatus(AJAX_STATUS_GENERATING_KEY);
    if ( !real )
    {
      // Wait while the browser updates the login window
      setTimeout(function()
        {
          ajaxLoginSubmitForm(true, username, password, captcha, remember_session);
        }, 20);
      return true;
    }
    var dh_start = (new Date()).getTime();
    // Perform Diffie Hellman stuff
    var dh_priv = dh_gen_private();
    var dh_pub = dh_gen_public(dh_priv);
    var secret = dh_gen_shared_secret(dh_priv, logindata.key_dh);
    // secret_hash is used to verify that the server guesses the correct secret
    var secret_hash = hex_sha1(secret);
    // crypt_key is the actual AES key
    var crypt_key = (hex_sha256(secret)).substr(0, (keySizeInBits / 4));
    var dh_time = (new Date()).getTime() - dh_start;
    console.debug("DH: complete, time = %dms", dh_time);
  }
  else
  {
    var crypt_key = logindata.key_aes;
  }
  
  ajaxLoginSetStatus(AJAX_STATUS_LOGGING_IN);
  
  // Encrypt the password and username
  var userinfo = {
      username: username,
      password: password
    };
    
  eval(setHook('login_build_userinfo'));
    
  userinfo = toJSONString(userinfo);
  var crypt_key_ba = hexToByteArray(crypt_key);
  userinfo = stringToByteArray(userinfo);
  
  userinfo = rijndaelEncrypt(userinfo, crypt_key_ba, 'ECB');
  userinfo = byteArrayToHex(userinfo);
  // Encrypted username and password (serialized with JSON) are now in the userinfo string
  
  // Collect other needed information
  if ( logindata.captcha_hash )
  {
    var captcha_hash = logindata.captcha_hash;
    var captcha_code = captcha;
  }
  else
  {
    var captcha_hash = false;
    var captcha_code = false;
  }
  
  // Ship it across the 'net
  if ( do_dh )
  {
    var json_packet = {
      mode: 'login_dh',
      userinfo: userinfo,
      captcha_code: captcha_code,
      captcha_hash: captcha_hash,
      dh_public_key: logindata.key_dh,
      dh_client_key: dh_pub,
      dh_secret_hash: secret_hash,
      level: logindata.user_level,
      remember: remember_session
    }
  }
  else
  {
    var json_packet = {
      mode: 'login_aes',
      userinfo: userinfo,
      captcha_code: captcha_code,
      captcha_hash: captcha_hash,
      key_aes: hex_md5(crypt_key),
      level: logindata.user_level,
      remember: remember_session
    }
  }
  }
  catch(e)
  {
    ajaxLoginSetStatus(AJAX_STATUS_ERROR);
    console.error('Exception caught in login process; backtrace follows');
    console.debug(e);
    return false;
  }
  // reset this...
  window.logindata.early_submit_run = false;
  ajaxLoginPerformRequest(json_packet);
}

window.ajaxLoginShowFriendlyError = function(response)
{
  var text = ajaxLoginGetErrorText(response);
  if ( text == false )
    return true;
    
  if ( document.getElementById('ajax_login_error_box') )
  {
    // console.info('Reusing existing error-box');
    document.getElementById('ajax_login_error_box').innerHTML = text;
    return true;
  }
  
  // console.info('Drawing new error-box');
  
  // calculate position for the top of the box
  var mb_bottom = $dynano('messageBoxButtons').Top() + $dynano('messageBoxButtons').Height();
  // if the box isn't done flying in yet, just estimate
  if ( mb_bottom < ( getHeight() / 2 ) )
  {
    mb_bottom = ( getHeight() / 2 ) + 120;
  }
  var win_bottom = getHeight() + getScrollOffset();
  var top = mb_bottom + ( ( win_bottom - mb_bottom ) / 2 ) - 32;
  // left position = 0.2 * window_width, seeing as the box is 60% width this works hackishly but nice and quick
  var left = getWidth() * 0.2;
  
  // create the div
  var errbox = document.createElement('div');
  errbox.className = 'error-box-mini';
  errbox.style.position = 'absolute';
  errbox.style.width = '60%';
  errbox.style.top = top + 'px';
  errbox.style.left = left + 'px';
  errbox.style.zIndex = getHighestZ();
  errbox.innerHTML = text;
  errbox.id = 'ajax_login_error_box';
  
  var body = document.getElementsByTagName('body')[0];
  body.appendChild(errbox);
}

window.ajaxLoginGetErrorText = function(response)
{
  if ( response.lockout )
  {
    // set this pluralality thing
    response.lockout.plural = response.lockout.time_rem == 1 ? '' : $lang.get('meta_plural');
  }
  
  if ( response.mode == 'initial' )
  {
    // Just showing the box for the first time. If there's an error now, it's based on a preexisting lockout.
    if ( response.lockout.active )
    {
      return $lang.get('user_err_locked_out_initial_' + response.lockout.policy, response.lockout);
    }
    return false;
  }
  else
  {
    // An attempt was made.
    switch(response.mode)
    {
      case 'login_failure':
        // Generic login user error.
        var error = '', x;
        if ( (x = $lang.get(response.error)) != response.error )
          error = x;
        else
          error = $lang.get('user_err_' + response.error);
        if ( response.lockout.active && response.lockout.policy == 'lockout' )
        {
          // Lockout enforcement was just activated.
          return $lang.get('user_err_locked_out_initial_' + response.lockout.policy, response.lockout);
        }
        else if ( response.lockout.policy != 'disable' && !response.lockout.active && response.lockout.fails > 0 )
        {
          // Lockout is in a warning state.
          error += ' ' + $lang.get('user_err_invalid_credentials_' + response.lockout.policy, response.lockout);
        }
        return error;
        break;
      case 'api_error':
        // Error in the API.
        return $lang.get('user_err_login_generic_title') + ': ' + $lang.get('user_' + response.error.toLowerCase());
        break;
    }
  }
  
  return typeof(response.error) == 'string' ? response.error : false;
}

window.ajaxShowCaptcha = function(code)
{
  var mydiv = document.createElement('div');
  mydiv.style.backgroundColor = '#FFFFFF';
  mydiv.style.padding = '10px';
  mydiv.style.position = 'absolute';
  mydiv.style.top = '0px';
  mydiv.id = 'autoCaptcha';
  mydiv.style.zIndex = String( getHighestZ() + 1 );
  var img = document.createElement('img');
  img.onload = function()
  {
    if ( this.loaded )
      return true;
    var mydiv = document.getElementById('autoCaptcha');
    var width = getWidth();
    var divw = $dynano(mydiv).Width();
    var left = ( width / 2 ) - ( divw / 2 );
    mydiv.style.left = left + 'px';
    fly_in_top(mydiv, false, true);
    this.loaded = true;
  };
  img.src = makeUrlNS('Special', 'Captcha/' + code);
  img.onclick = function() { this.src = this.src + '/a'; };
  img.style.cursor = 'pointer';
  mydiv.appendChild(img);
  domObjChangeOpac(0, mydiv);
  var body = document.getElementsByTagName('body')[0];
  body.appendChild(mydiv);
}

window.ajaxInitLogout = function()
{
  load_component(['messagebox', 'l10n', 'flyin', 'fadefilter', 'jquery', 'jquery-ui']);
  
  var title = $lang.get('user_logout_confirm_title');
  var message = ( auth_level > USER_LEVEL_MEMBER ) ? $lang.get('user_logout_confirm_body_nelev') : $lang.get('user_logout_confirm_body_normal');
  var buttons = [];
  buttons.push({
      text: $lang.get('user_logout_confirm_btn_logout'),
      color: 'red',
      style: {
        fontWeight: 'bold'
      },
      onclick: function()
      {
        miniPromptDestroy(this);
        window.location = makeUrlNS('Special', 'Logout/' + csrf_token + '/' + window.title);
        return false;
      }
    });
  if ( auth_level > USER_LEVEL_MEMBER )
  {
    buttons.push({
        text: $lang.get('user_logout_confirm_btn_deauth'),
        color: 'blue',
        onclick: function()
        {
          var mp = miniPromptGetParent(this);
          var whitey = whiteOutMiniPrompt(mp);
          
          ajaxLoginPerformRequest({
              mode:  'logout',
              level: auth_level,
              csrf_token: csrf_token
          }, function(response)
            {
              whiteOutReportSuccess(whitey);
            });
          return false;
        }
      });
  }
  buttons.push({
      text: $lang.get('etc_cancel'),
      onclick: function()
      {
        miniPromptDestroy(this);
        return false;
      }
    });
  
  miniPromptMessage({
      title: title,
      message: message,
      buttons: buttons
  });
}

window.mb_logout = function()
{
  ajaxInitLogout();
}

window.ajaxStartLogin = function()
{
  ajaxLogonToMember();
}

window.ajaxStartAdminLogin = function()
{
  // IE <6 pseudo-compatibility
  if ( KILL_SWITCH )
    return true;
  if ( auth_level < USER_LEVEL_ADMIN )
  {
    ajaxLoginInit(function(k) {
      ENANO_SID = k;
      auth_level = USER_LEVEL_ADMIN;
      var loc = makeUrlNS('Special', 'Administration');
      if ( (ENANO_SID + ' ').length > 1 )
        window.location = loc;
    }, USER_LEVEL_ADMIN);
    return false;
  }
  var loc = makeUrlNS('Special', 'Administration');
  window.location = loc;
}

window.ajaxAdminPage = function()
{
  // IE <6 pseudo-compatibility
  if ( KILL_SWITCH )
    return true;
  if ( auth_level < USER_LEVEL_ADMIN )
  {
    ajaxPromptAdminAuth(function(k) {
      ENANO_SID = k;
      auth_level = USER_LEVEL_ADMIN;
      var loc = String(window.location + '');
      window.location = append_sid(loc);
      var loc = makeUrlNS('Special', 'Administration', 'module=' + namespace_list['Admin'] + 'PageManager&source=ajax&page_id=' + ajaxEscape(title));
      if ( (ENANO_SID + ' ').length > 1 )
        window.location = loc;
    }, 9);
    return false;
  }
  var loc = makeUrlNS('Special', 'Administration', 'module=' + namespace_list['Admin'] + 'PageManager&source=ajax&page_id=' + ajaxEscape(title));
  window.location = loc;
}

window.ajaxLoginNavTo = function(namespace, page_id, min_level, get)
{
  // IE <6 pseudo-compatibility
  if ( KILL_SWITCH )
    return true;
  void(namespace);
  void(page_id);
  get = get || false;
  if ( auth_level < min_level )
  {
    ajaxPromptAdminAuth(function(k) {
      ENANO_SID = k;
      auth_level = min_level;
      var loc = makeUrlNS(namespace, page_id, get);
      if ( (ENANO_SID + ' ').length > 1 )
        window.location = loc;
    }, min_level);
    return false;
  }
  var loc = makeUrlNS(namespace, page_id, get);
  window.location = loc;
}

window.ajaxAdminUser = function(username)
{
  // IE <6 pseudo-compatibility
  if ( KILL_SWITCH )
    return true;
  if ( auth_level < USER_LEVEL_ADMIN )
  {
    ajaxPromptAdminAuth(function(k) {
      ENANO_SID = k;
      auth_level = USER_LEVEL_ADMIN;
      var loc = String(window.location + '');
      window.location = append_sid(loc);
      var loc = makeUrlNS('Special', 'Administration', 'module=' + namespace_list['Admin'] + 'UserManager&src=get&user=' + ajaxEscape(username));
      if ( (ENANO_SID + ' ').length > 1 )
        window.location = loc;
    }, 9);
    return false;
  }
  var loc = makeUrlNS('Special', 'Administration', 'module=' + namespace_list['Admin'] + 'UserManager&src=get&user=' + ajaxEscape(username));
  window.location = loc;
}

window.ajaxDynamicReauth = function(adminpage, level)
{
  if ( auth_level < USER_LEVEL_MEMBER )
  {
    ajaxStartLogin();
    return false;
  }
  
  var old_sid = ENANO_SID;
  var targetpage = adminpage;
  if ( !level )
  {
    level = USER_LEVEL_ADMIN;
  }
  
  ajaxLogonInit(function(k, response)
    {
      ajaxLoginReplaceSIDInline(k, old_sid, level);
      window.user_id = response.user_id;
      window.user_level = response.user_level;
      mb_current_obj.destroy();
      if ( typeof(targetpage) == 'string' )
      {
        ajaxPage(targetpage);
      }
      else if ( typeof(targetpage) == 'function' )
      {
        targetpage(k);
      }
    }, level);
  if ( typeof(adminpage) == 'string' )
  {
    ajaxLoginShowFriendlyError({
        error_code: 'admin_session_timed_out',
        respawn_info: {}
    });
  }
}

window.ajaxRenewSession = function()
{
  ajaxDynamicReauth(false);
}

window.ajaxTrashElevSession = function()
{
  load_component(['messagebox', 'fadefilter', 'l10n', 'flyin', 'jquery', 'jquery-ui']);
  miniPromptMessage({
    title: $lang.get('user_logout_confirm_title_elev'),
    message: $lang.get('user_logout_confirm_body_elev'),
    buttons: [
      {
        text: $lang.get('user_logout_confirm_btn_logout'),
        color: 'red',
        style: {
          fontWeight: 'bold'
        },
        onclick: function()
        {
          ajaxLoginPerformRequest({
              mode:  'logout',
              level: auth_level,
              csrf_token: csrf_token
          });
          miniPromptDestroy(this);
        }
      },
      {
        text: $lang.get('etc_cancel'),
        onclick: function()
        {
          miniPromptDestroy(this);
        }
      }
    ]
  });
}

/**
 * Take an SID and patch all internal links on the page.
 * @param string New key. If false, removes keys from the page.
 * @param string Old key. If false, only appends the new SID (more work as it uses DOM, use when dynamically going up to elevated)
 * @param int New level, not a huge deal but sets auth_level. Try to specify it as some functions depend on it.
 */

window.ajaxLoginReplaceSIDInline = function(key, oldkey, level)
{
  var host = String(window.location.hostname);
  var exp = new RegExp('^https?://' + host.replace('.', '\.') + contentPath.replace('.', '\.'), 'g');
  var rexp = new RegExp('^https?://' + host.replace('.', '\.'), 'g');
  
  if ( key )
  {
    if ( oldkey )
    {
      var body = document.getElementsByTagName('body')[0];
      var replace = new RegExp(oldkey, 'g');
      body.innerHTML = body.innerHTML.replace(replace, key);
      ENANO_SID = key;
    }
    else
    {
      // append SID to all internal links
      ENANO_SID = key;
      
      var links = document.getElementsByTagName('a');
      for ( var i = 0; i < links.length; i++ )
      {
        if ( links[i].href.match(exp, links[i]) && links[i].href.indexOf('#') == -1 )
        {
          var newurl = (String(append_sid(links[i].href))).replace(rexp, '');
          links[i].href = newurl;
        }
      }
      
      var forms = document.getElementsByTagName('form');
      for ( var i = 0; i < forms.length; i++ )
      {
        if ( forms[i].method.toLowerCase() == 'post' )
        {
          if ( forms[i].action.match(exp, links[i]) )
          {
            var newurl = (String(append_sid(forms[i].action))).replace(rexp, '');
            forms[i].action = newurl;
          }
        }
        else
        {
          if ( !forms[i].auth )
          {
            var auth = document.createElement('input');
            auth.type = 'hidden';
            auth.name = 'auth';
            auth.value = key;
            forms[i].appendChild(auth);
          }
          else
          {
            forms[i].auth.value = key;
          }
        }
      }
    }
    if ( level )
    {
      auth_level = level;
    }
    window.location.hash = '#auth:' + key;
  }
  else
  {
    auth_level = USER_LEVEL_MEMBER;
    ENANO_SID = false;
    if ( oldkey )
    {
      var links = document.getElementsByTagName('a');
      for ( var i = 0; i < links.length; i++ )
      {
        if ( links[i].href.match(exp, links[i]) && links[i].href.indexOf('#') == -1 )
        {
          links[i].href = links[i].href.replace(/\?auth=([a-f0-9]+)(&|#|$)/, '$2').replace(/&auth=([a-f0-9]+)/, '').replace(rexp, '');
        }
      }
    }
    window.location.hash = '#auth:false';
  }
  window.stdAjaxPrefix = append_sid(scriptPath + '/ajax.php?title=' + title);
}