Logins: reorganized data structures a bit. WiP - needs test routine done.
authorDan
Tue, 03 Nov 2009 22:08:48 -0500
changeset 1132 05fe0039d952
parent 1131 adfbe522c95f
child 1133 6e51ca5e29eb
Logins: reorganized data structures a bit. WiP - needs test routine done.
includes/clientside/jsres.php
includes/clientside/static/login.js
includes/clientside/static/paginate.js
includes/sessions.php
includes/template.php
language/english/user.json
plugins/SpecialUserFuncs.php
--- a/includes/clientside/jsres.php	Sun Oct 25 00:09:11 2009 -0400
+++ b/includes/clientside/jsres.php	Tue Nov 03 22:08:48 2009 -0500
@@ -12,7 +12,7 @@
  * warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for details.
  */
 
-// define('ENANO_JS_DEBUG', 1);
+define('ENANO_JS_DEBUG', 1);
 
 // if Enano's already loaded, we've been included from a helper script
 if ( defined('ENANO_CONFIG_FETCHED') )
--- a/includes/clientside/static/login.js	Sun Oct 25 00:09:11 2009 -0400
+++ b/includes/clientside/static/login.js	Tue Nov 03 22:08:48 2009 -0500
@@ -115,13 +115,14 @@
   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 and captcha
-    if ( document.getElementById('ajax_login_error_box') )
-    {
-      document.getElementById('ajax_login_error_box').parentNode.removeChild(document.getElementById('ajax_login_error_box'));
-    }
+    // 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);
@@ -130,7 +131,7 @@
           d.parentNode.removeChild(d);
         }, to);
     }
-    // Ask the server to clean our key
+    // Ask the server to delete the encryption key we're using
     ajaxLoginPerformRequest({
         mode: 'clean_key',
         key_aes: logindata.key_aes,
@@ -138,8 +139,10 @@
     });
   };
   
+  // 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;
   }
@@ -160,7 +163,8 @@
 }
 
 /**
- * For compatibility only.
+ * 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)
@@ -170,7 +174,7 @@
 
 /**
  * Sets the contents of the AJAX login window to the appropriate status message.
- * @param int One of AJAX_STATUS_*
+ * @param int One of AJAX_STATUS_* constants
  */
 
 window.ajaxLoginSetStatus = function(status)
@@ -365,7 +369,7 @@
     case null:
     case undefined:
       logindata.showing_status = false;
-      return null;
+      return;
       break;
   }
   logindata.showing_status = true;
@@ -374,6 +378,7 @@
 /**
  * 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)
@@ -420,12 +425,15 @@
     }
     return false;
   }
+  
   // Main mode switch
   switch ( response.mode )
   {
-    case 'build_box':
+    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;
@@ -433,49 +441,33 @@
       ajaxLoginSetStatus(AJAX_STATUS_SUCCESS);
       logindata.successfunc(response.key, response);
       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;
-      var do_respawn = ( typeof(response.respawn) == 'boolean' && response.respawn == true ) || typeof(response.respawn) != 'boolean';
-      if ( do_respawn )
-      {
-        $(mb_parent).effect("shake", {}, 200);
-        setTimeout(function()
-          {
-            document.getElementById('messageBox').style.backgroundColor = '#FFF';
-            
-            ajaxLoginBuildForm(response.respawn_info);
-            ajaxLoginShowFriendlyError(response);
-          }, 2500);
-      }
-      else
-      {
-        ajaxLoginShowFriendlyError(response);
-      }
-      break;
-    case 'login_success_reset':
+    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
-        ajaxLoginSetStatus(AJAX_STATUS_DESTROY);
-        document.getElementById('messageBox').style.backgroundColor = '#C0C0C0';
-        var mb_parent = document.getElementById('messageBox').parentNode;
-        $(mb_parent).effect("shake", {}, 1500);
-        setTimeout(function()
-          {
-            document.getElementById('messageBox').style.backgroundColor = '#FFF';
-            ajaxLoginBuildForm(response.respawn_info);
-            // don't show an error here, just silently respawn
-          }, 2500);
-      }
+      // 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 )
@@ -512,7 +504,7 @@
   var div = document.createElement('div');
   div.id = 'ajax_login_form';
   
-  var show_captcha = ( data.locked_out.locked_out && data.locked_out.lockout_policy == 'captcha' ) ? data.locked_out.captcha : false;
+  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 )
@@ -586,8 +578,6 @@
   tr2.appendChild(td2_2);
   table.appendChild(tr2);
   
-  eval(setHook('login_build_form'));
-  
   // Field - captcha
   if ( show_captcha )
   {
@@ -617,6 +607,14 @@
     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);
   
@@ -696,7 +694,7 @@
     lbl_dh.innerHTML = $lang.get('user_login_ajax_check_dh_ie');
     boxen.appendChild(lbl_dh);
   }
-  else if ( !data.allow_diffiehellman )
+  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');
@@ -769,24 +767,21 @@
   
   // 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.key_aes = data.crypto_aes_key;
+  logindata.key_dh = data.crypto.dh_public_key;
   logindata.captcha_hash = show_captcha;
-  logindata.loggedin_username = data.username
+  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.locked_out )
+  // 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');
-    var fake_packet = {
-      error_code: 'locked_out',
-      respawn_info: data
-    };
-    ajaxLoginShowFriendlyError(fake_packet);
   }
 }
 
@@ -978,11 +973,10 @@
 
 window.ajaxLoginShowFriendlyError = function(response)
 {
-  if ( !response.respawn_info )
-    return false;
-  if ( !response.error_code )
-    return false;
   var text = ajaxLoginGetErrorText(response);
+  if ( text == false )
+    return true;
+    
   if ( document.getElementById('ajax_login_error_box') )
   {
     // console.info('Reusing existing error-box');
@@ -1021,85 +1015,53 @@
 
 window.ajaxLoginGetErrorText = function(response)
 {
-  if ( !response.error_code.match(/^[a-z0-9]+_[a-z0-9_]+$/) )
+  if ( response.lockout )
   {
-    return response.error_code;
+    // set this pluralality thing
+    response.lockout.plural = response.lockout.time_rem == 1 ? '' : $lang.get('meta_plural');
   }
-  switch ( response.error_code )
+  
+  if ( response.mode == 'initial' )
   {
-    default:
-      eval(setHook('ajax_login_process_error'));
-      if ( !ls )
-      {
-        var ls = $lang.get('user_err_' + response.error_code);
-        if ( ls == 'user_err_' + response.error_code )
-          // Adding response here allows language strings to utilize additional information passed from the error packet
-          ls = $lang.get(response.error_code, response);
-      }
-      
-      return ls;
-      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.locked_out )
-      {
-        base += ' ';
-        var captcha_blurb = '';
-        switch(response.respawn_info.lockout_info.lockout_policy)
+    // 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' )
         {
-          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;
+          // 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);
         }
-        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_captcha', { 
-                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':
-            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;
-        }
-      }
-      return base;
-      break;
+        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)
@@ -1302,6 +1264,7 @@
   {
     level = USER_LEVEL_ADMIN;
   }
+  
   ajaxLogonInit(function(k, response)
     {
       ajaxLoginReplaceSIDInline(k, old_sid, level);
--- a/includes/clientside/static/paginate.js	Sun Oct 25 00:09:11 2009 -0400
+++ b/includes/clientside/static/paginate.js	Tue Nov 03 22:08:48 2009 -0500
@@ -237,7 +237,7 @@
   
   // White-out the old div and fade in the new one
   
-  if ( IE || is_Safari )
+  if ( IE || is_Safari || aclDisableTransitionFX )
   {
     current_div.style.display = 'none';
     new_div.style.display = 'block';
--- a/includes/sessions.php	Sun Oct 25 00:09:11 2009 -0400
+++ b/includes/sessions.php	Tue Nov 03 22:08:48 2009 -0500
@@ -676,12 +676,10 @@
    * @param string $password The password -OR- the MD5 hash of the password if $already_md5ed is true
    * @param bool $already_md5ed This should be set to true if $password is an MD5 hash, and should be false if it's plaintext. Defaults to false.
    * @param int $level The privilege level we're authenticating for, defaults to 0
-   * @param string $captcha_hash Optional. If we're locked out and the lockout policy is captcha, this should be the identifier for the code.
-   * @param string $captcha_code Optional. If we're locked out and the lockout policy is captcha, this should be the code the user entered.
    * @param bool $remember Optional. If true, remembers the session for X days. Otherwise, assigns a short session. Defaults to false.
    */
   
-  function login_without_crypto($username, $password, $already_md5ed = false, $level = USER_LEVEL_MEMBER, $captcha_hash = false, $captcha_code = false, $remember = false)
+  function login_without_crypto($username, $password, $already_md5ed = false, $level = USER_LEVEL_MEMBER, $remember = false)
   {
     global $db, $session, $paths, $template, $plugins; // Common objects
     
@@ -704,40 +702,6 @@
       return $this->login_compat($username, md5($password), $level);
     }
     
-    // Lockout check
-    if ( !defined('IN_ENANO_INSTALL') )
-    {
-      $lockout_data = $this->get_lockout_info($lockout_data);
-      
-      $captcha_good = false;
-      if ( $lockout_data['lockout_policy'] == 'captcha' && $captcha_hash && $captcha_code )
-      {
-        // policy is captcha -- check if it's correct, and if so, bypass lockout check
-        $real_code = $this->get_captcha($captcha_hash);
-        if ( strtolower($real_code) === strtolower($captcha_code) )
-        {
-          $captcha_good = true;
-        }
-      }
-      if ( $lockout_data['lockout_policy'] != 'disable' && !$captcha_good )
-      {
-        if ( $lockout_data['lockout_fails'] >= $lockout_data['lockout_threshold'] )
-        {
-          // ooh boy, somebody's in trouble ;-)
-          return array(
-              'success' => false,
-              'error' => 'locked_out',
-              'lockout_threshold' => $lockout_data['lockout_threshold'],
-              'lockout_duration' => ( $lockout_data['lockout_duration'] ),
-              'lockout_fails' => $lockout_data['lockout_fails'],
-              'lockout_policy' => $lockout_data['lockout_policy'],
-              'time_rem' => $lockout_data['time_rem'],
-              'lockout_last_time' => $lockout_data['lockout_last_time']
-            );
-        }
-      }
-    }
-    
     // Instanciate the Rijndael encryption object
     $aes = AESCrypt::singleton(AES_BITS, AES_BLOCKSIZE);
     
@@ -766,7 +730,14 @@
                       . '\''.$db->escape($_SERVER['REMOTE_ADDR']).'\')');
       
       // Do we also need to increment the lockout countdown?
-      if ( @$lockout_data['lockout_policy'] != 'disable' && !defined('IN_ENANO_INSTALL') )
+      if ( !defined('IN_ENANO_INSTALL') )
+        $lockout_data = $this->get_lockout_info();
+      else
+        $lockout_data = array(
+          'lockout_policy' => 'disable'
+          );
+      
+      if ( $lockout_data['lockout_policy'] != 'disable' && !defined('IN_ENANO_INSTALL') )
       {
         $ipaddr = $db->escape($_SERVER['REMOTE_ADDR']);
         // increment fail count
@@ -891,20 +862,11 @@
         $this->sql('INSERT INTO '.table_prefix.'logs(log_type,action,time_id,date_string,author,edit_summary) VALUES(\'security\', \'auth_bad\', '.time().', \''.enano_date(ED_DATE | ED_TIME).'\', \''.$db->escape($username).'\', \''.$db->escape($_SERVER['REMOTE_ADDR']).'\')');
         
       // Do we also need to increment the lockout countdown?
-      if ( !defined('IN_ENANO_INSTALL') && $lockout_data['lockout_policy'] != 'disable' )
+      if ( !defined('IN_ENANO_INSTALL') && getConfig('lockout_policy', 'lockout') !== 'disable' )
       {
         $ipaddr = $db->escape($_SERVER['REMOTE_ADDR']);
         // increment fail count
         $this->sql('INSERT INTO '.table_prefix.'lockout(ipaddr, timestamp, action) VALUES(\'' . $ipaddr . '\', ' . time() . ', \'credential\');');
-        $lockout_data['lockout_fails']++;
-        return array(
-            'success' => false,
-            'error' => ( $lockout_data['lockout_fails'] >= $lockout_data['lockout_threshold'] ) ? 'locked_out' : 'invalid_credentials',
-            'lockout_threshold' => $lockout_data['lockout_threshold'],
-            'lockout_duration' => ( $lockout_data['lockout_duration'] ),
-            'lockout_fails' => $lockout_data['lockout_fails'],
-            'lockout_policy' => $lockout_data['lockout_policy']
-          );
       }
         
       return array(
@@ -1071,7 +1033,7 @@
    * @return bool True if locked out, false otherwise
    */
   
-  function get_lockout_info(&$lockdata)
+  function get_lockout_info()
   {
     global $db;
     
@@ -1096,14 +1058,14 @@
       $row = $db->fetchrow($q);
       $locked_out = ( $fails >= $threshold );
       $lockdata = array(
-          'locked_out' => $locked_out,
-          'lockout_threshold' => $threshold,
-          'lockout_duration' => ( $duration / 60 ),
-          'lockout_fails' => $fails,
-          'lockout_policy' => $policy,
-          'lockout_last_time' => $row['timestamp'],
+          'active' => $locked_out,
+          'threshold' => $threshold,
+          'duration' => ( $duration / 60 ),
+          'fails' => $fails,
+          'policy' => $policy,
+          'last_time' => $row['timestamp'],
           'time_rem' => $locked_out ? ( $duration / 60 ) - round( ( time() - $row['timestamp'] ) / 60 ) : 0,
-          'captcha' => ''
+          'captcha' => $policy == 'captcha' ? $this->make_captcha() : ''
         );
       $db->free_result();
     }
@@ -1111,12 +1073,12 @@
     {
       // disabled; send back default dataset
       $lockdata = array(
-        'locked_out' => false,
-        'lockout_threshold' => $threshold,
-        'lockout_duration' => ( $duration / 60 ),
-        'lockout_fails' => 0,
-        'lockout_policy' => $policy,
-        'lockout_last_time' => 0,
+        'active' => false,
+        'threshold' => $threshold,
+        'duration' => ( $duration / 60 ),
+        'fails' => 0,
+        'policy' => $policy,
+        'last_time' => 0,
         'time_rem' => 0,
         'captcha' => ''
       );
@@ -3869,59 +3831,20 @@
     // Check for the mode
     if ( !isset($req['mode']) )
     {
-      return array(
-          'mode' => 'error',
-          'error' => 'ERR_JSON_NO_MODE'
-        );
+      return $this->get_login_response('api_error', 'ERR_JSON_NO_MODE');
     }
     
     // Main processing switch
     switch ( $req['mode'] )
     {
       default:
-        return array(
-            'mode' => 'error',
-            'error' => 'ERR_JSON_INVALID_MODE'
-          );
+        return $this->get_login_response('api_error', 'ERR_JSON_INVALID_MODE');
         break;
       case 'getkey':
         
         $this->start();
         
-        $locked_out = $this->get_lockout_info($lockdata);
-        
-        $response = array('mode' => 'build_box');
-        $response['allow_diffiehellman'] = $dh_supported;
-        
-        $response['username'] = ( $this->user_logged_in ) ? $this->username : false;
-        $response['aes_key'] = $this->rijndael_genkey();
-        
-        $response['extended_time'] = intval(getConfig('session_remember_time', '30'));
-        
-        // Lockout info
-        $response['locked_out'] = $locked_out;
-        
-        $response['lockout_info'] = $lockdata;
-        if ( $lockdata['lockout_policy'] == 'captcha' && $locked_out )
-        {
-          $response['lockout_info']['captcha'] = $this->make_captcha();
-        }
-        
-        // Can we do Diffie-Hellman? If so, generate and stash a public/private key pair.
-        if ( $dh_supported )
-        {
-          $dh_key_priv = dh_gen_private();
-          $dh_key_pub = dh_gen_public($dh_key_priv);
-          $dh_key_priv = $_math->str($dh_key_priv);
-          $dh_key_pub = $_math->str($dh_key_pub);
-          $response['dh_public_key'] = $dh_key_pub;
-          // store the keys in the DB
-          $q = $db->sql_query('INSERT INTO ' . table_prefix . "diffiehellman( public_key, private_key ) VALUES ( '$dh_key_pub', '$dh_key_priv' );");
-          if ( !$q )
-            $db->die_json();
-        }
-        
-        return $response;
+        return $this->get_login_response('initial');
         break;
       case 'login_dh':
         // User is requesting a login and has sent Diffie-Hellman data.
@@ -3937,10 +3860,7 @@
         // Check the key
         if ( !ctype_digit($dh_public) || !ctype_digit($req['dh_client_key']) )
         {
-          return array(
-            'mode' => 'error',
-            'error' => 'ERR_DH_KEY_NOT_NUMERIC'
-          );
+          return $this->get_login_response('api_error', 'ERR_DH_KEY_NOT_NUMERIC');
         }
         
         // Fetch private key
@@ -3950,10 +3870,7 @@
         
         if ( $db->numrows() < 1 )
         {
-          return array(
-            'mode' => 'error',
-            'error' => 'ERR_DH_KEY_NOT_FOUND'
-          );
+          return $this->get_login_response('api_error', 'ERR_DH_KEY_NOT_FOUND');
         }
         
         list($dh_private, $dh_key_id) = $db->fetchrow_num();
@@ -3972,10 +3889,7 @@
         $dh_secret_check = sha1($dh_secret);
         if ( $dh_secret_check !== $dh_hash )
         {
-          return array(
-            'mode' => 'error',
-            'error' => 'ERR_DH_HASH_NO_MATCH',
-          );
+          return $this->get_login_response('api_error', 'ERR_DH_HASH_NO_MATCH');
         }
         
         // All good! Generate the AES key
@@ -3987,10 +3901,7 @@
           $aes_key = $this->fetch_public_key($req['key_aes']);
           if ( !$aes_key )
           {
-            return array(
-              'mode' => 'error',
-              'error' => 'ERR_AES_LOOKUP_FAILED'
-            );
+            return $this->get_login_response('api_error', 'ERR_AES_LOOKUP_FAILED');
           }
           $userinfo_crypt = $req['userinfo'];
         }
@@ -4003,10 +3914,7 @@
         $userinfo_json = $aes->decrypt($userinfo_crypt, $aes_key, ENC_HEX, true);
         if ( !$userinfo_json )
         {
-          return array(
-            'mode' => 'error',
-            'error' => 'ERR_AES_DECRYPT_FAILED'
-          );
+          return $this->get_login_response('api_error', 'ERR_AES_DECRYPT_FAILED');
         }
         // de-JSON user info
         try
@@ -4015,23 +3923,42 @@
         }
         catch ( Exception $e )
         {
-          return array(
-            'mode' => 'error',
-            'error' => 'ERR_USERINFO_DECODE_FAILED'
-          );
+          return $this->get_login_response('api_error', 'ERR_USERINFO_DECODE_FAILED');
+        }
+        
+      case 'login_pt':
+        // plaintext login
+        if ( $req['mode'] == 'login_pt' )
+        {
+          $userinfo = isset($req['userinfo']) ? $req['userinfo'] : array();
         }
         
         if ( !isset($userinfo['username']) || !isset($userinfo['password']) )
         {
-          return array(
-            'mode' => 'error',
-            'error' => 'ERR_USERINFO_MISSING_VALUES'
-          );
+          return $this->get_login_response('api_error', 'ERR_USERINFO_MISSING_VALUES');
         }
         
         $username =& $userinfo['username'];
         $password =& $userinfo['password'];
         
+        // locked out? check captcha
+        $lockout_data = $this->get_lockout_info();
+        if ( $lockout_data['policy'] == 'captcha' && $lockout_data['active'] )
+        {
+          // policy is captcha -- check if it's correct, and if so, bypass lockout check
+          $real_code = $this->get_captcha($req['captcha_hash']);
+          if ( strtolower($real_code) !== strtolower($req['captcha_code']) )
+          {
+            // captcha is bad
+            return $this->get_login_response('login_failure', 'lockout_bad_captcha');
+          }
+        }
+        else if ( $lockout_data['policy'] == 'lockout' && $lockout_data['active'] )
+        {
+          // we're fully locked out
+          return $this->get_login_response('login_failure', 'lockout_request_denied');
+        }
+        
         // At this point if any extra info was injected into the login data packet, we need to let plugins process it
         /**
          * Called upon processing an incoming login request. If you added anything to the userinfo object during the jshook
@@ -4049,12 +3976,12 @@
           $result = eval($cmd);
           if ( $result === true )
           {
-            return array(
-                'mode' => 'login_success',
-                'key' => ( $this->sid_super ) ? $this->sid_super : false,
+            return $this->get_login_response('login_success', false, array(
+                'key' => $this->sid_super,
                 'user_id' => $this->user_id,
-                'user_level' => $this->user_level
-              );
+                'user_level' => $this->user_level,
+                'reset' => false
+              ));
           }
           else if ( is_array($result) )
           {
@@ -4063,45 +3990,33 @@
               // Pass back any additional information from the error response
               $append = $result;
               unset($append['mode'], $append['error']);
+              $append['from_plugin'] = true;
               
-              $return = array(
-                'mode' => 'login_failure',
-                'error_code' => $result['error'],
-                // Use this to provide a way to respawn the login box
-                'respawn_info' => $this->process_login_request(array('mode' => 'getkey'))
-              );
-              
-              $return = array_merge($append, $return);
-              return $return;
+              return $this->get_login_response('login_failure', $result['error'], $append);
             }
           }
         }
         
-        // If we're logging in with a temp password, attach to the login_password_reset hook to send our JSON response
-        // A bit hackish since it just dies with the response :-(
-        $plugins->attachHook('login_password_reset', '$this->process_login_request(array(\'mode\' => \'respond_password_reset\', \'user_id\' => $row[\'user_id\'], \'temp_password\' => $this->pk_encrypt($password)));');
-        
         // attempt the login
-        // function login_without_crypto($username, $password, $already_md5ed = false, $level = USER_LEVEL_MEMBER, $captcha_hash = false, $captcha_code = false)
-        $login_result = $this->login_without_crypto($username, $password, false, intval($req['level']), @$req['captcha_hash'], @$req['captcha_code'], @$req['remember']);
+        $login_result = $this->login_without_crypto($username, $password, false, intval($req['level']), @$req['remember']);
         
         if ( $login_result['success'] )
         {
-          return array(
-              'mode' => 'login_success',
-              'key' => ( $this->sid_super ) ? $this->sid_super : false,
+          return $this->get_login_response('login_success', false, array(
+              'key' => $this->sid_super,
               'user_id' => $this->user_id,
-              'user_level' => $this->user_level
-            );
+              'user_level' => $this->user_level,
+            ));
+        }
+        else if ( !$login_result['success'] && $login_result['error'] === 'valid_reset' )
+        {
+          return $this->get_login_response('reset_pass_used', false, array(
+              'redirect_url' => $login_result['redirect_url']
+            ));
         }
         else
         {
-          return array(
-              'mode' => 'login_failure',
-              'error_code' => $login_result['error'],
-              // Use this to provide a way to respawn the login box
-              'respawn_info' => $this->process_login_request(array('mode' => 'getkey'))
-            );
+          return $this->get_login_response('login_failure', 'invalid_credentials');
         }
         
         break;
@@ -4164,6 +4079,58 @@
     
   }
   
+  /**
+   * Generate a packet to send to the client for logins.
+   * @param string mode
+   * @param array 
+   * @return array
+   */
+  
+  function get_login_response($mode, $error = false, $base = array())
+  {
+    $this->start();
+    
+    // init
+    $response = $base;
+    // modules in the packet
+    $response['mode'] = $mode;
+    $response['error'] = $error;
+    $response['crypto'] = $mode !== 'login_success' ? $this->get_login_crypto_packet() : false;
+    $response['lockout'] = $mode !== 'login_success' ? $this->get_lockout_info() : false;
+    $response['extended_time'] = intval(getConfig('session_remember_time', '30'));
+    $response['username'] = $this->user_logged_in ? $this->username : false;
+    return $response;
+  }
+  
+  /**
+   * Get a packet of crypto flags for login.
+   * @return array
+   */
+  
+  function get_login_crypto_packet()
+  {
+    global $dh_supported, $_math;
+    
+    $response = array();
+    
+    $response['dh_enable'] = $dh_supported;
+    $response['aes_key'] = $this->rijndael_genkey();
+    
+    // Can we do Diffie-Hellman? If so, generate and stash a public/private key pair.
+    if ( $dh_supported )
+    {
+      $dh_key_priv = dh_gen_private();
+      $dh_key_pub = dh_gen_public($dh_key_priv);
+      $dh_key_priv = $_math->str($dh_key_priv);
+      $dh_key_pub = $_math->str($dh_key_pub);
+      $response['dh_public_key'] = $dh_key_pub;
+      // store the keys in the DB
+      $this->sql('INSERT INTO ' . table_prefix . "diffiehellman( public_key, private_key ) VALUES ( '$dh_key_pub', '$dh_key_priv' );");
+    }
+    
+    return $response;
+  }
+  
 }
 
 /**
--- a/includes/template.php	Sun Oct 25 00:09:11 2009 -0400
+++ b/includes/template.php	Tue Nov 03 22:08:48 2009 -0500
@@ -1212,6 +1212,7 @@
     $protected = is_object($this->page) ? $this->page->ns->cdata['really_protected'] : false;
     
     // Generate the dynamic javascript vars
+    // Sorry. I know. This block is a mess.
     $js_dynamic = '    <script type="text/javascript">// <![CDATA[
       // This section defines some basic and very important variables that are used later in the static Javascript library.
       // SKIN DEVELOPERS: The template variable for this code block is {JS_DYNAMIC_VARS}. This MUST be inserted BEFORE the tag that links to the main Javascript lib.
@@ -1234,11 +1235,11 @@
       var disable_redirect = ' . ( isset($_GET['redirect']) && $_GET['redirect'] == 'no' ? 'true' : 'false' ) . ';
       var pref_disable_js_fx = ' . ( @$session->user_extra['disable_js_fx'] == 1 ? 'true' : 'false' ) . ';
       var csrf_token = "' . $session->csrf_token . '";
-      var prot = ' . ( ($protected) ? 'true' : 'false' ) .'; // No, hacking this var won\'t work, it\'s re-checked on the server
+      var prot = ' . ( ($protected) ? 'true' : 'false' ) .';
       var ENANO_SPECIAL_CREATEPAGE = \''. makeUrl($paths->nslist['Special'].'CreatePage') .'\';
       var ENANO_CREATEPAGE_PARAMS = \'_do=&pagename='. $this->page_id .'&namespace=' . $this->namespace . '\';
       var ENANO_SPECIAL_CHANGESTYLE = \''. makeUrlNS('Special', 'ChangeStyle') .'\';
-      var namespace_list = new Array();
+      var namespace_list = [];
       var msg_loading_component = \'' . addslashes($lang->get('ajax_msg_loading_component')) . '\';
       var AES_BITS = '.AES_BITS.';
       var AES_BLOCKSIZE = '.AES_BLOCKSIZE.';
@@ -1246,9 +1247,9 @@
       var ENANO_LANG_ID = ' . $lang->lang_id . ';
       var ENANO_PAGE_TYPE = "' . addslashes($this->namespace_string) . '";';
     
-    foreach($paths->nslist as $k => $c)
+    foreach ( $paths->nslist as $k => $c )
     {
-      $js_dynamic .= "namespace_list['{$k}'] = '$c';";
+      $js_dynamic .= "namespace_list['{$k}'] = '" . addslashes($c) . "';";
     }
     $js_dynamic .= "\n    //]]>\n    </script>";
     
--- a/language/english/user.json	Sun Oct 25 00:09:11 2009 -0400
+++ b/language/english/user.json	Tue Nov 03 22:08:48 2009 -0500
@@ -47,6 +47,7 @@
       login_success_short: 'Success.',
       login_check_remember: 'Keep me logged in on this computer for %session_length% %length_units% unless I log out',
       login_check_remember_infinite: 'Keep me logged in on this computer until I log out',
+      login_btn_log_in: 'Log in',
       
       login_noact_title: 'Account error',
       login_noact_msg_intro: 'It appears that your user account has not yet been activated.',
@@ -90,11 +91,12 @@
       err_key_wrong_length: 'The encryption key was the wrong length.',
       err_too_big_for_britches: 'You are trying to authenticate at a level that your user account does not permit.',
       err_invalid_credentials: 'You have entered an invalid username or password. Please enter your login details again.',
-      err_invalid_credentials_lockout: ' You have used up %fails% out of %config.lockout_threshold% login attempts. After you have used up all %config.lockout_threshold% login attempts, you will be locked out from logging in for %config.lockout_duration% minutes.',
-      err_invalid_credentials_lockout_captcha: ' You have used up %fails% out of %config.lockout_threshold% login attempts. After you have used up all %config.lockout_threshold% login attempts, you will have to enter a visual confirmation code while logging in, effective for %config.lockout_duration% minutes.',
+      err_invalid_credentials_lockout: ' You have used up %fails% out of %threshold% login attempts. After you have used up all %threshold% login attempts, you will be locked out from logging in for %duration% minutes.',
+      err_invalid_credentials_captcha: ' You have used up %fails% out of %threshold% login attempts. After you have used up all %threshold% login attempts, you will have to enter a visual confirmation code while logging in, effective for %duration% minutes.',
       err_backend_fail: 'You entered the right credentials and everything was validated, but for some reason Enano couldn\'t register your session. This is an internal problem with the site and you are encouraged to contact site administration.',
-      err_locked_out: 'You have used up all %config.lockout_threshold% allowed login attempts. Please wait %time_rem% minute%plural% before attempting to log in again%captcha_blurb%.',
-      err_locked_out_captcha_blurb: ', or enter the visual confirmation code shown above in the appropriate box',
+      err_locked_out_initial_lockout: 'Your computer is locked out from logging in because you entered invalid credentials too many times. This restriction will expire in %time_rem% minute%plural%.',
+      err_locked_out_initial_captcha: 'Please enter the correct visual confirmation code to log in. This restriction will expire in %time_rem% minute%plural%.',
+      err_lockout_bad_captcha: 'The confirmation code you entered was incorrect.',
       err_admin_session_timed_out: 'Your session has timed out; please log in again using the form above.',
       err_email_not_valid: 'The e-mail address you entered is invalid.',
       
--- a/plugins/SpecialUserFuncs.php	Sun Oct 25 00:09:11 2009 -0400
+++ b/plugins/SpecialUserFuncs.php	Tue Nov 03 22:08:48 2009 -0500
@@ -48,101 +48,27 @@
 function page_Special_Login()
 {
   global $db, $session, $paths, $template, $plugins; // Common objects
-  global $__login_status;
-  global $lang;
-  
-  require_once(ENANO_ROOT . '/includes/math.php');
-  require_once(ENANO_ROOT . '/includes/diffiehellman.php');
-  global $dh_supported;
+  global $login_result;
+  global $lang, $output;
   
-  $locked_out = false;
-  // are we locked out?
-  $threshold = ( $_ = getConfig('lockout_threshold') ) ? intval($_) : 5;
-  $duration  = ( $_ = getConfig('lockout_duration') ) ? intval($_) : 15;
-  // convert to minutes
-  $duration  = $duration * 60;
-  $policy = ( $x = getConfig('lockout_policy') && in_array(getConfig('lockout_policy'), array('lockout', 'disable', 'captcha')) ) ? getConfig('lockout_policy') : 'lockout';
-  if ( $policy != 'disable' )
-  {
-    $ipaddr = $db->escape($_SERVER['REMOTE_ADDR']);
-    $timestamp_cutoff = time() - $duration;
-    $q = $session->sql('SELECT timestamp FROM '.table_prefix.'lockout WHERE timestamp > ' . $timestamp_cutoff . ' AND ipaddr = \'' . $ipaddr . '\' ORDER BY timestamp DESC;');
-    $fails = $db->numrows();
-    if ( $fails >= $threshold )
-    {
-      $row = $db->fetchrow();
-      $locked_out = true;
-      $lockdata = array(
-          'locked_out' => true,
-          'lockout_threshold' => $threshold,
-          'lockout_duration' => ( $duration / 60 ),
-          'lockout_fails' => $fails,
-          'lockout_policy' => $policy,
-          'lockout_last_time' => $row['timestamp'],
-          'time_rem' => ( $duration / 60 ) - round( ( time() - $row['timestamp'] ) / 60 ),
-          'captcha' => ''
-        );
-      if ( $policy == 'captcha' )
-      {
-        $lockdata['captcha'] = $session->make_captcha();
-      }
-    }
-    $db->free_result();
-  }
-  
-  if ( isset($_GET['act']) && $_GET['act'] == 'getkey' )
-  {
-    header('Content-type: text/javascript');
-    $username = ( $session->user_logged_in ) ? $session->username : false;
-    $response = Array(
-      'username' => $username,
-      'key' => $pubkey,
-      'challenge' => $challenge,
-      'locked_out' => false
-      );
-    
-    if ( $locked_out )
-    {
-      foreach ( $lockdata as $x => $y )
-      {
-        $response[$x] = $y;
-      }
-      unset($x, $y);
-    }
-    
-    // 1.1.3: generate diffie hellman key
-    $response['dh_supported'] = $dh_supported;
-    if ( $dh_supported )
-    {
-      $dh_key_priv = dh_gen_private();
-      $dh_key_pub = dh_gen_public($dh_key_priv);
-      $dh_key_priv = $_math->str($dh_key_priv);
-      $dh_key_pub = $_math->str($dh_key_pub);
-      $response['dh_public_key'] = $dh_key_pub;
-      // store the keys in the DB
-      $q = $db->sql_query('INSERT INTO ' . table_prefix . "diffiehellman( public_key, private_key ) VALUES ( '$dh_key_pub', '$dh_key_priv' );");
-      if ( !$q )
-        $db->die_json();
-    }
-    
-    $response = enano_json_encode($response);
-    echo $response;
-    return null;
-  }
-  
+  // Determine which level we're going up to
   $level = ( isset($_GET['level']) && in_array($_GET['level'], array('0', '1', '2', '3', '4', '5', '6', '7', '8', '9') ) ) ? intval($_GET['level']) : USER_LEVEL_MEMBER;
   if ( isset($_POST['login']) )
   {
-    if ( in_array($_POST['auth_level'], array('0', '1', '2', '3', '4', '5', '6', '7', '8', '9') ) )
+    if ( in_array($_POST['level'], array('0', '1', '2', '3', '4', '5', '6', '7', '8', '9') ) )
     {
-      $level = intval($_POST['auth_level']);
+      $level = intval($_POST['level']);
     }
   }
-  
+  // Don't allow going from guest straight to elevated
+  // FIXME do we want to allow this with a CSRF check?
   if ( $level > USER_LEVEL_MEMBER && !$session->user_logged_in )
   {
     $level = USER_LEVEL_MEMBER;
   }
+  
+  // If we're already at or above this level, redirect to the target page or, if no target
+  // specified, back to the main page.
   if ( $level <= USER_LEVEL_MEMBER && $session->user_logged_in )
   {
     if ( $target = $paths->getAllParams() )
@@ -152,176 +78,136 @@
     $paths->main_page();
   }
   
-  $template->header();
-  echo '<form action="'.makeUrl($paths->nslist['Special'].'Login').'" method="post" name="loginform" onsubmit="try{runEncryption();}catch(e){};">';
-  $header = ( $level > USER_LEVEL_MEMBER ) ? $lang->get('user_login_message_short_elev') : $lang->get('user_login_message_short');
-  if ( isset($_POST['login']) )
-  {
-    $errstring = $__login_status['error'];
-    switch($__login_status['error'])
-    {
-      case 'key_not_found':
-        $errstring = $lang->get('user_err_key_not_found');
-        break;
-      case 'ERR_DH_KEY_NOT_FOUND':
-        $errstring = $lang->get('user_err_dh_key_not_found'); // . " -- {$__login_status['debug']}";
-        break;
-      case 'ERR_DH_KEY_NOT_INTEGER':
-        $errstring = $lang->get('user_err_dh_key_not_numeric');
-        break;
-      case 'key_wrong_length':
-        $errstring = $lang->get('user_err_key_wrong_length');
-        break;
-      case 'too_big_for_britches':
-        $errstring = $lang->get('user_err_too_big_for_britches');
-        break;
-      case 'invalid_credentials':
-        $errstring = $lang->get('user_err_invalid_credentials');
-        if ( getConfig('lockout_policy', 'lockout') == 'lockout' )
-        {
-          $errstring .= $lang->get('user_err_invalid_credentials_lockout', array('fails' => $__login_status['lockout_fails']));
-        }
-        else if ( getConfig('lockout_policy', 'lockout') == 'captcha' )
-        {
-          $errstring .= $lang->get('user_err_invalid_credentials_lockout_captcha', array('fails' => $__login_status['lockout_fails']));
-        }
-        break;
-      case 'backend_fail':
-        $errstring = $lang->get('user_err_backend_fail');
-        break;
-      case 'locked_out':
-        $attempts = intval($__login_status['lockout_fails']);
-        if ( $attempts > $__login_status['lockout_threshold'])
-          $attempts = $__login_status['lockout_threshold'];
-        
-        $server_time = time();
-        $time_rem = ( intval(@$__login_status['lockout_last_time']) == time() ) ? $__login_status['lockout_duration'] : $__login_status['lockout_duration'] - round( ( $server_time - $__login_status['lockout_last_time'] ) / 60 );
-        if ( $time_rem < 1 )
-          $time_rem = $__login_status['lockout_duration'];
-        
-        $s = ( $time_rem == 1 ) ? '' : $lang->get('meta_plural');
-        
-        $captcha_string = ( $__login_status['lockout_policy'] == 'captcha' ) ? $lang->get('user_err_locked_out_captcha_blurb') : '';
-        $errstring = $lang->get('user_err_locked_out', array('plural' => $s, 'captcha_blurb' => $captcha_string, 'time_rem' => $time_rem));
-        
-        break;
-      default:
-        $errstring = $lang->get($errstring);
-        break;
-    }
-    echo '<div class="error-box-mini">'.$errstring.'</div>';
-  }
+  // Lockout aliasing
+  $lockout =& $login_result['lockout'];
+  
+  $output->header();
+  echo '<form action="' . makeUrl($paths->nslist['Special'].'Login') . '" method="post" name="loginform" onsubmit="try { return runEncryption(); } catch(e) { console.error(e); };">';
+  
   if ( $p = $paths->getAllParams() )
   {
-    echo '<input type="hidden" name="return_to" value="'.$p.'" />';
+    echo '<input type="hidden" name="return_to" value="' . htmlspecialchars($p) . '" />';
   }
   else if ( isset($_POST['login']) && isset($_POST['return_to']) )
   {
-    echo '<input type="hidden" name="return_to" value="'.htmlspecialchars($_POST['return_to']).'" />';
+    echo '<input type="hidden" name="return_to" value="' . htmlspecialchars($_POST['return_to']) . '" />';
+  }
+  
+  // determine what the "remember me" checkbox should say
+  $session_time = intval(getConfig('session_remember_time', '30'));
+  if ( $session_time === 0 )
+  {
+    // sessions are infinite
+    $text_remember = $lang->get('user_login_check_remember_infinite');
   }
+  else
+  {
+    // is the number of days evenly divisible by 7? if so, use weeks
+    if ( $session_time % 7 == 0 )
+    {
+      $session_time = $session_time / 7;
+      $unit = 'week';
+    }
+    else
+    {
+      $unit = 'day';
+    }
+    // if it's not equal to 1, pluralize it
+    if ( $session_time != 1 )
+    {
+      $unit .= $lang->get('meta_plural');
+    }
+    $text_remember = $lang->get('user_login_check_remember', array(
+        'session_length' => $session_time,
+        'length_units' => $lang->get("etc_unit_$unit")
+      ));
+  }
+  
+  if ( $error_text = login_get_error($login_result) )
+  {
+    echo '<div class="error-box-mini">' . htmlspecialchars($error_text) . '</div>';
+  }
+  
+  //
+  // START FORM
+  //
   ?>
     <div class="tblholder">
       <table border="0" style="width: 100%;" cellspacing="1" cellpadding="4">
         <tr>
-          <th colspan="3"><?php echo $header; ?></th>
+          <th colspan="3">
+            <!-- Table header: "Please enter..." -->
+            <?php echo ( $level > USER_LEVEL_MEMBER ) ? $lang->get('user_login_message_short_elev') : $lang->get('user_login_message_short'); ?>
+          </th>
         </tr>
         <tr>
           <td colspan="3" class="row1">
+            <!-- Introduction text -->
             <?php
             if ( $level <= USER_LEVEL_MEMBER )
-            {
               echo '<p>' . $lang->get('user_login_body', array('reg_link' => makeUrlNS('Special', 'Register'))) . '</p>';
-            }
             else
-            {
               echo '<p>' . $lang->get('user_login_body_elev') . '</p>';
-            }
             ?>
           </td>
         </tr>
         <tr>
+          <!-- Username field -->
           <td class="row2">
             <?php echo $lang->get('user_login_field_username'); ?>:
           </td>
           <td class="row1">
-            <input name="username" size="25" type="text" <?php
-              if ( $level <= USER_LEVEL_MEMBER )
-              {
-                echo 'tabindex="1" ';
-              }
-              else
-              {
-                echo 'tabindex="3" ';
-              }
-              if ( $session->user_logged_in )
-              {
-                echo 'value="' . $session->username . '"';
-              }
-              ?> />
+            <input name="username" size="25" type="text" value="<?php echo $session->user_logged_in ? htmlspecialchars($session->username) : ''; ?>" />
           </td>
-          <?php if ( $level <= USER_LEVEL_MEMBER ) { ?>
-          <td rowspan="<?php echo ( ( $locked_out && $lockdata['lockout_policy'] == 'captcha' ) ) ? '4' : '2'; ?>" class="row3">
+          <?php if ( $level <= USER_LEVEL_MEMBER ): ?>
+          <!-- Forgot password / create account links -->
+          <td rowspan="<?php echo ( ( $lockout['active'] && $lockout['policy'] == 'captcha' ) ) ? '4' : '2'; ?>" class="row3">
             <small><?php echo $lang->get('user_login_forgotpass_blurb', array('forgotpass_link' => makeUrlNS('Special', 'PasswordReset'))); ?><br />
             <?php echo $lang->get('user_login_createaccount_blurb', array('reg_link' => makeUrlNS('Special', 'Register'))); ?></small>
           </td>
-          <?php } ?>
+          <?php endif; ?>
         </tr>
         <tr>
+          <!-- Password field -->
           <td class="row2">
             <?php echo $lang->get('user_login_field_password'); ?>:
-          </td><td class="row1"><input name="pass" size="25" type="password" tabindex="<?php echo ( $level <= USER_LEVEL_MEMBER ) ? '2' : '1'; ?>" /></td>
+          </td><td class="row1"><input name="password" size="25" type="password" /></td>
          </tr>
+         
          <?php
-         if ( $locked_out && $lockdata['lockout_policy'] == 'captcha' )
+         // CAPTCHA?
+         if ( $lockout['active'] && $lockout['policy'] == 'captcha' )
          {
            ?>
+           <!-- CAPTCHA -->
            <tr>
-             <td class="row2" rowspan="2"><?php echo $lang->get('user_login_field_captcha'); ?>:<br /></td><td class="row1"><input type="hidden" name="captcha_hash" value="<?php echo $lockdata['captcha']; ?>" /><input name="captcha_code" size="25" type="text" tabindex="<?php echo ( $level <= USER_LEVEL_MEMBER ) ? '3' : '4'; ?>" /></td>
+             <td class="row2" rowspan="2">
+               <?php echo $lang->get('user_login_field_captcha'); ?>:
+               <br />
+             </td>
+             <td class="row1">
+               <input type="hidden" name="captcha_hash" value="<?php echo $lockout['captcha']; ?>" />
+               <input name="captcha_code" size="25" type="text" tabindex="<?php echo ( $level <= USER_LEVEL_MEMBER ) ? '3' : '4'; ?>" />
+             </td>
            </tr>
            <tr>
              <td class="row3">
-               <img src="<?php echo makeUrlNS('Special', 'Captcha/' . $lockdata['captcha']) ?>" onclick="this.src=this.src+'/a';" style="cursor: pointer;" />
+               <img src="<?php echo makeUrlNS('Special', 'Captcha/' . $lockout['captcha']) ?>" onclick="this.src=this.src+'/a';" style="cursor: pointer;" />
              </td>
            </tr>
            <?php
          }
+         
+         // Run hooks
          $code = $plugins->setHook('login_form_html');
          foreach ( $code as $cmd )
          {
            eval($cmd);
          }
+         
+         // level-2 only: "Remember me" switch
          if ( $level <= USER_LEVEL_MEMBER )
          {
-           // "remember me" switch
-           // first order of business is to determine what the checkbox should say
-           $session_time = intval(getConfig('session_remember_time', '30'));
-           if ( $session_time === 0 )
-           {
-             // sessions are infinite
-             $text_remember = $lang->get('user_login_check_remember_infinite');
-           }
-           else
-           {
-             // is the number of days evenly divisible by 7? if so, use weeks
-             if ( $session_time % 7 == 0 )
-             {
-               $session_time = $session_time / 7;
-               $unit = 'week';
-             }
-             else
-             {
-               $unit = 'day';
-             }
-             // if it's not equal to 1, pluralize it
-             if ( $session_time != 1 )
-             {
-               $unit .= 's';
-             }
-             $text_remember = $lang->get('user_login_check_remember', array(
-                 'session_length' => $session_time,
-                 'length_units' => $lang->get("etc_unit_$unit")
-               ));
-           }
            ?>
            <tr>
              <td class="row2">
@@ -334,9 +220,16 @@
                </label>
              </td>
            </tr>
+           
+         <!-- Crypto notice -->
            <?php
          }
-         if ( $level <= USER_LEVEL_MEMBER && ( !isset($_GET['use_crypt']) || ( isset($_GET['use_crypt']) && $_GET['use_crypt']!='0' ) ) )
+         
+         // lol DeMorgan'd
+         $crypto_disable = ( isset($_GET['use_crypt']) && $_GET['use_crypt'] == '0' );
+         
+         // Crypto disable: crypto on, normal login
+         if ( $level <= USER_LEVEL_MEMBER && !$crypto_disable )
          {
            echo '<tr>
              <td class="row3" colspan="3">';
@@ -349,7 +242,8 @@
            echo '  </td>
            </tr>';
          }
-         else if ( $level <= USER_LEVEL_MEMBER && ( isset($_GET['use_crypt']) && $_GET['use_crypt']=='0' ) )
+         // Crypto disable: crypto OFF, normal login
+         else if ( $level <= USER_LEVEL_MEMBER && $crypto_disable )
          {
            echo '<tr>
              <td class="row3" colspan="3">';
@@ -362,7 +256,8 @@
            echo '  </td>
            </tr>';
          }
-         else if ( $level > USER_LEVEL_MEMBER && !strstr($_SERVER['HTTP_USER_AGENT'], 'iPhone') && $dh_supported )
+         // Crypto disable: crypto on, ELEV login
+         else if ( $level > USER_LEVEL_MEMBER && $GLOBALS['dh_supported'] )
          {
            echo '<tr>';
            echo '<td class="row3" colspan="3">';
@@ -372,15 +267,17 @@
          }
          ?>
          
+         <!-- Submit button -->
          <tr>
            <th colspan="3" style="text-align: center" class="subhead">
              <input type="hidden" name="login" value="true" />
-             <input type="submit" value="Log in" tabindex="<?php echo ( $level <= USER_LEVEL_MEMBER ) ? '4' : '2'; ?>" />
+             <input type="submit" value="<?php echo $lang->get('user_login_btn_log_in'); ?>" />
            </th>
          </tr>
       </table>
     </div>
-      <input type="hidden" name="auth_level" value="<?php echo (string)$level; ?>" />
+    
+      <input type="hidden" name="level" value="<?php echo (string)$level; ?>" />
       <?php if ( $level <= USER_LEVEL_MEMBER ): ?>
       <script type="text/javascript">
         document.forms.loginform.username.focus();
@@ -422,19 +319,20 @@
       ?>
     </form>
     <?php
-      echo $session->aes_javascript('loginform', 'pass');
+      if ( !$crypto_disable )
+        echo $session->aes_javascript('loginform', 'password');
     ?>
   <?php
-  $template->footer();
+  $output->footer();
 }
 
 function page_Special_Login_preloader() // adding _preloader to the end of the function name calls the function before $session and $paths setup routines are called
 {
   global $db, $session, $paths, $template, $plugins; // Common objects
-  global $__login_status;
+  global $login_result;
   global $lang;
-  require_once( ENANO_ROOT . '/includes/math.php' );
   
+  // Are we calling the JSON interface?
   $paths->fullpage = $GLOBALS['urlname'];
   if ( $paths->getParam(0) === 'action.json' )
   {
@@ -459,82 +357,31 @@
     $db->close();
     exit;
   }
-  if ( isset($_GET['act']) && $_GET['act'] == 'ajaxlogin' )
-  {
-    echo 'This version of the Enano LoginAPI is deprecated. Please clear your browser\'s cache and try your login again. Developers, please use the action.json method instead.';
-    return true;
-  }
-  if(isset($_POST['login']))
+  
+  // No. Process incoming results from the HTML version.
+  if ( isset($_POST['login']) )
   {
-    $captcha_hash = ( isset($_POST['captcha_hash']) ) ? $_POST['captcha_hash'] : false;
-    $captcha_code = ( isset($_POST['captcha_code']) ) ? $_POST['captcha_code'] : false;
-    
-    try
-    {
-      $password = $session->get_aes_post('pass');
-    }
-    catch ( Exception $e )
-    {
-      $__login_status = array(
-        'mode' => 'error',
-        'error' => $e->getMessage()
-      );
-      return false;
-    }
-    
-    // These are to allow auth plugins to work universally between JSON and HTML login forms
-    $userinfo =& $_POST;
-    $userinfo['password'] =& $password;
-    $req = array(
-      'level' => intval($_POST['auth_level']),
-      'remember' => isset($_POST['remember'])
-    );
-    
-    // At this point if any extra fields were injected into the login form, we need to let plugins process it
+    $_POST['password'] = $session->get_aes_post();
     
-    /**
-     * Called upon processing an incoming login request from the plain HTML login form.. If you added anything to the form,
-     * that will be in the $userinfo array here and on $_POST. Expected return values are: true if your plugin has
-     * not only succeeded but ALSO issued a session key (bypass the whole Enano builtin login process) and an associative array
-     * with "mode" set to "error" and an error string in "error" to send an error back to the client. Any return value other
-     * than these will be ignored.
-     * @hook login_process_userdata_json
-     */
-     
-    $skip_normal_login = false;
+    $result = $session->process_login_request(array(
+        'mode' => 'login_pt',
+        'userinfo' => $_POST,
+        'level' => $_POST['level'],
+        'captcha_hash' => isset($_POST['captcha_hash']) ? $_POST['captcha_hash'] : false,
+        'captcha_code' => isset($_POST['captcha_code']) ? $_POST['captcha_code'] : false
+      ));
     
-    $code = $plugins->setHook('login_process_userdata_json');
-    foreach ( $code as $cmd )
+    if ( $result['mode'] === 'login_success' )
     {
-      $result = eval($cmd);
-      if ( $result === true )
-      {
-        $skip_normal_login = true;
-        $result = array('success' => true);
-        break;
-      }
-      else if ( is_array($result) )
-      {
-        if ( isset($result['mode']) && $result['mode'] === 'error' && isset($result['error']) )
-        {
-          $__login_status = array(
-            'mode' => 'error',
-            'error' => $result['error']
-          );
-          return false;
-        }
-      }
-    }
-    
-    if ( !$skip_normal_login )
-    {
-      $result = $session->login_without_crypto($_POST['username'], $password, false, intval($_POST['auth_level']), $captcha_hash, $captcha_code, isset($_POST['remember']));
-    }
-    
-    if($result['success'])
-    {
+      //
+      // LOGIN SUCCESS.
+      // Redirect as necessary.
+      //
+      
+      // Load our preferences
       $session->start();
       
+      // Decode get_add
       $get_add = false;
       if ( isset($_POST['get_fwd']) )
       {
@@ -553,9 +400,10 @@
         }
       }
       
-      $template->load_theme($session->theme, $session->style);
-      if(isset($_POST['return_to']))
+      // Going to a user-specified page?
+      if ( isset($_POST['return_to']) )
       {
+        // yea
         $name = get_page_title($_POST['return_to']);
         $subst = array(
             'username' => $session->username,
@@ -565,6 +413,7 @@
       }
       else
       {
+        // No, redirect them to the main page
         $subst = array(
             'username' => $session->username,
             'redir_target' => $lang->get('user_login_success_body_mainpage')
@@ -572,34 +421,90 @@
         redirect( makeUrl(get_main_page(), $get_add), $lang->get('user_login_success_title'), $lang->get('user_login_success_body', $subst) );
       }
     }
-    else
+    else if ( $result['mode'] === 'login_success_reset' )
     {
-      if ( $result['error'] === 'valid_reset' )
-      {
-        header('HTTP/1.1 302 Temporary Redirect');
-        header('Location: ' . $result['redirect_url']);
-        
-        $db->close();
-        exit();
-      }
-      $GLOBALS['__login_status'] = $result;
+      // They logged in with a temporary password; send them to the reset form
+      redirect($result['redirect_url'], '', '', 0);
     }
+    // Otherwise, the result is likely an error.
+    $login_result = $result;
+  }
+  else
+  {
+    $login_result = $session->process_login_request(array(
+        'mode' => 'getkey'
+      ));
+  }
+  
+  // This is a bit of a hack. The login form generates AES and DiffieHellman keys on its
+  // own, so we need to clean up the ones from the login request API.
+  if ( !empty($login_result['crypto']) )
+  {
+    $session->process_login_request(array(
+        'mode' => 'clean_key',
+        'key_aes' => $login_result['crypto']['aes_key'],
+        'key_dh' => $login_result['crypto']['dh_public_key'],
+      ));
   }
 }
 
-function SpecialLogin_SendResponse_PasswordReset($user_id, $passkey)
+/**
+ * Given a Login API response, find the appropriate error text, if any.
+ * @param array LoginAPI response
+ * @return mixed Error string, or bool(false) if no error.
+ */
+
+function login_get_error($response)
 {
-  $response = Array(
-      'result' => 'success_reset',
-      'user_id' => $user_id,
-      'temppass' => $passkey
-    );
+  global $lang;
+  
+  if ( !empty($response['lockout']) )
+  {
+    // set this pluralality thing
+    $response['lockout']['plural'] = $response['lockout']['time_rem'] == 1 ? '' : $lang->get('meta_plural');
+  }
   
-  $response = enano_json_encode($response);
-  echo $response;
+  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.
+        $error = '';
+        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_' . strtolower($response['error']));
+        break;
+    }
+  }
   
-  $db->close();
-  exit;
+  return is_string($response['error']) ? $response['error'] : false;
 }
 
 function page_Special_Logout()