Improved ban control page and allowed multiple entries/IP ranges; changed some parameters on jBox; user level changes are logged now
authorDan
Sat, 08 Sep 2007 22:58:38 -0400
changeset 128 01955bf53f96
parent 127 a2b20a832447
child 129 0b5244001799
Improved ban control page and allowed multiple entries/IP ranges; changed some parameters on jBox; user level changes are logged now
includes/clientside/static/dropdown.js
includes/clientside/static/toolbar.js
includes/dbal.php
includes/functions.php
includes/template.php
plugins/SpecialAdmin.php
plugins/admin/SecurityLog.php
--- a/includes/clientside/static/dropdown.js	Sat Sep 08 15:06:28 2007 -0400
+++ b/includes/clientside/static/dropdown.js	Sat Sep 08 22:58:38 2007 -0400
@@ -123,7 +123,7 @@
     if(typeof(others[i]) == 'object')
     {
       others[i].style.display = 'none';
-      others[i].previousSibling.className = '';
+      $(others[i].previousSibling).rmClass('liteselected');
     }
   }
   var others = obj.parentNode.getElementsByTagName('div');
@@ -134,13 +134,14 @@
       if ( others[i].className == 'submenu' )
       {
         others[i].style.display = 'none';
-        others[i].previousSibling.className = '';
+        $(others[i].previousSibling).rmClass('liteselected');
       }
     }
   }
   if(obj.nextSibling.tagName.toLowerCase() == 'ul' || ( obj.nextSibling.tagName.toLowerCase() == 'div' && obj.nextSibling.className == 'submenu' ))
   {
-    obj.className = 'liteselected';
+    $(a).addClass('liteselected');
+    //obj.className = 'liteselected';
     var ul = obj.nextSibling;
     var dim = fetch_dimensions(obj);
     var off = fetch_offset(obj);
@@ -197,7 +198,7 @@
   
   if (!isOverObj(a, false, event) && !isOverObj(ul, true, event))
   {
-    a.className = '';
+    $(a).rmClass('liteselected');
     
     if ( jBox_slide_enable )
     {
@@ -399,7 +400,7 @@
         {
           if ( !isOverObj(uls[j], false, e) )
           {
-            uls[j].previousSibling.className = '';
+            $(uls[j].previousSibling).rmClass('liteselected');
             //uls[j].style.display = 'none';
             slideIn(uls[j]);
           }
@@ -412,7 +413,7 @@
         {
           if ( !isOverObj(uls[j], false, e) )
           {
-            uls[j].previousSibling.className = '';
+            $(uls[j].previousSibling).rmClass('liteselected');
             //uls[j].style.display = 'none';
             slideIn(uls[j]);
           }
--- a/includes/clientside/static/toolbar.js	Sat Sep 08 15:06:28 2007 -0400
+++ b/includes/clientside/static/toolbar.js	Sat Sep 08 22:58:38 2007 -0400
@@ -9,7 +9,7 @@
   {
     if(obj.id == 'mdgToolbar_article' || obj.id == 'mdgToolbar_discussion')
     {
-      obj.className = '';
+      $(obj).rmClass('selected');
     }
     obj = obj.nextSibling;
   }
@@ -22,7 +22,7 @@
   obj = document.getElementById('pagebar_main').firstChild.nextSibling;
   while(obj)
   {
-    if ( obj.className != 'selected' )
+    if ( !$(obj).hasClass('selected') )
     {
       obj = obj.nextSibling;
       continue;
@@ -30,7 +30,7 @@
     if(obj.id != 'mdgToolbar_article' && obj.id != 'mdgToolbar_discussion')
     {
       if ( obj.className )
-        obj.className = '';
+        $(obj).rmClass('selected');
     }
     obj = obj.nextSibling;
   }
@@ -46,7 +46,7 @@
   if(typeof(dom) == 'object')
   {
     unselectAllButtonsMajor();
-    document.getElementById('mdgToolbar_'+which).className = 'selected';
+    $('mdgToolbar_'+which).addClass('selected');
   }
 }
 
@@ -57,7 +57,7 @@
   if(typeof(document.getElementById('mdgToolbar_'+which)) == 'object')
   {
     unselectAllButtonsMinor();
-    document.getElementById('mdgToolbar_'+which).className = 'selected';
+    $('mdgToolbar_'+which).addClass('selected');
   }
 }
 
--- a/includes/dbal.php	Sat Sep 08 15:06:28 2007 -0400
+++ b/includes/dbal.php	Sat Sep 08 22:58:38 2007 -0400
@@ -196,7 +196,7 @@
     $quotepos  = 0;
     $prev_is_quote = false;
     $just_started = false;
-    for($i=0;$i<strlen($q);$i++,$c=substr($q, $i, 1))
+    for ( $i = 0; $i < strlen($q); $i++, $c = substr($q, $i, 1) )
     {
       $next = substr($q, $i+1, 1);
       $next2 = substr($q, $i+2, 1);
@@ -206,8 +206,8 @@
       {
         if($quotechar)
         {
-          if(
-              ( $quotechar == $c && $quotechar != $next && ( $quotechar != $prev || $just_entered ) && $prev != '\\') ||
+          if (
+              ( $quotechar == $c && $quotechar != $next && ( $quotechar != $prev || $just_started ) && $prev != '\\') ||
               ( $prev2 == '\\' && $prev == $quotechar && $quotechar == $c )
             )
           {
@@ -222,17 +222,20 @@
         {
           $quotechar = $c;
           $quotepos  = $i;
-          $just_entered = true;
+          $just_started = true;
         }
         if($debug) echo '$db-&gt;check_query(): found quote char as pos: '.$i.'<br />';
         continue;
       }
-      $just_entered = false;
+      $just_started = false;
     }
     if(substr(trim($q), strlen(trim($q))-1, 1) == ';') $q = substr(trim($q), 0, strlen(trim($q))-1);
     for($i=0;$i<strlen($q);$i++,$c=substr($q, $i, 1))
     {
-      if( ( $c == ';' && $i != $sz-1 ) || $c . substr($q, $i+1, 1) == '--') // Don't permit semicolons in mid-query, and never allow comments
+      if ( 
+           ( ( $c == ';' && $i != $sz-1 ) || $c . substr($q, $i+1, 1) == '--' )
+        || ( in_array($c, Array('"', "'", '`')) )
+         ) // Don't permit semicolons in mid-query, and never allow comments
       {
         // Injection attempt!
         if($debug)
@@ -248,6 +251,11 @@
         return false;
       }
     }
+    if ( preg_match('/[\s]+(SAFE_QUOTE|[\S]+)=\\1($|[\s]+)/', $q, $match) )
+    {
+      if ( $debug ) echo 'Found always-true test in query, injection attempt caught, match:<br />' . '<pre>' . print_r($match, true) . '</pre>';
+      return false;
+    }
     return true;
   }
   
--- a/includes/functions.php	Sat Sep 08 15:06:28 2007 -0400
+++ b/includes/functions.php	Sat Sep 08 22:58:38 2007 -0400
@@ -2846,6 +2846,85 @@
   return $html;
 }
 
+/**
+ * For an input range of numbers (like 25-256) returns an array filled with all numbers in the range, inclusive.
+ * @param string
+ * @return array
+ */
+
+function int_range($range)
+{
+  if ( strval(intval($range)) == $range )
+    return $range;
+  if ( !preg_match('/^[0-9]+(-[0-9]+)?$/', $range) )
+    return false;
+  $ends = explode('-', $range);
+  if ( count($ends) != 2 )
+    return $range;
+  $ret = array();
+  if ( $ends[1] < $ends[0] )
+    $ends = array($ends[1], $ends[0]);
+  else if ( $ends[0] == $ends[1] )
+    return array($ends[0]);
+  for ( $i = $ends[0]; $i <= $ends[1]; $i++ )
+  {
+    $ret[] = $i;
+  }
+  return $ret;
+}
+
+/**
+ * Parses a range or series of IP addresses, and returns the raw addresses. Only parses ranges in the last two octets to prevent DOSing.
+ * Syntax for ranges: x.x.x.x; x|y.x.x.x; x.x.x-z.x; x.x.x-z|p.q|y
+ * @param string IP address range string
+ * @return array
+ */
+
+function parse_ip_range($range)
+{
+  $octets = explode('.', $range);
+  if ( count($octets) != 4 )
+    // invalid range
+    return $range;
+  $i = 0;
+  $possibilities = array( 0 => array(), 1 => array(), 2 => array(), 3 => array() );
+  foreach ( $octets as $octet )
+  {
+    $existing =& $possibilities[$i];
+    $inner = explode('|', $octet);
+    foreach ( $inner as $bit )
+    {
+      if ( $i >= 2 )
+      {
+        $bits = int_range($bit);
+        if ( $bits === false )
+          return false;
+        else if ( !is_array($bits) )
+          $existing[] = intval($bits);
+        else
+          $existing = array_merge($existing, $bits);
+      }
+      else
+      {
+        $bit = intval($bit);
+        $existing[] = $bit;
+      }
+    }
+    $existing = array_unique($existing);
+    $i++;
+  }
+  $ips = array();
+  
+  // The only way to combine all those possibilities. ;-)
+  foreach ( $possibilities[0] as $oc1 )
+    foreach ( $possibilities[1] as $oc2 )
+      foreach ( $possibilities[2] as $oc3 )
+        foreach ( $possibilities[3] as $oc4 )
+          $ips[] = "$oc1.$oc2.$oc3.$oc4";
+        
+  return $ips;
+}
+
 //die('<pre>Original:  01010101010100101010100101010101011010'."\nProcessed: ".uncompress_bitfield(compress_bitfield('01010101010100101010100101010101011010')).'</pre>');
 
 ?>
--- a/includes/template.php	Sat Sep 08 15:06:28 2007 -0400
+++ b/includes/template.php	Sat Sep 08 22:58:38 2007 -0400
@@ -943,7 +943,15 @@
       $h = fopen($tpl_filename, 'w');
       if(!$h) return $text;
       $t = addslashes($text);
-      fwrite($h, '<?php $md5 = \''.$md5.'\'; $tpl_text = \''.$t.'\'; ?>');
+      $notice = <<<EOF
+
+/*
+ * NOTE: This file was automatically generated by Enano and is based on compiled code. Do not edit this file.
+ * If you edit this file, any changes you make will be lost the next time the associated source template file is edited.
+ */
+
+EOF;
+      fwrite($h, '<?php ' . $notice . ' $md5 = \''.$md5.'\'; $tpl_text = \''.$t.'\'; ?>');
       fclose($h);
     }
     return $text; //('<pre>'.htmlspecialchars($text).'</pre>');
--- a/plugins/SpecialAdmin.php	Sat Sep 08 15:06:28 2007 -0400
+++ b/plugins/SpecialAdmin.php	Sat Sep 08 22:58:38 2007 -0400
@@ -860,19 +860,31 @@
           // We need to update group memberships
           if ( $old_level == USER_LEVEL_ADMIN ) 
           {
+            $q = $db->sql_query('INSERT INTO '.table_prefix.'logs(log_type,action,time_id,edit_summary,author,page_text) VALUES("security","u_from_admin",UNIX_TIMESTAMP(),"' . $db->escape($_SERVER['REMOTE_ADDR']) . '","' . $db->escape($session->username) . '","' . $db->escape($_POST['new_username']) . '");');
+            if ( !$q )
+              $db->_die();
             $session->remove_user_from_group($user_id, GROUP_ID_ADMIN);
           }
           else if ( $old_level == USER_LEVEL_MOD ) 
           {
+            $q = $db->sql_query('INSERT INTO '.table_prefix.'logs(log_type,action,time_id,edit_summary,author,page_text) VALUES("security","u_from_mod",UNIX_TIMESTAMP(),"' . $db->escape($_SERVER['REMOTE_ADDR']) . '","' . $db->escape($session->username) . '","' . $db->escape($_POST['new_username']) . '");');
+            if ( !$q )
+              $db->_die();
             $session->remove_user_from_group($user_id, GROUP_ID_MOD);
           }
           
           if ( $new_level == USER_LEVEL_ADMIN )
           {
+            $q = $db->sql_query('INSERT INTO '.table_prefix.'logs(log_type,action,time_id,edit_summary,author,page_text) VALUES("security","u_to_admin",UNIX_TIMESTAMP(),"' . $db->escape($_SERVER['REMOTE_ADDR']) . '","' . $db->escape($session->username) . '","' . $db->escape($_POST['new_username']) . '");');
+            if ( !$q )
+              $db->_die();
             $session->add_user_to_group($user_id, GROUP_ID_ADMIN, false);
           }
           else if ( $new_level == USER_LEVEL_MOD )
           {
+            $q = $db->sql_query('INSERT INTO '.table_prefix.'logs(log_type,action,time_id,edit_summary,author,page_text) VALUES("security","u_to_mod",UNIX_TIMESTAMP(),"' . $db->escape($_SERVER['REMOTE_ADDR']) . '","' . $db->escape($session->username) . '","' . $db->escape($_POST['new_username']) . '");');
+            if ( !$q )
+              $db->_die();
             $session->add_user_to_group($user_id, GROUP_ID_MOD, false);
           }
         }
@@ -2064,12 +2076,66 @@
   }
   if(isset($_POST['create']) && !defined('ENANO_DEMO_MODE'))
   {
-    $q = 'INSERT INTO '.table_prefix.'banlist(ban_type,ban_value,reason,is_regex) VALUES( ' . $db->escape($_POST['type']) . ', \'' . $db->escape($_POST['value']) . '\', \''.$db->escape($_POST['reason']).'\'';
-      if(isset($_POST['regex'])) $q .= ', 1';
-      else $q .= ', 0';
-    $q .= ');';
-    $e = $db->sql_query($q);
-    if(!$e) $db->_die('The banlist could not be updated.');
+    $type = intval($_POST['type']);
+    $value = trim($_POST['value']);
+    if ( !in_array($type, array(BAN_IP, BAN_USER, BAN_EMAIL)) )
+    {
+      echo '<div class="error-box">Hacking attempt.</div>';
+    }
+    else if ( empty($value) )
+    {
+      echo '<div class="error-box">Please enter something to ban.</div>';
+    }
+    else
+    {
+      $entries = array();
+      $input = explode(',', $_POST['value']);
+      $error = false;
+      foreach ( $input as $entry )
+      {
+        $entry = trim($entry);
+        if ( empty($entry) )
+        {
+          echo '<div class="error-box">Malformed entry.</div>';
+          $error = true;
+          break;
+        }
+        if ( $type == BAN_IP )
+        {
+          // parse a range of addresses
+          $range = parse_ip_range($entry);
+          if ( !$range )
+          {
+            $error = true;
+            echo '<div class="error-box">Malformed IP address expression.</div>';
+            break;
+          }
+          foreach ($range as $ip)
+          {
+            $entries[] = $ip;
+          }
+        }
+        else
+        {
+          $entries[] = $entry;
+        }
+      }
+      if ( !$error )
+      {
+        $regex = ( isset($_POST['regex']) ) ? '1' : '0';
+        $to_insert = array();                                                         
+        $reason = $db->escape($_POST['reason']);
+        foreach ( $entries as $entry )
+        {
+          $entry = $db->escape($entry);
+          $to_insert[] = "($type, '$entry', '$reason', $regex)";
+        }
+        $q = 'INSERT INTO '.table_prefix."banlist(ban_type, ban_value, reason, is_regex)\n  VALUES" . implode(",\n  ", $to_insert) . ';';
+        @set_time_limit(0);
+        $e = $db->sql_query($q);
+        if(!$e) $db->_die('The banlist could not be updated.');
+      }
+    }
   }
   else if ( isset($_POST['create']) && defined('ENANO_DEMO_MODE') )
   {
@@ -2077,25 +2143,29 @@
   }
   $q = $db->sql_query('SELECT ban_id,ban_type,ban_value,is_regex FROM '.table_prefix.'banlist ORDER BY ban_type;');
   if(!$q) $db->_die('The banlist data could not be selected.');
-  echo '<table border="0" cellspacing="1" cellpadding="4">';
+  echo '<div class="tblholder" style="max-height: 800px; clip: rect(0px,auto,auto,0px); overflow: auto;">
+          <table border="0" cellspacing="1" cellpadding="4">';
   echo '<tr><th>Type</th><th>Value</th><th>Regular Expression</th><th></th></tr>';
-  if($db->numrows() < 1) echo '<td colspan="4">No ban rules yet.</td>';
+  if($db->numrows() < 1) echo '<td class="row1" colspan="4">No ban rules yet.</td>';
+  $cls = 'row2';
   while($r = $db->fetchrow())
   {
+    $cls = ( $cls == 'row1' ) ? 'row2' : 'row1';
     if($r['ban_type']==BAN_IP) $t = 'IP address';
     elseif($r['ban_type']==BAN_USER) $t = 'Username';
     elseif($r['ban_type']==BAN_EMAIL) $t = 'E-mail address';
     if($r['is_regex']) $g = 'Yes'; else $g = 'No';
-    echo '<tr><td>'.$t.'</td><td>'.$r['ban_value'].'</td><td>'.$g.'</td><td><a href="'.makeUrlNS('Special', 'Administration', 'module='.$paths->nslist['Admin'].'BanControl&amp;action=delete&amp;id='.$r['ban_id']).'">Delete</a></td></tr>';
+    echo '<tr><td class="'.$cls.'">'.$t.'</td><td class="'.$cls.'">'.$r['ban_value'].'</td><td class="'.$cls.'">'.$g.'</td><td class="'.$cls.'"><a href="'.makeUrlNS('Special', 'Administration', 'module='.$paths->nslist['Admin'].'BanControl&amp;action=delete&amp;id='.$r['ban_id']).'">Delete</a></td></tr>';
   }
   $db->free_result();
-  echo '</table>';
+  echo '</table></div>';
   echo '<h3>Create new ban rule</h3>';
   echo '<form action="'.makeUrl($paths->nslist['Special'].'Administration', 'module='.$paths->cpage['module']).'" method="post">';
   ?>
   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 />
-  Reason to show to the banned user: <textarea name="reason" rows="7" cols="20"></textarea><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 />
+  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" />
   <?php
--- a/plugins/admin/SecurityLog.php	Sat Sep 08 15:06:28 2007 -0400
+++ b/plugins/admin/SecurityLog.php	Sat Sep 08 22:58:38 2007 -0400
@@ -152,6 +152,10 @@
     case "plugin_disable":   $return .= "Disabled plugin: {$r['page_text']}"; break;
     case "plugin_enable":    $return .= "Enabled plugin: {$r['page_text']}"; break;
     case "seclog_unauth":    $return .= "Unauthorized attempt to call security log fetcher"; break;
+    case "u_from_admin":     $return .= "User {$r['page_text']} demoted from Administrators group"; break;
+    case "u_from_mod":       $return .= "User {$r['page_text']} demoted from Moderators group"; break;
+    case "u_to_admin":       $return .= "User {$r['page_text']} added to Administrators group"; break;
+    case "u_to_mod":         $return .= "User {$r['page_text']} added to Moderators group"; break;
   }
   $return .= '</td><td class="'.$cls.'">'.date('d M Y h:i a', $r['time_id']).'</td><td class="'.$cls.'">'.$r['author'].'</td><td class="'.$cls.'" style="cursor: pointer;" onclick="ajaxReverseDNS(this);" title="Click for reverse DNS info">'.$r['edit_summary'].'</td></tr>';
   return $return;