root/spaminator/trunk/kittens-spaminator.php

Revision 215, 20.4 KB (checked in by kitten, 6 years ago)

update regex service info, trim inputs

Line 
1<?php
2/*
3Plugin Name: Kitten's Spaminator
4Version: 1.0rc7
5Plugin URI: http://blog.mookitty.co.uk/wordpress/spaminator/
6Description: Spam prevention and blocking using tarpitting and strike counting. Comments are assigned strikes for spam content, and a comment that meets the criteria for spam is blocked from posting.<br>If you're using WP 1.3 or higher, you have an admin menu under "Options" if you wish to change the defaults. For 1.2 users, you must edit the plugin file directly.<br><strong> Copyright 2004, Released under the GPL.</strong>
7Author: Kitten
8Author URI: http://blog.mookitty.co.uk
9
10INSTALLATION INSTRUCTIONS
11=========================
121) If you're viewing this on the web, select all and paste it into a new text file named "kittens-spaminator.php"
13
142) Copy the "kittens-spaminator.php" file to your wp-contents/plugins directory.
15
163) On your plugins admin page, activate the plugin.
17
184) Enjoy your freedom from spam!
19
20   4a) Configure options if you don't like the defaults. Use the admin page for WP 1.3+, edit
21       edit this file for 1.2.
22=========================
23
24Change Log:
250.1a   - initial release
260.2a   - add more stuff
270.3a   - add whitelisting, fix checker function
280.3b   - fix debug code error
290.4a   - changed even more stuff
300.5a   - add crapflood check, change default values
310.6a   - add stike if no email,etc
320.6b   - fix php error
330.7a   - add check for empty email (Donncha)
340.8a   - add encoded char check, from functions.php
350.9a   - add post ID check, from techgnome
360.9b   - clean up email stuff & version
370.10a  - add user regex for especially annoying spam
380.10b  - fix user regex bug
390.10c  - fix another user regex bug
400.10d  - clean up code, add TODO
410.11a  - add URL check, fix strpos errors, interface for $strike_cnt
420.11b  - add user regex for email
431.0rc  - Release candidate, add GPL license
441.0rc2 - Add admin page skel, 1.2 fixes, name whitelisting, "how" in email, clear TODOs
45         fix excessive links bug, change default for no referrer, double stripslashes on the
46                 comment (to make the comment_url filter work to detect strange urls), fix html
47                 entities
481.0rc3 - Add check for real url in URL field, expand character entities check, check
49                 all urls for dashes (most spam urls have dashes). Change user_regex_c to hopefully
50                 catch the garbage spammer. Add record keeping on passed comments.
511.0rc4 - Now used now 'preprocess' comment filter hook, only works with v1.5 nightlies from
52                 Jan 5th or later. New email address for returned mail.
531.0rc5 - New shiny admin interface + Kitten's regex service.
541.0rc6 - Fix install bug.
551.0rc7 - Sanitize regex service text, trim form fields.
56*/
57
58/**************----------------  CLASS DEFINITIONS  ----------------**************/
59/// Admin page
60if ( ! class_exists( 'spaminator_admin_page' ) ) :
61class spaminator_admin_page
62{
63        function spaminator_admin_page( $post = '' )
64        {
65                $this->installed = get_settings( 'spaminator_status' );
66                // Get and load old options
67                $this->opts = get_settings( 'spaminator_settings' );
68                if ( count( $this->opts ) == 6 ) {
69                        foreach ( $this->opts as $key => $val ) $this->$key = $val;
70                }
71                // Override with new options
72                if ( 'Save Options' == $post['save_spaminator_options'] ) {
73                        $this->save_options = trim( $post['save_spaminator_options'] );
74                        $this->strikes      = trim( $post['spaminator_strikes'] );
75                        $this->user_regex_c = trim( $post['spaminator_comment_regex'] );
76                        $this->user_regex_e = trim( $post['spaminator_email_regex'] );
77                        $this->nap_time     = trim( $post['spaminator_naptime'] );
78                        $this->send_mail    = trim( $post['spaminator_sendmail'] );
79                        $this->crap_flood   = trim( $post['spaminator_crapflood'] );
80                }
81
82                if ( 'Remove Options' == $post['remove_spaminator_options'] ) {
83                        $this->remove_options();
84                }
85
86                // Need to set things up
87                $this->install_options = $post['install_spaminator_options'];
88                $this->process_options();
89
90                // Get regex list
91                if ( 'Get Regexs' == $post['get_regexs'] ) {
92                        $this->get_regexs();
93                }
94        }
95
96        function process_options()
97        {
98                // Install
99                if ( 'Install now' == $this->install_options && 'installed' !=  $this->installed ) {
100                        add_option( 'spaminator_status', 'installed', 'Spaminator install flag' );
101                        $settings = array( 'strikes' => 5,
102                                                           'user_regex_e' => '/^byob.*[0-9]{1,4}/i',
103                                                           'user_regex_c' => '/bea?stiality|rape|incest/i',
104                                                           'nap_time' => 60,
105                                                           'send_mail' => TRUE,
106                                                           'crap_flood' => 60 );
107                        add_option( 'spaminator_settings', $settings, 'Spaminator options' );
108                        $this->installed = get_settings( 'spaminator_status' );
109                        $this->opts = get_settings( 'spaminator_settings' );
110                        if ( count( $this->opts ) == 6 ) {
111                                foreach ( $this->opts as $key => $val ) $this->$key = $val;
112                        } else {
113                                die( "There was a problem installing the default settings. Please try again." );
114                        }
115                }
116
117                // Save new options
118                if ( 'Save Options' == $this->save_options ) {
119                        $settings = array( 'strikes' => $this->strikes,
120                                                           'user_regex_c' => $this->user_regex_c,
121                                                           'user_regex_e' => $this->user_regex_e,
122                                                           'nap_time' => $this->nap_time,
123                                                           'send_mail' => $this->send_mail,
124                                                           'crap_flood' => $this->crap_flood );
125                        update_option( 'spaminator_settings', $settings );
126                        $this->updated = '<div class="updated"><p>The Spaminator&#146;s settings were updated successfully.</p></div>' . "\n";
127                }
128        }
129
130        function get_regexs()
131        {
132                $f = @fopen( "http://mookitty.co.uk/regexs.txt", 'r' );
133                $data = @fread( $f, 2048 ); // even if corrupted, only 2K max
134                @fclose( $f );
135
136                if ( strlen( $data ) < 1 ) {
137                        $this->regexs = 'Sorry, no data available.<br />See <a href="http://mookitty.co.uk/regexs.txt">this page</a> for the latest.';
138                } else {
139                        $this->regexs = htmlentities( $data );
140                }
141        }
142
143        function remove_options()
144        {
145                delete_option( 'spaminator_status' );
146                delete_option( 'spaminator_settings' );
147                $this->installed = '';
148                $this->updated = '<div class="updated"><p style="color: red;"><strong>The Spaminator&#146;s settings were removed.</strong> You are now using the built in defaults.</p></div>' . "\n";
149        }
150       
151        function display_admin_page()
152        {
153                if ( 'installed' == $this->installed ) {
154                        return $this->show_form();
155                } else {
156                        return $this->show_install_form();
157                }
158        }
159
160        function show_install_form()
161        {
162                $text  = $this->updated;
163                $text .= '<div class="wrap"><h2>Install The Spaminator&#146;s Config</h2>' . "\n";
164                $text .= '<form method="post" action="">' . "\n";
165                $text .= '<h3>Do you want to be able to configure The Spaminator from this admin page?</h3>' . "\n";
166                $text .= "<p>This will install The Spaminator's options in your database.</p>" . "\n";
167                $text .= '<input type="submit" name="install_spaminator_options" value="Install now" />' . "\n";
168                $text .= '</form>'. "\n";
169                $text .= '</div>' . "\n";
170                return $text;
171        }
172
173        function show_form()
174        {
175                $text  = $this->updated;
176                $text .= '<div class="wrap"><h2>Configure The Spaminator</h2>' . "\n";
177                $text .= '<form method="post" action="" name="spaminator_options">' . "\n";
178                $text .= '<table>' . "\n";
179                $text .= '<tr><td>Strikes:</td>' . "\n";
180                $text .= '<td><input type="text" name="spaminator_strikes" value="'.$this->strikes.'" /></td>' . "\n";
181                $text .= '<td>The number of "hits" needed to kill a comment as spam.</td></tr>' . "\n";
182               
183                $text .= '<tr><td>Send email?</td>' . "\n";
184                $text .= '<td><select name="spaminator_sendmail">' . "\n";
185                if ( $this->send_mail ) {
186                        $text .= '<option value="1" selected="selected">Yes&nbsp;&nbsp;</option>' . "\n";
187                        $text .= '<option value="0">No</option>' . "\n";
188                } else {
189                        $text .= '<option value="1">Yes&nbsp;&nbsp;</option>' . "\n";
190                        $text .= '<option value="0" selected="selected">No</option>' . "\n";
191                }
192                $text .= '</select></td>' . "\n";
193                $text .= '<td>Send email confirmation of each comment killed?</td></tr>' . "\n";
194               
195
196                $text .= '<tr><td>Nap Time:</td>' . "\n";
197                $text .= '<td><input type="text" name="spaminator_naptime" value="'.$this->nap_time.'" /></td>' . "\n";
198                $text .= '<td>How long to tarpit the spammer, in seconds.</td></tr>' . "\n";
199               
200                $text .= '<tr><td>Crap Flood:</td>' . "\n";
201                $text .= '<td><input type="text" name="spaminator_crapflood" value="'.$this->crap_flood.'" /></td>' . "\n";
202                $text .= '<td>Minimum amount of time allowed between comments from same IP address.</td></tr>' . "\n";
203               
204               
205                $text .= '<tr><td>Email regex:</td>' . "\n";
206                $text .= '<td><input type="text" name="spaminator_email_regex" value="'.$this->user_regex_e.'" /></td>' . "\n";
207                $text .= '<td>Special pattern in the email address of the commenter to kill comments.</td></tr>' . "\n";
208               
209               
210                $text .= '<tr><td>Comment regex:</td>' . "\n";
211                $text .= '<td><input type="text" name="spaminator_comment_regex" value="'.$this->user_regex_c.'" /></td>' . "\n";
212                $text .= '<td>Special pattern in the comment body to kill comments.</td></tr>' . "\n";
213               
214                $text .= '</table>' . "\n";
215                $text .= '<input type="submit" name="save_spaminator_options" value="Save Options" />' . "\n";
216                $text .= '</form>'. "\n";
217                $text .= '</div>' . "\n";
218
219                $text .= '<div class="wrap"><h2>Kitten\'s Regex Service</h2>' . "\n";
220                if ( !empty( $this->regexs ) ) {
221                        $text .= "<pre>$this->regexs</pre>";
222                } else {
223                        $text .= '<p>If you\'d like to see <a href="http://blog.mookitty.co.uk/wordpress/regex-service/">Kitten\'s custom regexs</a> that she\'s currently using to keep spam away, click the button below:</p>' . "\n";
224                        $text .= '<p><strong style="color: red">Notice:</strong> This is highly dependant on your server configuration, and may not work. You\'ve been warned.</p>' . "\n";
225                        $text .= '<form method="post" action="" name="kittens_regexs">' . "\n";
226                        $text .= '<input type="submit" name="get_regexs" value="Get Regexs" />' . "\n";
227                        $text .= '</form>' . "\n";
228                }
229                $text .= '</div>' . "\n";
230
231                $text .= '<div class="wrap"><h2>Remove The Spaminator&#146;s Options</h2>' . "\n";
232                $text .= '<p><strong style="color: red">Help!</strong> I\'ve totally screwed up my settings and want to use the built in defaults.</p>' . "\n";
233                $text .= '<form method="post" action="" name="remove_spaminator_options">' . "\n";
234                $text .= '<p>Really remove the user options?</p>' . "\n";
235                $text .= '<input type="submit" name="remove_spaminator_options" value="Remove Options" />' . "\n";
236                $text .= '</form>'. "\n";
237                $text .= '</div>' . "\n";
238
239                return $text;
240        }
241}
242endif;
243
244/// Spam redirector class
245if ( ! class_exists( 'spam_killer' ) ) :
246class spam_killer
247{       
248        // Class vars
249        var $how;
250        var $post;
251        var $strikes;
252        var $strike_cnt;
253        var $word_list;
254        var $nap_time;
255        var $send_mail;
256        var $crap_flood;
257
258        function spam_killer( $post )
259        {
260                $options = array(
261                //==================================================================
262                //      Adjust the number of strikes needed to reject a comment
263                                strikes => 5,           // Number
264                //==================================================================
265                //      Change this to fight a particular spammer
266                        user_regex_c => '/bea?stiality|rape|incest/i',  // Text string
267                        user_regex_e => '/^byob.*[0-9]{1,4}/i',         // Text string
268                //==================================================================
269                //      Adjust the nap time to vary the TarPit delay
270                                nap_time   => 60,               // Time in seconds
271                //==================================================================
272                //      Change to TRUE to enable sending email when a spammer is caught
273                                send_mail  => true,     // TRUE or FALSE
274                //==================================================================
275                //      Adjust the minimum time between posts (crapflooding)
276                                crap_flood => 60,               // Time in seconds
277                //==================================================================
278                );
279               
280                $installed = get_settings( 'spaminator_status' );
281
282                if ( 'installed' == $installed ) { // this is set via the admin page
283                        $saved_options = get_settings( 'spaminator_settings' );
284                        if ( is_array( $saved_options ) ) $options = $saved_options; // override
285                }
286
287                foreach ( $options as $key => $opt ) $this->$key = $opt;
288               
289                $this->post = $post;
290                $this->strike_cnt = 0;
291                $this->word_list = get_settings('moderation_keys');
292        }
293
294        function count_strikes( $str = 0, $how )
295        {
296                $this->how[] = $how; // list all checks so far
297                $this->strike_cnt += $str;
298               
299                // stats
300                $this->post['comment_content'] .= "<!-- X-spaminator-strike: $how, $str -->";
301
302                if ( $this->strike_cnt >= $this->strikes ) {
303
304                        // Maybe send mail - we got spammer tail
305                        if ( $this->send_mail ) {
306                                $why   = implode( ", ", $this->how );
307                                $body  = "The Spaminator has killed a comment.\r\n\r\n";
308                                $body .= "The details:\r\n";
309                                $body .= "Strikes : $this->strike_cnt/$this->strikes\r\n";
310                                $body .= "How     : $why\r\n";
311                                $body .= "IP Addr : ".$_SERVER['REMOTE_ADDR']."\r\n";
312                                $body .= "Referer : ".$_SERVER['HTTP_REFERER']."\r\n";
313                                $body .= "Client  : ".$_SERVER['HTTP_USER_AGENT']."\r\n";
314                                $body .= "Request : ".$_SERVER['REQUEST_METHOD']." ". $_SERVER['REQUEST_URI']."\r\n";
315                                $body .= "Post ID : ".$this->post['comment_post_ID']."\r\n";
316                                $body .= "Email   : ".$this->post['comment_author_email']."\r\n";
317                                $body .= "Author  : ".$this->post['comment_author']."\r\n";
318                                $body .= "URL     : ".$this->post['comment_author_url']."\r\n";
319                                $body .= "Body:\r\n";
320                                $body .= $this->post['comment_content']."\r\n\r\n";
321                                $body .= "--\r\nThis email has been sent because the Spaminator plugin is set to send emails when a suspected spam has been blocked.\r\nTo not receive these emails change the ".'$this->send_mail'." variable to FALSE.\r\n\r\nThanks for using this plugin, hope it helps!\r\nhttp://mookitty.co.uk/devblog/";
322                                $headers = "From: The Spaminator <wp.spaminator@gmail.com>";
323                                $to = "[" . get_settings('blogname') . "] Spaminator: Spammer caught!";
324                               
325                                @mail( get_settings('admin_email'), $to, $body, $headers );
326                        }
327                       
328                        // Let's waste spambot time:
329                        sleep ( $this->nap_time );
330                       
331                        // Make sure the bot thinks that spam was posted:
332                        header( "HTTP/1.0 200 OK" );
333
334                        // Tell humans something else:
335                        echo "<p>Sorry, you've been prevented from commenting on this blog.</p>\n";
336                        echo "<p>Either your comment content was found to contain spam, or<br />\n";
337                        echo "your IP address (or a subnet of your IP address) has spammed this blog before.</p>\n";
338                        echo "<p>If you think you got this page in error, your entered name might be too short.</p>\n";
339                        echo "<p>You can also complain to <a href=\"wp.spaminator@gmail.com\">wp.spaminator@gmail.com</a>. View source to see why you got blocked.\n";
340                        echo "<p>Strike count: $this->strike_cnt</p>\n";
341                        echo "\n<!-- ", implode( ", ", $this->how ), " -->";
342                        exit;
343                }
344        }
345
346        function process_comment()
347        {
348                global $wpdb, $table_prefix;
349                if ( empty($wpdb->comments) ) $wpdb->comments =  $table_prefix . 'comments';
350                if ( empty($wpdb->posts) ) $wpdb->posts =  $table_prefix . 'posts';
351               
352                /// Set up vars to use:
353               
354                $type    = $this->post['comment_type'];
355                $url     = parse_url( $this->post['comment_author_url'] );
356                $postID  = $this->post['comment_post_ID'];
357                $author  = $this->post['comment_author'];
358                if ( empty( $this->post['comment_author_email'] ) ) {
359                        if ( 'trackback' == $type ) {
360                                $this->post['comment_author_email'] = 'trackback@' . $url['host'];
361                        } elseif ( 'pingback' == $type ) {
362                                $this->post['comment_author_email'] = 'pingback@' . $url['host'];
363                        }
364                }
365                $email = $this->post['comment_author_email'];
366                // Filter the text so that links show up, etc.
367                $comment = apply_filters( 'comment_text', stripslashes(stripslashes($this->post['comment_content'])) );
368//              $forward = $this->post['redirect_to'];
369                $remote  = $_SERVER['REMOTE_ADDR'];
370                $referer = $_SERVER['HTTP_REFERER'];
371                $iplist  = preg_grep( "/^([0-9]{1,3}\.?){2}/", explode("\n", $this->word_list) );
372                $wdlist  = explode( "\n", $this->word_list );
373                $wdlist  = array_diff( $wdlist, $iplist );
374                // Get email for whitelist
375                if ( !empty( $email ) ) {
376                        $whlist  = $wpdb->get_row("SELECT comment_approved, comment_author FROM $wpdb->comments WHERE comment_author_email = '$email' AND comment_approved = '1' AND NOW()+0 > UNIX_TIMESTAMP( DATE_ADD( comment_date, INTERVAL 1  DAY ) ) GROUP BY comment_author;");
377                } else {
378                        $whlist = '0';
379                }
380
381                // This returns a unix timestamp
382                $cfchck  = $wpdb->get_var("SELECT UNIX_TIMESTAMP(comment_date) FROM $wpdb->comments WHERE comment_author_IP = '$remote' ORDER BY comment_date_gmt DESC LIMIT 1;");
383
384                /// Check for spam:
385
386                // User regex check
387                if (  preg_match( $this->user_regex_c, $comment ) > 0 ) {
388                        $this->count_strikes( 10, 'user regex - comment' );
389                }
390                if (  preg_match( $this->user_regex_e, $email ) > 0 ) {
391                        $this->count_strikes( 10, 'user regex - email ');
392                }
393               
394                // If non-existing post, spam for sure
395                if ( !$wpdb->get_var("SELECT ID FROM $wpdb->posts WHERE ID = '$postID'") ) {
396                        $this->count_strikes( 10, 'non-existant post' );
397                }
398
399                // If previously approved, they get a headstart
400                if ( '1' == $whlist->comment_approved  ) $this->count_strikes( -3, 'whitelist' );
401
402                // Check referer, see if a bot is directly inserting comment
403                if ( FALSE === strpos( $referer, get_settings('siteurl') ) ) {
404                        $this->count_strikes( 3, 'bad referer - spambot?' );
405                }
406
407                // Check IP, see if it's a known spammer
408                $this->checker( $iplist, $remote, 5, 'IP check' );
409
410                // Check for crapflood
411                if ( $cfchck + $this->crap_flood > time() ) {
412             $this->count_strikes( 3, 'crap flooding' );
413                }
414
415                // Count links
416                if ( (count(explode('http', $comment)) - 1 ) > ( (int) get_settings('comment_max_links') ) ) {
417                        $this->count_strikes( 3, 'excessive links' );
418                }
419
420                // Check email
421                $this->checker( $wdlist, $email, 1, 'email check' );
422
423                // Check author
424                // If the email is whitelisted, the name gets a free pass
425                // the most recent version of the name is used
426                if ( $author != $whlist->comment_author ) {
427                        $this->checker( $wdlist, $author, 1, 'author check' );
428                }
429
430                // Check URL
431                // 3 points here, in case of "no body links" spam
432                $this->checker( $wdlist, $url['host'], 3, 'author url' );
433
434                // Check comment
435                $this->checker( $wdlist, $comment, 1, 'comment body' );
436
437                // From functions.php:
438                // Useless numeric encoding is a pretty good spam indicator:
439                // Extract entities:
440                if (preg_match_all('/&#?(\d+);/', $url['host'] . $author . $email, $chars)) {
441                        foreach ($chars[1] as $char) {
442                        // If it's an encoded char in the normal ASCII set, reject
443                                if ($char < 128) $this->count_strikes( 1, 'html entity' );
444                        }
445                }
446
447                // Check for dashes in urls
448                // Urls that are auto formatted will get counted twice - feature, not bug!
449                preg_match_all( '#(http:)?//([^\s"\'<>]*)#i', $comment, $match );
450                array_push( $match[2], $url['host'] );
451                array_push( $match[2], $email );
452                $cnt_dash = 0;
453                foreach ( $match[2] as $u ) {
454                        $cnt_dash += substr_count( $u, '-' );
455                }
456                if ( $cnt_dash > 0 ) {
457                        $this->count_strikes( $cnt_dash, 'url dashes' );
458                }
459
460                // Check if provided url is legit
461                if ( strlen( $url['host'] ) > 0 && ! checkdnsrr( $url['host'], 'A' ) ) {
462                        $this->count_strikes( 4, "unknown url, $url" );
463                }
464
465                return $this->post;
466
467        } // end of function process_comment
468
469        function checker( $haystack, $needle, $strike_val, $how )
470        {
471                if ( is_array( $haystack ) && count( $haystack ) > 0 && strlen( $needle ) > 3 ) {
472                        foreach ( $haystack as $item ) {
473                                $item = trim( $item );
474                                // skip empties & shorts in mod keys, faster than filtering list
475                                if ( strlen( $item ) < 3 ) continue;
476
477                                $word_cnt = substr_count( $needle, $item );
478                                if ( $word_cnt > 0 ) {
479                                        $this->count_strikes( $strike_val * $word_cnt, $how . ' - ' . $item );
480                                }
481                        }
482                }
483                // Add a strike if email, etc is empty
484                if ( empty( $needle ) ) $this->count_strikes( 1, "empty field - $how" );
485                if ( ! empty( $needle ) && strlen( $needle ) <=3 ) $this->count_strikes( 1, 'short field' );
486
487                // Record keeping
488                $this->post['comment_content'] .= "<!-- X-spaminator-passed: $how -->";
489
490        } // end of function checker
491       
492} // end class spam killer
493endif;
494/**************--------------  END CLASS DEFINITIONS  --------------**************/
495
496/**************----------------  STANDALONE SECTION  ---------------**************/
497/// Control block if comment posted.
498if ( ! function_exists( 'spaminate_comment' ) ) {
499        function spaminate_comment( $comment ) {
500                $incoming_spam = new spam_killer( $comment );
501                $foo = $incoming_spam->process_comment();
502                return $foo;
503        }
504}
505
506// Adds the menu item
507if ( ! function_exists( 'add_spaminator_menu' ) ) {
508        function add_spaminator_menu()
509    {
510                add_options_page(__('Spaminator Config'), __('Spaminator'), 9, 'kittens-spaminator.php');
511        }
512} elseif ( 'kittens-spaminator.php' == $_GET['page'] ) {
513        $spamiator_iface = new spaminator_admin_page( $_POST );
514        echo $spamiator_iface->display_admin_page();
515}
516
517add_action('preprocess_comment', 'spaminate_comment');
518add_action('admin_menu', 'add_spaminator_menu');
519/**************--------------  END STANDALONE SECTION  -------------**************/
520
521?>
Note: See TracBrowser for help on using the browser.