Merging in changes from stable
authorDan
Sat, 20 Oct 2007 21:44:13 -0400
changeset 193 ebe99e82a59a
parent 187 9dbbec5e6096 (diff)
parent 192 9237767a23ae (current diff)
child 204 473cc747022a
Merging in changes from stable
includes/common.php
includes/functions.php
includes/render.php
includes/sessions.php
includes/template.php
plugins/SpecialAdmin.php
plugins/SpecialUserFuncs.php
--- a/.hgtags	Sat Oct 20 11:11:40 2007 -0400
+++ b/.hgtags	Sat Oct 20 21:44:13 2007 -0400
@@ -5,3 +5,4 @@
 6f0bbf88c3251ca597cb76ac8b59a1ee61d6dd3d rebrand
 0b5244001799fa29e83bf06c5f14eb69350f171c rebrand
 42c6c83b8a004163c9cc2d85f3c8eada3b73adf6 rebrand
+d53cc29308f4f4b97fc6d054e9e0855f37137409 rebrand
--- a/ajax.php	Sat Oct 20 11:11:40 2007 -0400
+++ b/ajax.php	Sat Oct 20 21:44:13 2007 -0400
@@ -2,7 +2,7 @@
 
 /*
  * Enano - an open-source CMS capable of wiki functions, Drupal-like sidebar blocks, and everything in between
- * Version 1.0.2 (Coblynau)
+ * Version 1.1.1
  * Copyright (C) 2006-2007 Dan Fuhry
  *
  * This program is Free Software; you can redistribute and/or modify it under the terms of the GNU General Public License
@@ -33,50 +33,35 @@
     define('ENANO_ROOT', dirname($filename));
     require(ENANO_ROOT.'/includes/functions.php');
     require(ENANO_ROOT.'/includes/dbal.php');
-    require(ENANO_ROOT.'/includes/json.php');
     $db = new mysql();
     $db->connect();
     
-    // result is sent using JSON
-    $json = new Services_JSON(SERVICES_JSON_LOOSE_TYPE);
-    $return = Array(
-        'mode' => 'success',
-        'users_real' => Array()
-      );
-    
-    // should be connected to the DB now
+    // should be connected now
     $name = (isset($_GET['name'])) ? $db->escape($_GET['name']) : false;
     if ( !$name )
     {
-      $return = array(
-        'mode' => 'error',
-        'error' => 'Invalid URI'
-      );
-      die( $json->encode($return) );
+      die('userlist = new Array(); errorstring=\'Invalid URI\'');
     }
-    $allowanon = ( isset($_GET['allowanon']) && $_GET['allowanon'] == '1' ) ? '' : ' AND user_id > 1';
-    $q = $db->sql_query('SELECT username FROM '.table_prefix.'users WHERE lcase(username) LIKE lcase(\'%'.$name.'%\')' . $allowanon . ' ORDER BY username ASC;');
+    $q = $db->sql_query('SELECT username,user_id FROM '.table_prefix.'users WHERE lcase(username) LIKE lcase(\'%'.$name.'%\');');
     if ( !$q )
     {
-      $return = array(
-        'mode' => 'error',
-        'error' => 'MySQL error selecting username data: '.addslashes(mysql_error())
-      );
-      die( $json->encode($return) );
+      die('userlist = new Array(); errorstring=\'MySQL error selecting username data: '.addslashes(mysql_error()).'\'');
     }
+    if($db->numrows() < 1)
+    {
+      die('userlist = new Array(); errorstring=\'No usernames found\';');
+    }
+    echo 'var errorstring = false; userlist = new Array();';
     $i = 0;
     while($r = $db->fetchrow())
     {
-      $return['users_real'][] = $r['username'];
+      echo "userlist[$i] = '".addslashes($r['username'])."'; ";
       $i++;
     }
     $db->free_result();
     
     // all done! :-)
     $db->close();
-    
-    echo $json->encode( $return );
-    
     exit;
   }
  
--- a/includes/captcha.php	Sat Oct 20 11:11:40 2007 -0400
+++ b/includes/captcha.php	Sat Oct 20 21:44:13 2007 -0400
@@ -1,7 +1,8 @@
 <?php
+
 /*
  * Enano - an open-source CMS capable of wiki functions, Drupal-like sidebar blocks, and everything in between
- * Version 1.0.2 (Coblynau)
+ * Version 1.1.1
  * Copyright (C) 2006-2007 Dan Fuhry
  * captcha.php - visual confirmation system used during registration
  *
--- a/includes/clientside/jsres.php	Sat Oct 20 11:11:40 2007 -0400
+++ b/includes/clientside/jsres.php	Sat Oct 20 21:44:13 2007 -0400
@@ -2,7 +2,7 @@
 
 /*
  * Enano - an open-source CMS capable of wiki functions, Drupal-like sidebar blocks, and everything in between
- * Version 1.0.2 (Coblynau)
+ * Version 1.1.1
  * Copyright (C) 2006-2007 Dan Fuhry
  * jsres.php - the Enano client-side runtime, a.k.a. AJAX on steroids
  *
@@ -40,7 +40,7 @@
 {
   echo "/*
  * Enano - an open-source CMS capable of wiki functions, Drupal-like sidebar blocks, and everything in between
- * Version 1.0.2 (Coblynau)
+ * Version 1.1.1
  * [Aggressively compressed] Javascript client code
  * Copyright (C) 2006-2007 Dan Fuhry
  * Enano is Free Software, licensed under the GNU General Public License; see http://enanocms.org/ for details.
--- a/includes/clientside/static/ajax.js	Sat Oct 20 11:11:40 2007 -0400
+++ b/includes/clientside/static/ajax.js	Sat Oct 20 21:44:13 2007 -0400
@@ -1184,3 +1184,33 @@
   new messagebox(MB_OK|MB_ICONINFORMATION, 'About the keep-alive feature', 'Keep-alive is a new Enano feature that keeps your administrative session from timing out while you are using the administration panel. This feature can be useful if you are editing a large page or doing something in the administration interface that will take longer than 15 minutes.<br /><br />For security reasons, Enano mandates that high-privilege logins last only 15 minutes, with the time being reset each time a page is loaded (or, more specifically, each time the session API is started). The consequence of this is that if you are performing an action in the administration panel that takes more than 15 minutes, your session may be terminated. The keep-alive feature attempts to relieve this by sending a "ping" to the server every 10 minutes.<br /><br />Please note that keep-alive state is determined by a cookie. Thus, if you log out and then back in as a different administrator, keep-alive will use the same setting that was used when you were logged in as the first administrative user. In the same way, if you log into the administration panel under your account from another computer, keep-alive will be set to "off".<br /><br /><b>For more information:</b><br /><a href="http://docs.enanocms.org/Help:Appendix_B" onclick="window.open(this.href); return false;">Overview of Enano'+"'"+'s security model');
 }
 
+function ajaxShowCaptcha(code)
+{
+  var mydiv = document.createElement('div');
+  mydiv.style.backgroundColor = '#FFFFFF';
+  mydiv.style.padding = '10px';
+  mydiv.style.position = 'absolute';
+  mydiv.style.top = '0px';
+  mydiv.id = 'autoCaptcha';
+  var img = document.createElement('img');
+  img.onload = function()
+  {
+    if ( this.loaded )
+      return true;
+    var mydiv = document.getElementById('autoCaptcha');
+    var width = getWidth();
+    var divw = $(mydiv).Width();
+    var left = ( width / 2 ) - ( divw / 2 );
+    mydiv.style.left = left + 'px';
+    fly_in_top(mydiv, false, true);
+    this.loaded = true;
+  };
+  img.src = makeUrlNS('Special', 'Captcha/' + code);
+  img.onclick = function() { this.src = this.src + '/a'; };
+  img.style.cursor = 'pointer';
+  mydiv.appendChild(img);
+  domObjChangeOpac(0, mydiv);
+  var body = document.getElementsByTagName('body')[0];
+  body.appendChild(mydiv);
+}
+
--- a/includes/clientside/static/misc.js	Sat Oct 20 11:11:40 2007 -0400
+++ b/includes/clientside/static/misc.js	Sat Oct 20 21:44:13 2007 -0400
@@ -302,6 +302,51 @@
 var ajax_auth_mb_cache = false;
 var ajax_auth_level_cache = false;
 var ajax_auth_error_string = false;
+var ajax_auth_show_captcha = false;
+
+function ajaxAuthErrorToString($data)
+{
+  var $errstring = $data.error;
+  // this was literally copied straight from the PHP code.
+  switch($data.error)
+  {
+    case 'key_not_found':
+      $errstring = 'Enano couldn\'t look up the encryption key used to encrypt your password. This most often happens if a cache rotation occurred during your login attempt, or if you refreshed the login page.';
+      break;
+    case 'key_wrong_length':
+      $errstring = 'The encryption key was the wrong length.';
+      break;
+    case 'too_big_for_britches':
+      $errstring = 'You are trying to authenticate at a level that your user account does not permit.';
+      break;
+    case 'invalid_credentials':
+      $errstring = 'You have entered an invalid username or password. Please enter your login details again.';
+      if ( $data.lockout_policy == 'lockout' )
+      {
+        $errstring += ' You have used up '+$data['lockout_fails']+' out of '+$data['lockout_threshold']+' login attempts. After you have used up all '+$data['lockout_threshold']+' login attempts, you will be locked out from logging in for '+$data['lockout_duration']+' minutes.';
+      }
+      else if ( $data.lockout_policy == 'captcha' )
+      {
+        $errstring += ' You have used up '+$data['lockout_fails']+' out of '+$data['lockout_threshold']+' login attempts. After you have used up all '+$data['lockout_threshold']+' login attempts, you will have to enter a visual confirmation code before logging in, effective for '+$data['lockout_duration']+' minutes.';
+      }
+      break;
+    case 'backend_fail':
+      $errstring = '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.';
+      break;
+    case 'locked_out':
+      $attempts = parseInt($data['lockout_fails']);
+      if ( $attempts > $data['lockout_threshold'])
+        $attempts = $data['lockout_threshold'];
+      $time_rem = $data.time_rem;
+      $s = ( $time_rem == 1 ) ? '' : 's';
+      $errstring = "You have used up all "+$data['lockout_threshold']+" allowed login attempts. Please wait "+$time_rem+" minute"+$s+" before attempting to log in again";
+      if ( $data['lockout_policy'] == 'captcha' )
+        $errstring += ', or enter the visual confirmation code shown above in the appropriate box';
+      $errstring += '.';
+      break;
+  }
+  return $errstring;
+}
 
 function ajaxPromptAdminAuth(call_on_ok, level)
 {
@@ -320,6 +365,17 @@
   var title = ( level > USER_LEVEL_MEMBER ) ? 'You are requesting a sensitive operation.' : 'Please enter your username and password to continue.';
   ajax_auth_mb_cache = new messagebox(MB_OKCANCEL|MB_ICONLOCK, title, loading_win);
   ajax_auth_mb_cache.onbeforeclick['OK'] = ajaxValidateLogin;
+  ajax_auth_mb_cache.onbeforeclick['Cancel'] = function()
+  {
+    if ( document.getElementById('autoCaptcha') )
+    {
+      var to = fly_out_top(document.getElementById('autoCaptcha'), false, true);
+      setTimeout(function() {
+          var d = document.getElementById('autoCaptcha');
+          d.parentNode.removeChild(d);
+        }, to);
+    }
+  }
   ajaxAuthLoginInnerSetup();
 }
 
@@ -335,6 +391,20 @@
           return false;
         }
         response = parseJSON(response);
+        var disable_controls = false;
+        if ( response.locked_out && !ajax_auth_error_string )
+        {
+          response.error = 'locked_out';
+          ajax_auth_error_string = ajaxAuthErrorToString(response);
+          if ( response.lockout_policy == 'captcha' )
+          {
+            ajax_auth_show_captcha = response.captcha;
+          }
+          else
+          {
+            disable_controls = true;
+          }
+        }
         var level = ajax_auth_level_cache;
         var form_html = '';
         var shown_error = false;
@@ -348,14 +418,28 @@
         {
           form_html += 'Please re-enter your login details, to verify your identity.<br /><br />';
         }
+        if ( ajax_auth_show_captcha )
+         {
+           var captcha_html = ' \
+             <tr> \
+               <td>Code in image:</td> \
+               <td><input type="hidden" id="ajaxlogin_captcha_hash" value="' + ajax_auth_show_captcha + '" /><input type="text" tabindex="3" size="25" id="ajaxlogin_captcha_code" /> \
+             </tr>';
+         }
+         else
+         {
+           var captcha_html = '';
+         }
+         var disableme = ( disable_controls ) ? 'disabled="disabled" ' : '';
         form_html += ' \
           <table border="0" align="center"> \
             <tr> \
-              <td>Username:</td><td><input tabindex="1" id="ajaxlogin_user" type="text"     size="25" /> \
+              <td>Username:</td><td><input tabindex="1" id="ajaxlogin_user" type="text"     ' + disableme + 'size="25" /> \
             </tr> \
             <tr> \
-              <td>Password:</td><td><input tabindex="2" id="ajaxlogin_pass" type="password" size="25" /> \
+              <td>Password:</td><td><input tabindex="2" id="ajaxlogin_pass" type="password" ' + disableme + 'size="25" /> \
             </tr> \
+            ' + captcha_html + ' \
             <tr> \
               <td colspan="2" style="text-align: center;"> \
                 <br /><small>Trouble logging in? Try the <a href="'+makeUrlNS('Special', 'Login/' + title, 'level=' + level)+'">full login form</a>.<br />';
@@ -383,8 +467,21 @@
         {
           $('ajaxlogin_user').object.focus();
         }
-        $('ajaxlogin_pass').object.onblur = function(e) { if ( !shift ) $('messageBox').object.nextSibling.firstChild.focus(); };
-        $('ajaxlogin_pass').object.onkeypress = function(e) { if ( !e && IE ) return true; if ( e.keyCode == 13 ) $('messageBox').object.nextSibling.firstChild.click(); };
+        if ( ajax_auth_show_captcha )
+        {
+          $('ajaxlogin_captcha_code').object.onblur = function(e) { if ( !shift ) $('messageBox').object.nextSibling.firstChild.focus(); };
+          $('ajaxlogin_captcha_code').object.onkeypress = function(e) { if ( !e && IE ) return true; if ( e.keyCode == 13 ) $('messageBox').object.nextSibling.firstChild.click(); };
+        }
+        else
+        {
+          $('ajaxlogin_pass').object.onblur = function(e) { if ( !shift ) $('messageBox').object.nextSibling.firstChild.focus(); };
+          $('ajaxlogin_pass').object.onkeypress = function(e) { if ( !e && IE ) return true; if ( e.keyCode == 13 ) $('messageBox').object.nextSibling.firstChild.click(); };
+        }
+        if ( disable_controls )
+        {
+          var panel = document.getElementById('messageBoxButtons');
+          panel.firstChild.disabled = true;
+        }
         /*
         ## This causes the background image to disappear under Fx 2
         if ( shown_error )
@@ -398,6 +495,11 @@
           fader.start();
         }
         */
+        if ( ajax_auth_show_captcha )
+        {
+          ajaxShowCaptcha(ajax_auth_show_captcha);
+          ajax_auth_show_captcha = false;
+        }
       }
     });
 }
@@ -412,6 +514,15 @@
   password = document.getElementById('ajaxlogin_pass').value;
   auth_enabled = false;
   
+  if ( document.getElementById('autoCaptcha') )
+  {
+    var to = fly_out_top(document.getElementById('autoCaptcha'), false, true);
+    setTimeout(function() {
+        var d = document.getElementById('autoCaptcha');
+        d.parentNode.removeChild(d);
+      }, to);
+  }
+  
   disableJSONExts();
   
   //
@@ -467,6 +578,12 @@
     'level' : ajax_auth_level_cache
   };
   
+  if ( document.getElementById('ajaxlogin_captcha_hash') )
+  {
+    json_data.captcha_hash = document.getElementById('ajaxlogin_captcha_hash').value;
+    json_data.captcha_code = document.getElementById('ajaxlogin_captcha_code').value;
+  }
+  
   json_data = toJSONString(json_data);
   json_data = encodeURIComponent(json_data);
   
@@ -509,18 +626,23 @@
             }
             break;
           case 'error':
-            if ( response.error == 'The username and/or password is incorrect.' )
+            if ( response.data.error == 'invalid_credentials' || response.data.error == 'locked_out' )
             {
-              ajax_auth_error_string = response.error;
+              ajax_auth_error_string = ajaxAuthErrorToString(response.data);
               mb_current_obj.updateContent('');
               document.getElementById('messageBox').style.backgroundColor = '#C0C0C0';
               var mb_parent = document.getElementById('messageBox').parentNode;
               new Spry.Effect.Shake(mb_parent, {duration: 1500}).start();
               setTimeout("document.getElementById('messageBox').style.backgroundColor = '#FFF'; ajaxAuthLoginInnerSetup();", 2500);
+              
+              if ( response.data.lockout_policy == 'captcha' && response.data.error == 'locked_out' )
+              {
+                ajax_auth_show_captcha = response.captcha;
+              }
             }
             else
             {
-              alert(response.error);
+              ajax_auth_error_string = ajaxAuthErrorToString(response.data);
               ajaxAuthLoginInnerSetup();
             }
             break;
--- a/includes/comment.php	Sat Oct 20 11:11:40 2007 -0400
+++ b/includes/comment.php	Sat Oct 20 21:44:13 2007 -0400
@@ -2,7 +2,7 @@
 
 /*
  * Enano - an open-source CMS capable of wiki functions, Drupal-like sidebar blocks, and everything in between
- * Version 1.0.2 (Coblynau)
+ * Version 1.1.1
  * Copyright (C) 2006-2007 Dan Fuhry
  *
  * This program is Free Software; you can redistribute and/or modify it under the terms of the GNU General Public License
--- a/includes/common.php	Sat Oct 20 11:11:40 2007 -0400
+++ b/includes/common.php	Sat Oct 20 21:44:13 2007 -0400
@@ -2,7 +2,7 @@
 
 /*
  * Enano - an open-source CMS capable of wiki functions, Drupal-like sidebar blocks, and everything in between
- * Version 1.0.2 (Coblynau)
+ * Version 1.1.1
  * Copyright (C) 2006-2007 Dan Fuhry
  *
  * This program is Free Software; you can redistribute and/or modify it under the terms of the GNU General Public License
@@ -23,7 +23,7 @@
   exit;
 }
 
-$version = '1.0.2';
+$version = '1.1.1';
 
 function microtime_float()
 {
@@ -68,9 +68,6 @@
 if ( file_exists( ENANO_ROOT . '/_nightly.php') )
   require(ENANO_ROOT.'/_nightly.php');
 
-// List of scheduled tasks
-$cron_tasks = array();
-
 // Start including files. LOTS of files. Yeah!
 require_once(ENANO_ROOT.'/includes/constants.php');
 dc_here('Enano CMS '.$version.' (dev) - debug window<br />Powered by debugConsole');
--- a/includes/constants.php	Sat Oct 20 11:11:40 2007 -0400
+++ b/includes/constants.php	Sat Oct 20 21:44:13 2007 -0400
@@ -2,7 +2,7 @@
 
 /*
  * Enano - an open-source CMS capable of wiki functions, Drupal-like sidebar blocks, and everything in between
- * Version 1.0.2 (Coblynau)
+ * Version 1.1.1
  * Copyright (C) 2006-2007 Dan Fuhry
  * constants.php - important defines used Enano-wide
  *
@@ -70,7 +70,7 @@
 define('MAX_PMS_PER_BATCH', 7); // The maximum number of users that users can send PMs to in one go; restriction does not apply to users with mod_misc rights
 define('SEARCH_RESULTS_PER_PAGE', 10);
 define('MYSQL_MAX_PACKET_SIZE', 1048576); // 1MB; this is the default in MySQL 4.x I think
-define('SEARCH_MODE', 'FULLTEXT'); // Can be FULLTEXT or BUILTIN
+define('SEARCH_MODE', 'BUILTIN'); // Can be FULLTEXT or BUILTIN
 
 // Sidebar
 
--- a/includes/dbal.php	Sat Oct 20 11:11:40 2007 -0400
+++ b/includes/dbal.php	Sat Oct 20 21:44:13 2007 -0400
@@ -2,7 +2,7 @@
 
 /*
  * Enano - an open-source CMS capable of wiki functions, Drupal-like sidebar blocks, and everything in between
- * Version 1.0.2 (Coblynau)
+ * Version 1.1.1
  * Copyright (C) 2006-2007 Dan Fuhry
  *
  * This program is Free Software; you can redistribute and/or modify it under the terms of the GNU General Public License
--- a/includes/email.php	Sat Oct 20 11:11:40 2007 -0400
+++ b/includes/email.php	Sat Oct 20 21:44:13 2007 -0400
@@ -2,7 +2,7 @@
 
 /*
  * Enano - an open-source CMS capable of wiki functions, Drupal-like sidebar blocks, and everything in between
- * Version 1.0.2 (Coblynau)
+ * Version 1.1.1
  * Copyright (C) 2006-2007 Dan Fuhry
  *
  * This program is Free Software; you can redistribute and/or modify it under the terms of the GNU General Public License
--- a/includes/functions.php	Sat Oct 20 11:11:40 2007 -0400
+++ b/includes/functions.php	Sat Oct 20 21:44:13 2007 -0400
@@ -2796,7 +2796,7 @@
   $strip_tags = implode('|', $strip_tags);
   
   // Strip out the tags and replace with placeholders
-  preg_match_all("#<($strip_tags)([ ]+.*?)?>(.*?)</($strip_tags)>#is", $html, $matches);
+  preg_match_all("#<($strip_tags)(.*?)>(.*?)</($strip_tags)>#is", $html, $matches);
   $seed = md5(microtime() . mt_rand()); // Random value used for placeholders
   for ($i = 0;$i < sizeof($matches[1]); $i++)
   {
@@ -2804,7 +2804,7 @@
   }
   
   // Optimize (but don't obfuscate) Javascript
-  preg_match_all('/<script([ ]+.*?)?>(.*?)(\]\]>)?<\/script>/is', $html, $jscript);
+  preg_match_all('/<script(.*?)>(.+?)<\/script>/is', $html, $jscript);
   
   // list of Javascript reserved words - from about.com
   $reserved_words = array('abstract', 'as', 'boolean', 'break', 'byte', 'case', 'catch', 'char', 'class', 'continue', 'const', 'debugger', 'default', 'delete', 'do',
@@ -2819,8 +2819,6 @@
   {
     $js =& $jscript[2][$i];
     
-    // echo('<pre>' . "-----------------------------------------------------------------------------\n" . htmlspecialchars($js) . '</pre>');
-    
     // for line optimization, explode it
     $particles = explode("\n", $js);
     
@@ -3168,20 +3166,6 @@
   return $score;
 }
 
-/**
- * Registers a task that will be run every X hours. Scheduled tasks should always be scheduled at runtime - they are not stored in the DB.
- * @param string Function name to call, or array(object, string method)
- * @param int Interval between runs, in hours. Defaults to 24.
- */
-
-function register_cron_task($func, $hour_interval = 24)
-{
-  global $cron_tasks;
-  if ( !isset($cron_tasks[$hour_interval]) )
-    $cron_tasks[$hour_interval] = array();
-  $cron_tasks[$hour_interval][] = $func;
-}
-
 //die('<pre>Original:  01010101010100101010100101010101011010'."\nProcessed: ".uncompress_bitfield(compress_bitfield('01010101010100101010100101010101011010')).'</pre>');
 
 ?>
--- a/includes/graphs.php	Sat Oct 20 11:11:40 2007 -0400
+++ b/includes/graphs.php	Sat Oct 20 21:44:13 2007 -0400
@@ -2,7 +2,7 @@
 
 /*
  * Enano - an open-source CMS capable of wiki functions, Drupal-like sidebar blocks, and everything in between
- * Version 1.0.2 (Coblynau)
+ * Version 1.1.1
  * Copyright (C) 2006-2007 Dan Fuhry
  *
  * This program is Free Software; you can redistribute and/or modify it under the terms of the GNU General Public License
--- a/includes/js-compressor.php	Sat Oct 20 11:11:40 2007 -0400
+++ b/includes/js-compressor.php	Sat Oct 20 21:44:13 2007 -0400
@@ -2,7 +2,7 @@
 
 /*
  * Enano - an open-source CMS capable of wiki functions, Drupal-like sidebar blocks, and everything in between
- * Version 1.0.2 (Coblynau)
+ * Version 1.1.1
  * Copyright (C) 2006-2007 Dan Fuhry
  * Javascript compression library - used to compact the client-side Javascript code (all 72KB of it!) to save some bandwidth
  *
--- a/includes/pageprocess.php	Sat Oct 20 11:11:40 2007 -0400
+++ b/includes/pageprocess.php	Sat Oct 20 21:44:13 2007 -0400
@@ -2,7 +2,7 @@
 
 /*
  * Enano - an open-source CMS capable of wiki functions, Drupal-like sidebar blocks, and everything in between
- * Version 1.0.2 (Coblynau)
+ * Version 1.1.1
  * pageprocess.php - intelligent retrieval of pages
  * Copyright (C) 2006-2007 Dan Fuhry
  *
--- a/includes/pageutils.php	Sat Oct 20 11:11:40 2007 -0400
+++ b/includes/pageutils.php	Sat Oct 20 21:44:13 2007 -0400
@@ -1,7 +1,8 @@
 <?php
+
 /*
  * Enano - an open-source CMS capable of wiki functions, Drupal-like sidebar blocks, and everything in between
- * Version 1.0.2 (Coblynau)
+ * Version 1.1.1
  * Copyright (C) 2006-2007 Dan Fuhry
  * pageutils.php - a class that handles raw page manipulations, used mostly by AJAX requests or their old-fashioned form-based counterparts
  *
--- a/includes/paths.php	Sat Oct 20 11:11:40 2007 -0400
+++ b/includes/paths.php	Sat Oct 20 21:44:13 2007 -0400
@@ -2,7 +2,7 @@
 
 /**
  * Enano - an open-source CMS capable of wiki functions, Drupal-like sidebar blocks, and everything in between
- * Version 1.0.2 (Coblynau)
+ * Version 1.1.1
  * Copyright (C) 2006-2007 Dan Fuhry
  * paths.php - The part of Enano that actually manages content. Everything related to page handling and namespaces is in here.
  *
--- a/includes/plugins.php	Sat Oct 20 11:11:40 2007 -0400
+++ b/includes/plugins.php	Sat Oct 20 21:44:13 2007 -0400
@@ -2,7 +2,7 @@
 
 /*
  * Enano - an open-source CMS capable of wiki functions, Drupal-like sidebar blocks, and everything in between
- * Version 1.0.2 (Coblynau)
+ * Version 1.1.1
  * Copyright (C) 2006-2007 Dan Fuhry
  *
  * This program is Free Software; you can redistribute and/or modify it under the terms of the GNU General Public License
--- a/includes/render.php	Sat Oct 20 11:11:40 2007 -0400
+++ b/includes/render.php	Sat Oct 20 21:44:13 2007 -0400
@@ -708,7 +708,6 @@
       ':-/'     => 'face-plain.png',
       ':joke:'  => 'face-plain.png',
       ']:-&gt;' => 'face-devil-grin.png',
-      ']:->'    => 'face-devil-grin.png',
       ':kiss:'  => 'face-kiss.png',
       ':-P'     => 'face-tongue-out.png',
       ':P'      => 'face-tongue-out.png',
--- a/includes/search.php	Sat Oct 20 11:11:40 2007 -0400
+++ b/includes/search.php	Sat Oct 20 21:44:13 2007 -0400
@@ -2,7 +2,7 @@
 
 /*
  * Enano - an open-source CMS capable of wiki functions, Drupal-like sidebar blocks, and everything in between
- * Version 1.0.2 (Coblynau)
+ * Version 1.1.1
  * Copyright (C) 2006-2007 Dan Fuhry
  * search.php - algorithm used to search pages
  *
--- a/includes/sessions.php	Sat Oct 20 11:11:40 2007 -0400
+++ b/includes/sessions.php	Sat Oct 20 21:44:13 2007 -0400
@@ -547,14 +547,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 +604,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);
@@ -590,7 +637,29 @@
         $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']).'\')');
-      return "The username and/or password is incorrect.";  
+    
+      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();
     
@@ -641,7 +710,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)
@@ -661,10 +733,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
     {
@@ -673,7 +750,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'
+      );
     }
   }
   
@@ -699,6 +797,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);
     
@@ -707,14 +844,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 )
+    if($db->numrows() < 1)
     {
       // 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']).'\')');
-      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'
+      );
     }
     $row = $db->fetchrow();
     
@@ -764,7 +922,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)
       {
@@ -779,10 +940,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
     {
@@ -791,7 +957,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'
+      );
     }
   }
   
--- a/includes/stats.php	Sat Oct 20 11:11:40 2007 -0400
+++ b/includes/stats.php	Sat Oct 20 21:44:13 2007 -0400
@@ -1,7 +1,8 @@
 <?php
+
 /*
  * Enano - an open-source CMS capable of wiki functions, Drupal-like sidebar blocks, and everything in between
- * Version 1.0.2 (Coblynau)
+ * Version 1.1.1
  * Copyright (C) 2006-2007 Dan Fuhry
  * stats.php - handles statistics for pages (disablable in the admin CP)
  *
--- a/includes/tagcloud.php	Sat Oct 20 11:11:40 2007 -0400
+++ b/includes/tagcloud.php	Sat Oct 20 21:44:13 2007 -0400
@@ -2,7 +2,7 @@
 
 /*
  * Enano - an open-source CMS capable of wiki functions, Drupal-like sidebar blocks, and everything in between
- * Version 1.0.2 (Coblynau)
+ * Version 1.1.1
  * Copyright (C) 2006-2007 Dan Fuhry
  *
  * This program is Free Software; you can redistribute and/or modify it under the terms of the GNU General Public License
--- a/includes/template.php	Sat Oct 20 11:11:40 2007 -0400
+++ b/includes/template.php	Sat Oct 20 21:44:13 2007 -0400
@@ -958,7 +958,6 @@
   
   function compile_tpl_code($text)
   {
-    global $db, $session, $paths, $template, $plugins; // Common objects
     // A random seed used to salt tags
     $seed = md5 ( microtime() . mt_rand() );
     
@@ -987,88 +986,29 @@
     // Conditionals
     //
     
-    $keywords = array('BEGIN', 'BEGINNOT', 'IFSET', 'IFPLUGIN');
-    $code = $plugins->setHook('template_compile_logic_keyword');
-    foreach ( $code as $cmd )
-    {
-      eval($cmd);
-    }
+    // If-else-end
+    $text = preg_replace('/<!-- BEGIN ([A-z0-9_-]+?) -->(.*?)<!-- BEGINELSE \\1 -->(.*?)<!-- END \\1 -->/is', '\'; if ( $this->tpl_bool[\'\\1\'] ) { echo \'\\2\'; } else { echo \'\\3\'; } echo \'', $text);
+    
+    // If-end
+    $text = preg_replace('/<!-- BEGIN ([A-z0-9_-]+?) -->(.*?)<!-- END \\1 -->/is', '\'; if ( $this->tpl_bool[\'\\1\'] ) { echo \'\\2\'; } echo \'', $text);
     
-    $keywords = implode('|', $keywords);
+    // If not-else-end
+    $text = preg_replace('/<!-- BEGINNOT ([A-z0-9_-]+?) -->(.*?)<!-- BEGINELSE \\1 -->(.*?)<!-- END \\1 -->/is', '\'; if ( !$this->tpl_bool[\'\\1\'] ) { echo \'\\2\'; } else { echo \'\\3\'; } echo \'', $text);
     
-    // Matches
-    //          1     2                               3                 4   56                       7     8
-    $regexp = '/(<!-- ('. $keywords .') ([A-z0-9_-]+) -->)(.*)((<!-- BEGINELSE \\3 -->)(.*))?(<!-- END \\3 -->)/isU';
-    
-    /*
-    The way this works is: match all blocks using the standard form with a different keyword in the block each time,
-    and replace them with appropriate PHP logic. Plugin-extensible now. :-)
+    // If not-end
+    $text = preg_replace('/<!-- BEGINNOT ([A-z0-9_-]+?) -->(.*?)<!-- END \\1 -->/is', '\'; if ( !$this->tpl_bool[\'\\1\'] ) { echo \'\\2\'; } echo \'', $text);
     
-    The while-loop is to bypass what is apparently a PCRE bug. It's hackish but it works. Properly written plugins should only need
-    to compile templates (using this method) once for each time the template file is changed.
-    */
-    while ( preg_match($regexp, $text) )
-    {
-      preg_match_all($regexp, $text, $matches);
-      for ( $i = 0; $i < count($matches[0]); $i++ )
-      {
-        $start_tag =& $matches[1][$i];
-        $type =& $matches[2][$i];
-        $test =& $matches[3][$i];
-        $particle_true  =& $matches[4][$i];
-        $else_tag =& $matches[6][$i];
-        $particle_else =& $matches[7][$i];
-        $end_tag =& $matches[8][$i];
-        
-        switch($type)
-        {
-          case 'BEGIN':
-            $cond = "isset(\$this->tpl_bool['$test']) && \$this->tpl_bool['$test']";
-            break;
-          case 'BEGINNOT':
-            $cond = "!isset(\$this->tpl_bool['$test']) || ( isset(\$this->tpl_bool['$test']) && !\$this->tpl_bool['$test'] )";
-            break;
-          case 'IFPLUGIN':
-            $cond = "getConfig('plugin_$test') == '1'";
-            break;
-          case 'IFSET':
-            $cond = "isset(\$this->tpl_strings['$test'])";
-            break;
-          default:
-            $code = $plugins->setHook('template_compile_logic_cond');
-            foreach ( $code as $cmd )
-            {
-              eval($cmd);
-            }
-            break;
-        }
-        
-        if ( !isset($cond) || ( isset($cond) && !is_string($cond) ) )
-          continue;
-        
-        $tag_complete = <<<TPLCODE
-        ';
-        /* START OF CONDITION: $type ($test) */
-        if ( $cond )
-        {
-          echo '$particle_true';
-        /* ELSE OF CONDITION: $type ($test) */
-        }
-        else
-        {
-          echo '$particle_else';
-        /* END OF CONDITION: $type ($test) */
-        }
-        echo '
-TPLCODE;
-        
-        $text = str_replace_once($matches[0][$i], $tag_complete, $text);
-        
-      }
-    }
+    // If set-else-end
+    $text = preg_replace('/<!-- IFSET ([A-z0-9_-]+?) -->(.*?)<!-- BEGINELSE \\1 -->(.*?)<!-- END \\1 -->/is', '\'; if ( isset($this->tpl_strings[\'\\1\']) ) { echo \'\\2\'; } else { echo \'\\3\'; } echo \'', $text);
+    
+    // If set-end
+    $text = preg_replace('/<!-- IFSET ([A-z0-9_-]+?) -->(.*?)<!-- END \\1 -->/is', '\'; if ( isset($this->tpl_strings[\'\\1\']) ) { echo \'\\2\'; } echo \'', $text);
     
-    // For debugging ;-)
-    // die("<pre>&lt;?php\n" . htmlspecialchars($text."\n\n".print_r($matches,true)) . "\n\n?&gt;</pre>");
+    // If plugin loaded-else-end
+    $text = preg_replace('/<!-- IFPLUGIN ([A-z0-9_\.-]+?) -->(.*?)<!-- BEGINELSE \\1 -->(.*?)<!-- END \\1 -->/is', '\'; if ( getConfig(\'plugin_\\1\') == \'1\' ) { echo \'\\2\'; } else { echo \'\\3\'; } echo \'', $text);
+    
+    // If plugin loaded-end
+    $text = preg_replace('/<!-- IFPLUGIN ([A-z0-9_\.-]+?) -->(.*?)<!-- END \\1 -->/is', '\'; if ( getConfig(\'plugin_\\1\') == \'1\' ) { echo \'\\2\'; } echo \'', $text);
     
     //
     // Data substitution/variables
@@ -1089,8 +1029,6 @@
       $text = str_replace_once($tag, "'; $match echo '", $text);
     }
     
-    // echo('<pre>' . htmlspecialchars($text) . '</pre>');
-    
     return $text;  
     
   }
@@ -1473,7 +1411,7 @@
   function username_field($name, $value = false)
   {
     $randomid = md5( time() . microtime() . mt_rand() );
-    $text = '<input name="'.$name.'" onkeyup="new AutofillUsername(this);" autocomplete="off" type="text" size="30" id="userfield_'.$randomid.'"';
+    $text = '<input name="'.$name.'" onkeyup="ajaxUserNameComplete(this)" autocomplete="off" type="text" size="30" id="userfield_'.$randomid.'"';
     if($value) $text .= ' value="'.$value.'"';
     $text .= ' />';
     return $text;
--- a/includes/wikiengine/Tables.php	Sat Oct 20 11:11:40 2007 -0400
+++ b/includes/wikiengine/Tables.php	Sat Oct 20 21:44:13 2007 -0400
@@ -1,8 +1,8 @@
 <?php
 
-/**
+/*
  * Enano - an open-source CMS capable of wiki functions, Drupal-like sidebar blocks, and everything in between
- * Version 1.0.2 (Coblynau)
+ * Version 1.1.1
  * Copyright (C) 2006-2007 Dan Fuhry
  *
  * This program is Free Software; you can redistribute and/or modify it under the terms of the GNU General Public License
--- a/index.php	Sat Oct 20 11:11:40 2007 -0400
+++ b/index.php	Sat Oct 20 21:44:13 2007 -0400
@@ -15,7 +15,7 @@
 
   // Set up gzip encoding before any output is sent
   
-  $aggressive_optimize_html = true;
+  $aggressive_optimize_html = false;
   
   global $do_gzip;
   $do_gzip = true;
--- a/install.php	Sat Oct 20 11:11:40 2007 -0400
+++ b/install.php	Sat Oct 20 21:44:13 2007 -0400
@@ -2,7 +2,7 @@
 
 /*
  * Enano - an open-source CMS capable of wiki functions, Drupal-like sidebar blocks, and everything in between
- * Version 1.0.2 (Coblynau)
+ * Version 1.1.1
  * Copyright (C) 2006-2007 Dan Fuhry
  * install.php - handles everything related to installation and initial configuration
  *
@@ -23,7 +23,7 @@
 
 define('IN_ENANO_INSTALL', 'true');
 
-define('ENANO_VERSION', '1.0.2');
+define('ENANO_VERSION', '1.1.1');
 // In beta versions, define ENANO_BETA_VERSION here
 
 if(!defined('scriptPath')) {
@@ -260,7 +260,7 @@
 }
 
 $template = new template_nodb();
-$template->load_theme('oxygen', 'bleu', false);
+$template->load_theme('stpatty', 'shamrock', false);
 
 $modestrings = Array(
               'welcome' => 'Welcome',
@@ -313,10 +313,9 @@
   case 'welcome':
     ?>
     <div style="text-align: center; margin-top: 10px;">
-      <img alt="[ Enano CMS Project logo ]" src="images/enano-artwork/installer-greeting-blue.png" style="display: block; margin: 0 auto; padding-left: 100px;" />
+      <img alt="[ Enano CMS Project logo ]" src="images/enano-artwork/installer-greeting-green.png" style="display: block; margin: 0 auto; padding-left: 100px;" />
       <h2>Welcome to Enano</h2>
-      <h3>version 1.0.2 &ndash; stable<br />
-      <span style="font-weight: normal;">also affectionately known as "coblynau" <tt>:)</tt></span></h3>
+      <h3>version 1.1.1 &ndash; unstable</h3>
       <?php
       if ( file_exists('./_nightly.php') )
       {
--- a/plugins/SpecialAdmin.php	Sat Oct 20 11:11:40 2007 -0400
+++ b/plugins/SpecialAdmin.php	Sat Oct 20 21:44:13 2007 -0400
@@ -4,13 +4,13 @@
 Plugin URI: http://enanocms.org/
 Description: Provides the page Special:Administration, which is the AJAX frontend to the various Admin pagelets. This plugin cannot be disabled.
 Author: Dan Fuhry
-Version: 1.0.2
+Version: 1.0.1
 Author URI: http://enanocms.org/
 */
 
 /*
  * Enano - an open-source CMS capable of wiki functions, Drupal-like sidebar blocks, and everything in between
- * Version 1.0.2 (Coblynau)
+ * Version 1.1.1
  * Copyright (C) 2006-2007 Dan Fuhry
  *
  * This program is Free Software; you can redistribute and/or modify it under the terms of the GNU General Public License
@@ -203,6 +203,16 @@
       setConfig('pw_strength_minimum', $strength);
     }
     
+    // Account lockout policy
+    if ( preg_match('/^[0-9]+$/', $_POST['lockout_threshold']) )
+      setConfig('lockout_threshold', $_POST['lockout_threshold']);
+    
+    if ( preg_match('/^[0-9]+$/', $_POST['lockout_duration']) )
+      setConfig('lockout_duration', $_POST['lockout_duration']);
+    
+    if ( in_array($_POST['lockout_policy'], array('disable', 'captcha', 'lockout')) )
+      setConfig('lockout_policy', $_POST['lockout_policy']);
+    
     echo '<div class="info-box">Your changes to the site configuration have been saved.</div><br />';
     
   }
@@ -351,6 +361,43 @@
         </td>
       </tr>
       
+    <!-- Account lockout -->
+    
+      <tr><th colspan="2">Account lockouts</th></tr>
+      
+      <tr><td class="row3" colspan="2">Configure Enano to prevent or restrict logins for a specified period of time if a user enters an incorrect password a specific number of times.</td></tr>
+      
+      <tr>
+        <td class="row2">Lockout threshold:<br />
+          <small>How many times can a user enter wrong credentials before a lockout goes into effect?</small>
+        </td>
+        <td class="row2">
+          <input type="text" name="lockout_threshold" value="<?php echo ( $_ = getConfig('lockout_threshold') ) ? $_ : '5' ?>" />
+        </td>
+      </tr>
+      
+      <tr>
+        <td class="row1">Lockout duration:<br />
+          <small>This is how long an account lockout should last, in minutes.</small>
+        </td>
+        <td class="row1">
+          <input type="text" name="lockout_duration" value="<?php echo ( $_ = getConfig('lockout_duration') ) ? $_ : '15' ?>" />
+        </td>
+      </tr>
+      
+      <tr>
+        <td class="row2">Lockout policy:<br />
+          <small>What should be done when a lockout goes into effect?</small>
+        </td>
+        <td class="row2">
+          <label><input type="radio" name="lockout_policy" value="disable" <?php if ( getConfig('lockout_policy') == 'disable' ) echo 'checked="checked"'; ?> /> Don't do anything</label><br />
+          <label><input type="radio" name="lockout_policy" value="captcha" <?php if ( getConfig('lockout_policy') == 'captcha' ) echo 'checked="checked"'; ?> /> Require visual confirmation</label><br />
+          <label><input type="radio" name="lockout_policy" value="lockout" <?php if ( getConfig('lockout_policy') == 'lockout' || !getConfig('lockout_policy') ) echo 'checked="checked"'; ?> /> Prevent all login attempts</label>
+        </td>
+      </tr>
+      
+    <!-- Password strength -->
+      
       <tr><th colspan="2">Password strength</th></tr>
       
       <tr>
@@ -2685,7 +2732,7 @@
           } 
           else 
           {
-            echo '<div class="wait-box">Please wait while the administration panel loads. You need to be using a recent browser with AJAX support in order to use Runt.</div>';
+            echo '<script type="text/javascript">document.write(\'<div class="wait-box">Please wait while the administration panel loads. You need to be using a recent browser with AJAX support in order to use Runt.</div>\');</script><noscript><div class="error-box">It looks like Javascript isn\'t enabled in your browser. Please enable Javascript or use a different browser to continue.</div></noscript>';
           }
           ?>
           </div>
@@ -2979,7 +3026,7 @@
             echo '<div class="warning-box" style="margin: 10px 0;">$_GET[\'side\'] contained an SQL injection attempt</div>';
             break;
           }
-          $query = $db->sql_query('UPDATE '.table_prefix.'sidebar SET sidebar_id=' . $db->escape($_GET['side']) . ' WHERE item_id=' . intval($_GET['id']) . ';');
+          $query = $db->sql_query('UPDATE '.table_prefix.'sidebar SET sidebar_id=' . $db->escape($_GET['side']) . ' WHERE item_id=' . $db->escape($_GET['id']) . ';');
           if(!$query)
           {
             echo $db->get_error();
@@ -2989,7 +3036,7 @@
           echo '<div class="info-box" style="margin: 10px 0;">Item moved.</div>';
           break;
         case 'delete':
-          $query = $db->sql_query('DELETE FROM '.table_prefix.'sidebar WHERE item_id=' . intval($_GET['id']) . ';'); // Already checked for injection attempts ;-)
+          $query = $db->sql_query('DELETE FROM '.table_prefix.'sidebar WHERE item_id=' . $db->escape($_GET['id']) . ';'); // Already checked for injection attempts ;-)
           if(!$query)
           {
             echo $db->get_error();
@@ -3004,7 +3051,7 @@
           echo '<div class="error-box" style="margin: 10px 0;">Item deleted.</div>';
           break;
         case 'disenable';
-          $q = $db->sql_query('SELECT item_enabled FROM '.table_prefix.'sidebar WHERE item_id=' . intval($_GET['id']) . ';');
+          $q = $db->sql_query('SELECT item_enabled FROM '.table_prefix.'sidebar WHERE item_id=' . $db->escape($_GET['id']) . ';');
           if(!$q)
           {
             echo $db->get_error();
@@ -3014,22 +3061,7 @@
           $r = $db->fetchrow();
           $db->free_result();
           $e = ( $r['item_enabled'] == 1 ) ? '0' : '1';
-          $q = $db->sql_query('UPDATE '.table_prefix.'sidebar SET item_enabled='.$e.' WHERE item_id=' . intval($_GET['id']) . ';');
-          if(!$q)
-          {
-            echo $db->get_error();
-            $template->footer();
-            exit;
-          }
-          if(isset($_GET['ajax']))
-          {
-            ob_end_clean();
-            die('GOOD');
-          }
-          break;
-        case 'rename';
-          $newname = $db->escape($_POST['newname']);
-          $q = $db->sql_query('UPDATE '.table_prefix.'sidebar SET block_name=\''.$newname.'\' WHERE item_id=' . intval($_GET['id']) . ';');
+          $q = $db->sql_query('UPDATE '.table_prefix.'sidebar SET item_enabled='.$e.' WHERE item_id=' . $db->escape($_GET['id']) . ';');
           if(!$q)
           {
             echo $db->get_error();
@@ -3043,7 +3075,7 @@
           }
           break;
         case 'getsource':
-          $q = $db->sql_query('SELECT block_content,block_type FROM '.table_prefix.'sidebar WHERE item_id=' . intval($_GET['id']) . ';');
+          $q = $db->sql_query('SELECT block_content,block_type FROM '.table_prefix.'sidebar WHERE item_id=' . $db->escape($_GET['id']) . ';');
           if(!$q)
           {
             echo $db->get_error();
@@ -3059,7 +3091,7 @@
         case 'save':
           if ( defined('ENANO_DEMO_MODE') )
           {
-            $q = $db->sql_query('SELECT block_type FROM '.table_prefix.'sidebar WHERE item_id=' . intval($_GET['id']) . ';');
+            $q = $db->sql_query('SELECT block_type FROM '.table_prefix.'sidebar WHERE item_id=' . $db->escape($_GET['id']) . ';');
             if(!$q)
             {
               echo 'var status=unescape(\''.hexencode($db->get_error()).'\');';
@@ -3075,13 +3107,13 @@
               $_POST['content'] = sanitize_html($_POST['content'], true);
             }
           }
-          $q = $db->sql_query('UPDATE '.table_prefix.'sidebar SET block_content=\''.$db->escape(rawurldecode($_POST['content'])).'\' WHERE item_id=' . intval($_GET['id']) . ';');
+          $q = $db->sql_query('UPDATE '.table_prefix.'sidebar SET block_content=\''.$db->escape(rawurldecode($_POST['content'])).'\' WHERE item_id=' . $db->escape($_GET['id']) . ';');
           if(!$q)
           {
             echo 'var status=unescape(\''.hexencode($db->get_error()).'\');';
             exit;
           }
-          $q = $db->sql_query('SELECT block_type,block_content FROM '.table_prefix.'sidebar WHERE item_id=' . intval($_GET['id']) . ';');
+          $q = $db->sql_query('SELECT block_type,block_content FROM '.table_prefix.'sidebar WHERE item_id=' . $db->escape($_GET['id']) . ';');
           if(!$q)
           {
             echo 'var status=unescape(\''.hexencode($db->get_error()).'\');';
@@ -3173,8 +3205,6 @@
           $parser = $template->makeParserText($vars['sidebar_section']);
           $c = $template->tplWikiFormat($row['block_content'], false, 'sidebar-editor.tpl');
           $c = preg_replace('#<a (.*?)>(.*?)</a>#is', '<a href="#" onclick="return false;">\\2</a>', $c);
-          // fix for the "Administration" link that somehow didn't get rendered properly
-          $c = preg_replace("/(^|\n)([ ]*)<a([ ]+.*)?>(.+)<\/a>(<br(.*)\/>)([\r\n]+|$)/isU", '\\1\\2<li><a\\3>\\4</a></li>\\7', $c);
           break;
         case BLOCK_HTML:
           $parser = $template->makeParserText($vars['sidebar_section_raw']);
@@ -3194,7 +3224,7 @@
           $c = ($template->fetch_block($row['block_content'])) ? $template->fetch_block($row['block_content']) : 'Can\'t find plugin block';
           break;
       }
-      $t = '<span title="Double-click to rename this block" id="sbrename_' . $row['item_id'] . '" ondblclick="ajaxRenameSidebarStage1(this, \''.$row['item_id'].'\'); return false;">' . $template->tplWikiFormat($row['block_name']) . '</span>';
+      $t = $template->tplWikiFormat($row['block_name']);
       if($row['item_enabled'] == 0) $t .= ' <span id="disabled_'.$row['item_id'].'" style="color: red;">(disabled)</span>';
       else           $t .= ' <span id="disabled_'.$row['item_id'].'" style="color: red; display: none;">(disabled)</span>';
       $side = ( $row['sidebar_id'] == SIDEBAR_LEFT ) ? SIDEBAR_RIGHT : SIDEBAR_LEFT;
--- a/plugins/SpecialUserFuncs.php	Sat Oct 20 11:11:40 2007 -0400
+++ b/plugins/SpecialUserFuncs.php	Sat Oct 20 21:44:13 2007 -0400
@@ -4,13 +4,13 @@
 Plugin URI: http://enanocms.org/
 Description: Provides the pages Special:Login, Special:Logout, Special:Register, and Special:Preferences.
 Author: Dan Fuhry
-Version: 1.0.2
+Version: 1.0.1
 Author URI: http://enanocms.org/
 */
 
 /*
  * Enano - an open-source CMS capable of wiki functions, Drupal-like sidebar blocks, and everything in between
- * Version 1.0.2
+ * Version 1.0 release candidate 2
  * Copyright (C) 2006-2007 Dan Fuhry
  *
  * This program is Free Software; you can redistribute and/or modify it under the terms of the GNU General Public License
@@ -104,14 +104,60 @@
   $pubkey = $session->rijndael_genkey();
   $challenge = $session->dss_rand();
   
+  $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' )
   {
     $username = ( $session->user_logged_in ) ? $session->username : false;
     $response = Array(
       'username' => $username,
       'key' => $pubkey,
-      'challenge' => $challenge
+      'challenge' => $challenge,
+      'locked_out' => false
       );
+    
+    if ( $locked_out )
+    {
+      foreach ( $lockdata as $x => $y )
+      {
+        $response[$x] = $y;
+      }
+      unset($x, $y);
+    }
+    
     $json = new Services_JSON(SERVICES_JSON_LOOSE_TYPE);
     $response = $json->encode($response);
     echo $response;
@@ -138,7 +184,48 @@
   $header = ( $level > USER_LEVEL_MEMBER ) ? 'Please re-enter your login details' : 'Please enter your username and password to log in.';
   if ( isset($_POST['login']) )
   {
-    echo '<p>'.$__login_status.'</p>';
+    $errstring = $__login_status['error'];
+    switch($__login_status['error'])
+    {
+      case 'key_not_found':
+        $errstring = 'Enano couldn\'t look up the encryption key used to encrypt your password. This most often happens if a cache rotation occurred during your login attempt, or if you refreshed the login page.';
+        break;
+      case 'key_wrong_length':
+        $errstring = 'The encryption key was the wrong length.';
+        break;
+      case 'too_big_for_britches':
+        $errstring = 'You are trying to authenticate at a level that your user account does not permit.';
+        break;
+      case 'invalid_credentials':
+        $errstring = 'You have entered an invalid username or password. Please enter your login details again.';
+        if ( $__login_status['lockout_policy'] == 'lockout' )
+        {
+          $errstring .= ' You have used up '.$__login_status['lockout_fails'].' out of '.$__login_status['lockout_threshold'].' login attempts. After you have used up all '.$data['lockout_threshold'].' login attempts, you will be locked out from logging in for '.$__login_status['lockout_duration'].' minutes.';
+        }
+        else if ( $__login_status['lockout_policy'] == 'captcha' )
+        {
+          $errstring .= ' You have used up '.$__login_status['lockout_fails'].' out of '.$__login_status['lockout_threshold'].' login attempts. After you have used up all '.$data['lockout_threshold'].' login attempts, you will have to enter a visual confirmation code before logging in, effective for '.$__login_status['lockout_duration'].' minutes.';
+        }
+        break;
+      case 'backend_fail':
+        $errstring = '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.';
+        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 = $__login_status['lockout_duration'] - round( ( $server_time - $__login_status['lockout_last_time'] ) / 60 );
+        
+        $s = ( $time_rem == 1 ) ? '' : 's';
+        $errstring = "You have used up all {$__login_status['lockout_threshold']} allowed login attempts. Please wait {$time_rem} minute$s before attempting to log in again";
+        if ( $__login_status['lockout_policy'] == 'captcha' )
+        $errstring .= ', or enter the visual confirmation code shown above in the appropriate box';
+        $errstring .= '.';
+        break;
+    }
+    echo '<div class="error-box-mini">'.$errstring.'</div>';
   }
   if ( $p = $paths->getAllParams() )
   {
@@ -189,7 +276,7 @@
               ?> />
           </td>
           <?php if ( $level <= USER_LEVEL_MEMBER ) { ?>
-          <td rowspan="2" class="row3">
+          <td rowspan="<?php echo ( ( $locked_out && $lockdata['lockout_policy'] == 'captcha' ) ) ? '4' : '2'; ?>" class="row3">
             <small>Forgot your password? <a href="<?php echo makeUrlNS('Special', 'PasswordReset'); ?>">No problem.</a><br />
             Maybe you need to <a href="<?php echo makeUrlNS('Special', 'Register'); ?>">create an account</a>.</small>
           </td>
@@ -198,6 +285,21 @@
         <tr>
           <td class="row2">Password:<br /></td><td class="row1"><input name="pass" size="25" type="password" tabindex="<?php echo ( $level <= USER_LEVEL_MEMBER ) ? '2' : '1'; ?>" /></td>
          </tr>
+         <?php
+         if ( $locked_out && $lockdata['lockout_policy'] == 'captcha' )
+         {
+           ?>
+           <tr>
+             <td class="row2" rowspan="2">Code in image:<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>
+           </tr>
+           <tr>
+             <td class="row3">
+               <img src="<?php echo makeUrlNS('Special', 'Captcha/' . $lockdata['captcha']) ?>" onclick="this.src=this.src+'/a';" style="cursor: pointer;" />
+             </td>
+           </tr>
+           <?php
+         }
+         ?>
          <?php if ( $level <= USER_LEVEL_MEMBER ) { ?>
          <tr>
            <td class="row3" colspan="3">
@@ -242,12 +344,12 @@
     $plugins->attachHook('login_password_reset', 'SpecialLogin_SendResponse_PasswordReset($row[\'user_id\'], $row[\'temp_password\']);');
     $json = new Services_JSON(SERVICES_JSON_LOOSE_TYPE);
     $data = $json->decode($_POST['params']);
+    $captcha_hash = ( isset($data['captcha_hash']) ) ? $data['captcha_hash'] : false;
+    $captcha_code = ( isset($data['captcha_code']) ) ? $data['captcha_code'] : false;
     $level = ( isset($data['level']) ) ? intval($data['level']) : USER_LEVEL_MEMBER;
-    $result = $session->login_with_crypto($data['username'], $data['crypt_data'], $data['crypt_key'], $data['challenge'], $level);
+    $result = $session->login_with_crypto($data['username'], $data['crypt_data'], $data['crypt_key'], $data['challenge'], $level, $captcha_hash, $captcha_code);
     $session->start();
-    //echo "$result\n$session->sid_super";
-    //exit;
-    if ( $result == 'success' )
+    if ( $result['success'] )
     {
       $response = Array(
           'result' => 'success',
@@ -256,9 +358,16 @@
     }
     else
     {
+      $captcha = '';
+      if ( $result['error'] == 'locked_out' && $result['lockout_policy'] == 'captcha' )
+      {
+        $session->kill_captcha();
+        $captcha = $session->make_captcha();
+      }
       $response = Array(
           'result' => 'error',
-          'error' => $result
+          'data' => $result,
+          'captcha' => $captcha
         );
     }
     $response = $json->encode($response);
@@ -267,17 +376,19 @@
     exit;
   }
   if(isset($_POST['login'])) {
+    $captcha_hash = ( isset($_POST['captcha_hash']) ) ? $_POST['captcha_hash'] : false;
+    $captcha_code = ( isset($_POST['captcha_code']) ) ? $_POST['captcha_code'] : false;
     if($_POST['use_crypt'] == 'yes')
     {
-      $result = $session->login_with_crypto($_POST['username'], $_POST['crypt_data'], $_POST['crypt_key'], $_POST['challenge_data'], intval($_POST['auth_level']));
+      $result = $session->login_with_crypto($_POST['username'], $_POST['crypt_data'], $_POST['crypt_key'], $_POST['challenge_data'], intval($_POST['auth_level']), $captcha_hash, $captcha_code);
     }
     else
     {
-      $result = $session->login_without_crypto($_POST['username'], $_POST['pass'], false, intval($_POST['auth_level']));
+      $result = $session->login_without_crypto($_POST['username'], $_POST['pass'], false, intval($_POST['auth_level']), $captcha_hash, $captcha_code);
     }
     $session->start();
     $paths->init();
-    if($result == 'success')
+    if($result['success'])
     {
       $template->load_theme($session->theme, $session->style);
       if(isset($_POST['return_to']))
--- a/plugins/admin/PageGroups.php	Sat Oct 20 11:11:40 2007 -0400
+++ b/plugins/admin/PageGroups.php	Sat Oct 20 21:44:13 2007 -0400
@@ -2,7 +2,7 @@
 
 /*
  * Enano - an open-source CMS capable of wiki functions, Drupal-like sidebar blocks, and everything in between
- * Version 1.0.2 (Coblynau)
+ * Version 1.1.1
  * Copyright (C) 2006-2007 Dan Fuhry
  *
  * This program is Free Software; you can redistribute and/or modify it under the terms of the GNU General Public License
@@ -602,23 +602,6 @@
                   echo '<div class="info-box">The affecting tag was updated.</div>';
               }
             }
-            else if ( $_POST['pg_type'] == PAGE_GRP_REGEX )
-            {
-              $target = $_POST['pg_target'];
-              if ( empty($target) )
-              {
-                echo '<div class="error-box">Please enter an expression to match against..</div>';
-              }
-              else
-              {
-                $target = $db->escape($target);
-                $q = $db->sql_query('UPDATE '.table_prefix.'page_groups SET pg_target=\'' . $target . '\' WHERE pg_id=' . $edit_id . ';');
-                if ( !$q )
-                  $db->_die();
-                else
-                  echo '<div class="info-box">The expression to match against was updated.</div>';
-              }
-            }
             else if ( $_POST['pg_type'] == PAGE_GRP_CATLINK )
             {
               $target = $_POST['pg_target'];
@@ -877,22 +860,6 @@
                   </td>
                 </tr>';
           break;
-        case PAGE_GRP_REGEX:
-          echo '<tr>
-                  <td class="row2">
-                    Regular expression to use:<br />
-                    <small>Be sure to include the starting and ending delimiters and any flags you might need.<br />
-                           These pages might help: <a href="http://us.php.net/manual/en/reference.pcre.pattern.modifiers.php">Pattern modifiers</a> &bull;
-                           <a href="http://us.php.net/manual/en/reference.pcre.pattern.syntax.php">Pattern syntax</a><br />
-                           Examples: <tt>/^(Special|Admin):/i</tt> &bull; <tt>/^Image:([0-9]+)$/</tt><br />
-                           Developers, remember that this will be matched against the full page identifier string. This means that <tt>/^About_Enano$/</tt>
-                           will NOT match the page Special:About_Enano.</small>
-                  </td>
-                  <td class="row1">
-                    <input type="text" name="pg_target" value="' . htmlspecialchars($row['pg_target']) . '" size="30" />
-                  </td>
-                </tr>';
-          break;
         case PAGE_GRP_CATLINK:
           
           // Build category list
--- a/plugins/admin/SecurityLog.php	Sat Oct 20 11:11:40 2007 -0400
+++ b/plugins/admin/SecurityLog.php	Sat Oct 20 21:44:13 2007 -0400
@@ -2,7 +2,7 @@
 
 /*
  * Enano - an open-source CMS capable of wiki functions, Drupal-like sidebar blocks, and everything in between
- * Version 1.0.2 (Coblynau)
+ * Version 1.1.1
  * Copyright (C) 2006-2007 Dan Fuhry
  *
  * This program is Free Software; you can redistribute and/or modify it under the terms of the GNU General Public License
--- a/plugins/admin/UserManager.php	Sat Oct 20 11:11:40 2007 -0400
+++ b/plugins/admin/UserManager.php	Sat Oct 20 21:44:13 2007 -0400
@@ -2,7 +2,7 @@
 
 /*
  * Enano - an open-source CMS capable of wiki functions, Drupal-like sidebar blocks, and everything in between
- * Version 1.0.2 (Coblynau)
+ * Version 1.1.1
  * Copyright (C) 2006-2007 Dan Fuhry
  *
  * This program is Free Software; you can redistribute and/or modify it under the terms of the GNU General Public License
--- a/schema.sql	Sat Oct 20 11:11:40 2007 -0400
+++ b/schema.sql	Sat Oct 20 21:44:13 2007 -0400
@@ -254,6 +254,16 @@
   PRIMARY KEY ( tag_id )
 ) CHARACTER SET `utf8`;
 
+-- Added in 1.1.1
+
+CREATE TABLE {{TABLE_PREFIX}}lockout(
+  id int(12) NOT NULL auto_increment,
+  ipaddr varchar(40) NOT NULL,
+  action ENUM('credential', 'level') NOT NULL DEFAULT 'credential',
+  timestamp int(12) NOT NULL DEFAULT 0,
+  PRIMARY KEY ( id )
+) CHARACTER SET `utf8`;
+
 INSERT INTO {{TABLE_PREFIX}}config(config_name, config_value) VALUES
   ('site_name', '{{SITE_NAME}}'),
   ('main_page', 'Main_Page'),
--- a/upgrade.php	Sat Oct 20 11:11:40 2007 -0400
+++ b/upgrade.php	Sat Oct 20 21:44:13 2007 -0400
@@ -2,7 +2,7 @@
 
 /*
  * Enano - an open-source CMS capable of wiki functions, Drupal-like sidebar blocks, and everything in between
- * Version 1.0.2 (Coblynau)
+ * Version 1.1.1
  * upgrade.php - upgrade script
  * Copyright (C) 2006-2007 Dan Fuhry
  *
@@ -61,7 +61,7 @@
 // Everything related to versions goes here!
 
 // Valid versions to upgrade from
-$valid_versions = Array('1.0b1', '1.0b2', '1.0b3', '1.0b4', '1.0RC1', '1.0RC2', '1.0RC3', '1.0', '1.0.1', '1.0.1.1', '1.0.2b1');
+$valid_versions = Array('1.0b1', '1.0b2', '1.0b3', '1.0b4', '1.0RC1', '1.0RC2', '1.0RC3', '1.0', '1.0.1', '1.0.1.1', '1.0.2b1', '1.0.2', 'Stable1.0ToUnstable1.1');
 
 // Basically a list of dependencies, which should be resolved automatically
 // If, for example, upgrading from 1.0b1 to 1.0RC1 requires one extra query that would not
@@ -76,9 +76,11 @@
     '1.0RC3' => Array('1.0'),
     '1.0' => Array('1.0.1'),
     '1.0.1' => Array('1.0.1.1'),
-    '1.0.1.1' => Array('1.0.2b1')
+    '1.0.1.1' => Array('1.0.2b1'),
+    '1.0.2b1' => Array('Stable1.0ToUnstable1.1'),
+    'Stable1.0ToUnstable1.1' => Array('1.1.1')
   );
-$this_version   = '1.0.2';
+$this_version   = '1.1.1';
 $func_list = Array(
     '1.0' => Array('u_1_0_1_update_del_votes'),
     '1.0b4' => Array('u_1_0_RC1_update_user_ids', 'u_1_0_RC1_add_admins_to_group', 'u_1_0_RC1_alter_files_table', 'u_1_0_RC1_destroy_session_cookie', 'u_1_0_RC1_set_contact_email', 'u_1_0_RC1_update_page_text'), // ,
@@ -445,7 +447,7 @@
     {
       if(isset($_POST['login']))
       {
-        $session->login_without_crypto($_POST['username'], $_POST['password'], false, $ul_admin);
+        $result = $session->login_without_crypto($_POST['username'], $_POST['password'], false, $ul_admin);
         if($session->sid_super)
         {
           header('Location: upgrade.php?mode=welcome&auth='.$session->sid_super);
@@ -462,7 +464,7 @@
         <?php
         if(isset($_POST['login']))
         {
-          echo '<tr><td colspan="2"><p style="color: red;">Login failed. Bad password?</p></td></tr>';
+          echo '<tr><td colspan="2"><p style="color: red;">Login failed: '. $result['error'] . '</p></td></tr>';
         }
         ?>
         <tr>
--- a/upgrade.sql	Sat Oct 20 11:11:40 2007 -0400
+++ b/upgrade.sql	Sat Oct 20 21:44:13 2007 -0400
@@ -3,7 +3,15 @@
 -- ALL NON-SQL LINES, even otherwise blank lines, must start with "--" or they will get sent to MySQL!
 -- Common tasks (version numbers)
 DELETE FROM {{TABLE_PREFIX}}config WHERE config_name='enano_version' OR config_name='enano_beta_version' OR config_name='enano_alpha_version' OR config_name='enano_rc_version';
-INSERT INTO {{TABLE_PREFIX}}config (config_name, config_value) VALUES( 'enano_version', '1.0.2' );
+INSERT INTO {{TABLE_PREFIX}}config (config_name, config_value) VALUES( 'enano_version', '1.1.1' );
+---BEGIN Stable1.0ToUnstable1.1---
+-- UPDATE {{TABLE_PREFIX}}groups SET group_id=9998 WHERE group_id=4;
+-- UPDATE {{TABLE_PREFIX}}group_members SET group_id=9998 WHERE group_id=4;
+-- INSERT INTO {{TABLE_PREFIX}}groups(group_id,group_name,group_type,system_group) VALUES(4, 'Regular members', 3, 1);
+CREATE TABLE {{TABLE_PREFIX}}lockout( id int(12) NOT NULL auto_increment, ipaddr varchar(40) NOT NULL, action ENUM('credential', 'level') NOT NULL DEFAULT 'credential', timestamp int(12) NOT NULL DEFAULT 0, PRIMARY KEY ( id ) ) CHARACTER SET `utf8`;
+---END Stable1.0ToUnstable1.1---
+---BEGIN 1.0.2---
+---END 1.0.2---
 ---BEGIN 1.0.2b1---
 -- This is really optional, but could reduce confusion if regex page groups get truncated for no apparent reason.
 ALTER TABLE {{TABLE_PREFIX}}page_groups MODIFY COLUMN pg_target text DEFAULT NULL;