Merging in fixes and updates from stable
authorDan
Sun, 18 Nov 2007 20:37:08 -0500
changeset 271 f088805540ae
parent 266 917dcc6c4ceb (current diff)
parent 270 5bcdee999015 (diff)
child 278 53ba55d33abb
Merging in fixes and updates from stable
includes/common.php
includes/dbal.php
includes/functions.php
includes/pageutils.php
includes/sessions.php
install.php
plugins/SpecialAdmin.php
plugins/SpecialUserFuncs.php
--- a/includes/common.php	Sat Nov 17 22:25:37 2007 -0500
+++ b/includes/common.php	Sun Nov 18 20:37:08 2007 -0500
@@ -273,6 +273,12 @@
     @call_user_func('page_'.$p[1].'_'.$p[0].'_preloader');
   }
   
+  // One quick security check...
+  if ( !is_valid_ip($_SERVER['REMOTE_ADDR']) )
+  {
+    die('SECURITY: spoofed IP address');
+  }
+  
   $session->start();
   $paths->init();
   
--- a/includes/dbal.php	Sat Nov 17 22:25:37 2007 -0500
+++ b/includes/dbal.php	Sun Nov 18 20:37:08 2007 -0500
@@ -121,13 +121,25 @@
     return $internal_text;
   }
   
-  function connect() {
+  function connect()
+  {
     $this->enable_errorhandler();
+    
     dc_here('dbal: trying to connect....');
-    @include(ENANO_ROOT.'/config.php');
-    if(isset($crypto_key))
+    
+    if ( defined('IN_ENANO_INSTALL') )
+    {
+      @include(ENANO_ROOT.'/config.new.php');
+    }
+    else
+    {
+      @include(ENANO_ROOT.'/config.php');
+    }
+      
+    if ( isset($crypto_key) )
       unset($crypto_key); // Get this sucker out of memory fast
-    if(!defined('ENANO_INSTALLED') && !defined('MIDGET_INSTALLED') && !defined('IN_ENANO_INSTALL') )
+    
+    if ( !defined('ENANO_INSTALLED') && !defined('MIDGET_INSTALLED') && !defined('IN_ENANO_INSTALL') )
     {
       dc_here('dbal: oops, looks like Enano isn\'t set up. Constants ENANO_INSTALLED, MIDGET_INSTALLED, and IN_ENANO_INSTALL are all undefined.');
       header('Location: install.php'); 
@@ -136,46 +148,74 @@
     $this->_conn = @mysql_connect($dbhost, $dbuser, $dbpasswd);
     unset($dbuser);
     unset($dbpasswd); // Security
-    if(!$this->_conn) { dc_here('dbal: uhoh!<br />'.mysql_error()); grinding_halt('Enano is having a problem', '<p>Error: couldn\'t connect to MySQL.<br />'.mysql_error().'</p>'); }
+    
+    if ( !$this->_conn )
+    {
+      dc_here('dbal: uhoh!<br />'.mysql_error());
+      grinding_halt('Enano is having a problem', '<p>Error: couldn\'t connect to MySQL.<br />'.mysql_error().'</p>');
+    }
+    
+    // Reset some variables
     $this->query_backtrace = '';
     $this->num_queries = 0;
+    
     dc_here('dbal: we\'re in, selecting database...');
     $q = $this->sql_query('USE `'.$dbname.'`;');
-    if(!$q) $this->_die('The database could not be selected.');
+    
+    if ( !$q )
+      $this->_die('The database could not be selected.');
+    
+    // We're in!
     dc_here('dbal: connected to MySQL');
+    
     $this->disable_errorhandler();
+    return true;
   }
   
-  function sql_query($q) {
+  function sql_query($q)
+  {
     $this->enable_errorhandler();
     $this->num_queries++;
-    $this->query_backtrace .= $q."\n";
+    $this->query_backtrace .= $q . "\n";
     $this->latest_query = $q;
     dc_here('dbal: making SQL query:<br /><tt>'.$q.'</tt>');
-    if(!$this->_conn) $this->_die('A database connection has not yet been established.');
-    if(!$this->check_query($q))
+    // First make sure we have a connection
+    if ( !$this->_conn )
+    {
+      $this->_die('A database connection has not yet been established.');
+    }
+    // Does this query look malicious?
+    if ( !$this->check_query($q) )
     {
       $this->report_query($q);
       grinding_halt('SQL Injection attempt', '<p>Enano has caught and prevented an SQL injection attempt. Your IP address has been recorded and the administrator has been notified.</p><p>Query was:</p><pre>'.htmlspecialchars($q).'</pre>');
     }
+    
     $r = mysql_query($q, $this->_conn);
     $this->latest_result = $r;
     $this->disable_errorhandler();
     return $r;
   }
   
-  function sql_unbuffered_query($q) {
+  function sql_unbuffered_query($q)
+  {
     $this->enable_errorhandler();
     $this->num_queries++;
     $this->query_backtrace .= '(UNBUFFERED) ' . $q."\n";
     $this->latest_query = $q;
     dc_here('dbal: making SQL query:<br /><tt>'.$q.'</tt>');
-    if(!$this->_conn) $this->_die('A database connection has not yet been established.');
-    if(!$this->check_query($q))
+    // First make sure we have a connection
+    if ( !$this->_conn )
+    {
+      $this->_die('A database connection has not yet been established.');
+    }
+    // Does this query look malicious?
+    if ( !$this->check_query($q) )
     {
       $this->report_query($q);
       grinding_halt('SQL Injection attempt', '<p>Enano has caught and prevented an SQL injection attempt. Your IP address has been recorded and the administrator has been notified.</p><p>Query was:</p><pre>'.htmlspecialchars($q).'</pre>');
     }
+    
     $r = mysql_unbuffered_query($q, $this->_conn);
     $this->latest_result = $r;
     $this->disable_errorhandler();
--- a/includes/functions.php	Sat Nov 17 22:25:37 2007 -0500
+++ b/includes/functions.php	Sun Nov 18 20:37:08 2007 -0500
@@ -2735,7 +2735,7 @@
     {
       $array[$i] = decode_unicode_url($val);
     }
-    else
+    else if ( is_array($val) )
     {
       $array[$i] = decode_unicode_array($val);
     }
@@ -2999,6 +2999,72 @@
   return $ips;
 }
 
+/**
+ * Parses a valid IP address range into a regular expression.
+ * @param string IP range string
+ * @return string
+ */
+
+function parse_ip_range_regex($range)
+{
+  // Regular expression to test the range string for validity
+  $regex = '/^(([0-9]+(-[0-9]+)?)(\|([0-9]+(-[0-9]+)?))*)\.'
+           . '(([0-9]+(-[0-9]+)?)(\|([0-9]+(-[0-9]+)?))*)\.'
+           . '(([0-9]+(-[0-9]+)?)(\|([0-9]+(-[0-9]+)?))*)\.'
+           . '(([0-9]+(-[0-9]+)?)(\|([0-9]+(-[0-9]+)?))*)$/';
+  if ( !preg_match($regex, $range) )
+  {
+    return false;
+  }
+  $octets = array(0 => array(), 1 => array(), 2 => array(), 3 => array());
+  list($octets[0], $octets[1], $octets[2], $octets[3]) = explode('.', $range);
+  $return = '^';
+  foreach ( $octets as $octet )
+  {
+    // alternatives array
+    $alts = array();
+    if ( strpos($octet, '|') )
+    {
+      $particles = explode('|', $octet);
+    }
+    else
+    {
+      $particles = array($octet);
+    }
+    foreach ( $particles as $atom )
+    {
+      // each $atom will be either
+      if ( strval(intval($atom)) == $atom )
+      {
+        $alts[] = $atom;
+        continue;
+      }
+      else
+      {
+        // it's a range - parse it out
+        $alt2 = int_range($atom);
+        if ( !$alt2 )
+          return false;
+        foreach ( $alt2 as $neutrino )
+          $alts[] = $neutrino;
+      }
+    }
+    $alts = array_unique($alts);
+    $alts = '|' . implode('|', $alts) . '|';
+    // we can further optimize/compress this by weaseling our way into using some character ranges
+    for ( $i = 1; $i <= 25; $i++ )
+    {
+      $alts = str_replace("|{$i}0|{$i}1|{$i}2|{$i}3|{$i}4|{$i}5|{$i}6|{$i}7|{$i}8|{$i}9|", "|{$i}[0-9]|", $alts);
+    }
+    $alts = str_replace("|1|2|3|4|5|6|7|8|9|", "|[1-9]|", $alts);
+    $alts = '(' . substr($alts, 1, -1) . ')';
+    $return .= $alts . '\.';
+  }
+  $return = substr($return, 0, -2);
+  $return .= '$';
+  return $return;
+}
+
 function password_score_len($password)
 {
   if ( !is_string($password) )
--- a/includes/pageutils.php	Sat Nov 17 22:25:37 2007 -0500
+++ b/includes/pageutils.php	Sun Nov 18 20:37:08 2007 -0500
@@ -24,6 +24,7 @@
   function checkusername($name)
   {
     global $db, $session, $paths, $template, $plugins; // Common objects
+    $name = str_replace('_', ' ', $name);
     $q = $db->sql_query('SELECT username FROM ' . table_prefix.'users WHERE username=\'' . $db->escape(rawurldecode($name)) . '\'');
     if ( !$q )
     {
--- a/includes/sessions.php	Sat Nov 17 22:25:37 2007 -0500
+++ b/includes/sessions.php	Sun Nov 18 20:37:08 2007 -0500
@@ -151,7 +151,7 @@
    */
    
   //var $valid_username = '([A-Za-z0-9 \!\@\(\)-]+)';
-  var $valid_username = '([^<>_&\?\'"%\n\r\t\a\/]+)';
+  var $valid_username = '([^<>&\?\'"%\n\r\t\a\/]+)';
    
   /**
    * What we're allowed to do as far as permissions go. This changes based on the value of the "auth" URI param.
@@ -260,7 +260,16 @@
   function __construct()
   {
     global $db, $session, $paths, $template, $plugins; // Common objects
-    include(ENANO_ROOT.'/config.php');
+    
+    if ( defined('IN_ENANO_INSTALL') )
+    {
+      @include(ENANO_ROOT.'/config.new.php');
+    }
+    else
+    {
+      @include(ENANO_ROOT.'/config.php');
+    }
+    
     unset($dbhost, $dbname, $dbuser, $dbpasswd);
     if(isset($crypto_key))
     {
@@ -563,7 +572,7 @@
    * @return string 'success' on success, or error string on failure
    */
    
-  function login_with_crypto($username, $aes_data, $aes_key, $challenge, $level = USER_LEVEL_MEMBER, $captcha_hash = false, $captcha_code = false)
+  function login_with_crypto($username, $aes_data, $aes_key_id, $challenge, $level = USER_LEVEL_MEMBER, $captcha_hash = false, $captcha_code = false)
   {
     global $db, $session, $paths, $template, $plugins; // Common objects
     
@@ -613,7 +622,7 @@
     
     // Fetch our decryption key
     
-    $aes_key = $this->fetch_public_key($aes_key);
+    $aes_key = $this->fetch_public_key($aes_key_id);
     if(!$aes_key)
       return array(
         'success' => false,
@@ -636,6 +645,7 @@
     $success = false;
     
     // Escaped username
+    $username = str_replace('_', ' ', $username);
     $db_username_lower = $this->prepare_text(strtolower($username));
     $db_username       = $this->prepare_text($username);
     
@@ -802,6 +812,10 @@
     
     $pass_hashed = ( $already_md5ed ) ? $password : md5($password);
     
+    // Replace underscores with spaces in username
+    // (Added in 1.0.2)
+    $username = str_replace('_', ' ', $username);
+    
     // Perhaps we're upgrading Enano?
     if($this->compat)
     {
@@ -1025,7 +1039,7 @@
   
   /**
    * Registers a session key in the database. This function *ASSUMES* that the username and password have already been validated!
-   * Basically the session key is a base64-encoded cookie (encrypted with the site's private key) that says "u=[username];p=[sha1 of password]"
+   * Basically the session key is a hex-encoded cookie (encrypted with the site's private key) that says "u=[username];p=[sha1 of password];s=[unique key id]"
    * @param int $user_id
    * @param string $username
    * @param string $password
@@ -1084,7 +1098,7 @@
   }
   
   /**
-   * Identical to register_session in nature, but uses the old login/table structure. DO NOT use this.
+   * Identical to register_session in nature, but uses the old login/table structure. DO NOT use this except in the upgrade script under very controlled circumstances.
    * @see sessionManager::register_session()
    * @access private
    */
@@ -1536,59 +1550,79 @@
   function check_banlist()
   {
     global $db, $session, $paths, $template, $plugins; // Common objects
-    if($this->compat)
-      $q = $this->sql('SELECT ban_id,ban_type,ban_value,is_regex FROM '.table_prefix.'banlist ORDER BY ban_type;');
-    else
-      $q = $this->sql('SELECT ban_id,ban_type,ban_value,is_regex,reason FROM '.table_prefix.'banlist ORDER BY ban_type;');
-    if(!$q) $db->_die('The banlist data could not be selected.');
-    $banned = false;
-    while($row = $db->fetchrow())
+    $col_reason = ( $this->compat ) ? '"No reason entered (session manager is in compatibility mode)" AS reason' : 'reason';
+    $is_banned = false;
+    if ( $this->user_logged_in )
     {
-      if($this->compat)
-        $row['reason'] = 'None available - session manager is in compatibility mode';
-      switch($row['ban_type'])
+      // check by IP, email, and username
+      $sql = "SELECT $col_reason, ban_value, ban_type, is_regex FROM " . table_prefix . "banlist WHERE \n"
+            . "    ( ban_type = " . BAN_IP    . " AND is_regex = 0 ) OR \n"
+            . "    ( ban_type = " . BAN_IP    . " AND is_regex = 1 AND '{$_SERVER['REMOTE_ADDR']}' REGEXP ban_value ) OR \n"
+            . "    ( ban_type = " . BAN_USER  . " AND is_regex = 0 AND ban_value = '{$this->username}' ) OR \n"
+            . "    ( ban_type = " . BAN_USER  . " AND is_regex = 1 AND '{$this->username}' REGEXP ban_value ) OR \n"
+            . "    ( ban_type = " . BAN_EMAIL . " AND is_regex = 0 AND ban_value = '{$this->email}' ) OR \n"
+            . "    ( ban_type = " . BAN_EMAIL . " AND is_regex = 1 AND '{$this->email}' REGEXP ban_value ) \n"
+            . "  ORDER BY ban_type ASC;";
+      $q = $this->sql($sql);
+      if ( $db->numrows() > 0 )
       {
-      case BAN_IP:
-        if(intval($row['is_regex'])==1) {
-          if(preg_match('#'.$row['ban_value'].'#i', $_SERVER['REMOTE_ADDR']))
+        while ( list($reason, $ban_value, $ban_type, $is_regex) = $db->fetchrow_num() )
+        {
+          if ( $ban_type == BAN_IP && $row['is_regex'] != 1 )
           {
+            // check range
+            $regexp = parse_ip_range_regex($ban_value);
+            if ( !$regexp )
+            {
+              continue;
+            }
+            if ( preg_match("/$regexp/", $_SERVER['REMOTE_ADDR']) )
+            {
+              $banned = true;
+            }
+          }
+          else
+          {
+            // User is banned
             $banned = true;
-            $reason = $row['reason'];
           }
         }
-        else {
-          if($row['ban_value']==$_SERVER['REMOTE_ADDR']) { $banned = true; $reason = $row['reason']; }
-        }
-        break;
-      case BAN_USER:
-        if(intval($row['is_regex'])==1) {
-          if(preg_match('#'.$row['ban_value'].'#i', $this->username))
+      }
+      $db->free_result();
+    }
+    else
+    {
+      // check by IP only
+      $sql = "SELECT $col_reason, ban_value, ban_type, is_regex FROM " . table_prefix . "banlist WHERE
+                ( ban_type = " . BAN_IP    . " AND is_regex = 0 ) OR
+                ( ban_type = " . BAN_IP    . " AND is_regex = 1 AND '{$_SERVER['REMOTE_ADDR']}' REGEXP ban_value )
+              ORDER BY ban_type ASC;";
+      $q = $this->sql($sql);
+      if ( $db->numrows() > 0 )
+      {
+        while ( list($reason, $ban_value, $ban_type, $is_regex) = $db->fetchrow_num() )
+        {
+          if ( $ban_type == BAN_IP && $row['is_regex'] != 1 )
           {
+            // check range
+            $regexp = parse_ip_range_regex($ban_value);
+            if ( !$regexp )
+              continue;
+            if ( preg_match("/$regexp/", $_SERVER['REMOTE_ADDR']) )
+            {
+              $banned = true;
+            }
+          }
+          else
+          {
+            // User is banned
             $banned = true;
-            $reason = $row['reason'];
           }
         }
-        else {
-          if($row['ban_value']==$this->username) { $banned = true; $reason = $row['reason']; }
-        }
-        break;
-      case BAN_EMAIL:
-        if(intval($row['is_regex'])==1) {
-          if(preg_match('#'.$row['ban_value'].'#i', $this->email))
-          {
-            $banned = true;
-            $reason = $row['reason'];
-          }
-        }
-        else {
-          if($row['ban_value']==$this->email) { $banned = true; $reason = $row['reason']; }
-        }
-        break;
-      default:
-        die('Ban error: rule "'.$row['ban_value'].'" has an invalid type ('.$row['ban_type'].')');
       }
+      $db->free_result();
     }
-    if($banned && $paths->get_pageid_from_url() != $paths->nslist['Special'].'CSS')
+    if ( $banned && $paths->get_pageid_from_url() != $paths->nslist['Special'].'CSS' )
     {
       // This guy is banned - kill the session, kill the database connection, bail out, and be pretty about it
       die_semicritical('Ban notice', '<div class="error-box">You have been banned from this website. Please contact the site administrator for more information.<br /><br />Reason:<br />'.$reason.'</div>');
@@ -1600,11 +1634,11 @@
   
   /**
    * Registers a user. This does not perform any type of login.
-   * @param string $username
-   * @param string $password This should be unencrypted.
-   * @param string $email
-   * @param string $real_name Optional, defaults to ''.
-   * @param bool   $coppa     Optional. If true, the account is not activated initially and an admin activation request is sent. The caller is responsible for sending the address info and notice.
+   * @param string New user's username
+   * @param string This should be unencrypted.
+   * @param string E-mail address.
+   * @param string Optional, defaults to ''.
+   * @param bool Optional. If true, the account is not activated initially and an admin activation request is sent. The caller is responsible for sending the address info and notice.
    */
    
   function create_user($username, $password, $email, $real_name = '', $coppa = false)
@@ -1615,6 +1649,7 @@
     $aes = new AESCrypt(AES_BITS, AES_BLOCKSIZE);
     
     if(!preg_match('#^'.$this->valid_username.'$#', $username)) return 'The username you chose contains invalid characters.';
+    $username = str_replace('_', ' ', $username);
     $user_orig = $username;
     $username = $this->prepare_text($username);
     $email = $this->prepare_text($email);
--- a/install.php	Sat Nov 17 22:25:37 2007 -0500
+++ b/install.php	Sun Nov 18 20:37:08 2007 -0500
@@ -105,7 +105,7 @@
   }
   if ( !$resumed && $allow_skip )
   {
-    echo_stage_success($stage_id, "[dbg: skipped] $stage_name");
+    echo_stage_success($stage_id, $stage_name);
     return false;
   }
   if ( !function_exists($function) )
@@ -142,7 +142,6 @@
   $neutral_color = ( $neutral_color == 'A' ) ? 'C' : 'A';
   echo '<tr><td style="width: 500px; background-color: #' . "{$neutral_color}{$neutral_color}FF{$neutral_color}{$neutral_color}" . '; padding: 0 5px;">' . htmlspecialchars($stage_name) . '</td><td style="padding: 0 5px;"><img alt="Done" src="images/good.gif" /></td></tr>' . "\n";
   ob_flush();
-  flush();
 }
 
 function echo_stage_failure($stage_id, $stage_name, $failure_explanation, $resume_stack)
@@ -153,12 +152,12 @@
   $neutral_color = ( $neutral_color == 'A' ) ? 'C' : 'A';
   echo '<tr><td style="width: 500px; background-color: #' . "FF{$neutral_color}{$neutral_color}{$neutral_color}{$neutral_color}" . '; padding: 0 5px;">' . htmlspecialchars($stage_name) . '</td><td style="padding: 0 5px;"><img alt="Failed" src="images/bad.gif" /></td></tr>' . "\n";
   ob_flush();
-  flush();
   close_install_table();
   $post_data = '';
   $mysql_error = mysql_error();
   foreach ( $_POST as $key => $value )
   {
+    // FIXME: These should really also be sanitized for double quotes
     $value = htmlspecialchars($value);
     $key = htmlspecialchars($key);
     $post_data .= "          <input type=\"hidden\" name=\"$key\" value=\"$value\" />\n";
@@ -388,11 +387,15 @@
   
   $cacheonoff = is_writable(ENANO_ROOT.'/cache/') ? '1' : '0';
   
+  $admin_user = $_POST['admin_user'];
+  $admin_user = str_replace('_', ' ', $admin_user);
+  $admin_user = mysql_real_escape_string($admin_user);
+  
   $schema = file_get_contents('schema.sql');
   $schema = str_replace('{{SITE_NAME}}',    mysql_real_escape_string($_POST['sitename']   ), $schema);
   $schema = str_replace('{{SITE_DESC}}',    mysql_real_escape_string($_POST['sitedesc']   ), $schema);
   $schema = str_replace('{{COPYRIGHT}}',    mysql_real_escape_string($_POST['copyright']  ), $schema);
-  $schema = str_replace('{{ADMIN_USER}}',   mysql_real_escape_string($_POST['admin_user'] ), $schema);
+  $schema = str_replace('{{ADMIN_USER}}',   $admin_user                                    , $schema);
   $schema = str_replace('{{ADMIN_PASS}}',   mysql_real_escape_string($admin_pass          ), $schema);
   $schema = str_replace('{{ADMIN_EMAIL}}',  mysql_real_escape_string($_POST['admin_email']), $schema);
   $schema = str_replace('{{ENABLE_CACHE}}', mysql_real_escape_string($cacheonoff          ), $schema);
@@ -462,6 +465,7 @@
     $key = $aes->hextostring($key);
     $admin_pass = $aes->encrypt($admin_pass, $key, ENC_HEX);
     $admin_user = mysql_real_escape_string($_POST['admin_user']);
+    $admin_user = str_replace('_', ' ', $admin_user);
     
     $q = @mysql_query("UPDATE {$_POST['table_prefix']}users SET password='$admin_pass' WHERE username='$admin_user';");
     if ( !$q )
@@ -1826,52 +1830,77 @@
       err('Hacking attempt was detected in table_prefix.');
     
       start_install_table();
-      // The stages connect, decrypt, genkey, and parse are preprocessing and don't do any actual data modification.
-      // Thus, they need to be run on each retry, e.g. never skipped.
-      run_installer_stage('connect', $lang->get('install_stg_connect_title'), 'stg_mysql_connect', $lang->get('install_stg_connect_body'), false);
-      if ( isset($_POST['drop_tables']) )
+      
+      // Are we just trying to auto-rename the config files? If so, skip everything else
+      if ( !isset($_GET['stage']) || ( isset($_GET['stage']) && $_GET['stage'] != 'renameconfig' ) )
       {
-        // Are we supposed to drop any existing tables? If so, do it now
-        run_installer_stage('drop', $lang->get('install_stg_drop_title'), 'stg_drop_tables', 'This step never returns failure');
+      
+        // The stages connect, decrypt, genkey, and parse are preprocessing and don't do any actual data modification.
+        // Thus, they need to be run on each retry, e.g. never skipped.
+        run_installer_stage('connect', $lang->get('install_stg_connect_title'), 'stg_mysql_connect', $lang->get('install_stg_connect_body'), false);
+        if ( isset($_POST['drop_tables']) )
+        {
+          // Are we supposed to drop any existing tables? If so, do it now
+          run_installer_stage('drop', $lang->get('install_stg_drop_title'), 'stg_drop_tables', 'This step never returns failure');
+        }
+        run_installer_stage('decrypt', $lang->get('install_stg_decrypt_title'), 'stg_decrypt_admin_pass', $lang->get('install_stg_decrypt_body'), false);
+        run_installer_stage('genkey', $lang->get('install_stg_genkey_title', array( 'aes_bits' => AES_BITS )), 'stg_generate_aes_key', $lang->get('install_stg_genkey_body'), false);
+        run_installer_stage('parse', $lang->get('install_stg_parse_title'), 'stg_parse_schema', $lang->get('install_stg_parse_body'), false);
+        run_installer_stage('sql', $lang->get('install_stg_sql_title'), 'stg_install', $lang->get('install_stg_sql_body'), false);
+        run_installer_stage('writeconfig', $lang->get('install_stg_writeconfig_title'), 'stg_write_config', $lang->get('install_stg_writeconfig_body'));
+        
+        // Mainstream installation complete - Enano should be usable now
+        // The stage of starting the API is special because it has to be called out of function context.
+        // To alleviate this, we have two functions, one that returns success and one that returns failure
+        // If the Enano API load is successful, the success function is called to report the action to the user
+        // If unsuccessful, the failure report is sent
+        
+        $template_bak = $template;
+        
+        $_GET['title'] = 'Main_Page';
+        require('includes/common.php');
+        
+        if ( is_object($db) && is_object($session) )
+        {
+          run_installer_stage('startapi', $lang->get('install_stg_startapi_title'), 'stg_start_api_success', '...', false);
+        }
+        else
+        {
+          run_installer_stage('startapi', $lang->get('install_stg_startapi_title'), 'stg_start_api_failure', $lang->get('install_stg_startapi_body'), false);
+        }
+        
+        // We need to be logged in (with admin rights) before logs can be flushed
+        $admin_password = stg_decrypt_admin_pass(true);
+        $session->login_without_crypto($_POST['admin_user'], $admin_password, false);
+        
+        // Now that login cookies are set, initialize the session manager and ACLs
+        $session->start();
+        $paths->init();
+        
+        run_installer_stage('importlang', $lang->get('install_stg_importlang_title'), 'stg_import_language', $lang->get('install_stg_importlang_body'));
+        run_installer_stage('initlogs', $lang->get('install_stg_initlogs_title'), 'stg_init_logs', $lang->get('install_stg_initlogs_body'));
+        
+        /*
+         * HACKERS:
+         * If you're making a custom distribution of Enano, put all your custom plugin-related code here.
+         * You have access to the full Enano API as well as being logged in with complete admin rights.
+         * Don't do anything horrendously fancy here, unless you add a new stage (or more than one) and
+         * have the progress printed out properly.
+         */
+        
+      } // check for stage == renameconfig
+      else
+      {
+        // If we did skip the main installer routine, set $template_bak to make the reversal later work properly
+        $template_bak = $template;
       }
-      run_installer_stage('decrypt', $lang->get('install_stg_decrypt_title'), 'stg_decrypt_admin_pass', $lang->get('install_stg_decrypt_body'), false);
-      run_installer_stage('genkey', $lang->get('install_stg_genkey_title', array( 'aes_bits' => AES_BITS )), 'stg_generate_aes_key', $lang->get('install_stg_genkey_body'), false);
-      run_installer_stage('parse', $lang->get('install_stg_parse_title'), 'stg_parse_schema', $lang->get('install_stg_parse_body'), false);
-      run_installer_stage('sql', $lang->get('install_stg_sql_title'), 'stg_install', $lang->get('install_stg_sql_body'), false);
-      run_installer_stage('writeconfig', $lang->get('install_stg_writeconfig_title'), 'stg_write_config', $lang->get('install_stg_writeconfig_body'));
+
+      // Final step is to rename the config file
+      // In early revisions of 1.0.2, this step was performed prior to the initialization of the Enano API. It was decided to move
+      // this stage to the end because it will fail more often than any other stage, thus making alternate routes imperative. If this
+      // stage fails, then no big deal, we'll just have the user rename the files manually and then let them see the pretty success message.
       run_installer_stage('renameconfig', $lang->get('install_stg_rename_title'), 'stg_rename_config', $lang->get('install_stg_rename_body'));
       
-      // Mainstream installation complete - Enano should be usable now
-      // The stage of starting the API is special because it has to be called out of function context.
-      // To alleviate this, we have two functions, one that returns success and one that returns failure
-      // If the Enano API init is successful, the success function is called to report the action to the user
-      // If unsuccessful, the failure report is sent
-      
-      $template_bak = $template;
-      
-      $_GET['title'] = 'Main_Page';
-      require('includes/common.php');
-      
-      if ( is_object($db) && is_object($session) )
-      {
-        run_installer_stage('startapi', $lang->get('install_stg_startapi_title'), 'stg_start_api_success', '...', false);
-      }
-      else
-      {
-        run_installer_stage('startapi', $lang->get('install_stg_startapi_title'), 'stg_start_api_failure', $lang->get('install_stg_startapi_body'), false);
-      }
-      
-      // We need to be logged in (with admin rights) before logs can be flushed
-      $admin_password = stg_decrypt_admin_pass(true);
-      $session->login_without_crypto($_POST['admin_user'], $admin_password, false);
-      
-      // Now that login cookies are set, initialize the session manager and ACLs
-      $session->start();
-      $paths->init();
-      
-      run_installer_stage('importlang', $lang->get('install_stg_importlang_title'), 'stg_import_language', $lang->get('install_stg_importlang_body'));
-      
-      run_installer_stage('initlogs', $lang->get('install_stg_initlogs_title'), 'stg_init_logs', $lang->get('install_stg_initlogs_body'));
       close_install_table();
       
       unset($template);
--- a/plugins/SpecialAdmin.php	Sat Nov 17 22:25:37 2007 -0500
+++ b/plugins/SpecialAdmin.php	Sun Nov 18 20:37:08 2007 -0500
@@ -2228,17 +2228,14 @@
         }
         if ( $type == BAN_IP )
         {
-          // parse a range of addresses
-          $range = parse_ip_range($entry);
-          if ( !$range )
+          if ( !isset($_POST['regex']) )
           {
-            $error = true;
-            echo '<div class="error-box">Malformed IP address expression.</div>';
-            break;
+            // as of 1.0.2 parsing is done at runtime
+            $entries[] = $entry;
           }
-          foreach ($range as $ip)
+          else
           {
-            $entries[] = $ip;
+            $entries[] = $entry;
           }
         }
         else
@@ -2290,7 +2287,7 @@
   ?>
   Type: <select name="type"><option value="<?php echo BAN_IP; ?>">IP address</option><option value="<?php echo BAN_USER; ?>">Username</option><option value="<?php echo BAN_EMAIL; ?>">E-mail address</option></select><br />
   Rule: <input type="text" name="value" size="30" /><br />
-  <small>You can ban multiple IP addresses, users, or e-mail addresses by separating entries with a single comma (User1,User2). Do not put a space after the comma. For IP addresses, you may specify ranges like 172|192.168.4-30|90-167.1-90, which will turn into 172 and 192 . 168 . 4-30 and 90-167 . 1 - 90, which matches 18,899 IP addresses. Don't specify large ranges (like the example one here) at once or you risk temporarily (~60sec) overloading the server.</small><br />
+  <small>You can ban multiple IP addresses, users, or e-mail addresses by separating entries with a single comma (User1,User2). Do not put a space after the comma. For IP addresses, you may specify ranges like 172|192.168.4-30|90-167.1-90, which will turn into 172 and 192 . 168 . 4-30 and 90-167 . 1 - 90, which matches 18,899 IP addresses.</small><br />
   Reason to show to the banned user: <textarea name="reason" rows="7" cols="40"></textarea><br />
   <input type="checkbox" name="regex" id="regex" />  <label for="regex">This rule is a regular expression</label> (advanced users only)<br />
   <input type="submit" style="font-weight: bold;" name="create" value="Create new ban rule" />
--- a/plugins/SpecialUserFuncs.php	Sat Nov 17 22:25:37 2007 -0500
+++ b/plugins/SpecialUserFuncs.php	Sun Nov 18 20:37:08 2007 -0500
@@ -820,7 +820,7 @@
             if(!namegood && ( field.name == 'username' || field.name == '_nil' ) ) 
             {
               //if(frm.username.value.match(/^([A-z0-9 \!@\-\(\)]+){2,}$/ig))
-              var regex = new RegExp('^([^<>_&\?]+){2,}$', 'ig');
+              var regex = new RegExp('^([^<>&\?]+){2,}$', 'ig');
               if ( frm.username.value.match(regex) )
               {
                 document.getElementById('s_username').src='<?php echo scriptPath; ?>/images/unknown.gif';
@@ -885,10 +885,8 @@
             
             if(!namegood)
             {
-              <?php
-              // sorry for this ugly hack but jedit gets f***ed otherwise
-              echo 'if(frm.username.value.match(/^([A-z0-9 \.:\!@\#\*]+){2,}$/ig))';
-              ?>
+              var regex = new RegExp('^([^<>&\?]+){2,}$', 'ig');
+              if ( frm.username.value.match(regex) )
               {
                 document.getElementById('s_username').src='<?php echo scriptPath; ?>/images/unknown.gif';
                 document.getElementById('e_username').innerHTML = '&nbsp;';