<?php
/*
Plugin Name: BotBlocker
Plugin URI: http://www.lform.com/botblocker/
Description: Kills spam-bots, leaves humans standing. No CAPTCHAS, no math questions, no passwords, just spam blocking that stops comment spam-bots dead in their tracks.
Version: 1.0.1
Author: Lform Design (Brandon Fenning)
Author URI: http://www.lform.com/botblocker/
License: GPLv2
*/
/*
* TODO: Test all the way back to IE 5.5
* TODO: Test on WP 3.0
* TODO: add complete obscusfication
* TODO: add themeable error page hook option
* TODO: make honey pot togglable
* TODO: add unit tests
*/
session_start();
new BotBlocker_plugin();
class BotBlocker_plugin {
protected $options;
protected $isSpam = FALSE;
protected $debug = FALSE;
protected $msg;
function __construct() {
add_action( 'admin_init', array( &$this, 'adminInit' ) );
register_activation_hook( __FILE__, array( &$this, 'activate' ) );
add_filter('comment_form_default_fields', array( &$this, 'generateHoneypot' ),1);
add_filter('preprocess_comment', array( &$this, 'validateComment' ));
add_action('init', array( &$this, 'preprocessComment' ));
add_action('wp_enqueue_scripts', array( &$this, 'addStylesheet' ));
add_action('admin_menu', array( &$this, 'adminSettingsPage' ));
$plugin = plugin_basename(__FILE__);
add_filter("plugin_action_links_$plugin", array( &$this, 'adminSettingsLink' ) );
add_action('comment_post', array( &$this, 'commentSaveAction' ));
add_filter('pre_comment_approved',array( &$this, 'commentApproval' ), '99', 2 );
add_action('comment_form_top', array( &$this, 'printError' ));
$this->options = $this->getWpOptions();
$this->msg = &$_SESSION['_spamMsg'];
}
function printError() {
$options = $this->getOptions();
if ($this->msg && $options['honeypot_error_type'] == 'Reload') {
echo '<div class="_errorMsg"><span>'.implode(' ', $this->msg).'</span></div>';
$this->msg = FALSE;
}
}
function logError($data) {
if ($data) {
$this->msg[] = $data;
}
}
function commentApproval($approved, $commentData) {
if ($this->isSpam == TRUE) {
$options = $this->getOptions();
if ($options['honeypot_reaction'] == 'Block') {
$approved = '0';
$errorMsg = $options['honeypot_error_msg_block'];
}
else if ($options['honeypot_reaction'] == 'Spam') {
$approved = 'spam';
$errorMsg = $options['honeypot_error_msg_flag'];
}
else if ($options['honeypot_reaction'] == 'Hold') {
$approved = '0';
$errorMsg = $options['honeypot_error_msg_hold'];
}
$this->logError($options['honeypot_error_msg'].' '.$errorMsg);
if ($options['honeypot_error_type'] == 'Die') {
if (!$options['honeypot_error_msg']) {
$options['honeypot_error_msg'] = 'spam-bot detected.';
}
$msg = $this->msg;
$this->msg = FALSE;
wp_die(__('<strong>ERROR</strong>: '.implode(' ', $msg)));
}
else if ($options['honeypot_error_type'] == 'Silent') {
}
}
return $approved;
}
function commentSaveAction($comment_id) {
$options = $this->getOptions();
// # Fires if WP_die not called
if ($this->isSpam == TRUE) {
if ($options['honeypot_reaction'] == 'Block') {
wp_delete_comment($comment_id, TRUE);
}
}
return FALSE;
}
function generateHoneypot($fields) {
$options = $this->getOptions();
$additionalRandom = '';
if ($options['honeypot_random'] == 'Yes') {
$additionalRandom = $this->getAdditionalRandom();
}
if ($options['obfuscation'] == 'Swap Email and Name') {
$fields['author'] = preg_replace('/name="author"/i', 'name="'.$additionalRandom.'email"', $fields['author']);
$fields['email'] = preg_replace('/name="email"/i', 'name="'.$additionalRandom.'author"', $fields['email']);
}
else if ($options['obfuscation'] == 'Completely Obfuscate') {
// # Todo: Add ability to completely obfuscate the field names with random characters
}
if ($options['honeypot_method'] == 'Smart') {
$todaysDecoy = $this->getTodaysDecoy($fields);
$honeypotName = $additionalRandom.$todaysDecoy;
}
else if ($options['honeypot_method'] == 'Static') {
$honeypotName = $additionalRandom.$options['honeypot_field'];
}
else {
$todaysRandom = $this->getTodaysRandom();
$honeypotName = $additionalRandom.$todaysRandom;
}
$honeyClass = '';
$honeyStyle = '';
$honeyJs = '';
if ($this->debug != TRUE) {
if ($options['honeypot_hide'] == 'CSS') {
$honeyClass = '_hidden hide';
}
else if ($options['honeypot_hide'] == 'Inline-CSS') {
$honeyStyle = 'style="display:none;"';
}
else if ($options['honeypot_hide'] == 'Javascript') {
$honeyJs = '<script>jQuery("#'.$honeypotName.'").hide();</script>';
}
}
$fields[$honeypotName] = '<input type="text" name="'.$honeypotName.'" id="'.$honeypotName.'" value="" class="'.$honeyClass.'" '.$honeyStyle.' />'.$honeyJs;
return $fields;
}
function preprocessComment() {
$additionalRandom = '';
$options = $this->getOptions();
if ($options['honeypot_random'] == 'Yes') {
$additionalRandom = $this->getAdditionalRandom();
}
// # If form obscusfication is enabled
if ($options['obfuscation'] == 'Swap Email and Name') {
$comment_author = ( isset($_POST[$additionalRandom.'email']) ) ? trim(strip_tags($_POST[$additionalRandom.'email'])) : null;
$comment_author_email = ( isset($_POST[$additionalRandom.'author']) ) ? trim($_POST[$additionalRandom.'author']) : null;
if (isset($_POST[$additionalRandom.'author'])) {
$_POST['author'] = $comment_author;
}
if (isset($_POST[$additionalRandom.'email'])) {
$_POST['email'] = $comment_author_email;
}
}
else if ($options['obfuscation'] == 'Completely Obfuscate') {
// # Todo: Add ability to completely obfuscate the field names with random characters
}
}
function validateComment($comment) {
$additionalRandom = '';
$options = $this->getOptions();
if ($options['honeypot_random'] == 'Yes') {
$additionalRandom = $this->getAdditionalRandom();
}
if ($options['honeypot_method'] == 'Smart') {
$todaysDecoy = $this->getTodaysDecoy($fields);
$honeypotField = $additionalRandom.$todaysDecoy;
}
else if ($options['honeypot_method'] == 'Static') {
$honeypotField = $additionalRandom.$options['honeypot_field'];
}
else {
$todaysRandom = $this->getTodaysRandom();
$honeypotField = $additionalRandom.$todaysRandom;
}
$isSpam = FALSE;
if (!isset($_POST[$honeypotField])) {
$isSpam = TRUE;
}
else if ($_POST[$honeypotField] != '') {
$isSpam = TRUE;
}
if ($isSpam) {
$this->isSpam = TRUE;
}
return $comment;
}
function getDecoyFields($fields = FALSE) {
$decoyFields = array(
'address',
'suite',
'company',
'phone',
'title',
'city',
'state',
'fax',
'newsletter',
'webites',
'zipcode',
'address2',
'firstname',
'lastname',
'birthday'
);
if ($fields && is_array($fields)) {
foreach ($decoyFields as $index=>$decoy) {
if (isset($fields[$decoy])) {
unset($decoyFields[$index]);
}
}
}
sort($decoyFields);
return $decoyFields;
}
function getAdditionalRandom() {
$options = $this->getOptions();
$additionalRandom = substr(sha1(date('Y-m-d').$options['seed']),0,6);
return $additionalRandom;
}
function getTodaysDecoy($fields) {
$options = $this->getOptions();
$decoyFields = $this->getDecoyFields($fields);
$max = count($decoyFields);
srand($options['seed'].date('Ymd'));
$randDecoyIndex = rand(0,$max);
return $decoyFields[$randDecoyIndex];
}
function getTodaysRandom() {
$options = $this->getOptions();
srand($options['seed'].date('Ymd'));
$number = rand(0,9999999);
$hash = substr(sha1($number),0,8);
return $hash;
}
function adminSettingsLink($links) {
$settings_link = '<a href="options-general.php?page=BotBlocker_options">Settings</a>';
array_unshift($links, $settings_link);
return $links;
}
function adminValidate($input) {
return $input;
}
function adminInit() {
register_setting( 'BotBlocker_options', 'BotBlocker_options', array( &$this, 'adminValidate' ));
}
function adminSettingsPage() {
add_options_page('BotBlocker Settings', 'BotBlocker', 'manage_options', 'BotBlocker_options', array( &$this, 'adminSettingsPageForm' ));
}
function addStylesheet() {
$pluginPath = WP_PLUGIN_URL.'/'.str_replace(basename( __FILE__),"",plugin_basename(__FILE__));
wp_register_style( 'BotBlockerCSS', $pluginPath.'/styles.css');
wp_enqueue_style('BotBlockerCSS');
}
function activate() {
$this->getWpOptions();
}
function getWpOptions() {
// Get options
$options = get_option('BotBlocker_options');
// options exist? if not set defaults
if ( !is_array($options) ) {
$options = array(
'honeypot_method'=>'Smart',
'honeypot_field'=>'field',
'honeypot_random'=>'Yes',
'honeypot_hide'=>'CSS',
'honeypot_error_type'=>'Die',
'honeypot_error_msg'=>'Spam bot detected.',
'honeypot_error_msg_block' => 'Your comment was blocked from being posted.',
'honeypot_error_msg_flag' => 'Your comment was marked as spam and is being held for moderation.',
'honeypot_error_msg_hold' => 'Your comment is being held for moderation.',
'honeypot_reaction'=>'Block',
'obfuscation'=>'Swap Email and Name',
'seed'=>rand(0,99999),
);
add_option("BotBlocker_options",$options);
}
if (!isset($options['seed'])) {
$options['seed'] = rand(0,99999);
add_option("BotBlocker_options",$options);
}
$this->options = $options;
return $options;
}
function getOptions($field = FALSE) {
if ($field) {
return $this->options['field'];
}
else {
return $this->options;
}
}
function adminSettingsPageForm() {
?>
<div class="wrap spam-form">
<h2>BotBlocker Settings</h2>
<form method="post" action="options.php">
<?php settings_fields('BotBlocker_options'); ?>
<?php
$options= $this->getOptions();
?>
<table class="form-table">
<tr valign="top">
<td scope="row" colspan="2" class="heading">
<h3>Honeypot Options</h3>
<p>While BotBlocker works fine out of the box, the honeypot can be configured a couple different ways:</p>
</td>
</tr>
<tr valign="top">
<th scope="row">Method</th>
<td>
<select name="BotBlocker_options[honeypot_method]">
<option value="Smart" <?php selected($options['honeypot_method'], 'Smart'); ?> />Smart</option>
<option value="Static" <?php selected($options['honeypot_method'], 'Static'); ?> />Static</option>
<option value="Random" <?php selected($options['honeypot_method'], 'Random'); ?> />Random</option>
</select><br>
<ul class="help">
<li>The <b>Smart</b> method chooses from a list of decoy fields, while ensuring it doesn't conflict with any fields you may have added, changes daily.<br>
<small>Decoy List: <? echo implode(', ', $this->getDecoyFields()) ?></small></li>
<li>The <b>Static</b> method will use a set name for the field every time.</li>
<li>The <b>Random</b> method will generate a string of alpha-numeric characters for the field name, changes daily.</li>
</ul>
</td>
</tr>
<tr valign="top"><th scope="row">Static Field Name</th>
<td><input type="text" name="BotBlocker_options[honeypot_field]" value="<?php echo $options['honeypot_field']; ?>" size="25" />
<br><small>If the method is set to <b>Static</b>, this will be the name of the honeypot field.</small></td>
</tr>
<tr valign="top">
<th scope="row">How should spam-bots be handled?</th>
<td>
<select name="BotBlocker_options[honeypot_reaction]">
<option value="Block" <?php selected($options['honeypot_reaction'], 'Block'); ?> />Block</option>
<option value="Spam" <?php selected($options['honeypot_reaction'], 'Spam'); ?> />Flag as Spam</option>
<option value="Hold" <?php selected($options['honeypot_reaction'], 'Hold'); ?> />Hold for Approval</option>
</select>
<br>
<ul class="help">
<li><b>Block</b> will prevent the comment from being submitted</li>
<li><b>Flag as spam</b> will hold it for moderation in the spam queue.</li>
<li><b>Hold for approval</b> will require the comment to be approved by a moderator.</li>
</ul>
</td>
</tr>
<tr valign="top">
<th scope="row">Spam-bot Error Action</th>
<td>
<select name="BotBlocker_options[honeypot_error_type]">
<option value="Die" <?php selected($options['honeypot_error_type'], 'Die'); ?> />Show Error</option>
<option value="Reload" <?php selected($options['honeypot_error_type'], 'Reload'); ?> />Show Error on Page</option>
<option value="Silent" <?php selected($options['honeypot_error_type'], 'Silent'); ?> />Silent</option>
</select>
<br>
<ul class="help">
<li><b>Show Error</b> will use the default Wordpress error pages to display any messages. (Default)</li>
<li><b>Show Error on Page</b> will show any messages at the top of the comment form where it was submitted.</li>
<li><b>Silent</b> will cause BotBlocker to perform its blocking actions without displaying any errors or alerts.</li>
</ul>
</td>
</tr>
<tr valign="top">
<th scope="row">Error Messages</th>
<td>
<table width="100%">
<tr valign="top">
<td>Global Message</td><td><input type="text" name="BotBlocker_options[honeypot_error_msg]" value="<?php echo $options['honeypot_error_msg']; ?>" size="50" />
<br><small>If using <b>Show Error</b>, this is the message that will appear for spam bots.</small></td>
</tr>
<tr valign="top">
<td>Blocked Message</td><td><input type="text" name="BotBlocker_options[honeypot_error_msg_block]" value="<?php echo $options['honeypot_error_msg_block']; ?>" size="50" />
<br><small>If using <b>Show Error</b> & <b>Block</b> on spam-bots, this is the extra message that will appear.</small></td>
</tr>
<tr valign="top">
<td>Flagged Message</td><td><input type="text" name="BotBlocker_options[honeypot_error_msg_flag]" value="<?php echo $options['honeypot_error_msg_flag']; ?>" size="50" />
<br><small>If using <b>Show Error</b> & <b>Flag as Spam</b> on spam-bots, this is the extra message that will appear.</small></td>
</tr>
<tr valign="top">
<td>Holding Message</td><td><input type="text" name="BotBlocker_options[honeypot_error_msg_hold]" value="<?php echo $options['honeypot_error_msg_hold']; ?>" size="50" />
<br><small>If using <b>Show Error</b> & <b>Holding for approval</b> on spam-bots, this is the extra message that will appear.</small></td>
</tr>
</table>
</td>
</tr>
<tr valign="top">
<th scope="row">Hiding Method</th>
<td>
<select name="BotBlocker_options[honeypot_hide]">
<option value="CSS" <?php selected($options['honeypot_hide'], 'CSS'); ?> />CSS</option>
<option value="Inline-CSS" <?php selected($options['honeypot_hide'], 'Inline-CSS'); ?> />Inline-CSS</option>
<option value="Javascript" <?php selected($options['honeypot_hide'], 'Javascript'); ?> />Javascript</option>
</select>
<br><small>The method by which the honeypot field is hidden from normal users. (Default: CSS)</small>
</td>
</tr>
<tr valign="top">
<th scope="row">Additional Randomness</th>
<td>
<select name="BotBlocker_options[honeypot_random]">
<option value="Yes" <?php selected($options['honeypot_random'], 'Yes'); ?> />Yes</option>
<option value="No" <?php selected($options['honeypot_random'], 'No'); ?> />No</option>
</select>
<br><small class="help"><b>Additional randomness</b> will prefix or suffix a random character to the selected method, which will change daily. (Default: Yes)</small>
</td>
</tr>
<tr valign="top">
<td scope="row" colspan="2" class="heading">
<h3>Obfuscation</h3>
<p>BotBlocker can take the default wordpress comment fields and rename them to make it harder for spam bots to figure out.<br></p>
</td>
</tr>
<tr valign="top">
<th scope="row">Obfuscation</th>
<td>
<select name="BotBlocker_options[obfuscation]">
<option value="Swap Email and Name" <?php selected($options['obfuscation'], 'Swap Email and Name'); ?> />Swap Email and Name</option>
<!-- <option value="Completely Obfuscate" <?php selected($options['obfuscation'], 'Completely Obfuscate' ); ?> />Completely Obfuscate</option> -->
<option value="None" <?php selected($options['obfuscation'], 'None'); ?> />None</option>
</select><br>
<ul class="help">
<li>The <b>Swap Email and Name</b> option swaps the field names for the name and email text boxes. Thus a spam bot will see 'name' for the email field, and wordpress will reject it as an invalid email. (Default)</li>
<!-- <li>The <b>Completely Obfuscate</b> option will make all the fields completely randomly generated names, making it impossible to identify what data should go where, changes daily.</li> -->
<li>The <b>None</b> option will disable obfuscation. If you have heavily customized your comment form, you may need to select this. </li>
</ul>
</td>
</tr>
<tr valign="top">
<td scope="row" colspan="2" class="heading">
<h3>Advanced Options</h3>
</td>
</tr>
<tr valign="top">
<th scope="row">Seed</th>
<td>
<input type="text" name="BotBlocker_options[seed]" value="<?php echo $options['seed']; ?>" size="25" />
<br><small>Randomly generated when you activated the plugin to prevent spam bot counter-detection. Not necessary to change.</small>
</td>
</tr>
</table>
<p class="submit"><br><br>
<input type="submit" class="button-primary" value="<?php _e('Save Changes') ?>" />
</p>
</form>
</div>
<style>
.spam-form { }
.spam-form p { line-height: 1.3; margin-bottom: 1em;}
.spam-form ul { margin-left: 1.5em;}
.spam-form li { list-style-type: disc; margin-bottom: 0.75em;}
.spam-form small {line-height:1.3; font-size: 0.9em; color:#666;}
.spam-form select {width:200px; margin-bottom: 5px;}
.spam-form input { margin-bottom: 5px;}
.spam-form table {}
.spam-form th {
font-size:1em;
width: 220px;
padding-bottom: 10px;
border-bottom: 1px solid #ddd;
background: #f4f4f4;
border-left: 35px solid #fff;
}
.spam-form td {
line-height:1.3;
border-bottom: 1px solid #eee;
}
.spam-form td.heading {
border: 0;
padding-left: 0;
padding-top: 20px;
border-left: 0px;
}
.spam-form tr:first-child td.heading {
padding-top: 0px;
}
.spam-form .help {
font-size: 0.9em;
color: #666;
}
</style>
<?php
}
}