Implemented IP logging for comments and registration
authorDan
Mon, 21 Jan 2008 10:09:48 -0500
changeset 359 e0787bb6285b
parent 358 b25d34fbc7ab
child 360 fad9bb5c094b
Implemented IP logging for comments and registration
includes/clientside/static/comments.js
includes/clientside/static/paginate.js
includes/comment.php
includes/functions.php
includes/sessions.php
install/includes/payload.php
install/schemas/mysql_stage1.sql
install/schemas/mysql_stage2.sql
install/schemas/postgresql_stage1.sql
install/schemas/postgresql_stage2.sql
language/english/admin.json
language/english/core.json
language/english/tools.json
language/english/user.json
plugins/SpecialUserFuncs.php
plugins/admin/SecurityLog.php
plugins/admin/UserManager.php
themes/oxygen/comment.tpl
themes/oxygen/css/bleu.css
--- a/includes/clientside/static/comments.js	Sun Jan 20 22:34:02 2008 -0500
+++ b/includes/clientside/static/comments.js	Mon Jan 21 10:09:48 2008 -0500
@@ -218,6 +218,16 @@
   // Moderation: Delete post link
   tplvars.MOD_DELETE_LINK='<a href="#mod_del_'+i+'" onclick="deleteComment(\''+i+'\'); return false;">' + $lang.get('comment_btn_mod_delete') + '</a>';
   
+  // Moderation: IP address link
+  if ( this_comment.have_ip )
+  {
+    tplvars.MOD_IP_LINK = '<span id="comment_ip_' + i + '"><a href="#mod_ip_' + i + '" onclick="viewCommentIP(' + this_comment.comment_id + ', ' + i + '); return false;">' + $lang.get('comment_btn_mod_ip_logged') + '</a></span>';
+  }
+  else
+  {
+    tplvars.MOD_IP_LINK = $lang.get('comment_btn_mod_ip_missing');
+  }
+  
   var tplbool = new Object();
   
   tplbool.signature = ( this_comment.signature == '' ) ? false : true;
@@ -376,6 +386,13 @@
   {
     document.getElementById('comment_source_' + data.id).value = data.src;
   }
+  if ( data.ip_addr )
+  {
+    var span = $('comment_ip_' + data.local_id).object;
+    if ( !span )
+      return false;
+    span.innerHTML = $lang.get('comment_msg_ip_address') + ' <a href="#rdns" onclick="ajaxReverseDNS(this); return false;">' + data.ip_addr + '</a>';
+  }
 }
 
 function approveComment(id)
@@ -480,6 +497,9 @@
   // Moderation: Delete post link
   tplvars.MOD_DELETE_LINK='<a href="#mod_del_'+i+'" onclick="deleteComment(\''+i+'\'); return false;">' + $lang.get('comment_btn_mod_delete') + '</a>';
   
+  // Moderation: IP address link
+  tplvars.MOD_IP_LINK = '<span id="comment_ip_' + i + '"><a href="#mod_ip_' + i + '" onclick="viewCommentIP(' + data.comment_id + ', ' + i + '); return false;">' + $lang.get('comment_btn_mod_ip_logged') + '</a></span>';
+  
   var tplbool = new Object();
   
   tplbool.signature = ( data.signature == '' ) ? false : true;
@@ -582,6 +602,22 @@
   }
 }
 
+function viewCommentIP(id, local_id)
+{
+  // set "loading" indicator on IP button
+  var span = $('comment_ip_' + local_id).object;
+  if ( !span )
+    return false;
+  span.innerHTML = '<img alt="..." src="' + ajax_load_icon + '" />';
+  
+  var parms = {
+    mode: 'view_ip',
+    id: id,
+    local_id: local_id
+  }
+  ajaxComments(parms);
+}
+
 function htmlspecialchars(text)
 {
   text = text.replace(/</g, '&lt;');
--- a/includes/clientside/static/paginate.js	Sun Jan 20 22:34:02 2008 -0500
+++ b/includes/clientside/static/paginate.js	Mon Jan 21 10:09:48 2008 -0500
@@ -68,7 +68,7 @@
 function _build_paginator(this_page)
 {
   var div_styling = ( IE ) ? 'width: 1px; margin: 10px auto 10px 0;' : 'display: table; margin: 10px 0 0 auto;';
-  var begin = '<div class="tblholder" style="'+div_styling+'"><table border="0" cellspacing="1" cellpadding="4"><tr><th>Page:</th>';
+  var begin = '<div class="tblholder" style="'+div_styling+'"><table border="0" cellspacing="1" cellpadding="4"><tr><th>' + $lang.get('paginate_lbl_page') + '</th>';
   var block = '<td class="row1" style="text-align: center; white-space: nowrap;">{LINK}</td>';
   var end = '</tr></table></div>';
   var blk = new templateParser(block);
@@ -78,7 +78,7 @@
   if ( this_page > 0 )
   {
     var url = '#page_'+(this_page);
-    var link = "<a href=\""+url+"\" onclick=\"jspaginator_goto('"+this.random_id+"', "+(this_page-1)+"); return false;\" style='text-decoration: none;'>&laquo; Prev</a>";
+    var link = "<a href=\""+url+"\" onclick=\"jspaginator_goto('"+this.random_id+"', "+(this_page-1)+"); return false;\" style='text-decoration: none;'>&laquo; " + $lang.get('paginate_btn_prev') + "</a>";
     cls = ( cls == 'row1' ) ? 'row2' : 'row1';
     blk.assign_vars({
         CLASS: cls,
@@ -126,7 +126,7 @@
       }
     }
     var url = '#page_1';
-    var link = ( 0 == this_page ) ? "<b>First</b>" : "<a href=\""+url+"\" onclick=\"jspaginator_goto('"+this.random_id+"', 0); return false;\" style='text-decoration: none;'>&laquo; First</a>";
+    var link = ( 0 == this_page ) ? "<b>" + $lang.get('paginate_btn_first') + "</b>" : "<a href=\""+url+"\" onclick=\"jspaginator_goto('"+this.random_id+"', 0); return false;\" style='text-decoration: none;'>&laquo; " + $lang.get('paginate_btn_first') + "</a>";
     blk.assign_vars({
         CLASS: cls,
         LINK: link
@@ -164,7 +164,7 @@
 
       cls = ( cls == 'row1' ) ? 'row2' : 'row1';
       var url = '#page_' + String( this.num_pages-1 );
-      var link = ( ( this.num_pages - 1 ) == this_page ) ? "<b>Last</b>" : "<a href=\""+url+"\" onclick=\"jspaginator_goto('"+this.random_id+"', "+(this.num_pages-1)+"); return false;\" style='text-decoration: none;'>Last &raquo;</a>";
+      var link = ( ( this.num_pages - 1 ) == this_page ) ? "<b>" + $lang.get('paginate_btn_last') + "</b>" : "<a href=\""+url+"\" onclick=\"jspaginator_goto('"+this.random_id+"', "+(this.num_pages-1)+"); return false;\" style='text-decoration: none;'>" + $lang.get('paginate_btn_last') + " &raquo;</a>";
       blk.assign_vars({
           CLASS: cls,
           LINK: link
@@ -177,7 +177,7 @@
   if ( this_page < ( this.num_pages - 1 ) )
   {
     var url = '#page_' + String(this_page + 2);
-    var link = "<a href=\""+url+"\" onclick=\"jspaginator_goto('"+this.random_id+"', "+(this_page+1)+"); return false;\" style='text-decoration: none;'>Next &raquo;</a>";
+    var link = "<a href=\""+url+"\" onclick=\"jspaginator_goto('"+this.random_id+"', "+(this_page+1)+"); return false;\" style='text-decoration: none;'>" + $lang.get('paginate_btn_next') + " &raquo;</a>";
     cls = ( cls == 'row1' ) ? 'row2' : 'row1';
     blk.assign_vars({
           CLASS: cls,
@@ -284,14 +284,18 @@
   var regex = new RegExp('\"', 'g');
   var submit_target = ( typeof(url_string) == 'object' ) ? ( toJSONString(url_string) ).replace(regex, '\'') : 'unescape(\'' + escape(url_string) + '\')';
   var onclick = 'paginator_submit(this, '+num_pages+', '+perpage+', '+submit_target+'); return false;';
-  div.innerHTML = 'Go to page:<br /><input type="text" size="2" style="padding: 1px; font-size: 8pt;" value="'+(parseInt(this_page)+1)+'" id="'+vtmp+'" />&emsp;<a href="#" onclick="'+onclick+'" style="font-size: 14pt; text-decoration: none;">&raquo;</a>&emsp;<a href="#" onclick="fly_out_top(this.parentNode, false, true); return false;" style="font-size: 14pt; text-decoration: none;">&times;</a>';
+  div.innerHTML = $lang.get('paginate_lbl_goto_page') + '<br /><input type="text" size="2" style="padding: 1px; font-size: 8pt;" value="'+(parseInt(this_page)+1)+'" id="'+vtmp+'" />&emsp;<a href="#" onclick="'+onclick+'" style="font-size: 14pt; text-decoration: none;">&raquo;</a>&emsp;<a href="#" onclick="fly_out_top(this.parentNode, false, true); return false;" style="font-size: 14pt; text-decoration: none;">&times;</a>';
   
   var body = document.getElementsByTagName('body')[0];
   domObjChangeOpac(0, div);
   
   body.appendChild(div);
   
-  document.getElementById(vtmp).onkeypress = function(e){if(e.keyCode==13)this.nextSibling.nextSibling.onclick();};
+  document.getElementById(vtmp).onkeypress = function(e)
+    {
+      if ( e.keyCode == 13 )
+        this.nextSibling.nextSibling.onclick();
+    };
   document.getElementById(vtmp).focus();
   
   // fade the div
@@ -315,7 +319,7 @@
   var offset = ( userinput - 1 ) * perpage;
   if ( userinput > max || isNaN(userinput) || userinput < 1 )
   {
-    new messagebox(MB_OK|MB_ICONSTOP, 'Invalid entry', 'Please enter a page number between 1 and ' + max + '.');
+    new messagebox(MB_OK|MB_ICONSTOP, $lang.get('paginate_err_bad_page_title'), $lang.get('paginate_err_bad_page_body', { max: max }));
     return false;
   }
   if ( typeof(formatstring) == 'object' )
--- a/includes/comment.php	Sun Jan 20 22:34:02 2008 -0500
+++ b/includes/comment.php	Mon Jan 21 10:09:48 2008 -0500
@@ -97,14 +97,14 @@
         {
           $ret['template'] = file_get_contents(ENANO_ROOT . '/themes/' . $template->theme . '/comment.tpl');
         }
-        $q = $db->sql_query('SELECT c.comment_id,c.name,c.subject,c.comment_data,c.time,c.approved,u.user_level,u.user_id,u.signature,u.user_has_avatar,u.avatar_type, b.buddy_id IS NOT NULL AS is_buddy, ( b.is_friend IS NOT NULL AND b.is_friend=1 ) AS is_friend FROM '.table_prefix.'comments AS c
+        $q = $db->sql_query('SELECT c.comment_id,c.name,c.subject,c.comment_data,c.time,c.approved,( c.ip_address IS NOT NULL ) AS have_ip,u.user_level,u.user_id,u.signature,u.user_has_avatar,u.avatar_type, b.buddy_id IS NOT NULL AS is_buddy, ( b.is_friend IS NOT NULL AND b.is_friend=1 ) AS is_friend FROM '.table_prefix.'comments AS c
                                LEFT JOIN '.table_prefix.'users AS u
                                  ON (u.user_id=c.user_id)
                                LEFT JOIN '.table_prefix.'buddies AS b
                                  ON ( ( b.user_id=' . $session->user_id.' AND b.buddy_user_id=c.user_id ) OR b.user_id IS NULL)
                                WHERE page_id=\'' . $this->page_id . '\'
                                  AND namespace=\'' . $this->namespace . '\'
-                               GROUP BY c.comment_id,c.name,c.subject,c.comment_data,c.time,c.approved,u.user_level,u.user_id,u.signature,u.user_has_avatar,u.avatar_type,b.buddy_id,b.is_friend
+                               GROUP BY c.comment_id,c.name,c.subject,c.comment_data,c.time,c.approved,c.ip_address,u.user_level,u.user_id,u.signature,u.user_has_avatar,u.avatar_type,b.buddy_id,b.is_friend
                                ORDER BY c.time ASC;');
         $count_appr = 0;
         $count_total = 0;
@@ -147,6 +147,9 @@
             // Format signature
             $row['signature'] = ( !empty($row['signature']) ) ? RenderMan::render($row['signature']) : '';
             
+            // Do we have the IP?
+            $row['have_ip'] = ( $row['have_ip'] == 1 );
+            
             // Add the comment to the list
             $ret['comments'][] = $row;
             
@@ -285,10 +288,13 @@
           $appr = ( getConfig('approve_comments') == '1' ) ? '0' : '1';
           $time = time();
           $date = enano_date('F d, Y h:i a', $time);
+          $ip = $_SERVER['REMOTE_ADDR'];
+          if ( !is_valid_ip($ip) )
+            die('Hacking attempt');
           
           // Send it to the database
-          $q = $db->sql_query('INSERT INTO '.table_prefix.'comments(page_id,namespace,name,subject,comment_data,approved, time, user_id) VALUES' .
-                              "('$this->page_id', '$this->namespace', '$name', '$subj', '$sql_text', $appr, $time, $session->user_id);");
+          $q = $db->sql_query('INSERT INTO '.table_prefix.'comments(page_id,namespace,name,subject,comment_data,approved, time, user_id, ip_address) VALUES' . "\n  " .
+                             "('$this->page_id', '$this->namespace', '$name', '$subj', '$sql_text', $appr, $time, {$session->user_id}, '$ip');");
           if(!$q)
             $db->die_json();
           
@@ -366,6 +372,45 @@
           );
         
         break;
+      case 'view_ip':
+        if ( !$session->get_permissions('mod_comments') )
+        {
+          return array(
+              'mode' => 'error',
+              'error' => 'Unauthorized'
+            );
+        }
+        // fetch comment info
+        if ( !is_int($data['id']) )
+        {
+          return array(
+              'mode' => 'error',
+              'error' => 'Unauthorized'
+            );
+        }
+        $id =& $data['id'];
+        $q = $db->sql_query('SELECT ip_address, name FROM ' . table_prefix . 'comments WHERE comment_id = ' . $id . ';');
+        if ( !$q || $db->numrows() < 1 )
+        {
+          $db->die_json();
+        }
+        list($ip_addr, $name) = $db->fetchrow_num($q);
+        $db->free_result();
+        $name = $db->escape($name);
+        $username = $db->escape($session->username);
+        // log this action
+        $q = $db->sql_query('INSERT INTO ' . table_prefix . "logs(time_id, log_type, action, page_text, author, edit_summary) VALUES\n  "
+                            . "( " . time() . ", 'security', 'view_comment_ip', '$name', '$username', '{$_SERVER['REMOTE_ADDR']}' );");
+        if ( !$q )
+          $db->die_json();
+        
+        // send packet
+        $ret = array(
+            'mode' => 'redraw',
+            'ip_addr' => $ip_addr,
+            'local_id' => $data['local_id']
+          );
+        break;
       default:
         $ret = Array(
           'mode' => 'error', 
--- a/includes/functions.php	Sun Jan 20 22:34:02 2008 -0500
+++ b/includes/functions.php	Mon Jan 21 10:09:48 2008 -0500
@@ -2079,6 +2079,8 @@
 function paginate($q, $tpl_text, $num_results, $result_url, $start = 0, $perpage = 10, $callers = Array(), $header = '', $footer = '')
 {
   global $db, $session, $paths, $template, $plugins; // Common objects
+  global $lang;
+  
   $parser = $template->makeParserText($tpl_text);
   $num_pages = ceil ( $num_results / $perpage );
   $out = '';
@@ -2093,7 +2095,7 @@
             'display: table; margin: 10px 0 0 auto;';
   $begin = '<div class="tblholder" style="'. $pg_css . '">
     <table border="0" cellspacing="1" cellpadding="4">
-      <tr><th>Page:</th>';
+      <tr><th>' . $lang->get('paginate_lbl_page') . '</th>';
   $block = '<td class="row1" style="text-align: center;">{LINK}</td>';
   $end = '</tr></table></div>';
   $blk = $template->makeParserText($block);
@@ -2140,7 +2142,7 @@
       }
     }
     $url = sprintf($result_url, '0');
-    $link = ( 0 == $start ) ? "<b>First</b>" : "<a href=".'"'."$url".'"'." style='text-decoration: none;'>&laquo; First</a>";
+    $link = ( 0 == $start ) ? "<b>" . $lang->get('pagination_btn_first') . "</b>" : "<a href=".'"'."$url".'"'." style='text-decoration: none;'>&laquo; " . $lang->get('pagination_btn_first') . "</a>";
     $blk->assign_vars(array(
       'CLASS'=>$cls,
       'LINK'=>$link
@@ -2182,7 +2184,7 @@
       $offset = strval($total);
       $url = sprintf($result_url, $offset);
       $j = $i + 1;
-      $link = ( $offset == strval($start) ) ? "<b>Last</b>" : "<a href=".'"'."$url".'"'." style='text-decoration: none;'>Last &raquo;</a>";
+      $link = ( $offset == strval($start) ) ? "<b>" . $lang->get('pagination_btn_last') . "</b>" : "<a href=".'"'."$url".'"'." style='text-decoration: none;'>" . $lang->get('pagination_btn_last') . " &raquo;</a>";
       $blk->assign_vars(array(
         'CLASS'=>$cls,
         'LINK'=>$link
@@ -2271,7 +2273,7 @@
   if ( $start > 0 )
   {
     $url = sprintf($result_url, abs($start - $perpage));
-    $link = "<a href=".'"'."$url".'"'." style='text-decoration: none;'>&laquo; Prev</a>";
+    $link = "<a href=".'"'."$url".'"'." style='text-decoration: none;'>&laquo; " . $lang->get('paginate_btn_prev') . "</a>";
     $cls = ( $cls == 'row1' ) ? 'row2' : 'row1';
     $blk->assign_vars(array(
       'CLASS'=>$cls,
@@ -2320,7 +2322,7 @@
       }
     }
     $url = sprintf($result_url, '0');
-    $link = ( 0 == $start ) ? "<b>First</b>" : "<a href=".'"'."$url".'"'." style='text-decoration: none;'>&laquo; First</a>";
+    $link = ( 0 == $start ) ? "<b>" . $lang->get('pagination_btn_first') . "</b>" : "<a href=".'"'."$url".'"'." style='text-decoration: none;'>&laquo; " . $lang->get('pagination_btn_first') . "</a>";
     $blk->assign_vars(array(
       'CLASS'=>$cls,
       'LINK'=>$link
@@ -2360,7 +2362,7 @@
       $offset = strval($total);
       $url = sprintf($result_url, $offset);
       $j = $i + 1;
-      $link = ( $offset == strval($start) ) ? "<b>Last</b>" : "<a href=".'"'."$url".'"'." style='text-decoration: none;'>Last &raquo;</a>";
+      $link = ( $offset == strval($start) ) ? "<b>" . $lang->get('pagination_btn_last') . "</b>" : "<a href=".'"'."$url".'"'." style='text-decoration: none;'>" . $lang->get('pagination_btn_last') . " &raquo;</a>";
       $blk->assign_vars(array(
         'CLASS'=>$cls,
         'LINK'=>$link
@@ -2374,7 +2376,7 @@
   {
     $link_offset = abs($start + $perpage);
     $url = htmlspecialchars(sprintf($result_url, strval($link_offset)));
-    $link = "<a href=".'"'."$url".'"'." style='text-decoration: none;'>Next &raquo;</a>";
+    $link = "<a href=".'"'."$url".'"'." style='text-decoration: none;'>" . $lang->get('paginate_btn_next') . " &raquo;</a>";
     $cls = ( $cls == 'row1' ) ? 'row2' : 'row1';
     $blk->assign_vars(array(
       'CLASS'=>$cls,
--- a/includes/sessions.php	Sun Jan 20 22:34:02 2008 -0500
+++ b/includes/sessions.php	Mon Jan 21 10:09:48 2008 -0500
@@ -14,7 +14,7 @@
  */
  
 // Prepare a string for insertion into a MySQL database
-function filter($str) { return $db->escape($str); }
+function filter($str) { global $db; return $db->escape($str); }
 
 /**
  * Anything and everything related to security and user management. This includes AES encryption, which is illegal in some countries.
@@ -1692,7 +1692,14 @@
     // Initialize AES
     $aes = AESCrypt::singleton(AES_BITS, AES_BLOCKSIZE);
     
-    if(!preg_match('#^'.$this->valid_username.'$#', $username)) return 'The username you chose contains invalid characters.';
+    // Since we're recording IP addresses, make sure the user's IP is safe.
+    $ip =& $_SERVER['REMOTE_ADDR'];
+    if ( !is_valid_ip($ip) )
+      return 'Invalid IP';
+    
+    if ( !preg_match('#^'.$this->valid_username.'$#', $username) )
+      return 'The username you chose contains invalid characters.';
+    
     $username = str_replace('_', ' ', $username);
     $user_orig = $username;
     $username = $this->prepare_text($username);
@@ -1766,13 +1773,13 @@
     $actkey = sha1 ( microtime() . mt_rand() );
 
     // We good, create the user
-    $this->sql('INSERT INTO '.table_prefix.'users ( username, password, email, real_name, theme, style, reg_time, account_active, activation_key, user_level, user_coppa ) VALUES ( \''.$username.'\', \''.$password.'\', \''.$email.'\', \''.$real_name.'\', \''.$template->default_theme.'\', \''.$template->default_style.'\', '.time().', '.$active.', \''.$actkey.'\', '.USER_LEVEL_CHPREF.', ' . $coppa_col . ' );');
+    $this->sql('INSERT INTO '.table_prefix.'users ( username, password, email, real_name, theme, style, reg_time, account_active, activation_key, user_level, user_coppa, user_registration_ip ) VALUES ( \''.$username.'\', \''.$password.'\', \''.$email.'\', \''.$real_name.'\', \''.$template->default_theme.'\', \''.$template->default_style.'\', '.time().', '.$active.', \''.$actkey.'\', '.USER_LEVEL_CHPREF.', ' . $coppa_col . ', \'' . $ip . '\' );');
     
     // Get user ID and create users_extra entry
     $q = $this->sql('SELECT user_id FROM '.table_prefix."users WHERE username='$username';");
     if ( $db->numrows() > 0 )
     {
-      $row = $db->fetchrow();
+      list($user_id) = $db->fetchrow_num();
       $db->free_result();
       
       $user_id =& $row['user_id'];
--- a/install/includes/payload.php	Sun Jan 20 22:34:02 2008 -0500
+++ b/install/includes/payload.php	Mon Jan 21 10:09:48 2008 -0500
@@ -141,7 +141,8 @@
       'REAL_NAME'            => '', // This has always been stubbed.
       'ADMIN_EMBED_PHP'      => strval(AUTH_DISALLOW),
       'UNIX_TIME'            => strval(time()),
-      'MAIN_PAGE_CONTENT'    => $wkt
+      'MAIN_PAGE_CONTENT'    => $wkt,
+      'IP_ADDRESS'           => $db->escape($_SERVER['REMOTE_ADDR'])
     );
   
   $sql_parser->assign_vars($vars);
--- a/install/schemas/mysql_stage1.sql	Sun Jan 20 22:34:02 2008 -0500
+++ b/install/schemas/mysql_stage1.sql	Mon Jan 21 10:09:48 2008 -0500
@@ -1,5 +1,5 @@
 -- 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/install/schemas/mysql_stage2.sql	Sun Jan 20 22:34:02 2008 -0500
+++ b/install/schemas/mysql_stage2.sql	Mon Jan 21 10:09:48 2008 -0500
@@ -1,5 +1,5 @@
 -- 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
@@ -26,6 +26,7 @@
   approved tinyint(1) default 1,
   user_id mediumint(8) NOT NULL DEFAULT -1,
   time int(12) NOT NULL DEFAULT 0,
+  ip_address varchar(39),
   PRIMARY KEY ( comment_id )
 ) CHARACTER SET `utf8` COLLATE `utf8_bin`;
 
@@ -103,6 +104,7 @@
   user_lang smallint(5) NOT NULL,
   user_has_avatar tinyint(1) NOT NULL,
   avatar_type ENUM('jpg', 'png', 'gif') NOT NULL,
+  user_registration_ip varchar(39),
   PRIMARY KEY  (user_id)
 ) CHARACTER SET `utf8` COLLATE `utf8_bin`;
 
@@ -318,9 +320,9 @@
   ('oxygen', 'Oxygen', 1, 'bleu.css', 1),
   ('stpatty', 'St. Patty', 2, 'shamrock.css', 1);
 
-INSERT INTO {{TABLE_PREFIX}}users(user_id, username, password, email, real_name, user_level, theme, style, signature, reg_time, account_active) VALUES
+INSERT INTO {{TABLE_PREFIX}}users(user_id, username, password, email, real_name, user_level, theme, style, signature, reg_time, account_active, user_registration_ip) VALUES
   (1, 'Anonymous', 'invalid-pass-hash', 'anonspam@enanocms.org', 'None', 1, 'oxygen', 'bleu', '', 0, 0),
-  (2, '{{ADMIN_USER}}', '{{ADMIN_PASS}}', '{{ADMIN_EMAIL}}', '{{REAL_NAME}}', 9, 'oxygen', 'bleu', '', UNIX_TIMESTAMP(), 1);
+  (2, '{{ADMIN_USER}}', '{{ADMIN_PASS}}', '{{ADMIN_EMAIL}}', '{{REAL_NAME}}', 9, 'oxygen', 'bleu', '', UNIX_TIMESTAMP(), 1, '{{IP_ADDRESS}}');
   
 INSERT INTO {{TABLE_PREFIX}}users_extra(user_id) VALUES
   (2);
--- a/install/schemas/postgresql_stage1.sql	Sun Jan 20 22:34:02 2008 -0500
+++ b/install/schemas/postgresql_stage1.sql	Mon Jan 21 10:09:48 2008 -0500
@@ -1,5 +1,5 @@
 -- 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/install/schemas/postgresql_stage2.sql	Sun Jan 20 22:34:02 2008 -0500
+++ b/install/schemas/postgresql_stage2.sql	Mon Jan 21 10:09:48 2008 -0500
@@ -1,5 +1,5 @@
 -- 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
@@ -8,7 +8,7 @@
 -- This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied
 -- warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for details.
 
--- mysql_stage2.sql - MySQL installation schema, main payload
+-- postgresql_stage2.sql - PostgreSQL installation schema, main payload
 
 CREATE TABLE {{TABLE_PREFIX}}categories(
   page_id varchar(64),
@@ -26,6 +26,7 @@
   approved smallint default 1,
   user_id int NOT NULL DEFAULT -1,
   time int NOT NULL DEFAULT 0,
+  ip_address varchar(39),
   PRIMARY KEY ( comment_id )
 );
 
@@ -99,6 +100,11 @@
   temp_password text,
   temp_password_time int NOT NULL DEFAULT 0,
   user_coppa smallint NOT NULL DEFAULT 0,
+  user_lang smallint NOT NULL,
+  user_has_avatar smallint NOT NULL,
+  avatar_type varchar(3) NOT NULL,
+  user_registration_ip varchar(39),
+  CHECK (user_has_avatar IN ('jpg', 'png', 'gif')),
   PRIMARY KEY  (user_id)
 );
 
@@ -300,7 +306,10 @@
   ('powered_btn', '1');
 
 INSERT INTO {{TABLE_PREFIX}}page_text(page_id, namespace, page_text, char_tag) VALUES
-  ('Main_Page', 'Article', '=== Enano has been successfully installed and is working. ===\n\nIf you can see this message, it means that you\'ve finished the Enano setup process and are ready to start building your website. Congratulations!\n\nTo edit this front page, click the Log In button to the left, enter the credentials you provided during the installation, and click the Edit This Page button that appears on the blue toolbar just above this text. You can also [http://docs.enanocms.org/Help:2.4 learn more] about editing pages.\n\nTo create more pages, use the Create a Page button to the left. If you enabled wiki mode, you don\'t have to log in first, however your IP address will be shown in the page history.\n\nVisit the [http://docs.enanocms.org/Help:Contents Enano documentation project website] to learn more about administering your site effectively and keeping things secure.\n\n\'\'\'NOTE:\'\'\' You\'ve just installed an unstable version of Enano. This release is completely unsupported and may contain security issues or serious usability bugs. You should not use this release on a production website. The Enano team will not provide any type of support at all for this experimental release.', '');
+  ('Main_Page', 'Article', '{{MAIN_PAGE_CONTENT}}', '');
+  
+INSERT INTO {{TABLE_PREFIX}}logs(time_id, date_string, log_type, action, page_id, namespace, author, page_text) VALUES
+  ({{UNIX_TIME}}, 'DEPRECATED', 'page', 'edit', 'Main_Page', 'Article', '{{ADMIN_USER}}', '{{MAIN_PAGE_CONTENT}}');
 
 INSERT INTO {{TABLE_PREFIX}}pages(page_order, name, urlname, namespace, special, visible, comments_on, protected, delvotes, delvote_ips) VALUES
   (NULL, 'Main Page', 'Main_Page', 'Article', 0, 1, 1, 1, 0, '');
@@ -309,9 +318,9 @@
   ('oxygen', 'Oxygen', 1, 'bleu.css', 1),
   ('stpatty', 'St. Patty', 2, 'shamrock.css', 1);
 
-INSERT INTO {{TABLE_PREFIX}}users(user_id, username, password, email, real_name, user_level, theme, style, signature, reg_time, account_active) VALUES
+INSERT INTO {{TABLE_PREFIX}}users(user_id, username, password, email, real_name, user_level, theme, style, signature, reg_time, account_active, user_registration_ip) VALUES
   (1, 'Anonymous', 'invalid-pass-hash', 'anonspam@enanocms.org', 'None', 1, 'oxygen', 'bleu', '', 0, 0),
-  (2, '{{ADMIN_USER}}', '{{ADMIN_PASS}}', '{{ADMIN_EMAIL}}', '{{REAL_NAME}}', 9, 'oxygen', 'bleu', '', {{UNIX_TIME}}, 1);
+  (2, '{{ADMIN_USER}}', '{{ADMIN_PASS}}', '{{ADMIN_EMAIL}}', '{{REAL_NAME}}', 9, 'oxygen', 'bleu', '', {{UNIX_TIME}}, 1, '{{IP_ADDRESS}}');
   
 INSERT INTO {{TABLE_PREFIX}}users_extra(user_id) VALUES
   (2);
--- a/language/english/admin.json	Sun Jan 20 22:34:02 2008 -0500
+++ b/language/english/admin.json	Mon Jan 21 10:09:48 2008 -0500
@@ -10,11 +10,6 @@
  * warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for details.
  */
 
-// This is the main language file for Enano. Feel free to use it as a base for your own translations.
-// All text in this file before the first left curly brace and all text after the last curly brace will
-// be trimmed. So you can use a limited amount of Javascript in this so that the language can be imported
-// via Javascript as well.
-
 var enano_lang = {
   categories: [
     'adm', 'acl', 'adminusers',
@@ -627,6 +622,7 @@
       field_active: 'Account is active and enabled',
       field_userlevel: 'User\'s site access level',
       field_userlevel_hint: 'If this is changed, the relevant group memberships will be updated accordingly.',
+      field_reg_ip: 'Registered from IP:',
       
       field_deleteaccount_title: 'Delete user account',
       field_deleteaccount: 'Permanently delete this user account when I click Save',
@@ -734,6 +730,7 @@
       entry_u_from_mod: 'User %username% demoted from Moderators group',
       entry_u_to_admin: 'User %username% added to Administrators group',
       entry_u_to_mod: 'User %username% added to Moderators group',
+      entry_view_comment_ip: 'IP address viewed on comment by %username%',
       tip_reverse_dns: 'Click for reverse DNS info',
     },
     acpbc: {
--- a/language/english/core.json	Sun Jan 20 22:34:02 2008 -0500
+++ b/language/english/core.json	Mon Jan 21 10:09:48 2008 -0500
@@ -17,7 +17,7 @@
 
 var enano_lang = {
   categories: [
-    'page', 'comment', 'onpage', 'etc', 'editor', 'history', 'catedit', 'tags', 'delvote', 'ajax', 'sidebar', 'perm', 'plugin',
+    'page', 'comment', 'onpage', 'etc', 'editor', 'history', 'catedit', 'tags', 'delvote', 'ajax', 'sidebar', 'perm', 'plugin', 'paginate',
   ],
   strings: {
     meta: {
@@ -34,6 +34,7 @@
       sidebar: 'Default sidebar blocks and buttons',
       perm: 'Page actions (for ACLs)',
       plugin: 'Plugin names and descriptions',
+      paginate: 'Pagination widget',
       plural: 's',
       enano_about_th: 'About the Enano Content Management System',
       enano_about_poweredby: '<p>This website is powered by <a href="http://enanocms.org/">Enano</a>, the lightweight and open source CMS that everyone can use. Enano is copyright &copy; 2006-2007 Dan Fuhry. For legal information, along with a list of libraries that Enano uses, please see <a href="http://enanocms.org/Legal_information">Legal Information</a>.</p><p>The developers and maintainers of Enano strongly believe that software should not only be free to use, but free to be modified, distributed, and used to create derivative works. For more information about Free Software, check out the <a href="http://en.wikipedia.org/wiki/Free_Software" onclick="window.open(this.href); return false;">Wikipedia page</a> or the <a href="http://www.fsf.org/" onclick="window.open(this.href); return false;">Free Software Foundation\'s</a> homepage.</p>',
@@ -120,6 +121,8 @@
       btn_mod_approve: 'Approve',
       btn_mod_unapprove: 'Unapprove',
       btn_mod_delete: 'Delete',
+      btn_mod_ip_logged: 'View IP',
+      btn_mod_ip_missing: 'IP not logged',
       btn_save: 'save',
       
       msg_comment_posted: 'Your comment has been posted. If it does not appear right away, it is probably awaiting approval.',
@@ -134,6 +137,8 @@
       
       msg_note_unapp: '(Unapproved)',
       
+      msg_ip_address: 'IP address:',
+      
       msg_delete_confirm: 'Do you really want to delete this comment?',
       
       postform_title: 'Got something to say?',
@@ -428,6 +433,16 @@
       specialuserprefs_title: 'User control panel',
       specialuserprefs_desc: 'Provides the page Special:Preferences.',
     },
+    paginate: {
+      lbl_page: 'Page:',
+      btn_first: 'First',
+      btn_last: 'Last',
+      btn_prev: 'Prev',
+      btn_next: 'Next',
+      lbl_goto_page: 'Go to page:',
+      err_bad_page_title: 'Invalid entry',
+      err_bad_page_body: 'Please enter a page number between 1 and %max%.',
+    },
     etc: {
       redirect_title: 'Redirecting...',
       redirect_body: 'Please wait while you are redirected.',
--- a/language/english/tools.json	Sun Jan 20 22:34:02 2008 -0500
+++ b/language/english/tools.json	Mon Jan 21 10:09:48 2008 -0500
@@ -10,11 +10,6 @@
  * warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for details.
  */
 
-// This is the main language file for Enano. Feel free to use it as a base for your own translations.
-// All text in this file before the first left curly brace and all text after the last curly brace will
-// be trimmed. So you can use a limited amount of Javascript in this so that the language can be imported
-// via Javascript as well.
-
 var enano_lang = {
   categories: [
     'search', 'specialpage', 'pagetools'
--- a/language/english/user.json	Sun Jan 20 22:34:02 2008 -0500
+++ b/language/english/user.json	Mon Jan 21 10:09:48 2008 -0500
@@ -10,11 +10,6 @@
  * warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for details.
  */
 
-// This is the main language file for Enano. Feel free to use it as a base for your own translations.
-// All text in this file before the first left curly brace and all text after the last curly brace will
-// be trimmed. So you can use a limited amount of Javascript in this so that the language can be imported
-// via Javascript as well.
-
 var enano_lang = {
   categories: [
     'user', 'usercp', 'groupcp', 'privmsgs', 'userfuncs',
--- a/plugins/SpecialUserFuncs.php	Sun Jan 20 22:34:02 2008 -0500
+++ b/plugins/SpecialUserFuncs.php	Mon Jan 21 10:09:48 2008 -0500
@@ -970,6 +970,7 @@
           }
           function regenCaptcha()
           {
+            var frm = document.forms.regform;
             document.getElementById('captchaimg').src = '<?php echo makeUrlNS("Special", "Captcha/"); ?>'+frm.captchahash.value+'/'+Math.floor(Math.random() * 100000);
             return false;
           }
--- a/plugins/admin/SecurityLog.php	Sun Jan 20 22:34:02 2008 -0500
+++ b/plugins/admin/SecurityLog.php	Mon Jan 21 10:09:48 2008 -0500
@@ -162,13 +162,14 @@
     case "filehist_enable" : $return .= $lang->get('acpsl_entry_filehist_enable') ; break;
     case "filehist_disable": $return .= $lang->get('acpsl_entry_filehist_disable'); break;
     case "magick_path"     : $return .= $lang->get('acpsl_entry_magick_path')     ; break;
-    case "plugin_disable"  : $return .= $lang->get('acpsl_entry_plugin_disable'   , array('plugin' => $r['page_text']))  ; break;
-    case "plugin_enable"   : $return .= $lang->get('acpsl_entry_plugin_enable'    , array('plugin' => $r['page_text']))   ; break;
+    case "plugin_disable"  : $return .= $lang->get('acpsl_entry_plugin_disable'   , array('plugin' => $r['page_text'])); break;
+    case "plugin_enable"   : $return .= $lang->get('acpsl_entry_plugin_enable'    , array('plugin' => $r['page_text'])); break;
     case "seclog_unauth"   : $return .= $lang->get('acpsl_entry_seclog_unauth')   ; break;
-    case "u_from_admin"    : $return .= $lang->get('acpsl_entry_u_from_admin'     , array('username' => $r['page_text']))    ; break;
-    case "u_from_mod"      : $return .= $lang->get('acpsl_entry_u_from_mod'       , array('username' => $r['page_text']))      ; break;
-    case "u_to_admin"      : $return .= $lang->get('acpsl_entry_u_to_admin'       , array('username' => $r['page_text']))      ; break;
-    case "u_to_mod"        : $return .= $lang->get('acpsl_entry_u_to_mod'         , array('username' => $r['page_text']))        ; break;
+    case "u_from_admin"    : $return .= $lang->get('acpsl_entry_u_from_admin'     , array('username' => $r['page_text'])); break;
+    case "u_from_mod"      : $return .= $lang->get('acpsl_entry_u_from_mod'       , array('username' => $r['page_text'])); break;
+    case "u_to_admin"      : $return .= $lang->get('acpsl_entry_u_to_admin'       , array('username' => $r['page_text'])); break;
+    case "u_to_mod"        : $return .= $lang->get('acpsl_entry_u_to_mod'         , array('username' => $r['page_text'])); break;
+    case "view_comment_ip" : $return .= $lang->get('acpsl_entry_view_comment_ip'  , array('username' => htmlspecialchars($r['page_text']))); break;
   }
   $return .= '</td><td class="'.$cls.'">'.enano_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="' . $lang->get('acpsl_tip_reverse_dns') . '">'.$r['edit_summary'].'</td></tr>';
   return $return;
--- a/plugins/admin/UserManager.php	Sun Jan 20 22:34:02 2008 -0500
+++ b/plugins/admin/UserManager.php	Mon Jan 21 10:09:48 2008 -0500
@@ -423,6 +423,10 @@
         );
       $form->email_public = ( isset($_POST['email_public']) );
       $form->account_active = ( isset($_POST['account_active']) );
+      // This is SAFE. The smartform calls is_valid_ip() on this value, thus preventing XSS
+      // attempts from making it into the form HTML. Badly coded templates may still be
+      // affected, but if have_reg_ip is checked for, then you're fine.
+      $form->reg_ip_addr = $_POST['user_registration_ip'];
       echo $form->render();
       return false;
     }
@@ -446,7 +450,7 @@
       echo 'No username provided';
       return false;
     }
-    $q = $db->sql_query('SELECT u.user_id AS authoritative_uid, u.username, u.email, u.real_name, u.signature, u.account_active, u.user_level, u.user_has_avatar, u.avatar_type, x.* FROM '.table_prefix.'users AS u
+    $q = $db->sql_query('SELECT u.user_id AS authoritative_uid, u.username, u.email, u.real_name, u.signature, u.account_active, u.user_level, u.user_has_avatar, u.avatar_type, u.user_registration_ip, x.* FROM '.table_prefix.'users AS u
                            LEFT JOIN '.table_prefix.'users_extra AS x
                              ON ( u.user_id = x.user_id OR x.user_id IS NULL )
                            WHERE ( ' . ENANO_SQLFUNC_LOWERCASE . '(u.username) = \'' . $db->escape(strtolower($username)) . '\' OR u.username = \'' . $db->escape($username) . '\' ) AND u.user_id != 1;');
@@ -485,6 +489,7 @@
           'hobbies'  => $row['user_hobbies'],
         );
       $form->email_public = ( $row['email_public'] == 1 );
+      $form->reg_ip_addr = ( $row['user_registration_ip'] ) ? $row['user_registration_ip'] : '';
       $html = $form->render();
       if ( !$html )
       {
@@ -761,6 +766,13 @@
   var $avi_type = 'png';
   
   /**
+   * The IP address of the user during registration
+   * @var string
+   */
+  
+  var $reg_ip_addr = '';
+  
+  /**
    * Constructor.
    */
   
@@ -1086,6 +1098,20 @@
                   </td>
                 </tr>
                 
+                <!-- BEGIN have_reg_ip -->
+                <tr>
+                  <td class="row2">
+                    {lang:acpum_field_reg_ip}
+                  </td>
+                  <td class="row1">
+                    {REG_IP_ADDR}
+                    <input type="hidden" name="user_registration_ip" value="{REG_IP_ADDR}" />
+                  </td>
+                </tr>
+                <!-- BEGINELSE have_reg_ip -->
+                <input type="hidden" name="user_registration_ip" value="" />
+                <!-- END have_reg_ip -->
+                
                 <tr>
                   <td class="row2">
                     {lang:acpum_field_deleteaccount_title}
@@ -1180,7 +1206,8 @@
         'LOCATION' => $location,
         'JOB' => $job,
         'HOBBIES' => $hobbies,
-        'FORM_ACTION' => $form_action
+        'FORM_ACTION' => $form_action,
+        'REG_IP_ADDR' => $this->reg_ip_addr
       ));
     
     if ( $this->has_avatar )
@@ -1199,7 +1226,8 @@
         'account_active' => ( $this->account_active === true ),
         'email_public' => ( $this->email_public === true ),
         'same_user' => ( $this->user_id == $session->user_id ),
-        'user_has_avatar' => ( $this->has_avatar )
+        'user_has_avatar' => ( $this->has_avatar ),
+        'have_reg_ip' => ( intval(@strlen($this->reg_ip_addr)) > 0 && is_valid_ip($this->reg_ip_addr) )
       ));
     
     $parsed = $parser->run();
--- a/themes/oxygen/comment.tpl	Sun Jan 20 22:34:02 2008 -0500
+++ b/themes/oxygen/comment.tpl	Mon Jan 21 10:09:48 2008 -0500
@@ -49,7 +49,7 @@
     <!-- BEGIN auth_mod -->
     <tr>
       <td class="row1">
-        <b>{lang:comment_lbl_mod_options}</b> {MOD_APPROVE_LINK} {MOD_DELETE_LINK}
+        <b>{lang:comment_lbl_mod_options}</b> {MOD_APPROVE_LINK} {MOD_DELETE_LINK} | {MOD_IP_LINK}
       </td>
     </tr>
     <!-- END auth_mod -->
--- a/themes/oxygen/css/bleu.css	Sun Jan 20 22:34:02 2008 -0500
+++ b/themes/oxygen/css/bleu.css	Mon Jan 21 10:09:48 2008 -0500
@@ -173,6 +173,10 @@
 div.menu a.current, div.menu a.current:hover, div.menu a.selected, div.menu a.selected:hover, div.menu_nojs a.current, div.menu_nojs a.current:hover, div.menu_nojs a.selected, div.menu_nojs a.selected:hover {
   color: #000040;
   background-color: #FFFFFF;
+  /*
+  Some people like this. Most people don't.
+  -moz-border-radius: 5px 5px 0 0;
+  */
 }
 div.menu ul, div.menu_nojs ul {
   display: none;