A simpler way to fight spam on WordPress

Update (2014-04-04) : The plugin is on now in WordPress's registry and on github, feel free to submit any patches.

There are 100s of different plugins that fight spam for WordPress, however almost all of them require user interaction someway or the other, and usually they require a remote service to verify the user's input.

While I was trying to combat spam on this site, I came across Anti-Spam, great concept, however it still required the user's interaction, and then my gears kicked in after analyzing the 1000s of spam comments that were posted on the site I wrote a very simple plugin to combat comment spam.

ChangeLog :

0.8 * Redid the auto-deleting logic, it shouldn't leave any traces of the comment in the database anymore.

* Added a comments_array filter so none of the spam comments pass to other plugins. v0.7.7 * Added an option to use an extra check using javascript.

* Added a debug option to embed the score array in comments that passes the plugin.

* Changed the hidden field name, again. v0.7.6 * Fixed a bug with the auto delete option not showing in the user interface. v0.7.5 * Changed the number of possible names of the hidden field.

* Set a higher priority on the preprocess_comment hook. v0.7 * Fixed a bug where default settings weren't loaded at all.

* Fixed the wording on the maximum number of urls allowed.

* Added a counter of how many comments have been blocked. v0.6 * Rewrote it to use OOP.

* Added configurable options in admin settings. v0.4 * First public release.

If the comment is a trackback.

If the time between loading the page and commenting is less than 10 seconds.

If the Session variable specific to this form is not set.

If the hidden email field have a different value than "-".

If the comment includes more than 3 urls.

If the referer isn't set properly.

Works like this :

I have submitted it to WordPress's plugin registry but it's still pending review, so here's the code until it gets accepted :

<?php /** * Plugin Name: OneOfOne's NoSpam * Plugin URI: http://limitlessfx.com/ * Description: Simple transparent no-spam plugin * Version: v0.8 * Author: OneOfOne * Author URI: http://limitlessfx.com/ * License: Apache-2 */ if ( !function_exists( 'add_action' ) ) { die('Nope.'); } session_start(); define('NOSPAM_VERSION', '0.8'); define('NOSPAM_MIN_TIME', 10.0); define('NOSPAM_MAX_URLS', 3); define('NOSPAM_AUTO_DELETE', false); define('NOSPAM_DEBUG', false); define('NOSPAM_JAVASCRIPT', false); class OneOfOneNoSpam { static $FIELD_NAMES = array('email'); static $OPTION_GROUP = 'ooo-nospam'; static $OPTION_COUNT = 'ooo-nospam-count'; private $options, $count; public function __construct() { $this->options = $this->sanitize(get_option(self::$OPTION_GROUP)); $this->count = absint(get_option(self::$OPTION_COUNT)); if(!$this->count) { update_option(self::$OPTION_COUNT, 0); } if(is_admin()) { add_action('admin_menu', array(&$this, 'add_plugin_page')); add_action('admin_init', array(&$this, 'admin_page_init')); } if(!is_user_logged_in()) { //todo comments_array add_action('comment_form', array(&$this, 'comment_form')); add_action('preprocess_comment' , array(&$this, 'preprocess_comment'), 0); add_filter('pre_comment_approved', array(&$this, 'pre_comment_approved') , 0, 2); add_filter('comments_array', array(&$this, 'filter_comments_array') , 0); } } public function add_plugin_page() { add_options_page('Settings Admin', 'OneOfOne\'s NoSpam', 'manage_options', 'ooo-nospam-admin', array(&$this, 'print_admin_page')); } public function print_admin_page() { ?> <div class="wrap"> <h2>OneOfOne's NoSpam v<?php echo NOSPAM_VERSION?> <h3>Spam comments blocked : <em><?php echo $this->count; ?></em></h3> <form method="post" action="options.php"> <?php // This prints out all hidden setting fields settings_fields(self::$OPTION_GROUP); ?> <table class="form-table"> <tr> <th scope="row">Auto Delete?</th> <td><?php echo $this->get_input_option('auto_delete', 2, 'checkbox'); ?> Yes </td> </tr> <tr> <th scope="row">Allowed URLs</th> <td><?php echo $this->get_input_option('max_urls'); ?> Maximum number of URLs allowed in a comment. </td> </tr> <tr> <th scope="row">Timeout (in seconds)</th> <td><?php echo $this->get_input_option('min_time'); ?> The minimum time spent on the page before commenting. </td> </tr> <tr> <th scope="row">Enable Javascript?</th> <td><?php echo $this->get_input_option('javascript', 2, 'checkbox'); ?> This will include an extra check using javascript. </td> </tr> <tr> <th scope="row">Enable Debugging?</th> <td><?php echo $this->get_input_option('debug', 2, 'checkbox'); ?> <small>This will embed debugging info in non-spam comments, <b>only enable if spam is bypassing the plugin.</b></small> </td> </tr> <tr> <th scope="row">Reset plugin options?</th> <td><?php echo $this->get_input_option('reset', 2, 'checkbox'); ?> Yes</td> </tr> </table> <?php do_settings_sections('ooo-nospam-admin'); submit_button(); ?> </form> </div> <?php } public function admin_page_init() { register_setting( self::$OPTION_GROUP, // Option group 'ooo-nospam', // Option name array(&$this, 'sanitize') // Sanitize ); } public function sanitize($input) { if(isset($input['reset']) && $input['reset'] === 'Y') { delete_option(self::$OPTION_GROUP); $input = array( 'min_time' => NOSPAM_MIN_TIME, 'max_urls' => NOSPAM_MAX_URLS, 'auto_delete' => NOSPAM_AUTO_DELETE ? 'Y' : '', 'javascript' => NOSPAM_JAVASCRIPT ? 'Y' : '', 'debug' => NOSPAM_DEBUG ); } $input['min_time'] = absint($input['min_time']) > 0 ? absint($input['min_time']) : NOSPAM_MIN_TIME; $input['max_urls'] = absint($input['max_urls']) > 0 ? absint($input['max_urls']) : NOSPAM_MAX_URLS; $input['javascript'] = isset($input['javascript']) ? $input['javascript'] : ''; $input['auto_delete'] = isset($input['auto_delete']) ? $input['auto_delete'] : ''; $input['debug'] = isset($input['debug']) ? $input['debug'] : ''; return $input; } public function comment_form() { $cid = sprintf('%f', microtime(true)); $fn = self::$FIELD_NAMES[mt_rand(0, count(self::$FIELD_NAMES) - 1)] .'-' . mt_rand(11, 99); $_SESSION['NS_' . $cid] = $fn; echo ' <p style="position:absolute; left:-99999px"> <input type="hidden" name="NS_CID" value="'. $cid .'" /> <input type="text" id="' . $fn . '" name="' . $fn . '" size="30" value="-"/> </p> '; if($this->options['javascript'] === 'Y') { echo '<script>(function(e){if(e)e.value="js";})(document.getElementById("' . $fn . '"));</script>'; } } public function preprocess_comment($data) { $type = $data['comment_type']; $cid = isset($_POST['NS_CID']) ? $_POST['NS_CID'] : 0; $fn = isset($_SESSION['NS_' . $cid]) ? $_SESSION['NS_' . $cid] : ''; unset($_SESSION['NS_' . $cid]); $dummy = array(); //workaround for older versions of php $nurls = preg_match_all('@http(?:s)?://@', $data['comment_content'], $dummy); $time = microtime(true) - floatval($cid); $checks = array( 'is-trackback' => $type === 'trackback' ? 1 : 0, 'no-session-token' => !$fn ? 1 : 0, 'hidden-field' => (!isset($_POST[$fn]) || $_POST[$fn] !== '-') ? 1.0 : 0, 'number-of-urls' => ($nurls > $this->options['max_urls']) ? $nurls : 0, 'referer' => isset($_SERVER['HTTP_REFERER']) && strpos($_SERVER['HTTP_REFERER'], get_site_url()) !== false ? 0 : 1, 'too-fast' => $time < $this->options['min_time'] ? $time : 0, 'javascript' => 0 ); if($this->options['javascript']) { $checks['javascript'] = $checks['hidden-field'] = $_POST[$fn] !== 'js' ? 1 : 0; } $score = 0; foreach($checks as $k => $v) { $score += $v; } if($score > 0) { $data['comment_approved'] = 'spam'; $checks['spam'] = 1; $data['comment_content'] .= "

" . json_encode($checks); } elseif($this->options['debug']) { $data['comment_content'] .= "

<!-- nospam-debug : " . json_encode($checks) . '-->'; } return $data; } public function pre_comment_approved($approved, $data) { $approved = $approved === 'spam' ? $approved : $data['comment_approved']; if ($approved === 'spam') { $this->update_spam_counter(); if($this->options['auto_delete']) { add_action('wp_insert_comment', array(&$this, 'handle_auto_delete'), 0, 2); } } return $approved; } public function handle_auto_delete($id, $comment) { if(!$comment && !is_object($cmt = get_comment($comment))){ return; } //$comment->comment_content .= print_r(array(strpos($comment->comment_content, '"spam":1'), $comment), true); //wp_update_comment((array)$comment); if(strpos($comment->comment_content, '"spam":1') !== false) { wp_delete_comment($id, true); } } public function filter_comments_array($comments = array()) { $ret = array(); foreach($comments as $k => $v) { if(strpos($v->comment_content, '"spam":1') === FALSE) { $ret[] = $v; } } return $ret; } private function update_spam_counter($i = 1) { global $wpdb; $wpdb->query( $wpdb->prepare( 'UPDATE ' . $wpdb->options . ' SET option_value = option_value + %d WHERE option_name = %s;', $i, self::$OPTION_COUNT ) ); } private function get_input_option($name, $size = 2, $type = 'text') { static $fmt_text = '<input id="%1$s" name="%2$s[%1$s]" size="%3$d" value="%4$s">'; static $fmt_checkbox = '<input type="checkbox" id="%1$s" name="%2$s[%1$s]" value="Y"%3$s>'; if($type === 'text') { return sprintf($fmt_text, $name, self::$OPTION_GROUP, $size, esc_attr($this->options[$name])); } else if($type === 'checkbox') { return sprintf($fmt_checkbox, $name, self::$OPTION_GROUP, isset($this->options[$name]) && $this->options[$name] === 'Y' ? ' checked="checked"' : ''); } } } add_action('init', 'init_ooo_nospam'); function init_ooo_nospam() { return new OneOfOneNoSpam(); } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 <?php /** * Plugin Name: OneOfOne's NoSpam * Plugin URI: http://limitlessfx.com/ * Description: Simple transparent no-spam plugin * Version: v0.8 * Author: OneOfOne * Author URI: http://limitlessfx.com/ * License: Apache-2 */ if ( ! function_exists ( 'add_action' ) ) { die ( 'Nope.' ) ; } session_start ( ) ; define ( 'NOSPAM_VERSION' , '0.8' ) ; define ( 'NOSPAM_MIN_TIME' , 10.0 ) ; define ( 'NOSPAM_MAX_URLS' , 3 ) ; define ( 'NOSPAM_AUTO_DELETE' , false ) ; define ( 'NOSPAM_DEBUG' , false ) ; define ( 'NOSPAM_JAVASCRIPT' , false ) ; class OneOfOneNoSpam { static $FIELD_NAMES = array ( 'email' ) ; static $OPTION_GROUP = 'ooo-nospam' ; static $OPTION_COUNT = 'ooo-nospam-count' ; private $options , $count ; public function __construct ( ) { $this -> options = $this -> sanitize ( get_option ( self :: $OPTION_GROUP ) ) ; $this -> count = absint ( get_option ( self :: $OPTION_COUNT ) ) ; if ( ! $this -> count ) { update_option ( self :: $OPTION_COUNT , 0 ) ; } if ( is_admin ( ) ) { add_action ( 'admin_menu' , array ( & $this , 'add_plugin_page' ) ) ; add_action ( 'admin_init' , array ( & $this , 'admin_page_init' ) ) ; } if ( ! is_user_logged_in ( ) ) { //todo comments_array add_action ( 'comment_form' , array ( & $this , 'comment_form' ) ) ; add_action ( 'preprocess_comment' , array ( & $this , 'preprocess_comment' ) , 0 ) ; add_filter ( 'pre_comment_approved' , array ( & $this , 'pre_comment_approved' ) , 0 , 2 ) ; add_filter ( 'comments_array' , array ( & $this , 'filter_comments_array' ) , 0 ) ; } } public function add_plugin_page ( ) { add_options_page ( 'Settings Admin' , 'OneOfOne\'s NoSpam' , 'manage_options' , 'ooo-nospam-admin' , array ( & $this , 'print_admin_page' ) ) ; } public function print_admin_page ( ) { ?> < div class = "wrap" > < h2 > OneOfOne 's NoSpam v <?php echo NOSPAM_VERSION ?> <h3>Spam comments blocked : <em> <?php echo $this -> count ; ?> </em></h3> <form method="post" action="options.php"> <?php // This prints out all hidden setting fields settings_fields ( self :: $OPTION_GROUP ) ; ?> <table class="form-table"> <tr> <th scope="row">Auto Delete?</th> <td> <?php echo $this -> get_input_option ( 'auto_delete' , 2 , 'checkbox' ) ; ?> Yes </td> </tr> <tr> <th scope="row">Allowed URLs</th> <td> <?php echo $this -> get_input_option ( 'max_urls' ) ; ?> Maximum number of URLs allowed in a comment. </td> </tr> <tr> <th scope="row">Timeout (in seconds)</th> <td> <?php echo $this -> get_input_option ( 'min_time' ) ; ?> The minimum time spent on the page before commenting. </td> </tr> <tr> <th scope="row">Enable Javascript?</th> <td> <?php echo $this -> get_input_option ( 'javascript' , 2 , 'checkbox' ) ; ?> This will include an extra check using javascript. </td> </tr> <tr> <th scope="row">Enable Debugging?</th> <td> <?php echo $this -> get_input_option ( 'debug' , 2 , 'checkbox' ) ; ?> <small>This will embed debugging info in non-spam comments, <b>only enable if spam is bypassing the plugin.</b></small> </td> </tr> <tr> <th scope="row">Reset plugin options?</th> <td> <?php echo $this -> get_input_option ( 'reset' , 2 , 'checkbox' ) ; ?> Yes</td> </tr> </table> <?php do_settings_sections ( 'ooo-nospam-admin' ) ; submit_button ( ) ; ?> </form> </div> <?php } public function admin_page_init() { register_setting( self::$OPTION_GROUP, // Option group ' ooo - nospam ', // Option name array(&$this, ' sanitize ') // Sanitize ); } public function sanitize($input) { if(isset($input[' reset ']) && $input[' reset '] === ' Y ') { delete_option(self::$OPTION_GROUP); $input = array( ' min_time ' => NOSPAM_MIN_TIME, ' max_urls ' => NOSPAM_MAX_URLS, ' auto_delete ' => NOSPAM_AUTO_DELETE ? ' Y ' : ' ', ' javascript ' => NOSPAM_JAVASCRIPT ? ' Y ' : ' ', ' debug ' => NOSPAM_DEBUG ); } $input[' min_time '] = absint($input[' min_time ']) > 0 ? absint($input[' min_time ']) : NOSPAM_MIN_TIME; $input[' max_urls '] = absint($input[' max_urls ']) > 0 ? absint($input[' max_urls ']) : NOSPAM_MAX_URLS; $input[' javascript '] = isset($input[' javascript ']) ? $input[' javascript '] : ' '; $input[' auto_delete '] = isset($input[' auto_delete ']) ? $input[' auto_delete '] : ' '; $input[' debug '] = isset($input[' debug ']) ? $input[' debug '] : ' '; return $input; } public function comment_form() { $cid = sprintf(' % f ', microtime(true)); $fn = self::$FIELD_NAMES[mt_rand(0, count(self::$FIELD_NAMES) - 1)] .' - ' . mt_rand(11, 99); $_SESSION[' NS_ ' . $cid] = $fn; echo ' < p style = "position:absolute; left:-99999px" > < input type = "hidden" name = "NS_CID" value = "'. $cid .'" / > < input type = "text" id = "' . $fn . '" name = "' . $fn . '" size = "30" value = "-" / > < / p > '; if($this->options[' javascript '] === ' Y ') { echo ' <script> ( function ( e ) { if ( e ) e . value = "js" ; } ) ( document . getElementById ( "' . $fn . '" ) ) ; </script> '; } } public function preprocess_comment($data) { $type = $data[' comment_type ']; $cid = isset($_POST[' NS_CID ']) ? $_POST[' NS_CID '] : 0; $fn = isset($_SESSION[' NS_ ' . $cid]) ? $_SESSION[' NS_ ' . $cid] : ' '; unset($_SESSION[' NS_ ' . $cid]); $dummy = array(); //workaround for older versions of php $nurls = preg_match_all(' @ http ( ? : s ) ? : //@', $data['comment_content'], $dummy); $time = microtime ( true ) - floatval ( $cid ) ; $checks = array ( 'is-trackback' = > $type === 'trackback' ? 1 : 0 , 'no-session-token' = > ! $fn ? 1 : 0 , 'hidden-field' = > ( ! isset ( $_POST [ $fn ] ) || $_POST [ $fn ] !== '-' ) ? 1.0 : 0 , 'number-of-urls' = > ( $nurls > $this -> options [ 'max_urls' ] ) ? $nurls : 0 , 'referer' = > isset ( $_SERVER [ 'HTTP_REFERER' ] ) && strpos ( $_SERVER [ 'HTTP_REFERER' ] , get_site_url ( ) ) !== false ? 0 : 1 , 'too-fast' = > $time < $this -> options [ 'min_time' ] ? $time : 0 , 'javascript' = > 0 ) ; if ( $this -> options [ 'javascript' ] ) { $checks [ 'javascript' ] = $checks [ 'hidden-field' ] = $_POST [ $fn ] !== 'js' ? 1 : 0 ; } $score = 0 ; foreach ( $checks as $k = > $v ) { $score += $v ; } if ( $score > 0 ) { $data [ 'comment_approved' ] = 'spam' ; $checks [ 'spam' ] = 1 ; $data [ 'comment_content' ] . = "

" . json_encode ( $checks ) ; } elseif ( $this -> options [ 'debug' ] ) { $data [ 'comment_content' ] . = "

<!-- nospam-debug : " . json_encode ( $checks ) . '-->' ; } return $data ; } public function pre_comment_approved ( $approved , $data ) { $approved = $approved === 'spam' ? $approved : $data [ 'comment_approved' ] ; if ( $approved === 'spam' ) { $this -> update_spam_counter ( ) ; if ( $this -> options [ 'auto_delete' ] ) { add_action ( 'wp_insert_comment' , array ( & $this , 'handle_auto_delete' ) , 0 , 2 ) ; } } return $approved ; } public function handle_auto_delete ( $id , $comment ) { if ( ! $comment && ! is_object ( $cmt = get_comment ( $comment ) ) ) { return ; } //$comment->comment_content .= print_r(array(strpos($comment->comment_content, '"spam":1'), $comment), true); //wp_update_comment((array)$comment); if ( strpos ( $comment -> comment_content , '"spam":1' ) !== false ) { wp_delete_comment ( $id , true ) ; } } public function filter_comments_array ( $comments = array ( ) ) { $ret = array ( ) ; foreach ( $comments as $k = > $v ) { if ( strpos ( $v -> comment_content , '"spam":1' ) === FALSE ) { $ret [ ] = $v ; } } return $ret ; } private function update_spam_counter ( $i = 1 ) { global $wpdb ; $wpdb -> query ( $wpdb -> prepare ( 'UPDATE ' . $wpdb -> options . ' SET option_value = option_value + %d WHERE option_name = %s;' , $i , self :: $OPTION_COUNT ) ) ; } private function get_input_option ( $name , $size = 2 , $type = 'text' ) { static $fmt_text = '<input id="%1$s" name="%2$s[%1$s]" size="%3$d" value="%4$s">' ; static $fmt_checkbox = '<input type="checkbox" id="%1$s" name="%2$s[%1$s]" value="Y"%3$s>' ; if ( $type === 'text' ) { return sprintf ( $fmt_text , $name , self :: $OPTION_GROUP , $size , esc_attr ( $this -> options [ $name ] ) ) ; } else if ( $type === 'checkbox' ) { return sprintf ( $fmt_checkbox , $name , self :: $OPTION_GROUP , isset ( $this -> options [ $name ] ) && $this -> options [ $name ] === 'Y' ? ' checked="checked"' : '' ) ; } } } add_action ( 'init' , 'init_ooo_nospam' ) ; function init_ooo_nospam ( ) { return new OneOfOneNoSpam ( ) ; }

To install it simply put the file in $WORDPRESS_ROOT/wp_content/plugins , for example :

|---[oneofone@Oa]---[~/code/php/limitlessfx] |---> cd wp-content/plugins/ |---[oneofone@Oa]---[~/code/php/limitlessfx/wp-content/plugins] |---> curl -O https://raw.githubusercontent.com/OneOfOne/ooo-nospam/master/ooo-nospam.php 1 2 3 4 | -- - [ oneofone @ Oa ] -- - [ ~ / code / php / limitlessfx ] | -- -> cd wp - content / plugins / | -- - [ oneofone @ Oa ] -- - [ ~ / code / php / limitlessfx / wp - content / plugins ] | -- -> curl - O https : / / raw .githubusercontent .com / OneOfOne / ooo - nospam / master / ooo - nospam .php

Then activate it in http://yoursite.com/wp-admin/plugins.php.