includes/sessions.php
changeset 229 97ae8e9d5e29
parent 228 b0a4d179be85
child 248 ed13b72b13cc
--- a/includes/sessions.php	Thu Sep 27 15:55:37 2007 -0400
+++ b/includes/sessions.php	Sat Nov 03 07:43:35 2007 -0400
@@ -362,6 +362,7 @@
   function start()
   {
     global $db, $session, $paths, $template, $plugins; // Common objects
+    global $lang;
     if($this->started) return;
     $this->started = true;
     $user = false;
@@ -381,6 +382,9 @@
         
         if(!$this->compat && $userdata['account_active'] != 1 && $data[1] != 'Special' && $data[1] != 'Admin')
         {
+          $language = intval(getConfig('default_language'));
+          $lang = new Language($language);
+          
           $this->logout();
           $a = getConfig('account_activation');
           switch($a)
@@ -480,6 +484,13 @@
         }
         $user = true;
         
+        // Set language
+        if ( !defined('ENANO_ALLOW_LOAD_NOLANG') )
+        {
+          $lang_id = intval($userdata['user_lang']);
+          $lang = new Language($lang_id);
+        }
+        
         if(isset($_REQUEST['auth']) && !$this->sid_super)
         {
           // Now he thinks he's a moderator. Or maybe even an administrator. Let's find out if he's telling the truth.
@@ -547,14 +558,55 @@
    * @param string $aes_key The MD5 hash of the encryption key, hex-encoded
    * @param string $challenge The 256-bit MD5 challenge string - first 128 bits should be the hash, the last 128 should be the challenge salt
    * @param int $level The privilege level we're authenticating for, defaults to 0
+   * @param array $captcha_hash Optional. If we're locked out and the lockout policy is captcha, this should be the identifier for the code.
+   * @param array $captcha_code Optional. If we're locked out and the lockout policy is captcha, this should be the code the user entered.
    * @return string 'success' on success, or error string on failure
    */
    
-  function login_with_crypto($username, $aes_data, $aes_key, $challenge, $level = USER_LEVEL_MEMBER)
+  function login_with_crypto($username, $aes_data, $aes_key, $challenge, $level = USER_LEVEL_MEMBER, $captcha_hash = false, $captcha_code = false)
   {
     global $db, $session, $paths, $template, $plugins; // Common objects
     
     $privcache = $this->private_key;
+
+    if ( !defined('IN_ENANO_INSTALL') )
+    {
+      // Lockout stuff
+      $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 == '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 ( $policy != 'disable' && !( $policy == 'captcha' && isset($real_code) && $real_code == $captcha_code ) )
+      {
+        $ipaddr = $db->escape($_SERVER['REMOTE_ADDR']);
+        $timestamp_cutoff = time() - $duration;
+        $q = $this->sql('SELECT timestamp FROM '.table_prefix.'lockout WHERE timestamp > ' . $timestamp_cutoff . ' AND ipaddr = \'' . $ipaddr . '\' ORDER BY timestamp DESC;');
+        $fails = $db->numrows();
+        if ( $fails >= $threshold )
+        {
+          // ooh boy, somebody's in trouble ;-)
+          $row = $db->fetchrow();
+          $db->free_result();
+          return array(
+              'success' => false,
+              'error' => 'locked_out',
+              'lockout_threshold' => $threshold,
+              'lockout_duration' => ( $duration / 60 ),
+              'lockout_fails' => $fails,
+              'lockout_policy' => $policy,
+              'time_rem' => ( $duration / 60 ) - round( ( time() - $row['timestamp'] ) / 60 ),
+              'lockout_last_time' => $row['timestamp']
+            );
+        }
+        $db->free_result();
+      }
+    }
     
     // Instanciate the Rijndael encryption object
     $aes = new AESCrypt(AES_BITS, AES_BLOCKSIZE);
@@ -563,13 +615,19 @@
     
     $aes_key = $this->fetch_public_key($aes_key);
     if(!$aes_key)
-      return 'Couldn\'t look up public key "'.$aes_key.'" for decryption';
+      return array(
+        'success' => false,
+        'error' => 'key_not_found'
+        );
     
     // Convert the key to a binary string
     $bin_key = hexdecode($aes_key);
     
     if(strlen($bin_key) != AES_BITS / 8)
-      return 'The decryption key is the wrong length';
+      return array(
+        'success' => false,
+        'error' => 'key_wrong_length'
+        );
     
     // Decrypt our password
     $password = $aes->decrypt($aes_data, $bin_key, ENC_HEX);
@@ -585,13 +643,34 @@
     $this->sql('SELECT password,old_encryption,user_id,user_level,theme,style,temp_password,temp_password_time FROM '.table_prefix.'users WHERE lcase(username)=\''.$db_username_lower.'\' OR username=\'' . $db_username . '\';');
     if($db->numrows() < 1)
     {
-      return "The username and/or password is incorrect.\n$db->latest_query";
       // This wasn't logged in <1.0.2, dunno how it slipped through
       if($level > USER_LEVEL_MEMBER)
         $this->sql('INSERT INTO '.table_prefix.'logs(log_type,action,time_id,date_string,author,edit_summary,page_text) VALUES(\'security\', \'admin_auth_bad\', '.time().', \''.date('d M Y h:i a').'\', \''.$db->escape($username).'\', \''.$db->escape($_SERVER['REMOTE_ADDR']).'\', ' . intval($level) . ')');
       else
         $this->sql('INSERT INTO '.table_prefix.'logs(log_type,action,time_id,date_string,author,edit_summary) VALUES(\'security\', \'auth_bad\', '.time().', \''.date('d M Y h:i a').'\', \''.$db->escape($username).'\', \''.$db->escape($_SERVER['REMOTE_ADDR']).'\')');
-        
+    
+      if ( $policy != 'disable' && !defined('IN_ENANO_INSTALL') )
+      {
+        $ipaddr = $db->escape($_SERVER['REMOTE_ADDR']);
+        // increment fail count
+        $this->sql('INSERT INTO '.table_prefix.'lockout(ipaddr, timestamp, action) VALUES(\'' . $ipaddr . '\', UNIX_TIMESTAMP(), \'credential\');');
+        $fails++;
+        // ooh boy, somebody's in trouble ;-)
+        return array(
+            'success' => false,
+            'error' => ( $fails >= $threshold ) ? 'locked_out' : 'invalid_credentials',
+            'lockout_threshold' => $threshold,
+            'lockout_duration' => ( $duration / 60 ),
+            'lockout_fails' => $fails,
+            'time_rem' => ( $duration / 60 ),
+            'lockout_policy' => $policy
+          );
+      }
+      
+      return array(
+          'success' => false,
+          'error' => 'invalid_credentials'
+        );
     }
     $row = $db->fetchrow();
     
@@ -642,7 +721,10 @@
     if($success)
     {
       if($level > $row['user_level'])
-        return 'You are not authorized for this level of access.';
+        return array(
+          'success' => false,
+          'error' => 'too_big_for_britches'
+        );
       
       $sess = $this->register_session(intval($row['user_id']), $username, $password, $level);
       if($sess)
@@ -662,10 +744,15 @@
         {
           eval($cmd);
         }
-        return 'success';
+        return array(
+          'success' => true
+        );
       }
       else
-        return 'Your login credentials were correct, but an internal error occurred while registering the session key in the database.';
+        return array(
+          'success' => false,
+          'error' => 'backend_fail'
+        );
     }
     else
     {
@@ -674,7 +761,28 @@
       else
         $this->sql('INSERT INTO '.table_prefix.'logs(log_type,action,time_id,date_string,author,edit_summary) VALUES(\'security\', \'auth_bad\', '.time().', \''.date('d M Y h:i a').'\', \''.$db->escape($username).'\', \''.$db->escape($_SERVER['REMOTE_ADDR']).'\')');
         
-      return 'The username and/or password is incorrect.';
+      // Do we also need to increment the lockout countdown?
+      if ( $policy != 'disable' && !defined('IN_ENANO_INSTALL') )
+      {
+        $ipaddr = $db->escape($_SERVER['REMOTE_ADDR']);
+        // increment fail count
+        $this->sql('INSERT INTO '.table_prefix.'lockout(ipaddr, timestamp, action) VALUES(\'' . $ipaddr . '\', UNIX_TIMESTAMP(), \'credential\');');
+        $fails++;
+        return array(
+            'success' => false,
+            'error' => ( $fails >= $threshold ) ? 'locked_out' : 'invalid_credentials',
+            'lockout_threshold' => $threshold,
+            'lockout_duration' => ( $duration / 60 ),
+            'lockout_fails' => $fails,
+            'time_rem' => ( $duration / 60 ),
+            'lockout_policy' => $policy
+          );
+      }
+        
+      return array(
+        'success' => false,
+        'error' => 'invalid_credentials'
+      );
     }
   }
   
@@ -700,6 +808,45 @@
       return $this->login_compat($username, $pass_hashed, $level);
     }
     
+    if ( !defined('IN_ENANO_INSTALL') )
+    {
+      // Lockout stuff
+      $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 == '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 ( $policy != 'disable' && !( $policy == 'captcha' && isset($real_code) && $real_code == $captcha_code ) )
+      {
+        $ipaddr = $db->escape($_SERVER['REMOTE_ADDR']);
+        $timestamp_cutoff = time() - $duration;
+        $q = $this->sql('SELECT timestamp FROM '.table_prefix.'lockout WHERE timestamp > ' . $timestamp_cutoff . ' AND ipaddr = \'' . $ipaddr . '\' ORDER BY timestamp DESC;');
+        $fails = $db->numrows();
+        if ( $fails > $threshold )
+        {
+          // ooh boy, somebody's in trouble ;-)
+          $row = $db->fetchrow();
+          $db->free_result();
+          return array(
+              'success' => false,
+              'error' => 'locked_out',
+              'lockout_threshold' => $threshold,
+              'lockout_duration' => ( $duration / 60 ),
+              'lockout_fails' => $fails,
+              'lockout_policy' => $policy,
+              'time_rem' => $duration - round( ( time() - $row['timestamp'] ) / 60 ),
+              'lockout_last_time' => $row['timestamp']
+            );
+        }
+        $db->free_result();
+      }
+    }
+    
     // Instanciate the Rijndael encryption object
     $aes = new AESCrypt(AES_BITS, AES_BLOCKSIZE);
     
@@ -709,7 +856,35 @@
     // Retrieve the real password from the database
     $this->sql('SELECT password,old_encryption,user_id,user_level,temp_password,temp_password_time FROM '.table_prefix.'users WHERE lcase(username)=\''.$this->prepare_text(strtolower($username)).'\';');
     if($db->numrows() < 1)
-      return 'The username and/or password is incorrect.';
+    {
+      // This wasn't logged in <1.0.2, dunno how it slipped through
+      if($level > USER_LEVEL_MEMBER)
+        $this->sql('INSERT INTO '.table_prefix.'logs(log_type,action,time_id,date_string,author,edit_summary,page_text) VALUES(\'security\', \'admin_auth_bad\', '.time().', \''.date('d M Y h:i a').'\', \''.$db->escape($username).'\', \''.$db->escape($_SERVER['REMOTE_ADDR']).'\', ' . intval($level) . ')');
+      else
+        $this->sql('INSERT INTO '.table_prefix.'logs(log_type,action,time_id,date_string,author,edit_summary) VALUES(\'security\', \'auth_bad\', '.time().', \''.date('d M Y h:i a').'\', \''.$db->escape($username).'\', \''.$db->escape($_SERVER['REMOTE_ADDR']).'\')');
+      
+      // Do we also need to increment the lockout countdown?
+      if ( $policy != 'disable' && !defined('IN_ENANO_INSTALL') )
+      {
+        $ipaddr = $db->escape($_SERVER['REMOTE_ADDR']);
+        // increment fail count
+        $this->sql('INSERT INTO '.table_prefix.'lockout(ipaddr, timestamp, action) VALUES(\'' . $ipaddr . '\', UNIX_TIMESTAMP(), \'credential\');');
+        $fails++;
+        return array(
+            'success' => false,
+            'error' => ( $fails >= $threshold ) ? 'locked_out' : 'invalid_credentials',
+            'lockout_threshold' => $threshold,
+            'lockout_duration' => ( $duration / 60 ),
+            'lockout_fails' => $fails,
+            'lockout_policy' => $policy
+          );
+      }
+      
+      return array(
+        'success' => false,
+        'error' => 'invalid_credentials'
+      );
+    }
     $row = $db->fetchrow();
     
     // Check to see if we're logging in using a temporary password
@@ -758,7 +933,10 @@
     if($success)
     {
       if((int)$level > (int)$row['user_level'])
-        return 'You are not authorized for this level of access.';
+        return array(
+          'success' => false,
+          'error' => 'too_big_for_britches'
+        );
       $sess = $this->register_session(intval($row['user_id']), $username, $real_pass, $level);
       if($sess)
       {
@@ -773,10 +951,15 @@
           eval($cmd);
         }
         
-        return 'success';
+        return array(
+          'success' => true
+          );
       }
       else
-        return 'Your login credentials were correct, but an internal error occured while registering the session key in the database.';
+        return array(
+          'success' => false,
+          'error' => 'backend_fail'
+        );
     }
     else
     {
@@ -785,7 +968,27 @@
       else
         $this->sql('INSERT INTO '.table_prefix.'logs(log_type,action,time_id,date_string,author,edit_summary) VALUES(\'security\', \'auth_bad\', '.time().', \''.date('d M Y h:i a').'\', \''.$db->escape($username).'\', \''.$db->escape($_SERVER['REMOTE_ADDR']).'\')');
         
-      return 'The username and/or password is incorrect.';
+      // Do we also need to increment the lockout countdown?
+      if ( $policy != 'disable' && !defined('IN_ENANO_INSTALL') )
+      {
+        $ipaddr = $db->escape($_SERVER['REMOTE_ADDR']);
+        // increment fail count
+        $this->sql('INSERT INTO '.table_prefix.'lockout(ipaddr, timestamp, action) VALUES(\'' . $ipaddr . '\', UNIX_TIMESTAMP(), \'credential\');');
+        $fails++;
+        return array(
+            'success' => false,
+            'error' => ( $fails >= $threshold ) ? 'locked_out' : 'invalid_credentials',
+            'lockout_threshold' => $threshold,
+            'lockout_duration' => ( $duration / 60 ),
+            'lockout_fails' => $fails,
+            'lockout_policy' => $policy
+          );
+      }
+        
+      return array(
+        'success' => false,
+        'error' => 'invalid_credentials'
+      );
     }
   }
   
@@ -857,7 +1060,7 @@
     {
       // Stash it in a cookie
       // For now, make the cookie last forever, we can change this in 1.1.x
-      setcookie( 'sid', $session_key, time()+315360000, scriptPath.'/' );
+      setcookie( 'sid', $session_key, time()+315360000, scriptPath.'/', null, ( isset($_SERVER['HTTPS']) ) );
       $_COOKIE['sid'] = $session_key;
     }
     // $keyhash is stored in the database, this is for compatibility with the older DB structure
@@ -919,6 +1122,7 @@
   function register_guest_session()
   {
     global $db, $session, $paths, $template, $plugins; // Common objects
+    global $lang;
     $this->username = $_SERVER['REMOTE_ADDR'];
     $this->user_level = USER_LEVEL_GUEST;
     if($this->compat || defined('IN_ENANO_INSTALL'))
@@ -932,6 +1136,12 @@
       $this->style = ( isset($_GET['style']) && file_exists(ENANO_ROOT.'/themes/'.$this->theme . '/css/'.$_GET['style'].'.css' )) ? $_GET['style'] : substr($template->named_theme_list[$this->theme]['default_style'], 0, strlen($template->named_theme_list[$this->theme]['default_style'])-4);
     }
     $this->user_id = 1;
+    if ( !defined('ENANO_ALLOW_LOAD_NOLANG') )
+    {
+      // This is a VERY special case we are allowing. It lets the installer create languages using the Enano API.
+      $language = intval(getConfig('default_language'));
+      $lang = new Language($language);
+    }
   }
   
   /**
@@ -959,7 +1169,7 @@
     }
     $keyhash = md5($key);
     $salt = $db->escape($keydata[3]);
-    $query = $db->sql_query('SELECT u.user_id AS uid,u.username,u.password,u.email,u.real_name,u.user_level,u.theme,u.style,u.signature,u.reg_time,u.account_active,u.activation_key,k.source_ip,k.time,k.auth_level,COUNT(p.message_id) AS num_pms,x.* FROM '.table_prefix.'session_keys AS k
+    $query = $db->sql_query('SELECT u.user_id AS uid,u.username,u.password,u.email,u.real_name,u.user_level,u.theme,u.style,u.signature,u.reg_time,u.account_active,u.activation_key,k.source_ip,k.time,k.auth_level,COUNT(p.message_id) AS num_pms,u.user_lang,x.* FROM '.table_prefix.'session_keys AS k
                                LEFT JOIN '.table_prefix.'users AS u
                                  ON ( u.user_id=k.user_id )
                                LEFT JOIN '.table_prefix.'users_extra AS x
@@ -1114,7 +1324,10 @@
     if($level > USER_LEVEL_CHPREF)
     {
       $aes = new AESCrypt(AES_BITS, AES_BLOCKSIZE);
-      if(!$this->user_logged_in || $this->auth_level < USER_LEVEL_MOD) return 'success';
+      if(!$this->user_logged_in || $this->auth_level < USER_LEVEL_MOD)
+      {
+        return 'success';
+      }
       // Destroy elevated privileges
       $keyhash = md5(strrev($this->sid_super));
       $this->sql('DELETE FROM '.table_prefix.'session_keys WHERE session_key=\''.$keyhash.'\' AND user_id=\'' . $this->user_id . '\';');
@@ -2117,13 +2330,30 @@
       return false;
     }
     
+    // cache of permission objects (to save RAM and SQL queries)
+    static $objcache = array();
+    
+    if ( count($objcache) == 0 )
+    {
+      foreach ( $paths->nslist as $key => $_ )
+      {
+        $objcache[$key] = array();
+      }
+    }
+    
+    if ( isset($objcache[$namespace][$page_id]) )
+    {
+      return $objcache[$namespace][$page_id];
+    }
+    
     //if ( !isset( $paths->pages[$paths->nslist[$namespace] . $page_id] ) )
     //{
     //  // Page does not exist
     //  return false;
     //}
     
-    $object = new Session_ACLPageInfo( $page_id, $namespace, $this->acl_types, $this->acl_descs, $this->acl_deps, $this->acl_base_cache );
+    $objcache[$namespace][$page_id] = new Session_ACLPageInfo( $page_id, $namespace, $this->acl_types, $this->acl_descs, $this->acl_deps, $this->acl_base_cache );
+    $object =& $objcache[$namespace][$page_id];
     
     return $object;