Changeset 47

Show
Ignore:
Timestamp:
02/22/05 13:15:24
Author:
brad
Message:

Major checkin that includes despam functionality and overall comment
and ping management. Also added wordlist support, comment and trackback
passphrase options.

Files:

Legend:

Unmodified
Added
Removed
Modified
Copied
Moved
  • trunk/plugins/spamlookup/mt-spamlookup.cgi

    r46 r47  
    3434 
    3535use MT::App; 
     36use MT::Util qw(format_ts offset_time mark_odd_rows); 
     37use MT::Author qw(APPROVED BLOCKED); 
    3638use vars qw(@ISA); 
    3739@ISA = qw(MT::App); 
     
    4850        unban_ip => \&unban_ip, 
    4951        reset_cache => \&reset_cache, 
    50         save => \&save_form         
     52        save => \&save_form, 
     53        despam => \&despam, 
     54        view => \&view, 
    5155    );  
    5256    $app->{default_mode} = 'default'; 
     
    5862    $app->{cfg}->set('AltTemplatePath', ''); 
    5963    $app->{requires_login} = 1; 
    60     $app->{breadcrumbs} = [ { bc_name => 'SpamLookup Configuration', 
     64    $app->{breadcrumbs} = [ { bc_name => 'SpamLookup', 
    6165                              bc_uri => '?__mode=show' } ]; 
     66    $app->{mode} = $mode; 
    6267    $app;    
     68} 
     69 
     70sub view { 
     71    my $app = shift; 
     72    my (%param) = @_; 
     73 
     74    my $query = $app->{query}; 
     75    my $type = $query->param('_type'); 
     76    my $Type = $type eq 'comments' ? 'Comments' : 'TrackBack Pings'; 
     77 
     78    $app->add_breadcrumb("View $Type" => '?__mode=view&_type='.$type); 
     79 
     80    my $tmpl; 
     81    if ($type eq 'comments') { 
     82        $tmpl = 'comments.tmpl'; 
     83    } else { 
     84        $tmpl = 'pings.tmpl'; 
     85    } 
     86 
     87    $app->select_objects(\%param); 
     88 
     89    $app->build_page($tmpl, \%param); 
     90} 
     91 
     92sub despam { 
     93    my $app = shift; 
     94    my (%param) = @_; 
     95 
     96    my $query = $app->{query}; 
     97    my $type = $query->param('_type') || 'comments'; 
     98    my $Type = $type eq 'comments' ? 'Comments' : 'TrackBack Pings'; 
     99 
     100    $app->add_breadcrumb("Despam $Type" => '?__mode=despam&_type='.$type); 
     101 
     102    $param{'type'} = $type; 
     103    $param{'type_'.$type} = 1; 
     104 
     105    my %to_rebuild; 
     106    my @ids = $query->param('is_spam'); 
     107    my $action = $query->param('action'); 
     108    if (@ids && ($action =~ m/Delete/)) { 
     109        if ($type eq 'comments') { 
     110            foreach (@ids) { 
     111                my $obj = MT::Comment->load($_); 
     112                next unless $obj; 
     113                $to_rebuild{$obj->entry_id} = 1; 
     114                $obj->remove; 
     115            } 
     116        } else { 
     117            require MT::TBPing; 
     118            foreach (@ids) { 
     119                my $obj = MT::TBPing->load($_); 
     120                next unless $obj; 
     121                $to_rebuild{$obj->tb_id} = 1; 
     122                $obj->remove; 
     123            } 
     124        } 
     125    } 
     126 
     127    $app->select_objects(\%param); 
     128 
     129    my $loop = $param{object_loop}; 
     130 
     131    if (!@$loop) { 
     132        if ($param{redirect}) { 
     133            $param{message} = qq{Didn't find any spam in the last }.$param{count}.qq{ records scanned in the date range } . format_ts("%Y-%m-%d %H:%M:%S", $param{start_date}) . " through " . format_ts("%Y-%m-%d %H:%M:%S", $param{end_date}) . ". Continuing search..."; ## next obj id is }.$obj->id; 
     134        } 
     135    } else { 
     136        $param{message} = "The following items were identified as spam, falling in the date range: " . format_ts("%Y-%m-%d %H:%M:%S", $param{start_date}) . " through " . format_ts("%Y-%m-%d %H:%M:%S", $param{end_date}); 
     137    } 
     138 
     139    if (%to_rebuild) { 
     140        require MT::Blog; 
     141        require MT::Entry; 
     142        MT::Util::start_background_task(sub { 
     143            my %blog_rebuild; 
     144 
     145            foreach my $id (keys %to_rebuild) { 
     146                if ($type eq 'comments') { 
     147                    my $entry = MT::Entry->load($id); 
     148                    next unless $entry; 
     149                    $app->rebuild_entry(Entry => $id, BuildDependencies => 1); 
     150                    $blog_rebuild{$entry->blog_id} = 1; 
     151                } else { 
     152                    my $tb = MT::Trackback->load($id); 
     153                    next unless $tb; 
     154                    my $blog = MT::Blog->load($tb->blog_id); 
     155                    $blog_rebuild{$tb->blog_id} = 1; 
     156                    if ($tb->entry_id) { 
     157                        $app->rebuild_entry(Entry => $tb->entry_id); 
     158                    } else { 
     159                        $app->_rebuild_entry_archive_type( 
     160                            Entry => undef, Blog => $blog, 
     161                            Category => MT::Category->load($tb->category_id), 
     162                            ArchiveType => 'Category' 
     163                        ); 
     164                    } 
     165                    if ($app->{cfg}->GenerateTrackBackRSS) { 
     166                        ## Now generate RSS feed for this trackback item. 
     167                        my $rss = _generate_rss($tb, 10); 
     168                        my $base = $blog->archive_path; 
     169                        my $feed = File::Spec->catfile($base, $tb->rss_file || $tb->id . '.xml');    
     170                        my $fmgr = $blog->file_mgr; 
     171                        $fmgr->put_data($rss, $feed); 
     172                    } 
     173                } 
     174            } 
     175 
     176            $app->rebuild_indexes( BlogID => $_ ) foreach keys %blog_rebuild; 
     177        }); 
     178    }    
     179 
     180    $app->build_page('despam.tmpl', \%param); 
     181} 
     182 
     183sub select_objects { 
     184    my $app = shift; 
     185    my ($param) = @_; 
     186 
     187    my $query = $app->{query}; 
     188    my $type = $query->param('_type') || 'comments'; 
     189 
     190    my $offset = $query->param('offset') || 0; 
     191    my $limit = $query->param('limit') || 25; 
     192    my $filter = $app->{mode} eq 'despam'; 
     193 
     194    my $iter; 
     195    if ($type eq 'comments') { 
     196        require MT::Comment; 
     197        my $terms = {}; 
     198        $terms->{visible} = 1 if $filter; 
     199        my $args = { offset => $offset, limit => ($limit*10)+1, 'sort' => 'created_on', direction => 'descend' }; 
     200        $iter = MT::Comment->load_iter($terms, $args); 
     201    } else { 
     202        require MT::TBPing; 
     203        require MT::Trackback; 
     204        my $terms = {}; 
     205        my $args = { offset => $offset, limit => ($limit * 10)+1, 'sort' => 'created_on', direction => 'descend' }; 
     206        $iter = MT::TBPing->load_iter($terms, $args); 
     207    } 
     208 
     209    my @loop; 
     210    require MT::Callback; 
     211    my $cb = new MT::Callback(method => 'Despam'); 
     212    my (%blogs, $obj, $start_date, $end_date); 
     213    my $count = 0; 
     214    while ($obj = $iter->()) { 
     215        $start_date = $obj->created_on unless defined $start_date; 
     216        $end_date = $obj->created_on; 
     217        $offset++; $count++; 
     218        last if $count == ($limit * 10); 
     219        next if ($type ne 'comments') && (($obj->created_by || 0) == 1) && $filter; 
     220        next unless $obj->ip || $filter;  # objects without an ip address aren't very despammable 
     221        my $data = $obj->column_values; 
     222        my $blog_id = $obj->blog_id; 
     223        my $blog; 
     224        if ($blogs{$blog_id}) { 
     225            $blog = $blogs{$blog_id}; 
     226        } else { 
     227            $blog = MT::Blog->load($blog_id); 
     228            $blogs{$blog_id} = $blog; 
     229        } 
     230        if ($filter) { 
     231            $cb->error(''); 
     232            my $result = MT::Plugin::SpamLookup::filter($cb, $app, $obj); 
     233            my $action; 
     234 
     235            if ($type eq 'comments') { 
     236                if ($result) { 
     237                    next if $obj->visible; 
     238                } 
     239            } else { 
     240                if ($result) { 
     241                    next unless $obj->created_by; 
     242                } 
     243                $result = 1 if $obj->created_by; 
     244            } 
     245            $action = $result ? 'Moderate' : 'Block'; 
     246            $data->{result} = $action; 
     247            $data->{"action_$action"} = 1; 
     248            $data->{reason} = $cb->errstr; 
     249        } 
     250 
     251        my $text; 
     252        if ($type eq 'comments') { 
     253            $data->{excerpt} = (substr($obj->text(), 0, 125) 
     254                             . (length($obj->text) > 125 ? "..." : "")); 
     255            $data->{author_display} = $data->{author}; 
     256            $data->{author_display} = substr($data->{author_display}, 0, 25) . '...' 
     257                if $data->{author_display} && $data->{author_display} =~ m(\S{25,}); 
     258            my $entry = MT::Entry->load($obj->entry_id()); 
     259            $data->{entry_title} = $entry->title() || $entry->text || ""; 
     260            $data->{entry_title} = substr($data->{entry_title}, 0, 25) . '...' 
     261                if $data->{entry_title} && $data->{entry_title} =~ m(\S{25,}); 
     262            $data->{commenter_id} = $obj->commenter_id() if $obj->commenter_id(); 
     263            my $cmntr = MT::Author->load({ id => $obj->commenter_id(), 
     264                                           type => MT::Author::COMMENTER }); 
     265            if ($cmntr) { 
     266                $data->{email_hidden} = $cmntr && $cmntr->is_email_hidden(); 
     267                require MT::Permission; 
     268             
     269                my $status = $cmntr->status($blog_id); 
     270                $data->{commenter_approved} = ($cmntr->status($blog_id) == APPROVED); 
     271                $data->{commenter_blocked} = ($cmntr->status($blog_id) == BLOCKED); 
     272            } 
     273            $text = $obj->url . ' ' . $obj->text; 
     274        } else { 
     275            my $tb = MT::Trackback->load($obj->tb_id); 
     276            my ($entry, $cat); 
     277            if ($tb->entry_id) { 
     278                $entry = MT::Entry->load($tb->entry_id); 
     279                $data->{entry_id} = $entry->id; 
     280            } elsif ($tb->category_id) { 
     281                $cat = MT::Category->load($tb->category_id); 
     282                $data->{category_id} = $cat->id; 
     283            } 
     284            $data->{tb_blog_name} = $obj->blog_name; 
     285 
     286            $text = $obj->source_url . ' ' . $obj->excerpt; 
     287            $data->{excerpt} = (substr($obj->excerpt(), 0, 125) 
     288                             . (length($obj->excerpt()) > 125 ? "..." : "")); 
     289            if ($entry) { 
     290                $data->{entry_title} = $entry->title() || $entry->text || ""; 
     291                $data->{entry_title} = substr($data->{entry_title}, 0, 25) . '...' 
     292                    if $data->{entry_title} && $data->{entry_title} =~ m(\S{25,}); 
     293            } 
     294            $data->{category_title} = $cat->label if $cat; 
     295        } 
     296        $data->{blog_name} = $blog->name; 
     297        $data->{created_on_relative_date} = relative_date(ts2epoch($blog->id, $obj->created_on()), offset_time(time, $blog), $obj->created_on, "%Y-%m-%d"); 
     298        if ($filter) { 
     299            my @urls = MT::Plugin::SpamLookup::extract_urls($text, 2); 
     300            my @domain_loop; 
     301            push @domain_loop, { domain => $_ } foreach @urls; 
     302            $data->{domain_loop} = \@domain_loop; 
     303        } 
     304        push @loop, $data; 
     305        last if scalar(@loop) == $limit; 
     306    } 
     307    $param->{count} = $count; 
     308    $param->{start_date} = $start_date; 
     309    $param->{end_date} = $end_date; 
     310    mark_odd_rows(\@loop); 
     311    if (!@loop) { 
     312        if ($obj) { 
     313            $param->{redirect} = 1; 
     314        } 
     315    } else { 
     316        $param->{more} = $obj ? 1 : 0; 
     317    } 
     318    $param->{object_loop} = \@loop; 
     319    $param->{offset} = $offset; 
     320    $param->{limit} = $limit; 
     321} 
     322 
     323use Time::Local qw( timegm ); 
     324sub ts2epoch { 
     325    my ($blog, $ts) = @_; 
     326    my ($yr, $mo, $dy, $hr, $mn, $sc) = unpack('A4A2A2A2A2A2A2', $ts); 
     327    $ts = timegm($sc, $mn, $hr, $dy, $mo-1, $yr-1900); 
     328} 
     329 
     330sub relative_date { 
     331    my ($ts1, $ts2, $ts, $fmt) = @_; 
     332    # make sure ts2 is always > ts1 
     333    ($ts2, $ts1) = ($ts1, $ts2) if $ts2 < $ts1; 
     334    my $delta = $ts2 - $ts1; 
     335    if ($delta <= 60) { 
     336        "Less than 1 minute"; 
     337    } elsif ($delta <= 86400) { 
     338        # less than 1 day 
     339        my $hours = int($delta / 3600); 
     340        my $min = int(($delta % 3600) / 60); 
     341        my $result; 
     342        if ($hours && $min) { 
     343            $result = sprintf('%d hour' . ($hours > 1 ? 's' : '') . ', %d minute' . ($min > 1 ? 's' : '') . ' ago', $hours, $min); 
     344        } elsif ($hours) { 
     345            $result = sprintf('%d hour' . ($hours > 1 ? 's' : '') . ' ago', $hours); 
     346        } elsif ($min) { 
     347            $result = sprintf('%d minute' . ($min > 1 ? 's' : '') . ' ago', $min); 
     348        } 
     349        $result; 
     350    } elsif ($delta <= 604800) { 
     351        # less than 1 week 
     352        my $days = int($delta / 86400); 
     353        my $hours = int(($delta % 86400) / 3600); 
     354        my $result; 
     355        if ($days && $hours) { 
     356            $result = sprintf('%d day' . ($days > 1 ? 's' : '') . ', %d hour' . ($hours > 1 ? 's' : '') . ' ago', $days, $hours); 
     357        } elsif ($days) { 
     358            $result = sprintf('%d day' . ($days > 1 ? 's' : '') . ' ago', $days); 
     359        } elsif ($hours) { 
     360            $result = sprintf('%d hour' . ($hours > 1 ? 's' : '') . ' ago', $hours); 
     361        } 
     362        $result; 
     363    } else { 
     364        format_ts($fmt, $ts); 
     365    } 
     366} 
     367 
     368sub _generate_rss { 
     369    my($tb, $lastn) = @_; 
     370    my $rss = <<RSS; 
     371<rss version="0.91"><channel> 
     372<title>@{[ $tb->title ]}</title> 
     373<link>@{[ $tb->url || '' ]}</link> 
     374<description>@{[ $tb->description || '' ]}</description> 
     375<language>en-us</language> 
     376RSS 
     377    my %arg; 
     378    if ($lastn) { 
     379        %arg = ('sort' => 'created_on', direction => 'descend', 
     380                limit => $lastn); 
     381    } 
     382    my $iter = MT::TBPing->load_iter({ tb_id => $tb->id }, \%arg); 
     383    while (my $ping = $iter->()) { 
     384        $rss .= sprintf qq(<item>\n<title>%s</title>\n<link>%s</link>\n), 
     385            encode_xml($ping->title), encode_xml($ping->source_url); 
     386        if ($ping->excerpt) { 
     387            $rss .= sprintf qq(<description>%s</description>\n), 
     388                encode_xml($ping->excerpt); 
     389        } 
     390        $rss .= qq(</item>\n); 
     391    } 
     392    $rss .= qq(</channel>\n</rss>); 
     393    $rss; 
    63394} 
    64395 
     
    122453    my $service=$data->{cache_ip_infoservice}; 
    123454    if($service =~ /%ip%/) { 
    124        foreach (@data) { 
    125            my $dest=$service; 
    126            $dest=~s/%ip%/$_->{ipaddr}/eg; 
    127            $_->{iptext}=qq{<a href="$dest">$_->{ipaddr}</a>}; 
    128        
     455        foreach (@data) { 
     456            my $dest = $service; 
     457            $dest =~ s/%ip%/$_->{ipaddr}/eg; 
     458            $_->{iptext} = qq{<a href="$dest">$_->{ipaddr}</a>}; 
     459       
    129460    } 
    130461    $param{cache_loop} = \@data; 
     
    218549        @domains = MT::Plugin::SpamLookup::extract_urls('http://' . $domain) 
    219550            if !@domains; 
    220         foreach my $domain (@domains) { 
     551        foreach $domain (@domains) { 
    221552            if ($domain =~ m/^(\d+)\.(\d+)\.(\d+)\.(\d+)$/) { 
    222553                $domain = "$4.$3.$2.$1"; 
     
    238569    my $app = shift; 
    239570    my (%param) = @_; 
     571 
     572    $app->add_breadcrumb("Configuration" => '?__mode=show'); 
     573 
    240574    my @config = MT::Plugin::SpamLookup->instance->config_params; 
    241575    my $data = MT::Plugin::SpamLookup->instance->get_config_hash(); 
    242576    $param{$_} = $data->{$_} foreach @config; 
     577    if (defined $param{"pingip_moderate"}) { 
     578        $param{"pingip_moderate_".$param{"pingip_moderate"}} = 1; 
     579    } 
     580    if (defined $param{"pingip_block"}) { 
     581        $param{"pingip_block_".$param{"pingip_block"}} = 1; 
     582    } 
     583    if (defined $param{"tbpass_entryfield"}) { 
     584        $param{"tbpass_entryfield_".$param{"tbpass_entryfield"}} = 1; 
     585    } 
     586    if (defined $param{"commentpass_entryfield"}) { 
     587        $param{"commentpass_entryfield_".$param{"commentpass_entryfield"}} = 1; 
     588    } 
    243589    if (defined $MT::Plugin::Moderate::VERSION) { 
    244590        $param{can_moderate_pings} = 1; 
    245591    } 
     592 
    246593    $app->build_page('spamlookup.tmpl', \%param); 
    247594} 
     
    250597    my $app = shift; 
    251598    my $auth = $app->{author}; 
    252     my $q = $app->{query}; 
     599    my $query = $app->{query}; 
    253600    my $pd = MT::Plugin::SpamLookup->instance->get_config_obj; 
    254601    my $config = $pd->data() || {}; 
    255602    my @config = MT::Plugin::SpamLookup->instance->config_params; 
    256603    foreach (@config) { 
    257         $config->{$_} = $q->param($_) || undef; 
     604        $config->{$_} = $query->param($_) || undef; 
    258605    } 
    259606    $pd->data($config); 
  • trunk/plugins/spamlookup/spamlookup.pl

    r45 r47  
    1111    name => 'SpamLookup', 
    1212    config_link => 'mt-spamlookup.cgi', 
    13     description => 'Controls feedback from open proxies and spammy URLs', 
     13    description => 'Controls feedback from open proxies and spammy URLs.', 
    1414    doc_link => undef 
    1515}); 
     
    1717MT->add_callback('CommentFilter', 6, $plugin, \&filter); 
    1818MT->add_callback('TBPingFilter', 6, $plugin, \&filter); 
     19MT->add_callback('TBPingThrottleFilter', 6, $plugin, \&tbfilter); 
    1920 
    2021use vars qw($DEBUG); 
     
    3334    qw( comment_filter ping_filter 
    3435        allow_auth allow_urlless allow_priorurls 
    35         pingip_moderate pingip_check 
     36        pingip_enabled pingip_moderate pingip_block 
    3637        ipbl_enabled ipbl_moderate ipbl_service 
    3738        domainbl_enabled domainbl_moderate domainbl_service 
     
    4041        headers_enabled headers_moderate headers_ignore 
    4142        urlcount_enabled urlcount_moderate_limit urlcount_block_limit 
     43        commentpass_enabled commentpass_formfield commentpass_entryfield commentpass_text commentpass_moderate 
     44        tbpass_enabled tbpass_formfield tbpass_entryfield tbpass_text tbpass_moderate 
     45        wordlist_moderate wordlist_block 
    4246    ); 
    4347} 
     
    7983    $data->{allow_urlless} = 0 unless exists $data->{allow_urlless}; 
    8084    $data->{allow_priorurls} = 0 unless exists $data->{allow_priorurls}; 
    81     $data->{pingip_check} = 1 unless exists $data->{pingip_check}; 
    82     $data->{pingip_moderate} = 1 unless exists $data->{pingip_moderate}; 
     85    $data->{pingip_enabled} = 1 unless exists $data->{pingip_enabled}; 
     86    $data->{pingip_moderate} = 2 unless exists $data->{pingip_moderate}; 
     87    $data->{pingip_block} = 3 unless exists $data->{pingip_block}; 
     88 
     89    $data->{tbpass_enabled} = 0 unless exists $data->{tbpass_enabled}; 
     90    if (!($data->{tbpass_formfield} && ($data->{tbpass_entryfield} || $data->{tbpass_text}))) { 
     91        $data->{tbpass_enabled} = 0; 
     92    } 
     93    $data->{commentpass_enabled} = 0 unless exists $data->{commentpass_enabled}; 
     94    if (!($data->{commentpass_entryfield} || $data->{commentpass_text})) { 
     95        $data->{commentpass_enabled} = 0; 
     96    } 
    8397 
    8498    $data->{ipbl_enabled} = 1 unless exists $data->{ipbl_enabled}; 
     
    113127    $data->{proxycheck_autoban} = 0 unless exists $data->{proxycheck_autoban}; 
    114128 
     129    $data->{wordlist_moderate} ||= ''; 
     130    $data->{wordlist_block} ||= ''; 
     131 
    115132    $data; 
     133} 
     134 
     135sub tbfilter { 
     136    my ($eh, $app, $obj) = @_; 
     137 
     138    my $plugin = $eh->{plugin}; 
     139    my $data = $plugin->get_config_hash(); 
     140    if ($data->{tbpass_enabled} && $obj->entry_id) { 
     141        my $text; 
     142        if (my $field = $data->{tbpass_entryfield}) { 
     143            require MT::Entry; 
     144            my $entry = MT::Entry->load($obj->entry_id); 
     145            if ($field eq 'title') { 
     146                $text = $entry->title; 
     147            } elsif ($field eq 'excerpt') { 
     148                $text = $entry->excerpt; 
     149            } elsif ($field eq 'keywords') { 
     150                $text = $entry->keywords; 
     151            } elsif ($field eq 'extended') { 
     152                $text = $entry->text_more; 
     153            } 
     154        } else { 
     155            $text = $data->{tbpass_text}; 
     156        } 
     157        $text =~ s/^\s*//s; 
     158        $text =~ s/\s*$//s; 
     159        $obj->passphrase($text); 
     160    } 
     161 
     162    1; 
    116163} 
    117164 
     
    120167 
    121168    # pull these from user configuration settings... 
     169    my $plugin = $eh->{plugin}; 
    122170    my $data = $plugin->get_config_hash(); 
     171 
     172    my $method = $eh->{method}; 
    123173 
    124174    my (@urls, $blog, $nurls); 
     
    129179 
    130180        # comment 
    131         @urls = extract_urls($obj->text, 0, \$nurls); 
    132         push @urls, extract_urls($obj->url) if $obj->url
     181        my $text = ($obj->text || '') . ' ' . ($obj->url || ''); 
     182        @urls = extract_urls($text, 0, \$nurls)
    133183        $data->{source_url} = $obj->url; 
    134184 
     
    149199        return 1 unless $data->{ping_filter}; 
    150200 
    151         $data->{__type} = 'Trackback ping'; 
     201        $data->{__type} = 'TrackBack ping'; 
    152202 
    153203        if ($data->{domainbl_enabled} || $data->{urlcount_enabled}) { 
    154             @urls = extract_urls($obj->excerpt, 0, \$nurls); 
    155             push @urls, extract_urls($obj->source_url) if $obj->source_url
     204            my $text = ($obj->excerpt || '') . ' ' . ($obj->source_url || ''); 
     205            @urls = extract_urls($text, 0, \$nurls)
    156206        } 
    157207        $data->{source_url} = $obj->source_url; 
    158     } 
     208    } else { 
     209        return 1;  # not sure what we got, but we don't know what to do with it 
     210    } 
     211 
    159212    $data->{__urls} = \@urls; 
    160213    $data->{__nurls} = $nurls; 
     
    176229 
    177230    my @tests; 
    178     push @tests, \&priorurl_check if $data->{allow_priorurls} && $data->{source_url}; 
    179     push @tests, \&pingip_check if $data->{pingip_check} && $data->{pingip_check} && $data->{__type} ne 'comment'; 
    180     push @tests, \&cache_check if $data->{caching_enabled} && $data->{cache_limit}; 
    181     push @tests, \&headers_check if $data->{headers_enabled}; 
    182     push @tests, \&ipbl_check if $data->{ipbl_enabled}; 
    183     push @tests, \&domainbl_check if $data->{domainbl_enabled}; 
    184     push @tests, \&urlcount_check if $data->{urlcount_enabled}; 
    185     push @tests, \&proxy_check if $data->{proxycheck_enabled}; 
     231    push @tests, [\&commentpass_check, 'commentpass_check'] 
     232        if $data->{commentpass_enabled} && ($data->{__type} eq 'comment') 
     233            && ($method ne 'Despam'); 
     234    push @tests, [\&tbpass_check, 'tbpass_check'] 
     235        if $data->{tbpass_enabled} && ($data->{__type} ne 'comment') 
     236            && ($method ne 'Despam'); 
     237    push @tests, [\&wordlist_check, 'wordlist_check'] 
     238        if ($data->{wordlist_moderate} || $data->{wordlist_block}); 
     239    push @tests, [\&priorurl_check, 'priorurl_check'] 
     240        if $data->{allow_priorurls} && $data->{source_url} 
     241            && ($method ne 'Despam'); 
     242    push @tests, [\&pingip_check, 'pingip_check'] 
     243        if $data->{pingip_enabled} && ($data->{__type} ne 'comment'); 
     244    push @tests, [\&cache_check, 'cache_check'] 
     245        if $data->{caching_enabled} && $data->{cache_limit}; 
     246    push @tests, [\&headers_check, 'headers_check'] 
     247        if $data->{headers_enabled} 
     248            && ($method ne 'Despam'); 
     249    push @tests, [\&ipbl_check, 'ipbl_check'] 
     250        if $data->{ipbl_enabled}; 
     251    push @tests, [\&domainbl_check, 'domainbl_check'] 
     252        if $data->{domainbl_enabled}; 
     253    push @tests, [\&urlcount_check, 'urlcount_check'] 
     254        if $data->{urlcount_enabled}; 
     255    push @tests, [\&proxy_check, 'proxy_check'] 
     256        if $data->{proxycheck_enabled} 
     257            && ($method ne 'Despam');  # this test should not be done during despamming, should it? 
    186258 
    187259    my ($result, @errors); 
    188260    foreach (@tests) { 
    189         $result = $_->($eh, $app, $obj, $data); 
    190         if ($eh->errstr =~ m/\w/) { 
     261        $result = $_->[0]->($eh, $app, $obj, $data); 
     262        if ((defined $eh->errstr) && ($eh->errstr =~ m/\w/)) { 
    191263            push @errors, $eh->errstr; 
    192264            $eh->error(undef); 
     
    208280                # MT-Moderate: 
    209281                $obj->created_by(1); 
    210                 $obj->save; 
    211282            } else { 
    212283                # trackbacks can't be moderated right now... 
     
    219290    } 
    220291 
    221     if ($data->{is_proxy} && $data->{proxycheck_autoban}) { 
    222         require MT::IPBanList; 
    223         my $ban = new MT::IPBanList; 
    224         $ban->ip($obj->ip); 
    225         $ban->blog_id($obj->blog_id); 
    226         $ban->save; 
    227         $app->log("SpamLookup: permanently banning IP ".$obj->ip." due to open proxy testing"); 
    228     } 
    229  
    230     if (($data->{caching_enabled} && $data->{cache_limit}) && 
    231         ($result != ACCEPT)) { 
    232         $cache->{$obj->ip}->{lastseen} = time; 
    233         $cache->{$obj->ip}->{hits}++; 
    234         $cache->{$obj->ip}->{blog_id} = $obj->blog_id; 
    235         $cache->{$obj->ip}->{result} = $result; 
    236         $cache->{$obj->ip}->{reason} = (join "\n", @errors) || ''; 
    237         my @keys = keys %$cache; 
    238         my $todelete = scalar(@keys) - $data->{cache_limit}; 
    239         if ($todelete > 0) { 
    240             foreach my $key (sort { $cache->{$a}->{lastseen} 
    241                                     <=> 
    242                                     $cache->{$b}->{lastseen} } @keys) { 
    243                 delete $cache->{$key}; 
    244                 last unless(--$todelete); 
     292    if ($method ne 'Despam') { 
     293        if ($data->{is_proxy} && $data->{proxycheck_autoban}) { 
     294            require MT::IPBanList; 
     295            my $ban = new MT::IPBanList; 
     296            $ban->ip($obj->ip); 
     297            $ban->blog_id($obj->blog_id); 
     298            $ban->save; 
     299            $app->log("SpamLookup: permanently banning IP ".$obj->ip." due to open proxy testing"); 
     300        } 
     301 
     302        if (($data->{caching_enabled} && $data->{cache_limit}) && 
     303            ($result != ACCEPT)) { 
     304            $cache->{$obj->ip}->{lastseen} = time; 
     305            $cache->{$obj->ip}->{hits}++; 
     306            $cache->{$obj->ip}->{blog_id} = $obj->blog_id; 
     307            $cache->{$obj->ip}->{result} = $result; 
     308            $cache->{$obj->ip}->{reason} = (join "\n", @errors) || ''; 
     309            my @keys = keys %$cache; 
     310            my $todelete = scalar(@keys) - $data->{cache_limit}; 
     311            if ($todelete > 0) { 
     312                foreach my $key (sort { $cache->{$a}->{lastseen} 
     313                                        <=> 
     314                                        $cache->{$b}->{lastseen} } @keys) { 
     315                    delete $cache->{$key}; 
     316                    last unless(--$todelete); 
     317                } 
    245318            } 
    246         } 
    247         unless ($pdata) { 
    248             $pdata = new MT::PluginData
    249             $pdata->plugin($plugin->name); 
    250             $pdata->key('cache'); 
    251         } 
    252         $pdata->data($cache)
    253         $pdata->save; 
     319            unless ($pdata) { 
     320                $pdata = new MT::PluginData; 
     321                $pdata->plugin($plugin->name)
     322                $pdata->key('cache'); 
     323            } 
     324            $pdata->data($cache); 
     325            $pdata->save
     326        } 
    254327    } 
    255328 
     
    260333## Each returns one of three states:  0 to reject, 1 to accept, undef 
    261334## to ignore, allowing the next test to run. 
     335 
     336sub wordlist_check { 
     337    my ($eh, $app, $obj, $data) = @_; 
     338 
     339    my $text = ''; 
     340    if ($data->{__type} eq 'comment') { 
     341        $text = join "\n", 
     342            "name:". ($obj->author || ''), 
     343            "email:" . ($obj->email || ''), 
     344            "url:" . ($obj->url || ''), 
     345            "text:" . ($obj->text || ''); 
     346    } else { 
     347        $text = join "\n", 
     348            "blog:". ($obj->blog_name || ''), 
     349            "title:" . ($obj->title || ''), 
     350            "url:" . ($obj->source_url || ''), 
     351            "text:" . ($obj->excerpt || ''); 
     352    } 
     353 
     354    my $decodedtext = decode_entities($text); 
     355 
     356    my $match; 
     357    if ($data->{wordlist_block}) { 
     358        $match = wordlist_match($text, $data->{wordlist_block}); 
     359        $match ||= wordlist_match($decodedtext, $data->{wordlist_block}); 
     360        if ($match) { 
     361            $eh->error("Blocking ".$data->{__type}." on blog id ".$obj->blog_id." based on wordlist match: ".$match); 
     362            return BLOCK; 
     363        } 
     364    } 
     365 
     366    if ($data->{wordlist_moderate}) { 
     367        $match = wordlist_match($text, $data->{wordlist_moderate}); 
     368        $match ||= wordlist_match($decodedtext, $data->{wordlist_moderate}); 
     369        if ($match) { 
     370            $eh->error("Moderating ".$data->{__type}." on blog id ".$obj->blog_id." based on wordlist match: ".$match); 
     371            return MODERATE; 
     372        } 
     373    } 
     374 
     375    return IGNORE; 
     376} 
     377 
     378sub wordlist_match { 
     379    my ($text, $patterns) = @_; 
     380 
     381    $text ||= ''; 
     382    my @patt = split /[\r\n]+/, $patterns; 
     383    foreach my $patt (@patt) { 
     384        $patt =~ s/^\s+//s; 
     385        $patt =~ s/\s+$//s; 
     386        next if $patt eq ''; 
     387        if ($patt =~ m!^/!) { 
     388            my $re = $patt; 
     389            my ($opt) = $re =~ m!/([^/]*)$!; 
     390            $re =~ s!^/!!; 
     391            $re =~ s!/[^/]*$!!; 
     392            $re = '(?'.$opt.':'.$re.')' if $opt; 
     393            $re = eval { qr/$re/ }; 
     394            $re = '\b' . quotemeta($patt) . '\b' if $@; 
     395            return 'Match on pattern: '.$patt if $text =~ m/$re/; 
     396        } else { 
     397            my $re = '\b' . quotemeta($patt) . '\b'; 
     398            return 'Match on phrase: '.$patt if $text =~ m/$re/i; 
     399        } 
     400    } 
     401    0; 
     402} 
     403 
     404sub tbpass_check { 
     405    my ($eh, $app, $obj, $data) = @_; 
     406 
     407     
     408    return IGNORE; 
     409} 
     410 
     411sub commentpass_check { 
     412    my ($eh, $app, $obj, $data) = @_; 
     413 
     414    my $formfield = $data->{commentpass_formfield}; 
     415    my $entryfield = $data->{commentpass_entryfield}; 
     416    my $text = $data->{commentpass_text}; 
     417 
     418    return IGNORE unless $formfield; 
     419 
     420    my $inpass = $app->{query}->param($formfield); 
     421    return BLOCK unless defined $inpass; 
     422 
     423    if ($entryfield) { 
     424        my $entry = MT::Entry->load($obj->entry_id); 
     425        if ($entryfield eq 'title') { 
     426            $text = $entry->title; 
     427        } elsif ($entryfield eq 'excerpt') { 
     428            $text = $entry->excerpt; 
     429        } elsif ($entryfield eq 'keywords') { 
     430            $text = $entry->keywords; 
     431        } elsif ($entryfield eq 'extended') { 
     432            $text = $entry->text_more; 
     433        } 
     434    } 
     435 
     436    foreach ($text, $inpass) { 
     437        s/^\s*//s; 
     438        s/\s*$//s; 
     439    } 
     440 
     441    return ACCEPT if lc($text) eq lc($inpass); 
     442 
     443    return $data->{commentpass_moderate} ? MODERATE : BLOCK; 
     444} 
    262445 
    263446sub pingip_check { 
     
    274457    } 
    275458 
    276     return IGNORE if $pingip eq $domainip; 
    277  
    278     # test for class C ip match 
    279     my $domainipc = $domainip; 
    280     $domainipc =~ s/\.\d+$//; 
    281     my $pingipc = $pingip; 
    282     $pingipc =~ s/\.\d+$//; 
    283     return IGNORE if $pingipc eq $domainipc; 
    284  
    285     my $action = ($data->{pingip_moderate} ? "Moderating" : "Blocking"); 
     459    my @domainip = split /\./, $domainip; 
     460    my @pingip = split /\./, $pingip; 
     461 
     462    my $distance = 4; 
     463    foreach (0..3) { 
     464        if ($domainip[$_] == $pingip[$_]) { 
     465            $distance--; 
     466        } else { 
     467            last; 
     468        } 
     469    } 
     470 
     471    return IGNORE if $distance == 0; 
     472 
     473    my $result = IGNORE; 
     474    $result = BLOCK if $distance >= $data->{pingip_block}; 
     475    $result ||= MODERATE if $distance >= $data->{pingip_moderate}; 
     476 
     477    my $action = ($result == MODERATE ? "Moderating" : "Blocking"); 
    286478    $eh->error("$action TrackBack ping for blog " . $obj->blog_id . 
    287479        " since domain IP does not match ping IP for source URL " . 
    288480        $obj->source_url . "; domain IP: $domainip; ping IP: $pingip"); 
    289     return $data->{pingip_moderate} ? MODERATE : BLOCK; 
     481 
     482    return $result; 
    290483} 
    291484 
     
    418611 
    419612    my $type = $data->{__type}; 
    420     my $nurls = $data->{__nurls}
     613    my $nurls = $data->{__nurls} || 0
    421614    if ($nurls >= $data->{urlcount_block_limit}) { 
    422615        $eh->error("Blocking $type on blog "  . $obj->blog_id . 
     
    463656    my $remote_ip = $obj->ip; 
    464657    foreach my $url (@$urls) { 
     658        next if $url !~ m/\./;  # ignore domain if it is just a single word 
    465659        if ($url =~ m/^(\d+)\.(\d+)\.(\d+)\.(\d+)$/) { 
    466660            $url = "$4.$3.$2.$1"; 
     
    503697sub checkdns { 
    504698    my ($name) = @_; 
     699    require MT::Request; 
     700    my $cache = MT::Request->instance->cache('checkdns_cache') || {}; 
     701    return $cache->{$name} if exists $cache->{$name}; 
    505702    my $iaddr = gethostbyname($name); 
    506703    return 0 unless $iaddr; 
    507704    require Socket; 
    508705    my $ip = Socket::inet_ntoa($iaddr); 
     706    $cache->{$name} = $ip; 
     707    MT::Request->instance->cache('checkdns_cache', $cache); 
    509708    return $ip ? $ip : undef; 
    510709} 
    511710 
    512711sub extract_urls { 
    513     my ($str, $single, $total) = @_; 
    514  
     712    my ($str, $mode, $total) = @_; 
     713 
     714    $mode ||= 0; 
    515715    # unmunge so we can see encoded urls as well 
    516716    $str = lc decode_entities($str); 
     
    523723        $domain =~ s/^www\.//s; 
    524724        next unless $domain; 
     725        next unless $domain =~ m/\./; 
    525726        my @parts = split /\./, $domain; 
    526727        next unless @parts; 
     
    532733            next; 
    533734        } 
    534         return $domain if $single; 
     735        return $domain if $mode == 1; 
     736        next if $seen{$domain}; 
    535737        $$total++ if(defined($total)); 
    536         my $last = $#parts; 
    537         my $start = length($parts[$last]) < 3 ? 2 : 1
    538         if ($start > $last) { 
    539             if (!$seen{$domain}) { 
     738        if ($mode == 0) {  # default mode, replicate for all subdomains 
     739            my $last = $#parts
     740            my $start = length($parts[$last]) < 3 ? 2 : 1; 
     741            if ($start > $last) { 
    540742                $seen{$domain} = 1; 
    541743                push @urls, $domain; 
    542744            } 
    543         } 
    544         foreach (my $i = $start; $i <= $last; $i++) { 
    545             my $partial = join '.', @parts[$last - $i .. $last]; 
    546             next if $seen{$partial}; 
    547             $seen{$partial} = 1; 
    548             push @urls, $partial; 
     745            foreach (my $i = $start; $i <= $last; $i++) { 
     746                my $partial = join '.', @parts[$last - $i .. $last]; 
     747                next if $seen{$partial}; 
     748                $seen{$partial} = 1; 
     749                push @urls, $partial; 
     750            } 
     751        } else { 
     752            $seen{$domain} = 1; 
     753            push @urls, $domain; 
    549754        } 
    550755    } 
     
    553758} 
    554759 
    555 # yanked from HTML::Entities, since some users don't have the module 
    556760sub decode_entities { 
    557761    my ($str) = @_; 
    558     my $c; 
    559     for ($str) { 
    560         s/(&\#(\d+);?)/$2 < 256 ? chr($2) : $1/eg; 
    561         s/(&\#[xX]([0-9a-fA-F]+);?)/$c = hex($2); $c < 256 ? chr($c) : $1/eg; 
    562     } 
    563     $str; 
     762    if (eval { require HTML::Entities; 1 }) { 
     763        return HTML::Entities::decode($str); 
     764    } else { 
     765        # yanked from HTML::Entities, since some users don't have the module 
     766        my $c; 
     767        for ($str) { 
     768            s/(&\#(\d+);?)/$2 < 256 ? chr($2) : $1/eg; 
     769            s/(&\#[xX]([0-9a-fA-F]+);?)/$c = hex($2); $c < 256 ? chr($c) : $1/eg; 
     770        } 
     771        $str; 
     772    } 
    564773} 
    565774 
  • trunk/plugins/spamlookup/tmpl/spamlookup.tmpl

    r43 r47  
    2222 
    2323<p> 
    24 The SpamLookup plugin allows you block comments and TrackBack pings 
    25 from open proxies. The plugin checks the IP address of each submitted 
    26 comment against your choice of IP blacklists of known open proxies. 
     24SpamLookup provides a combination of antispam tests that allow you to 
     25identify and moderate or block incoming or existing weblog spam. 
    2726</p> 
    2827 
    29  
    30 <form method="post" action="mt-spamlookup.cgi"> 
     28<p> 
     29<a href="http://en.wikipedia.org/wiki/Link_spam">Learn more about "Link spam"</a> 
     30</p> 
     31 
     32<p> 
     33<strong>Note:</strong> Please note that all of the following settings are 
     34installation-wide.  They will apply to all of your weblogs for this Movable 
     35Type installation. 
     36</p> 
     37 
     38<p> 
     39<a href="<TMPL_VAR NAME=SCRIPT_URL>?__mode=view&amp;_type=comments">View Comments</a><br /> 
     40<a href="<TMPL_VAR NAME=SCRIPT_URL>?__mode=view&amp;_type=pings">View TrackBacks</a><br /> 
     41<a href="<TMPL_VAR NAME=SCRIPT_URL>?__mode=despam&amp;_type=comments">Despam existing comments</a><br /> 
     42<a href="<TMPL_VAR NAME=SCRIPT_URL>?__mode=despam&amp;_type=pings">Despam existing TrackBack pings</a> 
     43</p> 
     44 
     45<form method="post" action="<TMPL_VAR NAME=SCRIPT_URL>"> 
    3146    <input type="hidden" name="__mode" value="save" /> 
    3247 
     
    4156    </p> 
    4257 
    43     <h3>Filter Options</h3> 
    44  
    45     <p> 
    46     <input type="checkbox" name="allow_auth" id="allow_auth" value="1" <TMPL_IF NAME=ALLOW_AUTH>checked="checked"</TMPL_IF> /> <label for="allow_auth">Don't filter authenticated comments</label><br /> 
     58    <h3>Filt