Merging Nighthawk (anti-spam work) and Scribus (AJAX work + debugging + CLI installer) branches
authorDan
Sun, 25 Jan 2009 21:21:07 -0500
changeset 827 2c20563245b2
parent 826 dcf5381ce8ba (diff)
parent 824 28d9fbcd4f0d (current diff)
child 828 c5bca53b857a
Merging Nighthawk (anti-spam work) and Scribus (AJAX work + debugging + CLI installer) branches
includes/clientside/static/comments.js
includes/pageprocess.php
--- a/includes/clientside/static/comments.js	Sun Jan 25 20:35:32 2009 -0500
+++ b/includes/clientside/static/comments.js	Sun Jan 25 21:21:07 2009 -0500
@@ -54,7 +54,8 @@
           materializeComment(response);
           break;
         case 'error':
-          new MessageBox(MB_OK|MB_ICONSTOP, ( response.title ? response.title : 'Error fetching comment data' ), response.error);
+          load_component(['messagebox', 'fadefilter', 'flyin']);
+          new MessageBox(MB_OK|MB_ICONSTOP, ( response.title ? response.title : $lang.get('comment_ajax_err_generic_title') ), response.error);
           break;
         default:
           alert(ajax.responseText);
@@ -180,8 +181,10 @@
   tplvars.DATA = this_comment.comment_data;
   tplvars.SIGNATURE = this_comment.signature;
   
-  if ( this_comment.approved != '1' )
+  if ( this_comment.approved == '0' )
     tplvars.SUBJECT += ' <span style="color: #D84308">' + $lang.get('comment_msg_note_unapp') + '</span>';
+  else if ( this_comment.approved == '2' )
+    tplvars.SUBJECT += ' <span style="color: #D84308">' + $lang.get('comment_msg_note_spam') + '</span>';
   
   // Name
   tplvars.NAME = this_comment.name;
--- a/includes/comment.php	Sun Jan 25 20:35:32 2009 -0500
+++ b/includes/comment.php	Sun Jan 25 21:21:07 2009 -0500
@@ -123,7 +123,7 @@
             $count_total++;
             ( $row['approved'] == 1 ) ? $count_appr++ : $count_unappr++;
             
-            if ( !$this->perms->get_permissions('mod_comments') && $row['approved'] == 0 )
+            if ( !$this->perms->get_permissions('mod_comments') && $row['approved'] != COMMENT_APPROVED )
               continue;
             
             // Localize the rank
@@ -142,7 +142,7 @@
                 <div id="posthide_'.$seed.'" style="display: none;">
                   ' . $row['comment_data'] . '
                 </div>
-                <p><span style="opacity: 0.4; filter: alpha(opacity=40);">Post from foe hidden.</span> <span style="text-align: right;"><a href="#showpost" onclick="document.getElementById(\'posthide_'.$seed.'\').style.display=\'block\'; this.parentNode.parentNode.parentNode.removeChild(this.parentNode.parentNode); return false;">Display post</a></span></p>
+                <p><span style="opacity: 0.4; filter: alpha(opacity=40);">' . $lang->get('comment_msg_foe_comment_hidden') . '</span> <span style="text-align: right;"><a href="#showpost" onclick="document.getElementById(\'posthide_'.$seed.'\').style.display=\'block\'; this.parentNode.parentNode.parentNode.removeChild(this.parentNode.parentNode); return false;">' . $lang->get('comment_btn_display_foe_comment') . '</a></span></p>
               ';
               $row['comment_data'] = $wrapper;
             }
@@ -193,7 +193,7 @@
         break;
       case 'edit':
         $cid = (string)$data['id'];
-        if ( !preg_match('#^([0-9]+)$#i', $cid) || intval($cid) < 1 )
+        if ( !ctype_digit($cid) || intval($cid) < 1 )
         {
           echo '{"mode":"error","error":"HACKING ATTEMPT"}';
           return false;
@@ -228,7 +228,7 @@
         break;
       case 'delete':
         $cid = (string)$data['id'];
-        if ( !preg_match('#^([0-9]+)$#i', $cid) || intval($cid) < 1 )
+        if ( !ctype_digit($cid) || intval($cid) < 1 )
         {
           echo '{"mode":"error","error":"HACKING ATTEMPT"}';
           return false;
@@ -266,17 +266,28 @@
         
         // Guest authorization
         if ( getConfig('comments_need_login') == '2' && !$session->user_logged_in )
-          $errors[] = 'You need to log in before posting comments.';
+          $errors[] = $lang->get('comment_err_need_login');
         
         // CAPTCHA code
         if ( getConfig('comments_need_login') == '1' && !$session->user_logged_in )
         {
           $real_code = $session->get_captcha($data['captcha_id']);
-          if ( strtolower($real_code) != strtolower($data['captcha_code']) )
-            $errors[] = 'The confirmation code you entered was incorrect.';
+          if ( strtolower($real_code) !== strtolower($data['captcha_code']) )
+            $errors[] = $lang->get('comment_err_captcha_wrong');
           $session->kill_captcha();
         }
         
+        // Spam check
+        $spam_policy = getConfig('comment_spam_policy', 'moderate');
+        $sc_name = ( $session->user_logged_in ) ? $session->username : $data['name'];
+        $sc_mail = ( $session->user_logged_in ) ? $session->email : false;
+        $sc_url  = ( $session->user_logged_in ) ? $session->user_extra['user_homepage'] : false;
+        $spamcheck = $spam_policy === 'accept' ? true : spamalyze($data['text'], $sc_name, $sc_mail, $sc_url);
+        if ( !$spamcheck && $spam_policy === 'reject' )
+        {
+          $errors[] = $lang->get('comment_err_spamcheck_failed_rejected');
+        }
+        
         if ( count($errors) > 0 )
         {
           $ret = Array(
@@ -295,7 +306,9 @@
           $src = $text;
           $sql_text = $db->escape($text);
           $text = RenderMan::render($text);
-          $appr = ( getConfig('approve_comments') == '1' ) ? '0' : '1';
+          $appr = ( getConfig('approve_comments') == '1' ) ? COMMENT_UNAPPROVED : COMMENT_APPROVED;
+          if ( $appr === COMMENT_APPROVED && $spam_policy === 'moderate' && !$spamcheck )
+            $appr = COMMENT_SPAM;
           $time = time();
           $date = enano_date('F d, Y h:i a', $time);
           $ip = $_SERVER['REMOTE_ADDR'];
@@ -358,7 +371,7 @@
         }
         
         $cid = (string)$data['id'];
-        if ( !preg_match('#^([0-9]+)$#i', $cid) || intval($cid) < 1 )
+        if ( !ctype_digit($cid) || intval($cid) < 1 )
         {
           echo '{"mode":"error","error":"HACKING ATTEMPT"}';
           return false;
--- a/includes/constants.php	Sun Jan 25 20:35:32 2009 -0500
+++ b/includes/constants.php	Sun Jan 25 21:21:07 2009 -0500
@@ -67,6 +67,11 @@
 define('PAGE_GRP_NORMAL', 3);
 define('PAGE_GRP_REGEX', 4);
 
+// Comment types
+define('COMMENT_APPROVED', 1);
+define('COMMENT_UNAPPROVED', 0);
+define('COMMENT_SPAM', 2);
+
 // Session key types
 // Short keys last for getConfig('session_short_time', '720'); in minutes and auto-renew.
 // Long keys last for getConfig('session_remember_time', '30'); in days and do NOT auto-renew.
--- a/includes/functions.php	Sun Jan 25 20:35:32 2009 -0500
+++ b/includes/functions.php	Sun Jan 25 21:21:07 2009 -0500
@@ -1971,6 +1971,11 @@
   // <
   // The rule is so specific because everything else will have been filtered by now
   $html = preg_replace('/<(script|iframe)(.+?)src=([^>]*)</i', '&lt;\\1\\2src=\\3&lt;', $html);
+  
+  // Vulnerability reported by fuzion from nukeit.org:
+  // XSS in closing HTML tag style attribute
+  // Fix: escape all closing tags with non-whitelisted characters
+  $html = preg_replace('!</((?:.*)([^a-z0-9-_:]+)(?:.*))>!', '&lt;/\\1&gt;', $html);
 
   // Restore stripped comments
   $i = 0;
@@ -2159,6 +2164,46 @@
 }
 
 /**
+ * Portal function allowing spam-filtering plugins.
+ * Hooking guide:
+ *   - Attach to spam_check
+ *   - Return either true or false - true if the message is spam-free, false if it fails your test
+ * @example
+ <code>
+ $plugins->attachHook('spam_check', 'return my_spam_check($string);');
+ function my_spam_check($string)
+ {
+   if ( stristr($string, 'viagra') )
+     return false;
+   
+   return true;
+ }
+ </code>
+ * @param string String to check for spam
+ * @param string Author name
+ * @param string Author e-mail
+ * @param string Author website
+ * @param string Author IP
+ * @return bool
+ */
+
+function spamalyze($string, $name = false, $email = false, $url = false, $ip = false)
+{
+  global $db, $session, $paths, $template, $plugins; // Common objects
+  if ( !$ip )
+    $ip =& $_SERVER['REMOTE_ADDR'];
+  
+  $code = $plugins->setHook('spam_check');
+  foreach ( $code as $cmd )
+  {
+    $result = eval($cmd);
+    if ( !$result )
+      return false;
+  }
+  return true;
+}
+
+/**
  * Paginates (breaks into multiple pages) a MySQL result resource, which is treated as unbuffered.
  * @param resource The MySQL result resource. This should preferably be an unbuffered query.
  * @param string A template, with variables being named after the column name
--- a/includes/pageprocess.php	Sun Jan 25 20:35:32 2009 -0500
+++ b/includes/pageprocess.php	Sun Jan 25 21:21:07 2009 -0500
@@ -198,6 +198,11 @@
         return false;
       }
     }
+    if ( $this->revision_id > 0 && !$this->perms->get_permissions('history_view') )
+    {
+      $this->err_access_denied();
+      return false;
+    }
     
     // Is there a custom function registered for handling this namespace?
     // DEPRECATED (even though it only saw its way into one alpha release.)
@@ -443,6 +448,13 @@
       }
     }
     
+    // Spam check
+    if ( !spamalyze($text) )
+    {
+      $this->raise_error($lang->get('editor_err_spamcheck_failed'));
+      return false;
+    }
+    
     //
     // Protection validated; update page content
     //
--- a/includes/pageutils.php	Sun Jan 25 20:35:32 2009 -0500
+++ b/includes/pageutils.php	Sun Jan 25 21:21:07 2009 -0500
@@ -694,7 +694,7 @@
       $i++;
       $strings = Array();
       $bool = Array();
-      if ( $session->get_permissions('mod_comments') || $row['approved'] )
+      if ( $session->get_permissions('mod_comments') || $row['approved'] == COMMENT_APPROVED )
       {
         $list .= $i . ' : { \'comment\' : unescape(\''.rawurlencode($row['comment_data']).'\'), \'name\' : unescape(\''.rawurlencode($row['name']).'\'), \'subject\' : unescape(\''.rawurlencode($row['subject']).'\'), }, ';
         
--- a/includes/plugins.php	Sun Jan 25 20:35:32 2009 -0500
+++ b/includes/plugins.php	Sun Jan 25 21:21:07 2009 -0500
@@ -121,8 +121,9 @@
    * @param array Deprecated.
    */
   
-  function setHook($name, $opts = Array()) {
-    if(isset($this->hook_list[$name]) && is_array($this->hook_list[$name]))
+  function setHook($name, $opts = Array())
+  {
+    if ( !empty($this->hook_list[$name]) && is_array($this->hook_list[$name]) )
     {
       return array(implode("\n", $this->hook_list[$name]));
     }
@@ -149,8 +150,9 @@
    </code>
    */
   
-  function attachHook($name, $code) {
-    if(!isset($this->hook_list[$name]))
+  function attachHook($name, $code)
+  {
+    if ( !isset($this->hook_list[$name]) )
     {
       $this->hook_list[$name] = Array();
     }
--- a/includes/sessions.php	Sun Jan 25 20:35:32 2009 -0500
+++ b/includes/sessions.php	Sun Jan 25 21:21:07 2009 -0500
@@ -3716,7 +3716,7 @@
       
       // Fetch private key
       $dh_public = $_POST['dh_public_key'];
-      if ( !preg_match('/^[0-9]+$/', $dh_public) )
+      if ( !ctype_digit($dh_public) )
       {
         throw new Exception('ERR_DH_KEY_NOT_INTEGER');
       }
@@ -3888,7 +3888,7 @@
         $dh_hash = $req['dh_secret_hash'];
         
         // Check the key
-        if ( !preg_match('/^[0-9]+$/', $dh_public) || !preg_match('/^[0-9]+$/', $req['dh_client_key']) )
+        if ( !ctype_digit($dh_public) || !ctype_digit($req['dh_client_key']) )
         {
           return array(
             'mode' => 'error',
--- a/includes/template.php	Sun Jan 25 20:35:32 2009 -0500
+++ b/includes/template.php	Sun Jan 25 21:21:07 2009 -0500
@@ -611,36 +611,30 @@
       {
         $db->_die();
       }
-      $nc = $db->numrows();
-      $nu = 0;
-      $na = 0;
+      $num_comments = $db->numrows();
+      $approval_counts = array(COMMENT_UNAPPROVED => 0, COMMENT_APPROVED => 0, COMMENT_SPAM => 0);
       
       while ( $r = $db->fetchrow() )
       {  
-        if ( !$r['approved'] )
-        {
-          $nu++;
-        }
-        else
-        {
-          $na++;
-        }
+        $approval_counts[$r['approved']]++;
       }
       
       $db->free_result();
-      $n = ( $session->check_acl_scope('mod_comments', $local_namespace) && $perms->get_permissions('mod_comments') ) ? (string)$nc : (string)$na;
-      if ( $session->check_acl_scope('mod_comments', $local_namespace) && $perms->get_permissions('mod_comments') && $nu > 0 )
+      // $n = ( $session->check_acl_scope('mod_comments', $local_namespace) && $perms->get_permissions('mod_comments') ) ? (string)$num_comments : (string)$na;
+      if ( $session->check_acl_scope('mod_comments', $local_namespace) && $perms->get_permissions('mod_comments') && ( $approval_counts[COMMENT_UNAPPROVED] + $approval_counts[COMMENT_SPAM] ) > 0 )
       {
         $subst = array(
-            'num_comments' => $nc,
-            'num_unapp' => $nu
+            'num_comments' => $num_comments,
+            'num_app' => $approval_counts[COMMENT_APPROVED],
+            'num_unapp' => $approval_counts[COMMENT_UNAPPROVED],
+            'num_spam' => $approval_counts[COMMENT_SPAM]
           );
         $btn_text = $lang->get('onpage_btn_discussion_unapp', $subst);
       }
       else
       {
         $subst = array(
-          'num_comments' => $nc
+          'num_comments' => $num_comments
         );
         $btn_text = $lang->get('onpage_btn_discussion', $subst);
       }
--- a/index.php	Sun Jan 25 20:35:32 2009 -0500
+++ b/index.php	Sun Jan 25 21:21:07 2009 -0500
@@ -281,7 +281,7 @@
       break;
     case 'rollback':
       $id = (isset($_GET['id'])) ? $_GET['id'] : false;
-      if(!$id || !preg_match('#^([0-9]+)$#', $id)) die_friendly('Invalid action ID', '<p>The URL parameter "id" is not an integer. Exiting to prevent nasties like SQL injection, etc.</p>');
+      if(!$id || !ctype_digit($id)) die_friendly('Invalid action ID', '<p>The URL parameter "id" is not an integer. Exiting to prevent nasties like SQL injection, etc.</p>');
       
       $id = intval($id);
       
--- a/language/english/admin.json	Sun Jan 25 20:35:32 2009 -0500
+++ b/language/english/admin.json	Sun Jan 25 21:21:07 2009 -0500
@@ -279,6 +279,11 @@
       field_comment_allow_guests_yes: 'Yes',
       field_comment_allow_guests_captcha: 'Require visual confirmation',
       field_comment_allow_guests_no: 'No (require login)',
+      field_comment_spam_policy: 'Spam policy:',
+      field_comment_spam_policy_hint: 'This requres a <a href="http://enanocms.org/Category:Spam_filtering">spam filtering plugin</a> to be installed.',
+      field_comment_spam_policy_moderate: 'Moderate comments (default)',
+      field_comment_spam_policy_reject: 'Reject post',
+      field_comment_spam_policy_accept: 'Ignore and accept posts',
       
       // Section: disable site
       heading_disablesite: 'Disable all site access',
--- a/language/english/core.json	Sun Jan 25 20:35:32 2009 -0500
+++ b/language/english/core.json	Sun Jan 25 21:21:07 2009 -0500
@@ -211,12 +211,21 @@
       msg_count_unapp_one: 'However, there is <span id="comment_count_unapp_inner">1</span> additional comment awaiting approval.',
       msg_count_unapp_plural: 'However, there are <span id="comment_count_unapp_inner">%num_unapp%</span> additional comments awaiting approval.',
       
+      msg_foe_comment_hidden: 'Post from foe hidden.',
+      btn_display_foe_comment: 'Display post',
+      
       msg_note_unapp: '(Unapproved)',
+      msg_note_spam: '(Spam)',
       
       msg_ip_address: 'IP address:',
       
       msg_delete_confirm: 'Do you really want to delete this comment?',
       
+      err_captcha_wrong: 'The confirmation code you entered was incorrect.',
+      err_spamcheck_failed_rejected: 'Your comment was rejected because it appears to be spam.',
+      err_spamcheck_failed_flagged: 'Your comment was posted, but it appears to be spam and has been flagged as such for a moderator to review.',
+      ajax_err_generic_title: 'Error fetching comment data',
+      
       postform_title: 'Got something to say?',
       postform_blurb: 'If you have comments or suggestions on this article, you can shout it out here.',
       postform_blurb_unapp: 'Before your post will be visible to the public, a moderator will have to approve it.',
@@ -250,7 +259,7 @@
       lbl_page_external: 'external page',
       
       btn_discussion: 'discussion (%num_comments%)',
-      btn_discussion_unapp: 'discussion (%num_comments% total, %num_unapp% unapp.)',
+      btn_discussion_unapp: '<span title="Approved: %num_app% | Unapproved: %num_unapp% | Spam: %num_spam%">discussion (%num_comments%) [!]</span>',
       btn_edit: 'edit this page',
       btn_viewsource: 'view source',
       btn_history: 'history',
@@ -332,6 +341,7 @@
       err_no_permission: 'You do not have permission to edit this page.',
       err_page_protected: 'This page is protected, and you do not have permission to edit protected pages.',
       err_captcha_wrong: 'The confirmation code you entered is incorrect.',
+      err_spamcheck_failed: 'Your edit was rejected because it looks like spam.',
       
       msg_editor_heading: 'Editing page',
       msg_saved: 'Your changes to this page have been saved.',
--- a/plugins/PrivateMessages.php	Sun Jan 25 20:35:32 2009 -0500
+++ b/plugins/PrivateMessages.php	Sun Jan 25 21:21:07 2009 -0500
@@ -60,7 +60,7 @@
       break;
     case 'View':
       $id = $argv[1];
-      if ( !preg_match('#^([0-9]+)$#', $id) )
+      if ( !ctype_digit($id) )
       {
         die_friendly('Message error', '<p>Invalid message ID</p>');
       }
@@ -106,7 +106,7 @@
       break;
     case 'Move':
       $id = $argv[1];
-      if ( !preg_match('#^([0-9]+)$#', $id) )
+      if ( !ctype_digit($id) )
       {
         die_friendly('Message error', '<p>Invalid message ID</p>');
       }
@@ -136,7 +136,7 @@
       break;
     case 'Delete':
       $id = $argv[1];
-      if ( !preg_match('#^([0-9]+)$#', $id) )
+      if ( !ctype_digit($id) )
       {
         die_friendly('Message error', '<p>Invalid message ID</p>');
       }
@@ -365,7 +365,7 @@
       break;
     case 'Edit':
       $id = $argv[1];
-      if ( !preg_match('#^([0-9]+)$#', $id) )
+      if ( !ctype_digit($id) )
       {
         die_friendly('Message error', '<p>Invalid message ID</p>');
       }
--- a/plugins/SpecialAdmin.php	Sun Jan 25 20:35:32 2009 -0500
+++ b/plugins/SpecialAdmin.php	Sun Jan 25 21:21:07 2009 -0500
@@ -304,6 +304,10 @@
     if(isset($_POST['enable-comments']))  setConfig('enable_comments', '1');
     else                                  setConfig('enable_comments', '0');
     setConfig('comments_need_login', $_POST['comments_need_login']);
+    if ( in_array($_POST['comment_spam_policy'], array('moderate', 'reject', 'accept')) )
+    {
+      setConfig('comment_spam_policy', $_POST['comment_spam_policy']);
+    }
     
     // Powered by link
     if ( isset($_POST['enano_powered_link']) ) setConfig('powered_btn', '1');
@@ -350,10 +354,10 @@
     setConfig('register_tou', RenderMan::preprocess_text($_POST['register_tou'], true, false));
     
     // Account lockout policy
-    if ( preg_match('/^[0-9]+$/', $_POST['lockout_threshold']) )
+    if ( ctype_digit($_POST['lockout_threshold']) )
       setConfig('lockout_threshold', $_POST['lockout_threshold']);
     
-    if ( preg_match('/^[0-9]+$/', $_POST['lockout_duration']) )
+    if ( ctype_digit($_POST['lockout_duration']) )
       setConfig('lockout_duration', $_POST['lockout_duration']);
     
     if ( in_array($_POST['lockout_policy'], array('disable', 'captcha', 'lockout')) )
@@ -604,6 +608,27 @@
           </label>
         </td>
       </tr>
+      
+      <tr>
+        <td class="row2">
+          <?php echo $lang->get('acpgc_field_comment_spam_policy'); ?><br />
+          <small><?php echo $lang->get('acpgc_field_comment_spam_policy_hint'); ?></small>
+        </td>
+        <td class="row2">
+          <label>
+            <input name="comment_spam_policy" type="radio" value="moderate" <?php if ( getConfig('comment_spam_policy', 'moderate') == 'moderate' ) echo 'checked="checked"'; ?>/>
+            <?php echo $lang->get('acpgc_field_comment_spam_policy_moderate'); ?>
+          </label>
+          <label>
+            <input name="comment_spam_policy" type="radio" value="reject" <?php if ( getConfig('comment_spam_policy', 'moderate') == 'reject' ) echo 'checked="checked"'; ?>/>
+            <?php echo $lang->get('acpgc_field_comment_spam_policy_reject'); ?>
+          </label>
+          <label>
+            <input name="comment_spam_policy" type="radio" value="accept" <?php if ( getConfig('comment_spam_policy', 'moderate') == 'accept' ) echo 'checked="checked"'; ?>/>
+            <?php echo $lang->get('acpgc_field_comment_spam_policy_accept'); ?>
+          </label>
+        </td>
+      </tr>
             
     <!-- Site disablement -->
     
--- a/plugins/admin/LangManager.php	Sun Jan 25 20:35:32 2009 -0500
+++ b/plugins/admin/LangManager.php	Sun Jan 25 21:21:07 2009 -0500
@@ -47,7 +47,7 @@
         
         // Is this parameter in the form of an integer?
         // (designed to ease validation later)
-        if ( preg_match('/^[0-9]+$/', $parm) )
+        if ( ctype_digit($parm) )
           // Yes, run intval(), this enabling is_int()-ish checks
           $parm = intval($parm);