# Title: MT-Blacklist # Summary: A plugin for preventing comment and Trackback spam # Author: Jay Allen (http://www.jayallen.org/) # Version: 1.5 # Date: October 27, 2003 # # Information about this plugin can be found at # http://www.jayallen.org/projects/mt-blacklist # # Until I get some sleep and can think about licenses: # Copyright 2003 Jay Allen # This code cannot be redistributed without # permission from the author. # CACHE OBJECT REFERENCE # # $_cache->{blacklist} - reference to an array of objects # $_cache->{blog}} - reference to a hash of blog_id -> blog object # $_cache->{blogs_loaded} - scalar boolean indicating a full load of blogs # $_cache->{config} - reference to a hash of config_key = config_value # $_cache->{entry} - reference to a hash of entry_id -> entry object # $_cache->{pattern} - scalar string containing all blacklisted terms # $_cache->{perms}->{blog} - reference to hash of blog permission objects # $_cache->{perms}->{blog}{$b_id} - reference to specific blog permission object # $_cache->{perms}->{canEditAllEntries}{$b_id} - scalar boolean indicating blog/user permission # $_cache->{perms}->{canEditEntry}{$e->id} # $_cache->{plugindataobject} package jayallen::Blacklist; use vars qw($VERSION $_cache @_warnings); $VERSION = '1.5'; use MT::App; @ISA = qw( MT::App ); use strict; use MT::PluginData; sub init { my $app = shift; $app->SUPER::init (@_) or return; $app->add_methods ( 'default' => \&default, 'restore' => \&restore, 'add' => \&add, 'remove' => \&remove, 'remove_all' => \&remove_all, 'view' => \&view, 'editentry' => \&editentry, 'quickadd' => \&quickadd, 'quickdelete' => \&quickdelete, 'add' => \&add, 'add_confirm' => \&add_confirm, 'publish' => \&publish, 'config' => \&config, 'search' => \&search, 'despam' => \&despam, 'despam_multi' => \&despam_multi, 'despam_confirm' => \&despam_confirm, 'debug' => \&debug_app, ); $app->{default_mode} = 'default'; $app->{requires_login} = 1; $app->{user_class} = 'MT::Author'; $app->{template_dir} = 'cms'; if ($_cache->{config} = _getConfig()) { $app->_versionCheck(); } else { #First use -- INSTALL $app->restore(1); } $_cache->{blacklist} = _getBlacklist(); $app->_debug_status($app->{'query'}->param('debug') || 0); $app->{dumpwarnings} = ($app->{'query'}->param('db') || 0); # Uncomment the following line to enable # persistent debugging. Alternatively, # for page by page debugging, you can add # debug=1 parameter to the URL of any page. # # $app->_debug_status(1); $app; } sub default { my $app = shift; my $q = $app->{query}; my ($sort_col,$custom_sort_field); if ($sort_col = $q->param('sort')) { $custom_sort_field++; } else { $sort_col ||= 'string'; } my $html = $app->_navigation_html('Killing Blog Spam Dead','listpage'); # QUICK ADD FORM $html .= $q->start_form('get',$app->uri); $html .= $q->p($q->hidden('__mode', 'quickadd'),"Quick Add: ".$q->textfield(-class=>'text', -name => 'string', -size => 30). $q->submit(-name => '', -value => 'Add').$q->br.'NOTE: Comments are not allowed using quick add'); $html .= $q->end_form; my %messages = ( 'added' => sub { local $_ = shift; $q->p({-class=>'msg_success'}, 'Your entry was sucessfully entered and is highlighted below.') }, 'dupe' => sub { local $_ = shift; $q->p({-class=>'msg_warning'}, 'Your entry was not added because it was either a duplicate of or masked by the highlighted entry below.') }, 'notadded' => sub { local $_ = shift; $q->p({-class=>'msg_failure'}, 'Your entry could not be added because of an unknown error.') }, 'removed' => sub { local $_ = shift; $q->p({-class=>'msg_success'}, 'The entry "'.$q->param('rmstring').'" was removed from the blacklist.') }, 'notremoved' => sub { local $_ = shift; $q->p({-class=>'msg_failure'}, 'The entry "'.$q->param('rmstring').'" could not be removed because of an unknown error.') }, 'quickdeleted' => sub { local $_ = shift; $q->p({-class=>'msg_success'}, 'You successfully deleted '.$q->param('num').' entries.') }, 'DEFAULT' => sub { } ); $html .= &{$messages{($q->param('msg') || 'DEFAULT')}}($q) || ''; # # BLACKLIST FRAGMENT OVERVIEW TABLE # shift(@{$_cache->{blacklist}}); return $html . $q->strong('No entries in blacklist.') unless @{ $_cache->{blacklist} }; my $i = 0; my $sort_dir = $q->param('dir') || 'default'; my @blacklist = map { $_->{'index'} = $i++; $_ } @{ $_cache->{blacklist} }; if ($sort_col ne 'string') { $sort_col = $q->param('sort') || 'string'; # what about bad input? @blacklist = sort { $a->{$sort_col} cmp $b->{$sort_col} } @blacklist; @blacklist = reverse @blacklist if $sort_dir eq 'reverse'; } $html .= $q->start_form('get',$app->uri); my @sortbar = ( { 'display' => 'URL', 'link' => $app->uri }, { 'display' => 'Comments', 'link' => $app->uri.'?sort=comment' }, { 'display' => 'Date Added', 'link' => $app->uri.'?sort=dateAdded' }, { 'display' => 'Date modified', 'link' => $app->uri.'?sort=dateModified' }, { 'display' => 'Added by', 'link' => $app->uri.'?sort=addedBy' }, { 'display' => 'Modified by', 'link' => $app->uri.'?sort=lastModifiedBy' } ); my $sort_by .= 'Sort by: '. join(' | ', map { ''.$_->{'display'}.'' } @sortbar); $html .= $q->p($q->hidden('__mode'), 'There are '.scalar(@blacklist).' entries in your blacklist.'.$q->br. $sort_by ); if ($sort_dir eq 'reverse') { $q->delete('dir'); } else { $q->param('dir', 'reverse'); } my @headings = ('URL Fragment','','', ''. ($custom_sort_field ? ucfirst($sort_col) : 'Comment'). '','Delete'); $q->param('__mode', 'quickdelete'); my @rows = $q->th(\@headings); $i = 0; my $highlight = defined($q->param('highlight')) ? $q->param('highlight') : -1; use MT::Util; for ($i=0;$i{'string'}, 'info', $q->a({-href=>'?__mode=remove&string='. MT::Util::encode_url($_->{'string'})},'remove'), ($custom_sort_field ? $_->{$sort_col} : $_->{'comment'}), $q->checkbox(-name=>'deleteEntry', -value=>$_->{'string'}, -label=>'') ]; if ($highlight == ($i+1)) { unshift(@data, {-class=>'highlight'}); } push(@rows,$q->td(@data)); } push(@rows, $q->td({-colspan=>5,-class=>'right noline'},$q->submit(-name=>'',-value=>'Delete checked entries'))); $html .= $q->table($q->Tr(\@rows) ); $html .= $q->end_form; if (@blacklist > 20) { # Quick add form $html .= $q->start_form('get',$app->uri); $q->param('__mode', 'quickadd'); $html .= $q->p($q->hidden('__mode'), "Quick Add: ".$q->textfield(-class=>'text', -name => 'string', -size => 30). $q->submit(-name => '', -value => 'Add').$q->br.'NOTE: Comments are not allowed using quick add'); $html .= $q->end_form; } $html .= $app->_debug(@blacklist); $html; } sub quickadd { my $app = shift; my $q = $app->{query}; if ($q->param('string') ne '') { my $struct = $app->_prepareBlacklistEntry({'string' => $q->param('string')}); my ($rc,$pos) = $app->_masterAdd($struct); if ($rc) { $app->redirect ($app->uri.'?msg=added&highlight='.$pos); } elsif ($pos) { $app->redirect ($app->uri.'?msg=dupe&highlight='.$pos); } elsif ($app->errstr ne '') { # JAYBO - should return proper error string by url encoding error die $app->errstr; } else { $app->redirect ($app->uri.'?msg=notadded&string='.$struct->{'string'}); } } else { $app->redirect ($app->uri); } } sub quickdelete { # get the blacklist # make sure the entry isnt on the blacklist # eval the entry #add the entry to the blacklist array # save the blacklist # send the user back to view (with entry highlighted) my $app = shift; my $q = $app->{query}; my @delete_strings = $q->param('deleteEntry'); my $rc = $app->_rmFromBlacklist(@delete_strings); return $rc ? $app->redirect($app->uri.'?msg=quickdeleted&num='.$rc) : $app->redirect($app->uri.'?msg=notquickdeleted'); my @pared_blacklist; foreach my $entry (@{ $_cache->{blacklist} }) { my $found = 0; foreach my $delete (@delete_strings) { next unless $delete eq $entry->{'string'}; $found++; last; } push(@pared_blacklist,$entry) unless $found; } if ($app->_saveBlacklist(\@pared_blacklist)) { $app->redirect($app->uri.'?msg=quickdeleted&num='. (scalar(@{ $_cache->{blacklist} })-scalar(@pared_blacklist)). '&tot='.scalar(@delete_strings)); } else { $app->redirect($app->uri.'?msg=notquickdeleted'); } } sub remove { my $app = shift; my $q = $app->{query}; my $str = $q->param('string'); $app->redirect($app->uri) unless $str ne ''; my $rc = $app->_rmFromBlacklist($str); if ($rc) { $app->redirect($app->uri.'?msg=removed&rmstring='.$str); } else { $app->redirect($app->uri.'?msg=notremoved&rmstring='.$str); } } sub view { my $app = shift; my $q = $app->{query}; my $id = $q->param('id'); my ($blEntry,$numEntries) = $app->_getBlacklistEntry($id); my $html = $app->_navigation_html('Killing Blog Spam Dead','viewpage'); my $prev = $id > 1 ? '« previous' : ''; my $next = ($id < ($numEntries-1)) ? 'next »' : ''; $html .= $q->table( $q->Tr({-align=>'left',-valign=>'top'}, [ $q->th({-class=>'prevlink'},$prev). $q->th({-class=>'nextlink'},$next), $q->th({-colspan=>2,-class=>'entryString'},$blEntry->{'string'}), $q->th('Comment').$q->td($blEntry->{'comment'}), $q->th('Added by').$q->td($blEntry->{'addedBy'}.' at '.MT::Util::format_ts("%Y/%m/%d %H:%M:%S",$blEntry->{'dateAdded'})), $q->th('Last modified by').$q->td($blEntry->{'lastModifiedBy'}.' at '.MT::Util::format_ts("%Y/%m/%d %H:%M:%S",$blEntry->{'dateModified'})), ] ) )."\n\n"; # Comment editing form $html .= $q->start_form('post',$app->uri); $q->param('__mode','editentry'); $html .= $q->p( $q->hidden(-name=>'__mode'), $q->hidden(-name=>'id'), $q->hidden('string',$blEntry->{'string'}), $q->strong("Edit comment: ").$q->br. ''. $q->submit(-name => '', -value => 'Submit') ); $html .= $q->end_form; $html .= $app->_debug($blEntry); $html; } sub editentry { my $app = shift; my $q = $app->{query}; my $id = $app->_findBlacklistIndex($q->param('string')); if (defined($id)) { $_cache->{blacklist}->[$id]->{'comment'} = $q->param('comment'); $_cache->{blacklist}->[$id]->{'comment'} =~ s#[\n\r]+# #g; $_cache->{blacklist}->[$id]->{'dateModified'} = _getTimestamp(); $_cache->{blacklist}->[$id]->{'lastModifiedBy'} = $app->{user}->name.' (ID: '.$app->{user}->id.')'; $app->_saveBlacklist(); $app->redirect($app->uri.'?__mode=view&id='.$id); } else { return $q->div({-class=>'msg_failure'}. $q->p("There was an error saving your information: Blacklist entry ID not found")); } } sub add { my $app = shift; my $q = $app->{query}; my @urls = $q->param('url'); my $html = $app->_navigation_html('Add Entries to Blacklist', 'addpage'); $html .= $q->p('Using the form below, you can import one or many entries into the system. ', $q->a({-href=>$q->self_url.'#format', -class=>'helplink'},'[?]') ); # Blacklist importing textarea $q->param('__mode','add_confirm'); $html .= $q->start_form('post',$app->uri); $html .= $q->p( $q->hidden(-name=>'__mode'), $q->strong("Import blacklist: ").$q->br. ''.$q->br. $q->submit(-name => '', -value => 'Import entries') ); $html .= $q->end_form; my @rules = ( 'One entry per line. ', 'Whitespace will be trimmed.', 'Everything on a line after a # is saved in the comment field for that entry.', 'Blank lines and those with only comments are ignored.', 'All "http://" and leading "www." strings will be stripped from the entries.', 'Also, it\'s best to omit anything after the domain name (i.e. directory path) unless it is important.', 'String matching is case-insensitive', 'Perl regular expressions are allowed but optional (as always)' ); $html .= $q->h2({-id=>'format'},'Entry format:').$q->ul($q->li(\@rules)); my $example_entry =<h2('Example:').$q->pre($example_entry); $html .= $q->p('You get the idea... '); $html .= $app->_debug($_cache->{blacklist}); # Is applyToAllWeblogs set? # Yes: # ENTRY settings deletecheckbox # deletesubmitbutton # # No: # ENTRY settings deletecjheckbox # O weblog1 O weblog2 O weblog3 O weblog4 # deletesubmitbutton $html; } sub add_confirm { my $app = shift; my $q = $app->{query}; my $html = $app->_navigation_html('Import Results','addpage'); my @new_entries; my $import = $q->param('entryimport'); unless (@new_entries = $app->_diceImportFileData($import)) { $html .= $q->div({-class=>'msg_failure'}, $q->p("No data could be extracted from your submission. Please go back and check your format again.")); return $html; } if (@new_entries == 1) { $app->{query}->param('string',$new_entries[0]{'string'}); $app->quickadd(); } # Add the unique entries to the database my %strings = ( 'saved' => 0, 'dupes' => [], 'unique' => [] ); ($strings{'saved'}, $strings{'dupes'}, $strings{'unique'}, $strings{'invalid'},) = $app->_masterAdd(@new_entries); if ($strings{'dupes'} && @{$strings{'dupes'}}) { $html .= $q->div({-class=>'msg_warning'}, $q->p("The following entries are either duplicates or longer versions of already existing substrings and will not be added:"), $q->ul($q->li($strings{'dupes'}))); } if ($strings{'dupes'} && @{$strings{'dupes'}}) { $html .= $q->div({-class=>'msg_warning'}, $q->p("The following entries used invalid regular expression syntax and could not be added:"), $q->ul($q->li($strings{'invalid'}))); } return $html unless @{$strings{'unique'}}; if ($strings{'saved'}) { $html .= $q->div({-class=>'msg_success'}, $q->p("The following entries were successfully added to the blacklist:"), $q->ul($q->li($strings{'unique'}))); } else { $html .= $q->div({-class=>'msg_failure'}, $q->p("There was an error adding your unique entries. Please hit the BACK button and try again")); return $html; } $html; } sub publish { my $app = shift; my $q = $app->{query}; my $html = $app->_navigation_html('Publish Blacklist','publishpage'); $html .= $q->p('If you like, you can publish your blacklist for others to use on their systems.'.$q->br.'If you would like this to happen automatically when changes are made, you can set it up in the configuration section.'); # If the form has bee submitted, process and display results my $blPath; if ($blPath = $q->param('publishLoc')) { $blPath =~ s!\\!/!g; ## Change backslashes to forward slashes # Check for security holes and stupidity if (($blPath =~ m!\.\.|\0|\|!) || ($blPath =~ m!\S+\s+\S+!)) { return $app->error($app->translate("Invalid path or filename '[_1]'", $blPath)); } ## Untaint. We already checked for security holes in $blPath. ($blPath) = $blPath =~ /(.+)/s; # Write out Blacklist file my ($rc,$result) = $app->_writeBlacklistFile($blPath); if ($rc) { # Update MT-Blacklist config with default publishLoc if ($blPath ne $_cache->{config}->{'publishLoc'}) { $_cache->{config}->{'publishLoc'} = $blPath; $app->_saveConfig(); } $html .= $q->p({-class=>'msg_success'},"Your blacklist file was successfully published."); } else { $html .= $q->p({-class=>'msg_failure'},"The was an error in publishing your blacklist file:".$q->br.($result || '')); } } my $docroot = $ENV{'DOCUMENT_ROOT'} ? $ENV{'DOCUMENT_ROOT'} : '/FULL/FILE/PATH/TO'; $docroot =~ s#/?$#/#; my $suggested_path = $docroot.'blacklist.txt'; # Set up blacklist file path for form unless ($_cache->{config}->{'publishLoc'}) { $_cache->{config}->{'publishLoc'} = $suggested_path; } $html .= $q->start_form('post',$app->uri); $html .= $q->p( $q->hidden(-name=>'__mode'), $q->strong("Full blacklist file publish path: ").$q->br. $q->textfield(-class=>'text', -name => 'publishLoc', -size=>50, -value => $_cache->{config}->{'publishLoc'}). $q->submit(-name => '', -value => 'Publish') ); $html .= $q->end_form; if ($ENV{'DOCUMENT_ROOT'} && ($_cache->{config}->{'publishLoc'} ne $suggested_path)) { $html .= $q->p($q->strong('Suggested path: '). $q->br. $suggested_path); } $html; } sub config { my $app = shift; my $q = $app->{query}; # Check for user permissions. A user must be able # to configure at least one blog to be allowed to # view this page my $blogs = _getBlogs(); my %bloglabels; for (values %{$blogs}) { my $perms = $app->_getBlogPermissions($_->id); if ($perms) { if ($perms->can_create_blog) { $app->{user}->can_create_blog(1); } if ($perms->can_edit_config || $perms->can_edit_templates) { $bloglabels{$_->id} = $_->name; $app->{user}->can_edit_publishloc(1); } } } return $app->error("You are not authorized to configure any blogs.") unless %bloglabels; my $msg = ''; my %orig_config; if ($q->param('formSubmitted')) { %orig_config = %{$_cache->{config}}; # # EASY SCALAR CONFIG VARIABLES # If variable is defined in submitted paramters, we update the config # If not, the original config value stays # foreach ('blacklistActive', 'logDenials', 'despamAction', 'spamSearchDepth', 'denyResponse') { next if (! $app->{user}->can_create_blog or ($q->param($_) eq '')); $_cache->{config}->{$_} = $q->param($_); } if ($app->{user}->can_edit_publishloc && $q->param('autoPublish') && ($q->param('autoPublish') ne '')) { $_cache->{config}->{autoPublish} = $q->param('autoPublish'); } # # DIFFICULT FILTERED USER ARRAY REFERENCE CONFIG VARIABLES # # The user can only update his or her own blogs configuration for # overrideCommentPosting and overridePingPosting. Since the user # cannot submit values for other blogs, the other blogs # get excised from the array reference if we did it as above. # # The idea below is to create a hash of original config vars: # # $hash{VARIABLE}{BLOG_ID} = 0 or 1 # # Then we update the hash with ONLY values which changed # for ONLY blogs configurable by the user. # my %all_blog_ids; %all_blog_ids = map {$_,''} keys %{$blogs}; my (%original_override_status,%new_override_status); foreach my $key ('overrideCommentPosting','overridePingPosting') { # Get status for all blogs my %hash; @hash{@{$_cache->{config}->{$key}}} = (); foreach my $blog_id (keys %all_blog_ids) { if (exists($hash{$blog_id})) { $original_override_status{$key}{$blog_id} = 1; } else { $original_override_status{$key}{$blog_id} = 0; } } #$test .= "Original config for $key: ".$app->_debug(\%original_override_status); #JAYBO # Initialize new hash with original status $new_override_status{$key} = $original_override_status{$key}; #$test .= "Incoming param status for $key: ".$app->_debug($q->param($key)) if $q->param($key); my %request; foreach ($q->param($key)) { $request{$_} = (); } #$test .= "Request Dump: ". $app->_debug(\%request); foreach my $userblog (keys %bloglabels) { #$test .= $app->_debug("Exists RequestUserblog $userblog: ". exists($request{$userblog})."
\n". # "Orig status $key userblog $userblog: ".$original_override_status{$key}{$userblog}); if (exists($request{$userblog}) && ! $original_override_status{$key}{$userblog}) { $new_override_status{$key}{$userblog} = 1; } elsif (! exists($request{$userblog}) && $original_override_status{$key}{$userblog}) { $new_override_status{$key}{$userblog} = 0; } } #$test .= "Final config for $key: ".$app->_debug(\%new_override_status); #JAYBO $_cache->{config}->{$key} = []; foreach (keys %{$new_override_status{$key}}) { next unless $new_override_status{$key}{$_}; push(@{$_cache->{config}->{$key}}, $_); } } if ($q->param('publishLoc') && $app->{user}->can_edit_publishloc) { my $blPath = $q->param('publishLoc'); $blPath =~ s!\\!/!g; ## Change backslashes to forward slashes # Check for security holes and stupidity if (($blPath =~ m!\.\.|\0|\|!) || ($blPath =~ m!\S+\s+\S+!)) { return $app->error($app->translate("Invalid path or filename '[_1]'", $blPath)); } ## Untaint. We already checked for security holes in $blPath. ($blPath) = $blPath =~ /(.+)/s; $_cache->{config}->{'publishLoc'} = $blPath; } if ($app->_saveConfig()) { $app->log('User \''.$app->{user}->name.'\' changed the MT-Blacklist configuration'); $msg =$q->p({-class=>'msg_success'},'Your configuration was saved.'); # my $subject = "Blacklist configuration change"; # my $body = 'User '.$app->{user}->name.' (UserID: '.$app->{user}->id. # ') just changed the configuration for MT-Blacklist. '."\n\n". # 'Listed below is each configuration setting that was changed '."\n\n"; # # my %englishizer = ( # 0 => 'off', # 1 => 'on', # 'blacklistActive' => 'MT-Blacklist status', # 'autoPublish' => 'blacklist auto-publishing status', # 'logDenials' => 'activity log usage status', # 'despamAction' => 'default despam action', # 'publishLoc' => 'blacklist auto-publish location', # 'overrideCommentPosting' => 'comment blocking', # 'overridePingPosting' => 'ping blocking', # 'overrideCommentTags' => 'comment filtering', # 'overridePingTags' => 'ping filtering' # ); # my $html = 'HI'; # # foreach ('blacklistActive','autoPublish','publishLoc','logDenials','despamAction') { # if ($app->{Blacklist_config}->{$_} ne $orig_config{$_}) { # $html .= $q->p("Old $englishizer{$_}: $englishizer{$orig_config{$_}}\n",$q->br, # "New $englishizer{$_}: $englishizer{$app->{Blacklist_config}->{$_}}\n"); # } # } # return $html; # foreach ('overrideCommentPosting','overridePingPosting', # 'overrideCommentTags','overridePingTags') { # next if $q->param($_) eq ''; # $app->{Blacklist_config}->{$_} = [ $q->param($_) ]; # } # # 'Blacklist_config' => { # 'logDenials' => 1, # 'overrideCommentPosting' => [ # '3', # '4' # ], # 'overrideCommentTags' => [ # '3', # '4' # ], # 'publishLoc' => '/home/j/other_sites/plugindev/www/blacklist.txt', # 'autoPublish' => 1, # 'blacklistActive' => 1, # 'pluginVersion' => '1.1-dev', # 'overridePingPosting' => [ # '3', # '4' # ], # 'overridePingTags' => [ # '3', # '4' # ], } else { $msg =$q->p({-class=>'msg_failure'},'There was an error in saving your configuration.'); } } my $html = $app->_navigation_html('Configuration','configpage') . $msg; $html .= $q->start_form('post',$app->uri); $q->param('__mode','config'); # Only users who can create blogs can flip the master switch if ($app->{user}->can_create_blog) { $html .= $q->h3('MT-Blacklist Master Switch'); $html .= $q->p({-class=>'question'}, 'Would you like to activate MT-Blacklist? ', $q->radio_group(-name => 'blacklistActive', '-values' => [1,0], '-default' => $_cache->{config}->{'blacklistActive'}, -labels => {1=>'Yes', 0=>'No'}) ). $q->p({-class=>'description'},'If you select Yes, MT-Blacklist will use the settings below in its operations. If you select No, your MT installation will continue as if MT-Blacklist did not exist.'); } $html .= $q->h3('Spam Blocking'); $html .= $q->p({-class=>'question'},'What actions would you like MT-Blacklist to take for each weblog?'); $html .= $q->p({-class=>'description'},'When MT-Blacklist protection is active, the plugin will only block the submission types you specify for the weblogs you specify. Unchecking all boxes has the same effect as deactivating the plugin in the option above (except that your expend more energy doing so).',$q->br, 'Note: If you uncheck any of the following, MT-Blacklist will not override Movable Type\'s native methods for that specific combination. This means that you will not receive the de-spam link in your notification email for that combination.'); my (%override); foreach my $cfgkey ('overrideCommentPosting','overridePingPosting') { %{$override{$cfgkey}} = map { $_,1} @{$_cache->{config}->{$cfgkey}}; } my @topheadingrow = $q->th('') . $q->th({-colspan=>2},'Blocking spam'); my @secondheadingrow = $q->th(['','Comments','Trackbacks']); my @datarows = (); foreach (keys %bloglabels) { my @data = ( $q->checkbox(-name=>'overrideCommentPosting', -checked=> $override{overrideCommentPosting}{$_} ? 'checked' : 0, -value=>$_, -label=>''), $q->checkbox(-name=>'overridePingPosting', -checked=> $override{overridePingPosting}{$_} ? 'checked' : 0, -value=>$_, -label=>'') ); push(@datarows,$q->th({-class=>'blognames'},$bloglabels{$_}).$q->td([@data])); } $html .= $q->table({-id=>'actiontable'}, $q->Tr({-class=>'topheader'},\@topheadingrow), $q->Tr({-class=>'bottomheader'},\@secondheadingrow), $q->Tr(\@datarows) ); if ($app->{user}->can_create_blog) { $html .= $q->p({-class=>'question'},'What response would you like to return for denied comments/pings?'); $html .= $q->p({-class=>'description'},'Below you can customize your response to a user who has had their submission denied. '); $html .= $q->p( $q->textfield(-class=>'text', -name => 'denyResponse', -size=>70, -value => $_cache->{config}->{'denyResponse'}) ); $html .= $q->p({-class=>'description'},'Variables'.$q->br. '__TYPE__ = the submission type (either "comment" or "ping")'.$q->br. '__BLACKLIST__ = the first entry on the blacklist that was matched'.$q->br. '__TEXT__ = the specific text fragment that was matched in the submission '); $html .= $q->p({-class=>'question'},'Would you like logging of blocked posts?', $q->radio_group(-name => 'logDenials', '-values' => [1,0], '-default' =>$_cache->{config}->{'logDenials'}, -labels => {1=>'Yes', 0=>'No'}) ). $q->p({-class=>'description'},'MT-blacklist can log blocked attempts of comment and trackback spam in your Movable Type activity log. The first blacklisted string matched, the IP address of the probable spammer and time of denial will be logged. This is not only a great way too see how effective the plugin is, but it could also be the only way you will know if you mistakenly entered something that is blocking every submission.'); $html .= $q->h3('Search & De-spam'); my @despamAction_labels = ( {value => 0, label => "Don't delete the comment/ping or rebuild"}, {value => 'delete', label => "Delete the comment/ping"}, {value => 'deleteRebuildEntry', label => "Delete the comment/ping and rebuild the entry"}, {value => 'deleteRebuildEntryIndexes', label => "Delete the comment/ping and rebuild the entry/indexes"}); my $despamAction_default = $_cache->{config}->{'despamAction'} || 0; $html .= $q->p({-class=>'question'},'What is your preferred de-spamming action?'); $html .= $q->p({-class=>'description'},'Below you can set the default action to be taken when using de-spam mode from the link in your comment notification email. This configuration setting is only the default and can be changed on a case-by-case basis on the de-spam page.'); $html .= '

'; $html .= $q->p({-class=>'question'}, 'How deep should search go? (# of comments/pings)     ', $q->textfield(-class=>'text', -name => 'spamSearchDepth', -size=>4, -value => $_cache->{config}->{'spamSearchDepth'}) ); $html .= $q->p({-class=>'description'},'Spam search mode (accessed by clicking the de-spam button on the toolbar or from the de-spam results page) will search back through your most recent comments/pings for those which match your blacklist. Here you can set your default number of comments/pings to search back through. The higher the number of comments/pings, the longer it takes so setting this low is a good choice. Plus, you can specify a higher value on the results page in case of a major spam attack.'); } # Only users who can create blogs can fiddle with file writing if ($app->{user}->can_edit_publishloc) { $html .= $q->h3('Automatic Blacklist Publishing'); $html .= $q->p({-class=>'question'},'Would you like to publish your blacklist on your site after each change?', $q->radio_group(-name => 'autoPublish', '-values' => [1,0], '-default' =>$_cache->{config}->{'autoPublish'}, -labels => {1=>'Yes', 0=>'No'}) ). $q->p({-class=>'description'},'If you so choose (and we encourage this choice), you can publish your blacklist to your website automatically upon change. This offers others near-real-time protection against spammers you have discovered. For the sake of standards, it is recommended that the file be named blacklist.txt and reside in the root directory of your website. The possible network effect here is certainly delicious. '); $html .= $q->p( $q->strong('If yes, enter the full path and filename of the file:'). $q->br. $q->textfield(-class=>'text', -name => 'publishLoc', -size=>50, -value => $_cache->{config}->{'publishLoc'}) ); } $html .= $q->p( $q->hidden('__mode'), $q->hidden(-name=>'formSubmitted', -value=>1), $q->submit(-name => '', -value => 'Save Configuration') ); $html .= $q->end_form; # Only users who can create blogs may do a restore if ($app->{user}->can_create_blog) { $q->param('__mode','restore'); $html .= $q->div({-id=>'restoreform'}, $q->h4("Restore default settings"), $q->p('If for any reason you need to restore the default installation settings, you can do so below. This will delete your entire blacklist and configuration settings and restore the original list and settings.'), $q->start_form('post',$app->uri), $q->p( $q->hidden('__mode'), $q->checkbox(-name=>'yesImSure', -value=>'1', -label=>'I don\'t really want my blacklist and configuration data.') ), $q->p( $q->submit(-name => '', -value => 'Restore default configuration and blacklist') ), $q->end_form); } $html .= $app->_debug($_cache->{config}); # $html .= $test; $html; } sub remove_all { my $app = shift; my $q = $app->{query}; $app->_saveBlacklist([{string => ''}]); $app->redirect($app->uri); } sub restore { my $app = shift; my $q = $app->{query}; my $newInstall = shift || ''; unless ($newInstall) { $app->{user}->can_create_blog or return $app->error("You are not authorized to perform this action."); } my $idiotCheck = $q->param('yesImSure') || $newInstall; unless ($idiotCheck == 1) { return $app->redirect($app->uri); } my $initial_blacklist = $app->_defaultBlacklist(); my $blacklist_data = [$app->_diceImportFileData($initial_blacklist)]; # NEW WAY for (@$blacklist_data) { $app->_prepareBlacklistEntry($_); $_->{'addedBy'} = "MT-Blacklist $VERSION Install"; $_->{'lastModifiedBy'} = "MT-Blacklist $VERSION Install"; } $app->_saveBlacklist($blacklist_data); my $docroot = $ENV{'DOCUMENT_ROOT'} ? $ENV{'DOCUMENT_ROOT'} : '/FULL/FILE/PATH/TO'; $docroot =~ s#/?$#/#; my $config_data = { pluginVersion => $VERSION, blacklistActive => 0, publishLoc => $docroot.'blacklist.txt', autoPublish => (-d $docroot ? 1 : 0), updateNotifyURLs => [], logDenials => 1, despamAction => 'deleteRebuildEntryIndexes', spamSearchDepth => 25, denyResponse => 'Your __TYPE__ could not be submitted due to questionable content: __TEXT__' }; my $blogs = _getBlogs(); my @blog_ids = map { $_ } keys %{$blogs}; foreach ('overrideCommentPosting','overridePingPosting') { $config_data->{$_} = [ @blog_ids ]; } $app->_saveConfig($config_data); return $newInstall ? $app->redirect($app->uri.'?__mode=config') : $app->redirect($app->uri); } sub despam { my $app = shift; my $q = $app->{query}; my $html = $app->_navigation_html('De-spamming your blog','despampage'); my $type = $q->param('_type') || ''; my (@extract_strings,$comment,$tb,$entry,$comment_mt_url,$raw_source); if ($type eq 'comment') { require MT::Comment; $comment = MT::Comment->load($q->param('id')) or return $app->error("The $type could not be loaded for de-spamming. (". ucfirst($type) .': '. $q->param('id') .')'); $comment_mt_url = $app->{cfg}->CGIPath. 'mt.cgi?__mode=view&_type=comment&id='. $comment->id. '&blog_id='. $comment->blog_id; @extract_strings = ($comment->text,$comment->url,$comment->author); $raw_source = ($comment->author || '') ."\n".($comment->url || '')."\n\n".($comment->text || ''); } elsif ($type eq 'ping') { require MT::TBPing; my $ping = MT::TBPing->load($q->param('id')) or return $app->error("The $type could not be loaded for de-spamming. (". ucfirst($type) .': '. $q->param('id') .')'); @extract_strings = ($ping->title,$ping->excerpt,$ping->source_url); $raw_source = ($ping->title || '')."\n".($ping->source_url || '')."\n\n".($ping->excerpt || ''); } else { return $app->error("Improper object type: ".$type); } $html.= $q->h2("De-spamming Movable Type"); $html .= $q->p('Below you will find the extracted URLs from ', $type eq 'comment' ? $q->a({-href=>$comment_mt_url},'the comment') : 'the ping', ' in question. On this page, you can add those URLs to your blacklist, delete the comment and rebuild. All in one step!' ); my @unique = $app->_extractURLs(@extract_strings); $html .= $q->start_form('post',$app->uri); $html .= $q->h3('Found URLs'); if (@unique) { $html .= $q->p({-class=>'description'},"The following unique URL fragments were found in the $type."). $q->p({-class=>'description'}, "Please select the ones you would like to ban and edit them as you would on the ", $q->a({-href=>$app->uri.'?__mode=add'},'Add page'),'(e.g. adding comments). If there are strings that you want to ban that were not extracted, you can copy and paste them from the raw source of the '.$type.' submission '.$q->a({-href=>'#rawsource'},'below').': '); my $rows = scalar(@unique) > 20 ? 20 : scalar(@unique); $rows = $rows < 5 ? 5 : $rows; $html .= '

'; } else { $html .= $q->p('No URL fragments were found.'); } $html .= $q->h3('Taking out the trash'). $q->p({-class=>'description'},"You have the option to delete the $type ", 'and rebuild the entry if you like.',$q->hidden('id')); my @despamAction_labels = ( {value => 0, label => "Don't delete the $type or rebuild"}, {value => 'delete', label => "Delete the $type"}, {value => 'deleteRebuildEntry', label => "Delete the $type and rebuild the entry"}, {value => 'deleteRebuildEntryIndexes', label => "Delete the $type and rebuild the entry/indexes"}); my $despamAction_default = $_cache->{config}->{'despamAction'} || 0; $html .= '

'; $q->param('__mode','despam_confirm'); $html .= $q->p({-class=>'right'}, $q->hidden('__mode'),$q->hidden('_type'), $q->submit(-name => '', -value => 'Go forth now and do my bidding!')).$q->hr; $html.= $q->h3({-id=>'rawsource'},"The original $type content:"); my $rows = () = $raw_source =~ /\n/g; ; $rows = $rows > 20 ? 20 : $rows; $rows = $rows < 5 ? 5 : $rows; $html .= $q->p(''); $html .= $q->end_form; $html; } sub despam_confirm { # This could use some cleaning up and abstraction # of common actions with add_confirm my $app = shift; my $q = $app->{query}; my $type = $q->param('_type'); my $html = $app->_navigation_html('De-spamming Results','despampage'); $html .= $q->h2('De-spamming results'); # ADD ENTRIES TO BLACKLIST my @new_entries; my %results = ( 'success' => [], 'dupes' => [] ); # ADD ENTRIES TO BLACKLIST IF PRESENT if (my $import = $q->param('foundURLs')) { foreach (@new_entries = $app->_diceImportFileData($import)) { if (defined($app->_fuzzyFindBlacklistIndex($_->{'string'}))) { push(@{$results{'dupes'}}, $_->{'string'}); next; } push(@{ $_cache->{blacklist} }, $_); push(@{$results{'success'}}, $_->{'string'}); } if (@{$results{'success'}}) { $results{'blacklist_saved'} = $app->_saveBlacklist(); my $logstring = 'User \''.$app->{user}->name.'\' added '. (@{$results{'success'}} == 1 ? '\''.${$results{'success'}}[0].'\'' : (@{$results{'success'}}).' entries'). ' to the MT-Blacklist'; $app->log($logstring); } } # DELETE THE COMMENT/PING IF REQUESTED my ($object,$findMoreLink); if (my $action = $q->param('despamAction')) { # Load up the ping/comment my $id = $q->param('id'); if ($type eq 'comment') { use MT::Comment; $object = MT::Comment->load($id); } elsif ($type eq 'ping') { use MT::TBPing; $object = MT::TBPing->load($id); } return $app->error("Could not load $type for deletion (ID: $id):") unless $object ; $findMoreLink = $q->p('Find other '.$type.'s that match your blacklist or this user\'s IP address.'); if ($app->_deleteObject($object)) { $results{'obj_removal'} = $q->strong({-class=>'msg_success'},'SUCCEEDED'); if ($action eq 'deleteRebuildEntry') { $results{'entry_rebuild'} = $app->_rebuildObjectEntry($object); } elsif ($action eq 'deleteRebuildEntryIndexes') { $results{'entry_rebuild'} = $app->_rebuildObjectAll($object); } if ($results{'entry_rebuild'}) { $results{'entry_rebuild'} = $q->strong({-class=>'msg_success'},'SUCCEEDED'); } elsif (exists($results{'entry_rebuild'})) { $results{'entry_rebuild'} = $q->strong({-class=>'msg_failure'},'FAILED - '. ($app->errstr || 'Unknown error')); } else { $results{'entry_rebuild'} = $q->strong({-class=>'msg_info'},'Not requested'); } } else { $results{'obj_removal'} = $q->strong({-class=>'msg_error'},'FAILED'. ($app->errstr || 'Unknown error')); $results{'entry_rebuild'} = $q->strong({-class=>'msg_info'},'NOT DONE'); } } else { $results{'entry_rebuild'} = $results{'obj_removal'} = $q->strong({-class=>'msg_info'},'Not requested'); } $html .= $findMoreLink || ''; # If we didn't do anything, tell the user. unless (@new_entries || $q->param('despamAction')) { $html .= $q->div({-class=>'msg_info'}, $q->ul($q->li(["No action was taken."])) ); return $html; } $html .= $q->div({-class=>'msg_info'}, $q->ul($q->li([ucfirst($type).' removal: '.$results{'obj_removal'}, 'Entry rebuild: '.$results{'entry_rebuild'} ])) ); if (@{$results{'dupes'}}) { $html .= $q->div({-class=>'msg_warning'}, $q->p("The following entries are duplicates or longer versions of already existing substrings and will not be added:"), $q->ul($q->li($results{'dupes'}))); } return $html unless @{$results{'success'}}; if ($results{'blacklist_saved'}) { $html .= $q->div({-class=>'msg_success'}, $q->p("The following entries were successfully added to the blacklist:"), $q->ul($q->li($results{'success'}))); } else { $html .= $q->div({-class=>'msg_failure'}, $q->p("There was an error adding your entries. Please hit the BACK button, uncheck the 'delete $type' checkbox and try again")); } $html .= $findMoreLink || ''; $html; } sub despam_multi { my $app = shift; my $q = $app->{query}; my $type = $q->param('_type'); my $html = $app->_navigation_html('De-spamming Results','despampage'); $html .= $q->h2('De-spamming results'); my @objects = $q->param('deleteObject'); my $rebuild = $q->param('rebuildEntries'); unless (@objects) { my $html = $app->_navigation_html('De-spamming Results','despampage'); $html .= $q->h2('De-spamming results'); $html .= $q->div({-class=>'msg_info'}, $q->ul($q->li([ 'No action taken.' ])) ); return $html; } # Define anonymous subroutine to grab entry my ($entry_grab,$extract_strings); if ($type eq 'comment') { $extract_strings =sub { my $obj = shift; return ($obj->text,$obj->url,$obj->author); }; } else { $extract_strings = sub { my $obj = shift; return ($obj->blog_name,$obj->title,$obj->excerpt,$obj->source_url); }; } my (%entries_modified,@errors,@deleted,@extracted_urls) = (); my (%editAllPosts, %blogPerms, %editPost); foreach my $id (@objects) { # Load up the ping/comment my $object; if ($type eq 'comment') { use MT::Comment; $object = MT::Comment->load($id); } elsif ($type eq 'ping') { use MT::TBPing; $object = MT::TBPing->load($id); } my $e; if ($object) { unless ($app->_canEditObject($type,$object)) { push(@errors, 'You have no permission to edit '.$object->id); next; } if ($app->_deleteObject($object)) { push(@deleted, $object); $entries_modified{$object->id} = _getObjectEntry($type,$object); push(@extracted_urls, $app->_extractURLs($extract_strings->($object))); } else { push(@errors, "Could not delete $type (ID: $id):". ($object->errstr || 'Unknown error')); } } else { push(@errors, "Could not load $type for deletion (ID: $id).") ; } } if (%entries_modified && $rebuild) { my %seen = (); foreach my $entry (values %entries_modified) { next if $seen{$entry->id}; if ($app->_rebuildEntry($entry,1)) { $seen{$entry->id}++; } else { push(@errors, 'Error rebuilding entry ID '.$entry->id); } } } $html .= $q->div({-class=>'msg_info'}, $q->ul($q->li([scalar(@deleted).' of '. scalar(@objects).' '.$type.'s were successfully deleted.']))); if (@extracted_urls) { $q->param('__mode', 'add'); $html .= $q->start_form('post',$app->uri); $html .= $q->div({-class=>'msg_info'}, $q->p('URLs were found in the deleted comments. ', $q->hidden('__mode'), (map { $q->hidden('url',$_) } @extracted_urls), $q->submit(-name=>'',-value=>'Click to inspect and add'))); $html .= $q->end_form; } if (@errors) { $html .= $q->div({-class=>'msg_failure'}, $q->p('The following errors were encountered:'), $q->ul($q->li([@errors]))); } $html; } sub search { my $app = shift; my $q = $app->{query}; my $type = $q->param('_type') || 'comment'; my $matchType = $q->param('matchType') || 'blacklist'; my $ip = $q->param('ip') if ($matchType eq 'ip'); my $searchDepth = $q->param('n') || $_cache->{config}->{spamSearchDepth}; my $showSpamText = defined($q->param('showSpamText')) ? $q->param('showSpamText') : ''; if ($showSpamText ne '') { $_cache->{config}->{showSpamText} = $showSpamText ? 1 : 0; $app->_saveConfig(); } my (%blognames,@filtered_objects); my $objects = $app->_getUserObjects($type,$searchDepth); foreach my $obj (@{$objects}) { my $string; if ($type eq 'comment') { foreach ($obj->author, $obj->url, $obj->email, $obj->text) { $string .= $_ if $_ && $_ ne ''; } } else { foreach ($obj->blog_name, $obj->title, $obj->excerpt, $obj->source_url) { $string .= $_ if $_ && $_ ne ''; } } my $e = _getObjectEntry($type,$obj); unless ($blognames{$obj->blog_id}) { my $blog = _getBlog($obj->blog_id) or return $app->error('Could not load blog ID '.$obj->blog_id); $blognames{$obj->blog_id} = $blog->name; } # Check whether comment matches IP address if ($ip && $obj->ip && ($obj->ip eq $ip)) { push(@filtered_objects, {class => 'ip_address', blog => $blognames{$obj->blog_id}, entry => $e, object => $obj, matched => $ip}); # Check whether comment matches the blacklist } elsif (!$ip && (my ($rc,$blacklist_string,$matched_string) = _matchBlacklist($obj->blog_id, $string))) { push(@filtered_objects, {class => 'blacklist', blog => $blognames{$obj->blog_id}, entry => $e, object => $obj, blacklist_string => $blacklist_string, matched_string => $matched_string}); } } my $html = $app->_navigation_html('Comment/Trackback Search','searchpage'); my %defaults = (blacklist => '', ip => '', comment => '', ping => ''); $defaults{$matchType} = 'checked="checked" '; $defaults{$type} = ' selected="selected" '; $html .= $q->start_form('get',$app->uri); $html .= $q->div({-class => 'searchbox'},"Search through the last ", ' ', ' ', ' for
', '
Blacklist matches
'. ' IP Address '. '
'. ''); $html .= $q->end_form; $html .= $q->h2(ucfirst($type).' spam search'); # my $type_toggle = $type eq 'comment' ? 'ping' : 'comment'; # (my $toggle_type_link = $q->self_url) =~ # s/;?_type=(?:comment|ping)//; # $toggle_type_link = $q->p('Search for '.$type_toggle. 's'); if (! @filtered_objects) { $html .= $q->p('Out of the last '.$searchDepth.' '.$type.'s, there are none on entries for which you have editing privileges that match '. ($ip ? "the IP Address $ip." : 'your current blacklist. ')); # return $html.$toggle_type_link; return $html; } # $html .= $toggle_type_link; my $spamTextToggle = $_cache->{config}->{showSpamText} ? 0 : 1; (my $spamTextToggleURL = $q->self_url) =~ s/(.+?)(?:;showSpamText=[01])?$/$1;showSpamText=$spamTextToggle/; $html .= $q->start_form('post',$app->uri); $q->param('__mode', 'despam_multi'); $q->param('_type', $type); my $match_text = $ip ? "the IP address $ip" : 'your current blacklist'; $html .= $q->p({-class=>'description'},$q->hidden('__mode'), $q->hidden('_type'), 'Out of the last ',$searchDepth,' ',$type.'s, there are '.scalar(@filtered_objects). ' '.$type.'s on entries for which you have editing '. "privileges that match $match_text. ". 'Select the ones below which you would like to delete. You may '. 'toggle the '.$type.' text if you like.'); $html .= ''; my @headings; if ($type eq 'comment') { @headings = ('Spam','Author','IP Address','Email','URL'); } else { @headings = ('Spam','Blog','Title','Source URL'); } $html .= ''; require MT::Util; my $count = 0; foreach (@filtered_objects) { my $obj = $_->{object}; my $alternate = ($count++ % 2 == 1) ? 'odd' : 'even'; if ($type eq 'comment') { $html .= ''; } else { $html .= ''; } unless ($ip) { $html .= ''; } if ($_cache->{config}->{showSpamText}) { my $text = $type eq 'comment' ? MT::Util::first_n_words(MT::Util::encode_html($obj->text),10) : $obj->excerpt; $html .= ''; } my $comment_mt_url; if ($type eq 'comment') { $comment_mt_url = $app->{cfg}->CGIPath. 'mt.cgi?__mode=view&_type=comment&id='. $obj->id. '&blog_id='. $obj->blog_id; } $html .= ''; } $html .= '
'.join('', @headings).'
'. ''. $obj->author.''. $obj->ip.''. $obj->email.''. $obj->url.'
'. ''. $obj->blog_name.''. $obj->title.''. $obj->source_url.'
Blacklist entry matched: '. $_->{blacklist_string}.'
'. $text.'
'. ($comment_mt_url ? '' : '').'Posted to '.$_->{blog}.' entry "'. $_->{entry}->title.'"'. ($comment_mt_url ? '' : '').'
'; $html .= $q->p(''. 'Rebuild the relevant entries after '.$type.' deletion'); $html .= $q->p($q->submit(-name => '', -value => 'Delete checked '.$type.'s')); $html .= $q->end_form; $html; } sub debug_app { my $app = shift; $app->{user}->can_create_blog or return $app->error("You are not authorized to view this page"); my $html = $app->_navigation_html('Debug Mode','debugpage'); $app->_debug_status(1); $html .= '

APP OBJECT

'.$app->_debug($app); $html .= '

CACHE OBJECT

'.$app->_debug($_cache); $html; } sub _versionCheck { my $app = shift; # # If the config is current, then do nothing # return 1 if $_cache->{config}->{pluginVersion} eq $VERSION; # # Get rid of old configuration directives # foreach ('despamShowRawContent', 'applyToAllWeblogs', 'protectedBlogs', 'overridePingTags', 'overrideCommentTags') { if (exists($_cache->{config}->{$_})) { delete $_cache->{config}->{$_}; } } # # Add new configuration directives # my $blogs = _getBlogs(); my @blog_ids = map { $_ } keys %{$blogs}; foreach ('overrideCommentPosting','overridePingPosting') { next if exists($_cache->{config}->{$_}); $_cache->{config}->{$_} = [ @blog_ids ]; } $_cache->{config}->{denyResponse} ||= 'Your __TYPE__ could not be submitted due to questionable content: __TEXT__'; $_cache->{config}->{denyResponse} =~ s/__STRING__/__TEXT__/g; $_cache->{config}->{despamAction} ||= 'deleteRebuildEntryIndexes'; $_cache->{config}->{spamSearchDepth} ||= '25'; # # Save updated configuration # $_cache->{config}->{pluginVersion} = $VERSION; $app->_saveConfig(); # # Update default blacklist entries if present # my %modified = ( '([\w\-_.]+.)?lstor.[a-z]+' => '([\w\-_.]+\.)?(l(so|os)tr)\.[a-z]{2,}', 'dr.ag' => 'dr\.ag', 'penis[_.\-]?enlargement([\w_.\-]+)?\.\w+' => '(penis)[_.\-]?enlargement([\w\-_.]+)?\.[a-z]{2,}', 'ragazze-?\w+.\w+' => '(ragazze)-?\w+\.[a-z]{2,}', 'buy[\w_.\-]*online[\w_.\-]*.[A-Za-z]+' => '(buy)[\w\-_.]*online[\w\-_.]*\.[a-z]{2,}' ); my %deletions = ('[\w\-_.]*phentermine[\w\-_.]*.[a-z]+' => 1, '[\w\-_.]*phentermine[\w\-_.]*\.[a-z]+' => 1, '[\w\-_.]*viagra[\w\-_.]*.[a-z]+' => 1, 'vigrx.isgreat.tv' => 1 ); my @additions = ('(phentermine|viagra|vig-?rx)[\w\-_.]*\.[a-z]{2,} # Catchall regexp for domains containing phentermine, viagra, vigrx and vig-rx', 'barcodes.cn', 'certificationking.net', 'chickz.com', 'discount-life-insurance.us', 'hgh-online.com', 'menguma.com', 'pj-city.com', 'svitonline.com', 'wet-4all.com', 'wethorny.com', 'zipcodedownload.com', 'zipcodesmap.com' ); my $new_blacklist_data = [$app->_diceImportFileData(join("\n", @additions))]; for (@$new_blacklist_data) { $_->{'addedBy'} = "MT-Blacklist $VERSION Upgrade"; $_->{'lastModifiedBy'} = "MT-Blacklist $VERSION Upgrade"; } my $blacklist = _getBlacklist(); my %seen; for (@{$blacklist}) { if ($modified{$_->{string}}) { $_->{string} = $modified{$_->{string}}; } elsif ($deletions{$_->{string}}) { undef $_; next; } elsif ($_->{string} eq '') { undef $_; next; } $seen{$_}++; } @{$blacklist} = grep {defined} @{$blacklist}; push(@{$blacklist}, @$new_blacklist_data); $app->_saveBlacklist($blacklist); } sub _checkRegexp { my $app = shift; my $reg = shift; my $line = 'jay_rocks'; eval { $line =~ m#$reg#; 1; }; return $@ ? $app->error($@) : 1; } sub _masterAdd { my $app = shift; my @entries = @_; my $q = $app->{query}; return unless @entries; my $last_dupe = ''; my %results = ( 'blacklist_saved' => 0, 'unique' => [], 'dupes' => [], 'invalid' => [] ); foreach (@entries) { if (! $app->_checkRegexp($_->{string})) { push(@{$results{'invalid'}}, $app->errstr); next; } elsif ($last_dupe = $app->_fuzzyFindBlacklistIndex($_->{'string'})) { push(@{$results{'dupes'}}, $_->{'string'}); next; } push(@{ $_cache->{blacklist} }, $_); push(@{$results{'unique'}}, $_->{'string'}); } # If we had at least one new entry, save the blacklist if (@{$results{'unique'}}) { $results{'blacklist_saved'} = $app->_saveBlacklist(); my $logstring = 'User \''.$app->{user}->name.'\' added '. (@{$results{'unique'}} == 1 ? '\''.${$results{'unique'}}[0].'\'' : (@{$results{'unique'}}).' entries'). ' to the MT-Blacklist'; $app->log($logstring); } # Return results to caller with multiple items since it's easy. if (@entries > 1) { return ($results{'blacklist_saved'}, $results{'dupes'}, $results{'unique'}, $results{'invalid'}); } # Now handle callers with only one item my $str = ${$results{'unique'}}[0]; # A dupe gets back (0,dupe_id) if ($last_dupe ne "") { return ($results{'blacklist_saved'},$last_dupe); } # An invalid regex gets back just 0 with $app->errstr return 0 if $app->errstr; return $results{'blacklist_saved'} ? # A unique and successful save gets (1,new_id) ($results{'blacklist_saved'}, $app->_findBlacklistIndex($str)+1) : # It is a mystery why I have to add 1 # A unique and failed save (0) $results{'blacklist_saved'}; } sub _findBlacklistIndex { my $app = shift; my ($str,$fuzzy) = @_; my @strings = map { $_->{'string'}} @{ $_cache->{blacklist} }; my $i; for ($i = 0; $i < @strings; $i++) { next if $strings[$i] eq ''; if ($strings[$i] eq $str) { return $i; } } if ($fuzzy) { for ($i = 0; $i < @strings; $i++) { next if $strings[$i] eq ''; # Case-insentive substring match if ( index(lc($str),lc($strings[$i])) != -1) { return $i; } } } return; } sub _fuzzyFindBlacklistIndex { my $app = shift; my $str = shift; return $app->_findBlacklistIndex($str,1); } sub _prepareBlacklistEntry { my $app = shift; my $struct = shift; my $user; if (! $app->{user}) { $user = ''; } elsif (! defined($_cache->{blacklist}) && ! defined($_cache->{config})) { $user = 'MT-Blacklist $VERSION Install'; } else { $user = $app->{user}->name.' (ID: '.$app->{user}->id.')'; } # Maybe this is a mistake, but I am protecting users # from the own ignorance and removing http://www from # the blacklist strings. $struct->{'string'} =~ s#^http://(www)?##; my $ts = _getTimestamp(); $struct->{'weblogs'} ||= [-1]; $struct->{'origin'} ||= 'local'; $struct->{'sent'} ||= []; $struct->{'comment'} ||= ''; $struct->{'comment'} =~ s#[\n\r]+# #g; # Convert newlines $struct->{'dateAdded'} ||= $ts; $struct->{'addedBy'} ||= $user; $struct->{'dateModified'} ||= $ts; $struct->{'lastModifiedBy'} ||= $user; $struct; } sub _rmFromBlacklist { my $app = shift; my @delete_strings = @_ or return; my @pared_blacklist; foreach my $entry (@{$_cache->{blacklist}}) { my $found = 0; foreach my $delete (@delete_strings) { next unless $delete eq $entry->{'string'}; $found++; last; } push(@pared_blacklist,$entry) unless $found; } my $diff = scalar(@{$_cache->{blacklist}}) - scalar(@pared_blacklist); my $logstring = 'User \''.$app->{user}->name.'\' deleted '. ($diff == 1 ? '\''.$delete_strings[0].'\'' : (scalar(@{ $_cache->{blacklist} })-scalar(@pared_blacklist).' entries')). ' from the MT-Blacklist'; if ($app->_saveBlacklist(\@pared_blacklist)) { $app->log($logstring); $diff; } else { $app->log($logstring); 0; } } sub _deleteObject { my $app = shift; my $object = shift or return $app->error("No object provided for deletion"); $object->remove or $app->error("Could not remove object: ".($object->errstr || 'Unknown error')); } sub _saveBlacklist { my $app = shift; my $data = shift; my $rc = $app->_saveBlacklistData('blacklist',$data); $app->_writeBlacklistFile() if $rc && $_cache->{config}->{'autoPublish'}; $rc; } sub _saveConfig { my $app = shift; my $data = shift; $app->_saveBlacklistData('config',$data); } sub _saveBlacklistData { my $app = shift; my ($key,$data) = @_; if (!ref($data)) { $data = $key eq 'blacklist' ? $_cache->{blacklist} : $_cache->{config}; } # Within the application the blacklist will # always have a dummy first entry at index 0. # When it is saved, it is stripped from the array. # We also sort by string before saving if ($key eq 'blacklist') { while ($data->[0]->{string} eq '') { shift(@{$data}); } @$data = sort { $a->{string} cmp $b->{string} } @$data; } my $blObj = _getPluginDataObject($key); if (!$blObj) { $blObj = MT::PluginData->new; $blObj->plugin ('Blacklist'); $blObj->key ($key); } $blObj->data ($data); my $rc = $blObj->save or die "Could not save your $key data: ". ($blObj->errstr || 'Unknown error'); # Put the blank string back on unshift(@{$_cache->{$key}}, {string => ''}) if $key eq 'blacklist'; $_cache->{$key} = $data; $rc; } sub _writeBlacklistFile { my $app = shift; my $blPath = shift || $_cache->{config}->{'publishLoc'}; return (0,"No blacklist publish location set.") unless $blPath; # Create blacklist file content my $data = _getBlacklistMasthead(); if ($_cache->{blacklist}) { foreach (@{ $_cache->{blacklist} }) { my $len = length($_->{'string'}) > 35 ? length($_->{'string'}) : 35; $data .= sprintf("%-".$len."s ", $_->{'string'}); $data .= $_->{'comment'} ? '# '.$_->{'comment'}."\n" : "\n"; } } # Write file use MT::FileMgr; my $fmgr; $fmgr = MT::FileMgr->new('Local') or return (0,$fmgr->errstr); defined($fmgr->put_data($data, $blPath)) or return (0,$fmgr->errstr); 1; } sub _rebuildObjectAll { return _rebuildObjectPage(@_); } sub _rebuildObjectEntry { return _rebuildObjectPage(@_,'entry'); } sub _rebuildObjectPage { my $app = shift; my ($object,$type) = @_; $type ||= ''; my $entry = _getEntry($object->entry_id); if ($type eq 'entry') { $app->rebuild_entry( Entry => $entry ); } else { $app->rebuild_entry( Entry => $entry, BuildDependencies => 1 ); } } sub _rebuildEntry { my $app = shift; my ($entry, $dependancies) = @_; if ($dependancies) { $app->rebuild_entry( Entry => $entry, BuildDependencies => 1 ); } else { $app->rebuild_entry( Entry => $entry ); } } sub _diceImportFileData { my $app = shift; my $data = shift; chomp $data; my @banned; my @lines = split /\r?\n/, $data; foreach my $line (@lines) { my ($banned_url,$comment) = $line =~ m( ^ # Beginning of line \s* # Optional whitespace ([^\s\#]+)\s* # The url fragment (?:\s+\#\s*(.*))? # Optional line-ending # comment (minus the #) $ # End of line )x; next if !$banned_url; # Remove whitespace from endings $banned_url =~ s/^\s+|\s+$//; $comment =~ s/^\s+|\s+$// if $comment; push(@banned, $app->_prepareBlacklistEntry({string=>$banned_url,comment=>$comment})); } @banned; } sub _getBlacklistEntry { my $app = shift; my $id = shift; return (${ $_cache->{blacklist} }[$id],scalar(@{ $_cache->{blacklist} })); } sub _canEditObject { my $app = shift; my ($type,$object) = @_; my $rc; mywarn("In canEditObject with $type ID ".$object->id); if ($type eq 'comment') { $rc = $app->_canEditComment($object); } elsif ($type eq 'ping') { $rc = $app->_canEditPing($object); } elsif ($type eq 'entry') { $rc = $app->_canEditEntry($object); } else { die "Unknown object type in canEditObject"; } mywarn("Returning from canEditObject with a $rc for $type ID ".$object->id); } sub _canEditComment { my $app = shift; my $c = shift or return $app->error("No comment specified for permission check."); mywarn("In canEditComment with comment ID ".$c->id); my $entry = _getEntry($c->entry_id); $app->_canEditEntry($entry); } sub _canEditPing { my $app = shift; my $p = shift or return $app->error("No ping specified for permission check."); mywarn("In canEditPing with ping ID ".$p->id); my $tb = _getTrackback($p->tb_id); my $entry = _getEntry($tb->entry_id); $app->_canEditEntry($entry); } sub _canEditEntry { my $app = shift; my $e = shift or return $app->error("No entry specified for permission check."); mywarn("In canEditEntry with entry ID ".$e->id); if ($app->_canEditAllEntries($e->blog_id)) { $_cache->{perms}->{canEditEntry}{$e->id} = 1; mywarn("Returning from canEditEntry with 1 (CAN_EDIT_ALL_BLOGS)"); return $_cache->{perms}->{canEditEntry}{$e->id} } elsif (exists($_cache->{perms}->{canEditEntry}{$e->id})) { mywarn("(CACHE) Returning from canEditEntry with ".$_cache->{perms}->{canEditEntry}{$e->id}); return $_cache->{perms}->{canEditEntry}{$e->id}; } $_cache->{perms}->{canEditEntry}{$e->id} = $_cache->{perms}->{blog}{$e->blog_id}->can_edit_entry($e, $app->{user}); mywarn("(DB) Returning from canEditEntry with ".$_cache->{perms}->{canEditEntry}{$e->id}); $_cache->{perms}->{canEditEntry}{$e->id}; } sub _canEditAllEntries { my $app = shift; my $b_id = shift or return $app->error("No blog ID specified for permission check."); mywarn("In canEdirAllEntries with blog ID ".$b_id); if (exists($_cache->{perms}->{canEditAllEntries}{$b_id})) { mywarn("(CACHE) Returning from canEdiAllEntries with ".$_cache->{perms}->{canEditAllEntries}{$b_id}); return $_cache->{perms}->{canEditAllEntries}{$b_id}; } if ($_cache->{perms}->{blog}{$b_id}) { mywarn("(CACHE) Returning from _canEditAllEntries with perms for blog ID ".$b_id); return $_cache->{perms}->{blog}{$b_id}; } else { mywarn("(DB) Returning from _canEditAllEntries with perms for blog ID ".$b_id); my $perm = $app->_getBlogPermissions($b_id); $_cache->{perms}->{canEditAllEntries}{$b_id} = $perm->can_edit_all_posts; } } sub _getBlogPermissions { my $app = shift; my $id = shift; mywarn("In getBlogPermissions for blog ID $id"); if (my $p = $_cache->{perms}->{blog}{$id}) { mywarn("(CACHE) Returning permissions for blog ID $id"); return $p; } else { mywarn("(DB) Loading permissions for blog ID $id"); require MT::Permission; $_cache->{perms}->{blog}{$id} = MT::Permission->load( {author_id => $app->{user}->id, blog_id => $id }); } } sub _getUserObjects { my $app = shift; my $q = $app->{query}; my ($type,$searchDepth) = @_; my $iter; if ($type eq 'comment') { require MT::Comment; $iter = MT::Comment->load_iter({}, {direction => 'descend' }); } else { require MT::TBPing; $iter = MT::TBPing->load_iter({}, {direction => 'descend' }); } my $count = 0; my @objects; while (my $obj = $iter->()) { mywarn("(DB) Loaded $type ID ".$obj->id); push(@objects, $obj) if $app->_canEditObject($type,$obj); last if $searchDepth && ($searchDepth == ++$count); } return (\@objects); } sub _extractURLs { my $app = shift; my @strings = @_; my @urls; foreach (@strings) { next unless ($_ and $_ ne ''); local $_ = _sanitizeInput($_); while (m#http://(?:www.)?([^\s/'">]+)#gi) { push(@urls,$1); } } my %seen = (); my @unique = grep { ! $seen{lc($_)} ++ } @urls; @unique = sort { lc($a) cmp lc($b) } @unique; } sub _navigation_html { my $app = shift; my ($tagline,$pageclass) = @_; my $q = $app->{query}; $tagline = "- $tagline" if $tagline; my $styles = _blacklistAppCSS(); my $html = < MT-Blacklist $tagline $styles
'; $html; } sub _debug { my $app = shift; return '' unless $app->_debug_status() && $app->{user}->can_create_blog; #JAYBO unless (@_) { my ($package, $filename, $line) = caller(); @_ = ('No debugging info available at '.$package.', line '.$line); } if ((@_ > 1) || ref($_[0])){ require Data::Dumper; return '
'.Data::Dumper::Dumper(@_).'
'; } else { return '

'.(shift).'

'; } } sub _debug_status { my $app = shift; if (my $status = shift) { $app->{debug_status} = $status; } $app->{debug_status}; } sub _getTimestamp { use MT::Util; my @ts = MT::Util::offset_time_list(time); my $ts = sprintf '%04d%02d%02d%02d%02d%02d', $ts[5]+1900, $ts[4]+1, @ts[3,2,1,0]; $ts; } sub _getBlacklistMasthead { return < /* */ EOD } sub _defaultBlacklist { my $list = <<'EOD'; (ragazze)-?\w+\.[a-z]{2,} # Catchall regexp for many spam sites (buy)[\w\-_.]*online[\w\-_.]*\.[a-z]{2,} # Catchall regexp for many spam sites (phentermine|viagra|vig-?rx)[\w\-_.]*\.[a-z]{2,} # Catchall regexp for many spam sites ([\w\-_.]+\.)?(l(so|os)tr)\.[a-z]{2,} # Catchall regex for lsotr.xxx and lostr.xxx with or without a subdomain (penis)[_.\-]?enlargement([\w\-_.]+)?\.[a-z]{2,} # Catchall regexp for many spam sites xxxxxx.lsotr.xxx 00000-online-casino.com 0adult-cartoon.com 0adult-manga.com 0cartoon-porn.com 0cartoon-sex.com 0cartoon.com 0casino-online.com 0casinoonline.com 0free-hentai.com 0freehentai.com 0hentai-anime.com 0hentai-manga.com 0hentaimanga.com 0internet-casino.com 0livesex.com 0manga-porno.com 0manga-sesso.com 0manga.com 0sesso-amatoriale.com 0sesso-orale.biz 0sesso.biz 0sesso.us 0sessoanale.com 0sessogratis.us 0sex-toons.com 0sfondi-desktop.com 0sfondi.com 0suonerie.com 0tatuaggi.com 0toons.com 0video-porno.com 0virtual-casino.com 0xxx-cartoon.com 101pills.com 123sessogratis.com 1concerttickets.com 1footballtickets.com 1st-shemale-sex.com accompagnatrici.cc adult-manga.org adultfriendfindersite.com adultlingerieuk.com adultserviceproviders.com all-gay-porn.us allmagic.ru amateur-porn-gallery.com amateur-site.us anal-sex-pictures.us anime-manga.us anmichelle-22.da.ru annunci-coppie.net annunci-erotici.net annunci-erotici.org annunci-personali.org annunci-sesso.org annunci-sesso.us annuncisesso.us aquatyca.net autofinanzierung-zum-festzins.de autokredit-tipp.de autumn-jade.com banialoba3w.150m.com barcodes.cn basi-musicali.com bast3.ru beauty-farm.net belle-donne.biz belle-ragazze.net belle-ragazze.org belleragazze.biz belleragazze.org bellissime-donne.com bellissime-donne.net bellissimedonne.com bellissimedonne.org benessere.us best-diet-pills-online.com best-prescription-diet-pills.com big-black-butts.net big-hooters.net big-natural-boobs.us bigbras-club.com bigmoms.com bigtitchaz.com blackbusty.com blackjack-homepage.com bon-referencement.com boobmorning.com boobspost.com breast-augmentation.top-big-tits.com btd-online-casino.com busty-models.us bustyangelique.com bustydustystash.com bustykerrymarie.com buy-adult-sex-toys.com buy-sex-toys.net buy-sexy-lingerie-online.com buycheappills.net calendari-donne.com calendari-donne.net calendaridonne.com calendaridonne.net canzoni-italiane.com canzoni-italiane.net canzoni-italiane.org canzoni-karaoke.com canzoni-mp3.com canzoni-mp3.us canzoni-musica.com canzoni.cc canzonisanremo.com canzonistraniere.com cartoni-animati.com cartoni-hentai.com cartoni-hentai.net cartoni-hentai.org cartoni-porno.com cartonierotici.com cartonigiapponesi.com cartonihentai.net casino-en-ligne.fr.vu casino-in-linea.it.st certificationking.net cheap-adult-sex-toys.com cheap-online-pharmacy.org cheap-pills-online.com cherrybrady.com chloesworld.com chickz.com cialis.incredishop.com classifiche-italiane.org classifiche-musicali.com classifiche-musicali.net classifiche-musicali.org classifichemusicali.com computer-und-erotische-spiele-download.com cycatki.com danni.com dedichepersonali.com desiraesworld.com devon-daniels.com dianepoppos.com diet-pills-shop.com dieta-dimagrante.net dieta-mediterranea.net dieta-zona.com dieta.cc diete-dimagranti.com diete.bz dieting-review.com discount-airfares-guide.com discount-life-insurance.us disney-hentai.org donne-belle.net donne-famose.biz donne-muscolose.net donne-muscolose.org donne-nere.net donne-nere.org donne-nude.biz donne-porche.com donne-vogliose.com donne.bz donnebelle.net donnefamose.biz donnegrasse.org donnemature.biz donnemuscolose.com donnenere.com donnenere.net donnenude.biz donneporche.org donnesexy.org donnevogliose.net donnevogliose.org dr\.ag dragonball-porno.com dragonball-x.biz dragonball-xxx.biz dragonballporno.net dragonballx.cc dragonballxxx.biz drugstore.st drunk-girls-flashing.com e-discus.com e-order-propecia.com e-order-xenical.com ecblast.com effective-penis-pills.com elcenter-s.ru envoyer-des-fleurs.com eonsystems.com erotische-geschichten-portal.com esesso-gratis.com evromaster.ru evrostroyka.narod.ru exoticmoms.com fat-lesbians.net figa.nu film-porno.us final-fantasy-hentai.org find-lesbian-porn.com fioricet.st fitnessx.net forex.inc.ru foto-gay.us foto-porno.us foto-porno.ws free-adult-check.com free-net-sex.com fumetti-porno.org fumettiporno.org gagnerargent.com gambling-homepage.com gamblingguidance.co.uk gayx.us generic-propecia.net genimat.220v.org genimat.cjb.net genimut.dr.ag get-hardcore-sex.com getaprescription.net giochi-hentai.com giochi-online.us giochix.com godere.org gogito.com grannysexthumbs.com guardami.org hardcore-porn-links.com hardcore-sex.bz hardcorecash.net hautesavoieimmobilier.com hentai-gratis.us hentai-hard.com hentai-xxx.us hentaigratis.net hentaimanga.us hentaix.net hentaixxx.us hentay.us hgh-online.com hobbs-farm.com homelivecams.com hornymoms.net hotel-bordeaux.cjb.net immagini-hentai.org immobilierdessavoie.com inescudna.com inter-ross.ru inviare-mms.net invio-mms.us ipharmacy.com # Catchall for many spam sites itramadol.com juliamiles.co.uk koihoo.com kraskidliavas.ru kredite-portal.de kredite-sofortzusage.de kupibuket.ru las-vegas-real-estate-1.com lasvegasrealtor.com legalblonde.com lesbichex.com levitraguide.com link-dir.com linseysworld.com lizziemills.com logos-logos.be mainjob.ru manga-free.net manga-free.org manga-x.biz manga-xxx.org mature-big-tits.net mediaaustralia.com.au megapornstation.com menguma.com menzyme.com mmsanimati.com mneuron.com moltobene.ru mortgage-rates-guide.net mp3download.bz mp3x.biz musica-da-scaricare.net musica-gratis.biz musica-gratis.org musica-karaoke.net musica-mp3.biz musicamp3.us musicenergy.com my-sex-toys-store.com mybestcasinos.net mydatingagency.com naturalknockers.net net-mature.com netizen.org nichehit.com nicolepeters.com oldgrannyfucking.com onepiecex.net online--pharmacy.us online-prescription.st online-prescriptions-internet-pharmacy.com operazione-trionfo.net partnersuche-partnervermittlung.com peepissing.com penisenlargementmagazine.com penisimprovement.com penisresearch.com perfect-dedicated-server.com pharmacyprices.net picsfreesex.com piercingx.com pilltip.com pj-city.com pokemon-hentai.com pokemon-hentai.org pokemonhentai.net pokemonx.biz poker-homepage.com pompini.nu porn-4u.net porn-house.us pornogratis.bz pornostars.cc pornwww.com pregnant-sex-free.us prescription-drugs.st prescriptions.md propecia-store.com prozac.st quality-penis-pills.com racconti-gay.org raf-ranking.com ragazze.bz rampantrabbitvibrator.co.uk ratenkredit-center.de ratenkredit-shop.de real-sex.us ricettegolose.com rx-store.com # Catchall for many spam sites sailor-moon-hentai.org sailor-moon-hentai.us salute-bellezza.net salute-bellezza.org salute-benessere.org salute-e-benessere.net salute-igiene.com salute-malattie.com salute-malattie.net sarennasworld.com scarica-mp3.biz scarica-mp3.com scarica-musica-mp3.org scarica-musica.com scarica-musica.org scaricamp3.us scaricare-canzoni.com scaricare-canzoni.net scaricare-canzoni.org scaricare-mp3.org seitensprung-gratis.com selena-u.ru sesso-gratis.cc sesso-online.net sessoanalex.com sessox.biz sex-4you.org sex-manga.us sexe.vc sexo9.com sexshop-sexeshop.com sextoysportal.com sexwebclub.com sfondi-desktop-gratis.com siti-porno.us ski-resorts-guide.com slot-machines-slots.com sms-sms-sms.org soma.st sonnerie-hifi-sms.com sonnerie-logos.be sonnerie-portable-composer.com sonnerie-portable.be sonneries-gsm-sms.com sorglos-kredit.de spiele-kostenlose.com spiele-planet.com sting.cc suonerie-loghi-gratis.com suonerieloghix.com suoneriex.net susiewildin.com svitonline.com sylviapanda.com tatuaggi-gratis.com tatuaggi-piercing.org tatuaggi-tribali.com tatuaggi.cc tatuaggi.us tatuaggitribali.com terminator-sales.com testi-canzoni.com testi-canzoni.net testi-musicali.com testi-musicali.net testi.cc tette.bz tettone.cc theceleb.com themadpiper.ent tiffany-towers.com tits-center.com tits-cumshots.net top-dedicated-servers.com tramadol.st troie.bz trucchi-giochi.us u-w-m.ru ultram.st ultrampharmacy.com unbeatablerx.com uni-card.ru us-meds.com vacation-rentals-guide.com viaggix.com viapaxton.com video-porno.nu videohentai.org web-revenue.com webcam-erotiche.com webcopywizard.net wet-4all.com wethorny.com www-sesso # Catchall for many spam sites x-ring-tones.com xlboobs.net xsesso.biz yaninediaz.com yukka.inc.ru zipcodedownload.com zipcodesmap.com EOD $list; } # ##################################### # # # METHODS USED BY BOTH APP AND PLUGIN # # # ##################################### sub _getBlacklist { # This function is used by both # the plugin and the application # Return cached data if exists return $_cache->{blacklist} if $_cache->{blacklist} && @{$_cache->{blacklist}}; my $list = _getBlacklistData('blacklist'); # Within the application the blacklist will # always have a dummy first entry at index 0. # When it is saved, it is stripped from the array. while ($list->[0]->{string} eq '') { shift(@{$list}); } unshift(@{$list}, {string => ''}); # Cache the data $_cache->{blacklist} = $list; $list; } sub _getConfig { # This function is used by both # the plugin and the application # Return cached data if exists return $_cache->{config} if $_cache->{config}; my $config = _getBlacklistData('config'); # Cache the data $_cache->{config} = $config; $config or return; } sub _getBlacklistData { # This function is used by both # the plugin and the application my $key = shift; my $data = _getPluginDataObject($key); return $data->data if $data; } sub _getPluginDataObject { # This function is used by both # the plugin and the application my $key = shift; mywarn("In getPluginDataObject for $key"); if ($_cache->{plugindataobject}{$key}) { mywarn("(CACHE) Returning from _getPluginDataObject $key PluginDataObject"); $_cache->{plugindataobject}{$key}; } else { mywarn("(DB) Returning from _getPluginDataObject $key PluginDataObject"); $_cache->{plugindataobject}{$key} = MT::PluginData->load ({ plugin => 'Blacklist', key => $key }); } } sub _getEntry { my $id = shift; mywarn("In _getEntry with entry ID $id"); if (my $e = $_cache->{entry}->{$id}) { mywarn("(CACHE) Returning from _getEntry entry ID $id\n"); return $e ; } mywarn("(DB) Loading from _getEntry entry ID $id\n"); require MT::Entry; $_cache->{entry}->{$id} = MT::Entry->load($id); } sub _getTrackback { my $id = shift; mywarn("In loadTrackback with trackback ID $id from caller"); if (my $trackback = $_cache->{trackback}->{$id}) { mywarn("(CACHE) Returning trackback ID $id\n"); return $trackback ; } mywarn("(DB) Loading trackback ID $id\n"); require MT::Trackback; $_cache->{trackback}->{$id} = MT::Trackback->load($id); } sub _getObjectEntry { my ($type,$object) = @_; if ($type eq 'comment') { _getCommentEntry($object); } elsif ($type eq 'ping') { _getPingEntry($object); } else { die "Unknown object type in _getObjectEntry"; } } sub _getCommentEntry { my $c = shift; my $entry = _getEntry($c->entry_id); } sub _getPingEntry { my $p = shift; my $tb = _getTrackback($p->tb_id); my $entry = _getEntry($tb->entry_id); } sub _getBlog { my $id = shift; mywarn("In loadBlog with Blog ID ".$id); if (my $b = $_cache->{blog}->{$id}) { mywarn("(CACHE) Returning blog ID $id"); return $b; } else { mywarn("(DB) Returning blog ID $id"); require MT::Blog; $_cache->{blog}->{$id} = MT::Blog->load($id); } } sub _getBlogs { require MT::Blog; mywarn("In loadBlogs"); if ($_cache->{blogs_loaded}) { mywarn("(CACHE) Returning blogs"); return $_cache->{blog}; } else { mywarn("(DB) Returning blogs"); my @blogs = MT::Blog->load(); $_cache->{blogs_loaded}++; %{$_cache->{blog}} = map { $_->id, $_ } @blogs; $_cache->{blog}; } } # ##################################### # # # START OF MT-BLACKLIST PLUGIN METHODS # # # ##################################### # # RE-redefinition of MT::App::Comments::post # # In order to include the link in the comment notification # email, I have to use the entire 'post' method, because # Ben has never abstracted the emails as templates. Grrrrr. # sub comment_post_hdlr { use MT::Util qw( remove_html ); my $app = shift; my $q = $app->{query}; if (my $state = $q->param('comment_state')) { require MT::Serialize; my $ser = MT::Serialize->new($app->{cfg}->Serializer); $state = $ser->unserialize(pack 'H*', $state); $state = $$state; for my $f (keys %$state) { $q->param($f, $state->{$f}); } } my $entry_id = $q->param('entry_id') or return $app->error("No entry_id"); my $entry = _getEntry($entry_id) or return $app->error($app->translate( "Invalid entry ID '[_1]'.", scalar $q->param('entry_id'))); unless ($entry->allow_comments eq '1') { return $app->handle_error($app->translate( "Comments are not allowed on this entry.")); } require MT::IPBanList; my $iter = MT::IPBanList->load_iter({ blog_id => $entry->blog_id }); my $user_ip = $app->remote_ip; while (my $ban = $iter->()) { my $banned_ip = $ban->ip; if ($user_ip =~ /$banned_ip/) { return $app->handle_error($app->translate( "You are not allowed to post comments.")); } } my $blog = _getBlog($entry->blog_id); if (!$blog->allow_anon_comments && (!$q->param('author') || !$q->param('email'))) { return $app->handle_error($app->translate( "Name and email address are required.")); } if (!$q->param('text')) { return $app->handle_error($app->translate("Comment text is required.")); } ### INSERTED CODE STARTS HERE ### if (my ($blRc,$blStr,$textStr,$blResponse) = _matchBlacklist($entry->blog_id, $q->param('text'), $q->param('url'), $q->param('email'), $q->param('author'))) { $app->log('MT-Blacklist comment denial on '.$blog->name.": $blStr") if defined($blStr); if (defined($blResponse)) { $blResponse =~ s/__TYPE__/comment/g; $blResponse =~ s/__BLACKLIST__/$blStr/g if defined($blStr); $blResponse =~ s/__TEXT__/$blStr/g if defined($textStr); } return $app->handle_error($blResponse); } ### INSERTED CODE ENDS HERE ### my $comment = MT::Comment->new; $comment->ip($app->remote_ip); $comment->blog_id($entry->blog_id); $comment->entry_id($q->param('entry_id')); $comment->author(remove_html(scalar $q->param('author'))); my $email = $q->param('email') || ''; if ($email) { require MT::Util; if (my $fixed = MT::Util::is_valid_email($email)) { $email = $fixed; } else { return $app->handle_error($app->translate( "Invalid email address '[_1]'", $email)); } } $comment->email(remove_html($email)); my $url = $q->param('url') || ''; if ($url) { require MT::Util; if (my $fixed = MT::Util::is_valid_url($url)) { $url = $fixed; } else { return $app->handle_error($app->translate( "Invalid URL '[_1]'", $url)); } } $comment->url(remove_html($url)); $comment->text($q->param('text')); $comment->save; $app->rebuild_indexes( Blog => $blog ) or return $app->error($app->translate( "Rebuild failed: [_1]", $app->errstr)); $app->rebuild_entry( Entry => $entry ) or return $app->error($app->translate( "Rebuild failed: [_1]", $app->errstr)); my $link_url; if (!$q->param('static')) { my $url = $app->base . $app->uri; $url .= '?entry_id=' . $q->param('entry_id'); $link_url = $url; } else { my $static = $q->param('static'); if ($static == 1) { $link_url = $entry->permalink; } else { $link_url = $static . '#' . $comment->id; } } if ($blog->email_new_comments) { require MT::Mail; my $author = $entry->author; $app->set_language($author->preferred_language) if $author && $author->preferred_language; if ($author && $author->email) { my %head = ( To => $author->email, From => $comment->email || $author->email, Subject => '[' . $blog->name . '] ' . $app->translate('New Comment Posted to \'[_1]\'', $entry->title) ); my $charset = $app->{cfg}->PublishCharset || 'iso-8859-1'; $head{'Content-Type'} = qq(text/plain; charset="$charset"); my $body = $app->translate( 'A new comment has been posted on your blog [_1], on entry #[_2] ([_3]).', $blog->name, $entry->id, $entry->title); require Text::Wrap; $Text::Wrap::cols = 72; $body = Text::Wrap::wrap('', '', $body) . "\n$link_url\n\n" . $app->translate('IP Address:') . ' ' . $comment->ip . "\n" . $app->translate('Name:') . ' ' . $comment->author . "\n" . $app->translate('Email Address:') . ' ' . $comment->email . "\n" . $app->translate('URL:') . ' ' . $comment->url . "\n\n" . $app->translate('Comments:') . "\n\n" . $comment->text . "\n"; ### INSERTED CODE STARTS HERE ### my $comment_view_url = $app->base.$app->path.'mt-blacklist.cgi?__mode=despam&_type=comment&id='.$comment->id; $body .= "\n\n----------------------\nDe-spam using MT-Blacklist:\n". $comment_view_url. "\n\n"; ### INSERTED CODE ENDS HERE ### MT::Mail->send(\%head, $body); } } return $app->redirect($link_url); } # # Redefinition of MT::App::Trackback::ping # # In order to include the link in the comment notification # email, I have to use the entire 'post' method, because # Ben has never abstracted the emails as templates. Grrrrr. # sub ping_post_hdlr { my $app = shift; my $q = $app->{query}; my($tb_id, $pass) = $app->_get_params; return $app->_response(Error => $app->translate("Need a TrackBack ID (tb_id).")) unless $tb_id; use MT::Util qw( first_n_words encode_xml is_valid_url ); use File::Spec; require MT::App::Trackback; #print STDERR "In ".__PACKAGE__." at ".__LINE__."\n"; #warn "In ".__PACKAGE__." at ".__LINE__."\n"; my($title, $excerpt, $url, $blog_name) = map scalar $q->param($_), qw( title excerpt url blog_name); MT::App::Trackback::no_utf8($tb_id, $title, $excerpt, $url, $blog_name); return $app->_response(Error => $app->translate("Need a Source URL (url).")) unless $url; if (my $fixed = MT::Util::is_valid_url($url)) { $url = $fixed; } else { return $app->_response(Error => $app->translate("Invalid URL '[_1]'", $url)); } require MT::TBPing; my $tb = _getTrackback($tb_id) or return $app->_response(Error => $app->translate("Invalid TrackBack ID '[_1]'", $tb_id)); return $app->_response(Error => $app->translate("This TrackBack item is disabled.")) if $tb->is_disabled; if ($tb->passphrase && (!$pass || $pass ne $tb->passphrase)) { return $app->_response(Error => $app->translate("This TrackBack item is protected by a passphrase.")); } ## Check if this user has been banned from sending TrackBack pings. require MT::IPBanList; my $iter = MT::IPBanList->load_iter({ blog_id => $tb->blog_id }); my $user_ip = $app->remote_ip; while (my $ban = $iter->()) { my $banned_ip = $ban->ip; if ($user_ip =~ /$banned_ip/) { return $app->_response(Error => $app->translate("You are not allowed to send TrackBack pings.")); } } ## Check if user has pinged recently #my @past = MT::TBPing->load({ tb_id => $tb_id, ip => $host_ip }); #if (@past) { # @past = sort { $b->created_on cmp $a->created_on } @past; #} ### INSERTED CODE STARTS HERE ### if (my ($blRc,$blStr,$textStr,$blResponse) = _matchBlacklist($tb->blog_id,$title,$excerpt,$url)) { my $blog = _getBlog($tb->blog_id); $app->log('MT-Blacklist trackback denial on '.$blog->name.": $blStr") if defined($blStr); $blResponse =~ s/__TYPE__/ping/g; $blResponse =~ s/__BLACKLIST__/$blStr/g if defined($blStr); $blResponse =~ s/__TEXT__/$blStr/g if defined($textStr); return $app->_response(Error => $blResponse); } ### INSERTED CODE ENDS HERE ### my $ping = MT::TBPing->new; $ping->blog_id($tb->blog_id); $ping->tb_id($tb_id); $ping->source_url($url); $ping->ip($app->remote_ip || ''); if ($excerpt) { if (length($excerpt) > 255) { $excerpt = substr($excerpt, 0, 252) . '...'; } $title = first_n_words($excerpt, 5) unless defined $title; $ping->excerpt($excerpt); } $ping->title(defined $title && $title ne '' ? $title : $url); $ping->blog_name($blog_name); $ping->save; ## If this is a trackback item for a particular entry, we need to ## rebuild the indexes in case the <$MTEntryTrackbackCount$> tag ## is being used. We also want to place the RSS files inside of the ## Local Site Path. my($blog_id, $entry, $cat); if ($tb->entry_id) { $entry = _getEntry($tb->entry_id); $blog_id = $entry->blog_id; } elsif ($tb->category_id) { require MT::Category; $cat = MT::Category->load($tb->category_id); $blog_id = $cat->blog_id; } my $blog = _getBlog($blog_id); $app->rebuild_indexes( Blog => $blog ) or return $app->_response(Error => $app->translate("Rebuild failed: [_1]", $app->errstr)); if ($app->{cfg}->GenerateTrackBackRSS) { ## Now generate RSS feed for this trackback item. my $rss = MT::App::Trackback::_generate_rss($tb, 10); my $base = $blog->archive_path; my $feed = File::Spec->catfile($base, $tb->rss_file || $tb->id . '.xml'); my $fmgr = $blog->file_mgr; $fmgr->put_data($rss, $feed) or return $app->_response(Error => $app->translate("Can't create RSS feed '[_1]': ", $feed, $fmgr->errstr)); } if ($blog->email_new_pings) { require MT::Mail; my($author, $subj, $body); if ($entry) { $author = $entry->author; $app->set_language($author->preferred_language) if $author && $author->preferred_language; $subj = $app->translate('New TrackBack Ping to Entry [_1] ([_2])', $entry->id, $entry->title); $body = $app->translate('A new TrackBack ping has been sent to your weblog, on the entry [_1] ([_2]).', $entry->id, $entry->title); } elsif ($cat) { require MT::Author; $author = MT::Author->load($cat->created_by); $app->set_language($author->preferred_language) if $author && $author->preferred_language; $subj = $app->translate('New TrackBack Ping to Category [_1] ([_2])', $cat->id, $cat->label); $body = $app->translate('A new TrackBack ping has been sent to your weblog, on the category [_1] ([_2]).', $cat->id, $cat->label); } if ($author && $author->email) { my %head = ( To => $author->email, From => $author->email, Subject => '[' . $blog->name . '] ' . $subj ); my $charset = $app->{cfg}->PublishCharset || 'iso-8859-1'; $head{'Content-Type'} = qq(text/plain; charset="$charset"); require Text::Wrap; $Text::Wrap::cols = 72; $body = Text::Wrap::wrap('', '', $body) . "\n\n" . $app->translate('IP Address:') . ' ' . $ping->ip . "\n" . $app->translate('URL:') . ' ' . $ping->source_url . "\n" . $app->translate('Title:') . ' ' . $ping->title . "\n" . $app->translate('Weblog:') . ' ' . $ping->blog_name . "\n\n" . $app->translate('Excerpt:') . "\n" . $ping->excerpt . "\n"; ### INSERTED CODE STARTS HERE ### my $ping_view_url = $app->base.$app->path. 'mt-blacklist.cgi?__mode=despam&_type=ping&id='. $ping->id; $body .= "\n\n".('-'x22). "\nDe-spam using MT-Blacklist:\n". $ping_view_url. "\n\n"; ### INSERTED CODE ENDS HERE ### MT::Mail->send(\%head, $body); } } return $app->_response; } # # _matchBlacklist method # # Here we match blacklisted strings against the user input. # Plain strings and regular expressions are allowed. Nothing # needs to be escaped as far as I know since our delimiters # and regular expression metacharaters are mutually exclusive # aside from the period which makes no difference. # sub _matchBlacklist { my (@strings, @blacklisted_strings, $str, $deny); my $blog_id = shift; @strings = @_; # Read in config my $config; $config = _getConfig(); unless ($ENV{REQUEST_URI} =~ m/__mode=search/) { return unless $config && $config->{'blacklistActive'}; } # Sanitize text $str = _sanitizeInput(join(' ', @strings)); return unless $str =~ /\S/; # Build combined pattern unless ($_cache->{pattern}) { # Read in blacklist @blacklisted_strings = _readBlacklist(); shift(@blacklisted_strings); $_cache->{pattern} = join('|', @blacklisted_strings); } # Match against pattern in one expression return unless $str =~ m#$_cache->{pattern}#io; my $matched_string = $&; # We match an entry, find out which one it was @blacklisted_strings = _readBlacklist(); shift(@blacklisted_strings); # Match against blacklist foreach my $blacklist_string (@blacklisted_strings) { if ($str =~ m#$blacklist_string#i) { return (1,$blacklist_string,$matched_string,$config->{denyResponse}); } } # We should never get here... die "Oops!"; } # # Read blacklist # # This subroutine doesn't READ the blacklist so # much as it transforms it into a simple array # of strings/regexps sub _readBlacklist { return map { $_->{'string'}} @{ _getBlacklist() }; } # # Sanitize user input # # Here, we sanitize the user input to prevent spammers # from trying to circumvent matching. I'm sure that # this will grow over time as spammers attempt to # prove that the are smarter than the thousands # of highly connected people that they are pissing off. sub _sanitizeInput { my $str = shift; # Remove any #'s since we will be using it as a delimiter # This is safe since it isn't something that would # be included in a blacklist. $str =~ tr/#//d; # Remove any HTML comments in the form of $str =~ s/\x3c\!.+?\x3e//g; # #THANKS to Stepan Riha for the next three # Convert decimal entities (p => p) $str =~ s/&#(\d{1,3});/chr($1)/eg; # Convert hex entities (p => p) $str =~ s/&#x(\d{2});/chr(hex($1))/eg; # Convert URL encodings (%70 => p) $str =~ s/\%(\d{2})/chr(hex($1))/eg; return $str; } sub mywarn { my $warning = shift; push(@_warnings, $warning); } sub dumpwarn { my $app = shift; $app->_debug_status(1); return $app->_debug(join("
\n", @_warnings)); } 1;