includes/clientside/static/login.js
author Dan
Wed, 26 Mar 2008 02:56:23 -0400
changeset 509 175df10e0b56
parent 478 376a01d8386d
child 532 03429d7b1537
permissions -rw-r--r--
Added a copy of Firebug Lite for debugging purposes. License is uncertain but being treated as MPL. (If is is not MPL then it is under something more permissive that permits relicensing anyway)

/*
 * AJAX-based intelligent login interface
 */

/*
 * FRONTEND
 */

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

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

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

function ajaxLogonToElev()
{
  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;

/**
 * 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_DESTROY = 65535;

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

var AJAX_STATE_EARLY_INIT = 1;
var AJAX_STATE_LOADING_KEY = 2;

/**
 * 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
 */

function ajaxLoginInit(call_on_finish, user_level)
{
  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, '');
  
  logindata.mb_object.onclick['Cancel'] = function()
  {
    // 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);
    }
    // Ask the server to clean our key
    ajaxLoginPerformRequest({
        mode: 'clean_key',
        key_aes: logindata.key_aes,
        key_dh: logindata.key_dh
    });
  };
  
  logindata.mb_object.onbeforeclick['OK'] = function()
  {
    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' });
}

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

function ajaxLoginSetStatus(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_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_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);
      
    case AJAX_STATUS_DESTROY:
    case null:
    case undefined:
      logindata.showing_status = false;
      return null;
      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
 */

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

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

function ajaxLoginProcessResponse(response)
{
  // Did the server send a plaintext error?
  if ( response.mode == 'error' )
  {
    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);
    return false;
  }
  // Main mode switch
  switch ( response.mode )
  {
    case 'build_box':
      // Rid ourselves of any loading windows
      ajaxLoginSetStatus(AJAX_STATUS_DESTROY);
      // 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);
      break;
    case 'login_failure':
      // Rid ourselves of any loading windows
      ajaxLoginSetStatus(AJAX_STATUS_DESTROY);
      document.getElementById('messageBox').style.backgroundColor = '#C0C0C0';
      var mb_parent = document.getElementById('messageBox').parentNode;
      new Spry.Effect.Shake(mb_parent, {duration: 1500}).start();
      setTimeout(function()
        {
          document.getElementById('messageBox').style.backgroundColor = '#FFF';
          ajaxLoginBuildForm(response.respawn_info);
          ajaxLoginShowFriendlyError(response);
        }, 2500);
      break;
    case 'login_success_reset':
      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;
      }
      else
      {
        // treat as a failure
        ajaxLoginSetStatus(AJAX_STATUS_DESTROY);
        document.getElementById('messageBox').style.backgroundColor = '#C0C0C0';
        var mb_parent = document.getElementById('messageBox').parentNode;
        new Spry.Effect.Shake(mb_parent, {duration: 1500}).start();
        setTimeout(function()
          {
            document.getElementById('messageBox').style.backgroundColor = '#FFF';
            ajaxLoginBuildForm(response.respawn_info);
            // don't show an error here, just silently respawn
          }, 2500);
      }
      break;
    case 'noop':
      break;
  }
}

/*
 * RESPONSE HANDLERS
 */

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

function ajaxLoginBuildForm(data)
{
  // let's hope this effectively preloads the image...
  var _ = document.createElement('img');
  _.src = ( ajax_login_successimg_path ) ? ajax_login_successimg_path : scriptPath + '/images/check.png';
  
  var div = document.createElement('div');
  div.id = 'ajax_login_form';
  
  var show_captcha = ( data.locked_out && data.lockout_info.lockout_policy == 'captcha' ) ? data.lockout_info.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);
  }
  
  // Done building the main part of the form
  form.appendChild(table);
  
  // Field: enable Diffie Hellman
  if ( IE || is_iPhone )
  {
    var lbl_dh = document.createElement('span');
    lbl_dh.style.fontSize = 'smaller';
    lbl_dh.style.display = 'block';
    lbl_dh.style.textAlign = 'center';
    lbl_dh.innerHTML = $lang.get('user_login_ajax_check_dh_ie');
    form.appendChild(lbl_dh);
  }
  else
  {
    var lbl_dh = document.createElement('label');
    lbl_dh.style.fontSize = 'smaller';
    lbl_dh.style.display = 'block';
    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');
    form.appendChild(lbl_dh);
  }
  
  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 )
  {
    form.style.marginBottom = '10px';
    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) }) + '<br />';
    // Always shown
    links.innerHTML += $lang.get('user_login_ajax_link_forgotpass', { forgotpass_link: makeUrlNS('Special', 'PasswordReset') }) + '<br />';
    if ( !show_captcha )
      links.innerHTML += $lang.get('user_login_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
  if ( IE )
  {
    setTimeout(
      function()
      {
        if ( logindata.loggedin_username )
          document.getElementById('ajax_login_field_password').focus();
        else
          document.getElementById('ajax_login_field_username').focus();
      }, 200);        
  }
  else
  {
    if ( data.username )
      f_password.focus();
    else
      f_username.focus();
  }
  
  // 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.aes_key;
  logindata.key_dh = data.dh_public_key;
  logindata.captcha_hash = show_captcha;
  logindata.loggedin_username = data.username
  
  // Are we locked out? If so simulate an error and disable the controls
  if ( data.lockout_info.lockout_policy == 'lockout' && data.locked_out )
  {
    f_username.setAttribute('disabled', 'disabled');
    f_password.setAttribute('disabled', 'disabled');
    var fake_packet = {
      error_code: 'locked_out',
      respawn_info: data
    };
    ajaxLoginShowFriendlyError(fake_packet);
  }
}

function ajaxLoginSubmitForm(real, username, password, captcha)
{
  // 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);
  }
  // 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 ( IE || is_iPhone )
    {
      // 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 ( !username )
  {
    var username = document.getElementById('ajax_login_field_username').value;
  }
  if ( !password )
  {
    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;
  }
  
  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);
        }, 200);
      return true;
    }
    // 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));
  }
  else
  {
    var crypt_key = logindata.key_aes;
  }
  
  ajaxLoginSetStatus(AJAX_STATUS_LOGGING_IN);
  
  // Encrypt the password and username
  var userinfo = toJSONString({
      username: username,
      password: password
    });
  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
    }
  }
  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
    }
  }
  ajaxLoginPerformRequest(json_packet);
}

function ajaxLoginShowFriendlyError(response)
{
  if ( !response.respawn_info )
    return false;
  if ( !response.error_code )
    return false;
  var text = ajaxLoginGetErrorText(response);
  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 = $('messageBoxButtons').Top() + $('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.innerHTML = text;
  errbox.id = 'ajax_login_error_box';
  
  var body = document.getElementsByTagName('body')[0];
  body.appendChild(errbox);
}

function ajaxLoginGetErrorText(response)
{
  switch ( response.error_code )
  {
    default:
      return $lang.get('user_err_' + response.error_code);
      break;
    case 'locked_out':
      if ( response.respawn_info.lockout_info.lockout_policy == 'lockout' )
      {
        return $lang.get('user_err_locked_out', { 
                  lockout_threshold: response.respawn_info.lockout_info.lockout_threshold,
                  lockout_duration: response.respawn_info.lockout_info.lockout_duration,
                  time_rem: response.respawn_info.lockout_info.time_rem,
                  plural: ( response.respawn_info.lockout_info.time_rem == 1 ) ? '' : $lang.get('meta_plural'),
                  captcha_blurb: ''
                });
        break;
      }
    case 'invalid_credentials':
      var base = $lang.get('user_err_invalid_credentials');
      if ( response.respawn_info.locked_out )
      {
        base += ' ';
        var captcha_blurb = '';
        switch(response.respawn_info.lockout_info.lockout_policy)
        {
          case 'captcha':
            captcha_blurb = $lang.get('user_err_locked_out_captcha_blurb');
            break;
          case 'lockout':
            break;
          default:
            base += 'WTF? Shouldn\'t be locked out with lockout policy set to disable.';
            break;
        }
        base += $lang.get('user_err_locked_out', { 
                  captcha_blurb: captcha_blurb,
                  lockout_threshold: response.respawn_info.lockout_info.lockout_threshold,
                  lockout_duration: response.respawn_info.lockout_info.lockout_duration,
                  time_rem: response.respawn_info.lockout_info.time_rem,
                  plural: ( response.respawn_info.lockout_info.time_rem == 1 ) ? '' : $lang.get('meta_plural')
                });
      }
      else if ( response.respawn_info.lockout_info.lockout_policy == 'lockout' || response.respawn_info.lockout_info.lockout_policy == 'captcha' )
      {
        // if we have a lockout policy of captcha or lockout, then warn the user
        switch ( response.respawn_info.lockout_info.lockout_policy )
        {
          case 'captcha':
            base += $lang.get('user_err_invalid_credentials_lockout', { 
                fails: response.respawn_info.lockout_info.lockout_fails,
                lockout_threshold: response.respawn_info.lockout_info.lockout_threshold,
                lockout_duration: response.respawn_info.lockout_info.lockout_duration
              });
            break;
          case 'lockout':
            break;
        }
      }
      return base;
      break;
  }
}