# Title:	MT-Blacklist
# Summary:	A plugin for preventing comment and Trackback spam 
# Author:	Jay Allen (http://www.jayallen.org/)
# Version:	1.6.5
# Date:		Aug 4, 2004
# Localizer:	Jedi (http://Jedi.z6i.org/blog/)
# L10N Date:	Aug 15, 2004
# L10N Version:	.01
#
# 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. 

package jayallen::Blacklist;
use vars qw($VERSION $_cache @_warnings $blacklist_config_directory);
$VERSION = '1.6.5.01';

require 5.005;
 
####################################################################
#
# PATH TO FILESYSTEM-BASED BLACKLIST CONFIGURATION
#
# The following configuration option should only be uncommented 
# if you want to use a filesystem-based configuration and
# blacklist instead of one stored in your database.
#
# This is particularly useful for those who do not have the
# Storable perl module installed or anyone who seems to be having 
# problems storing data in a database.
#
# Below, please input the full path to the directory where you 
# would like to store your data.  The directory should be world-
# writeable and world-readable (Permissions: 777 or -rwxrwxrwx) 
# UNLESS you know you can use more strict permissions (due to suexec 
# or cgi-wrap).  The directory should NOT be located within your 
# public website directory hierarchy.
# 
# SUGGESTED LOCATION: mt/extlib/jayallen/Blacklist_Config
#
# $blacklist_config_directory = 'mt/extlib/jayallen/Blacklist_Config/'; # L10N BUG?!
#
####################################################################




# 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}


# CHANGES SINCE LAST PUBLIC VERSION
#
#	Added arbitrary text/regexp to despam search
#	Added NO FILTER to despam search to get LastN comments/trackbacks


use MT::App;
@ISA = qw( MT::App );
use strict;

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,
        'permdebug'	=> \&debug_perms,
    );
    $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('把網誌垃圾趕盡殺絕','listpage');

	# QUICK ADD FORM
    $html .= $q->start_form('get',$app->uri);
    $html .= $q->p($q->hidden('__mode', 'quickadd'),"即刻加入: ".$q->textfield(-class=>'text', -name => 'string', -size => 30). $q->submit(-name => '', -value => '加入').$q->br.'<span class="description">注意: 即刻加入時無法填寫註解</span>');
    $html .= $q->end_form;


    my %messages = (
        'added' => sub { local $_ = shift;
                        $q->p({-class=>'msg_success'},
                            '妳所輸入的內容已被成功地儲存起來了, 並以高亮度標示如下.')
                        },
        'dupe' => sub { local $_ = shift;
                        $q->p({-class=>'msg_warning'},
                            '妳所輸入的內容並未被加入, 有可能這個內容已重複了, 並以高亮度標示如下.')
                        },
        'notadded' => sub { local $_ = shift;
                        $q->p({-class=>'msg_failure'},
                            '妳所輸入的內容並未被加入, 但出錯的原因不明.')
                        },
        'removed' => sub { local $_ = shift;
                        $q->p({-class=>'msg_success'},
                            '已經從黑名單裡移除 "'.$q->param('rmstring').'" 項目了.')
                        },
        'notremoved' => sub { local $_ = shift;
                        $q->p({-class=>'msg_failure'},
                            '由於未知的錯誤, 而無法從黑名單裡移除 "'.$q->param('rmstring').'" 項目.')
                        },
        'quickdeleted' => sub { local $_ = shift;
                        $q->p({-class=>'msg_success'},
                            '妳已經成功地刪除了 '.$q->param('num').' 則項目.')
                        },
        'notquickdeleted' => sub { local $_ = shift;
                        $q->p({-class=>'msg_warning'},
                            '沒有任何項目遭到刪除.')
                        },
        'DEFAULT' => sub { }
    );

	$html .= &{$messages{($q->param('msg') || 'DEFAULT')}}($q) || '';

    #
    # BLACKLIST FRAGMENT OVERVIEW TABLE
    #
    shift(@{$_cache->{blacklist}});
	unless (@{ $_cache->{blacklist} }) {
		$html .= $q->p($q->strong('黑名單裡沒有任何項目.'));
		$html .= $q->p({-style => 'text-align:left'}, '請確定妳已從<a href="http://www.jayallen.org/comment_spam/">MT-Blacklist 垃圾評論交換所</a>取得了最新的<a href="http://www.jayallen.org/comment_spam/blacklist.txt">禁用垃圾清單</a>.').
			$q->p({-style => 'text-align:left'}, '妳祇需要把整個檔案的內容複製起來, 並貼進<a href="'.$app->uri.'?__mode=add" title="MT-Blacklist Add screen">加入畫面</a>的文字區塊裡, 就能夠完全抵禦所有已知的垃圾使者的攻擊.').
			$q->p({-style => 'text-align:left'}, '在 MT-Blacklist 的點對點黑名單散佈功能完成前, 保持更新的最好方法還是訂閱<a href="http://www.jayallen.org/comment_spam/">交換所 RSS feeds</a>.');
 		return $html;
	}		


	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' => '網址',
            'link' => $app->uri
        },
        {
            'display' => '註解',
            'link' => $app->uri.'?sort=comment'
        },
        {
            'display' => '加入日期',
            'link' => $app->uri.'?sort=dateAdded'
        },
        {
            'display' => '修改日期',
            'link' => $app->uri.'?sort=dateModified'
        },
        {
            'display' => '加入者',
            'link' => $app->uri.'?sort=addedBy'
        },
        {
            'display' => '修改者',
            'link' => $app->uri.'?sort=lastModifiedBy'
        }
	);


	my $sort_by .= '以此排序: '. 
		join(' | ', 
			map { '<a href="'.$_->{'link'}.'">'.$_->{'display'}.'</a>' } 
			@sortbar);


    $html .= $q->p($q->hidden('__mode'),
        '妳的黑名單裡總共有 '.scalar(@blacklist).' 則項目.'.$q->br.
        $sort_by );

	if ($sort_dir eq 'reverse') {
		$q->delete('dir');
	} else {
		$q->param('dir', 'reverse');
	}
    my @headings = ('網址片段','','',
    		'<a href="'.$q->self_url.'">'.
    		($custom_sort_field ? ucfirst($sort_col) : '註解').
    		'</a>','刪除');

    $q->param('__mode', 'quickdelete');
    my @rows = $q->th(\@headings);
    $i = 0;
    my $highlight = defined($q->param('highlight')) ? $q->param('highlight')-1 : -1;
    use MT::Util;
    for ($i=0;$i<scalar(@blacklist);$i++) {

        local $_ = $blacklist[$i];
        my @data = [
            $app->_stringBreak($_->{'string'},45),
#            '<a href="?__mode=view&amp;id='.($i+1).'">info</a>',
            '<a href="?__mode=view&amp;string='.MT::Util::encode_url($_->{'string'}).'">資訊</a>',
            $q->a({-href=>'?__mode=remove&string='.
                MT::Util::encode_url($_->{'string'})},'移除'),
            ($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->hidden('__mode'),$q->submit(-name=>'',-value=>'刪除所選的項目')));
    $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'),
        	"即刻加入: ".$q->textfield(-class=>'text', -name => 'string', -size => 30). $q->submit(-name => '', -value => '加入').$q->br.'<span class="description">注意: 使用快速加入時無法加上註解</span>');
        $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+1));
        } 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 ($string,$id) = '';
	$string = $q->param('string');
	if ($string and $string ne '') {
		$id = $app->_findBlacklistIndex($string);
	} else {
		$id = $q->param('id') unless $string;
	}

	my ($blEntry,$numEntries) = $app->_getBlacklistEntry($id);

    my $html = $app->_navigation_html('Killing Blog Spam Dead','viewpage');
	my $prev = $id > 1 ?
		'<a href="?__mode=view&id='.($id-1).'">&laquo; 前一頁</a>' :
		''; 
	my $next = ($id < ($numEntries-1)) ? 
		'<a href="?__mode=view&id='.($id+1).'">下一頁 &raquo;</a>' :
		'';

    $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('註解').$q->td($blEntry->{'comment'}),
               $q->th('加入者').$q->td($blEntry->{'addedBy'}.' at '.MT::Util::format_ts("%Y/%m/%d %H:%M:%S",$blEntry->{'dateAdded'})),
               $q->th('最後修改者: ').$q->td($blEntry->{'lastModifiedBy'}.' 修改於: '.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("編輯註解: ").$q->br.
		'<textarea name="comment" rows="5" cols="40" class="text">'.$blEntry->{'comment'}.'</textarea>'.
		$q->submit(-name => '', -value => '送出')
	);
    $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("儲存資訊時出錯了: 找不到黑名單項目 ID"));
	}
}


sub add {

	my $app = shift;
	my $q = $app->{query};
	my @urls = $q->param('url');
	
	
    my $html = $app->_navigation_html('加入黑名單項目', 'addpage');

    $html .= $q->p('妳可以用下面這個表單來匯入多個項目. ',
    			$q->a({-href=>$q->self_url.'#format', -class=>'helplink'},'[?]'),
    			'請時時檢查<a href="http://www.jayallen.org/comment_spam/">MT-Blacklist 交換中心</a>的<a href="http://www.jayallen.org/comment_spam/blacklist.txt">更新清單</a>, 以保持最佳防護.'
    			);


	# Blacklist importing textarea
	$q->param('__mode','add_confirm');
    $html .= $q->start_form('post',$app->uri);
    $html .= $q->p(
		$q->hidden(-name=>'__mode'),
		$q->strong("匯入黑名單: ").$q->br.
		'<textarea name="entryimport" rows="20" cols="80" class="text">'.
		(@urls ? join("\n", @urls) : '').
		'</textarea>'.$q->br. 
		$q->submit(-name => '', -value => '匯入項目')
	);
    $html .= $q->end_form;
	
	my @rules = (
        '一列一則項目. ',
        '空白會遭到移除.',
        '所有在 # 之後的內容都會被儲存在該項目的註解欄位裡.',
        '空白或者祇有註解的列都將被忽略.',
        '所有的 "http://" 和開頭的 "www." 字串都將遭到移除.',
		'同樣地, 妳最好也省略網域名稱後的任何東西 (也就是目錄跟路徑), 除非他們真的很重要.',
        '字串比對時沒有大小寫的區別',
        '(就如同往常一樣, ) 妳可以使用 Perl 的正規表示式, 但絕非必須如此'
	);
	$html .= $q->h2({-id=>'format'},'項目格式:').$q->ul($q->li(\@rules));
	

	my $example_entry =<<'EOD';
# 這一列將會被忽略.
    # 這一列也是
commentspammer.com		# 這些文字會成為左邊這個網址的註解
testdomain1.com  testdomain2.com # 這 *整列* 都將被忽略

# 這一列跟前後的空白列都將被忽略

# 以下的項目將會擋掉所有在任何輸入欄位裡出現
# viagra 這個<strong>字</strong>的引用通告/評論
# (提示: 這可能不真的是妳想要的結果...)
viagra

# 這個也會, 但是會比較慢
.+viagra.+

# 底下這個超級正規表示式將會檔掉所有帶有 lolita, 
# phentermine, viagra, virgx, 或 vig-rx 的網址.
# 如果妳想用同一個字串檔掉多數項目的話, 就應該
# 考慮加入這個項目.
(lolita|phentermine|viagra|vig-?rx)[\w\-_.]*\.[a-z]{2,} 

# 底下這個正規表示式跟上面那個很像, 不過他會鎖定
# <strong>這兩個字</strong>: nostril 和 enlargement.  
(nostril)[_.\-]?enlargement([\w\-_.]+)?\.[a-z]{2,} 
EOD

	$html .= $q->h2('範例:').$q->pre($example_entry);
	$html .= $q->p('妳該懂了吧... ');
	$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('匯入結果','addpage');

	my @new_entries;
    my $import = $q->param('entryimport');
    unless (@new_entries = $app->_diceImportFileData($import)) {

        $html .= $q->div({-class=>'msg_failure'},
            $q->p("從妳送出的東西中無法擷取出任何資料.  請回到前一頁並且再次檢查妳所輸入的格式."));
        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("下列這些項目乃與既有項目重複了, 或者祇是既有項目的較長版本, 因而不會被加入:"),
			$q->ul($q->li($strings{'dupes'})));
	}

	if ($strings{'dupes'} && @{$strings{'dupes'}}) {
		$html .= $q->div({-class=>'msg_warning'},
			$q->p("下列這些項目用了無效的正規表示式語法, 因此不會被加入:"),
			$q->ul($q->li($strings{'invalid'})));
	}


	return $html unless @{$strings{'unique'}};
	

	if ($strings{'saved'}) {
	
        $html .= $q->div({-class=>'msg_success'},
            $q->p("下列這些項目已經成功地被加入到黑名單中了:"),
            $q->ul($q->li($strings{'unique'})));
	
	} else {
	
		$html .= $q->div({-class=>'msg_failure'},
			$q->p("加入妳的項目時出錯了. 請按下前一頁的按鈕然後再試一次"));
			return $html;
	}


	$html;
}


sub publish {

	my $app = shift;
	my $q = $app->{query};
	
    for (values %{_getBlogs()}) {
		my $perms = $app->_getBlogPermissions($_->id);

		if ($perms && 
			($perms->can_create_blog || $perms->can_edit_config || 
				$perms->can_edit_templates)) { 
    		    $app->{user}->can_edit_publishloc(1);
    		    last;
		}
	}
    return $app->error("妳未經授權以發佈黑名單.") 
    	unless $app->{user}->can_edit_publishloc;


    my $html = $app->_navigation_html('發佈黑名單','publishpage');

    $html .= $q->p('如果妳高興的話, 可以發佈妳的黑名單, 以提供給其他系統上的其他使用者.'.$q->br.'如果妳希望每當有所變更時就自動進行這個動作的話, 可以在<a href="?__mode=config">組態區段</a>裡加以設定.');


	# 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("無效的路徑或檔名 '[_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'},"妳的黑名單檔案已成功地發佈了.");
        } else {
        	$html .= $q->p({-class=>'msg_failure'},"發佈妳的黑名單檔案時出錯了:".$q->br.($result || ''));
        }
	}	

	my $docroot = './';
	$docroot =~ s#/?$#/#;
#	my $suggested_path = $docroot.'blacklist.txt'; # L10N Bug?!
	my $suggested_path = '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("完整的黑名單檔案發佈路徑: ").$q->br.
        $q->textfield(-class=>'text', -name => 'publishLoc', -size=>50, -value => $_cache->{config}->{'publishLoc'}).
        $q->submit(-name => '', -value => '發佈')
    );
    $html .= $q->end_form;

	if ($ENV{'DOCUMENT_ROOT'} && 
			($_cache->{config}->{'publishLoc'} ne $suggested_path)) { 
				
		$html .= $q->p($q->strong('建議路徑: '). $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("妳未經授權以組態任何網誌.") 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 && 
				defined($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})."<br />\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("無效的路徑或檔名 '[_1]'", $blPath));
            }
        
            ## Untaint. We already checked for security holes in $blPath.
            ($blPath) = $blPath =~ /(.+)/s;
            $_cache->{config}->{'publishLoc'} = $blPath;
	 	}           

	 	if ($app->_saveConfig()) {
			$app->log('使用者 \''.$app->{user}->name.'\' 變更了 MT-Blacklist 組態設定');
			$msg =$q->p({-class=>'msg_success'},'妳的組態設定已被儲存起來了.');
# 			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'},'儲存妳的組態設定時出錯了.');
        }
	}
	
    my $html = $app->_navigation_html('組態設定','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 主開關');
        $html .= $q->p({-class=>'question'},
                        '妳想要啟動 MT-Blacklist 嗎?  ',
                        $q->radio_group(-name => 'blacklistActive',
                                '-values' => [1,0],
                                '-default' => $_cache->{config}->{'blacklistActive'},
                                -labels => {1=>'要', 0=>'不要'})
                   ).
                $q->p({-class=>'description'},'如果妳選擇了要, MT-Blacklist 將會使用下列的設定以運作.  如果妳選擇了不要, 那麼妳的 MT 就會保持原來的樣子, 就像 MT-Blacklist 根本不存在一樣.');
    
 	}



	

	$html .= $q->h3('阻擋垃圾');

    $html .= $q->p({-class=>'question'},'請問妳希望要 MT-Blacklist 如何處理每一份網誌?');
	$html .= $q->p({-class=>'description'},'當 MT-Blacklist 開始保護妳的網誌時, 這個外掛模組就祇會阻擋妳所指定網誌及遞送方式.  如果妳把<strong>所有的</strong>核選框都給取消了, 實際的效果就跟妳用上面的選項把整個外掛模組給關掉一樣 (除了妳會花費更多能量以外).',$q->br,
			'<strong>注意:</strong> 如果妳取消了任何下列的核選框的話, MT-Blacklist 將不會推翻 Movable Type 對該遞送方式的原始方法.  這表是妳將不會在妳的通知書電子郵件裡收到清垃圾鏈結.');

    my (%override);
    foreach my $cfgkey ('overrideCommentPosting','overridePingPosting') { 
        %{$override{$cfgkey}} = map { $_,1} @{$_cache->{config}->{$cfgkey}};
    
    }
    
    my @topheadingrow = $q->th('') . 
                        $q->th({-colspan=>2},'阻擋垃圾');

    my @secondheadingrow = $q->th(['','迴響','引用']);

    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'},'妳想要怎麼回應那些遭拒的迴響/引用呢?');
        $html .= $q->p({-class=>'description'},'妳可以在底下自訂要給遭拒使用者的回應.  ');
        $html .= $q->p(
            $q->textfield(-class=>'text', -name => 'denyResponse', -size=>70, -value => $_cache->{config}->{'denyResponse'})
        );
        $html .= $q->p({-class=>'description'},'<strong>變數</strong>'.$q->br.
        '__TYPE__ = 遞送的種類 (可能是 "迴響" 或 "引用")'.$q->br.
        '__BLACKLIST__ = 吻合黑名單內容的第一個項目'.$q->br.
        '__TEXT__ = 實際遞送來的內容中, 與黑名單吻合的特定文字片段 ');


        $html .= $q->p({-class=>'question'},'妳想要記錄下被阻擋起來的發表嗎?',
                        $q->radio_group(-name => 'logDenials',
                                    '-values' => [1,0],
                                    '-default' =>$_cache->{config}->{'logDenials'},
                                    -labels => {1=>'是', 0=>'否'})
                ).
                $q->p({-class=>'description'},'MT-blacklist 可以把被阻擋下來的迴響或引用垃圾嘗試記載在妳的 Movable Type 活動日誌裡.  第一個吻合黑名單內容的字串, 送出垃圾的可能 IP 地址以及遭拒的時間都將被記載下來.  這不僅是驗證這個外掛模組有多有效的方法, 更是察覺誤判誤檔的為一機會.');


        $html .= $q->h3('搜尋及清垃圾');
        
        my @despamAction_labels = (
                {value => 0, label => "不要刪除迴響/通告或重建"},
                {value => 'delete', label => "刪除迴響/通告"},
                {value => 'deleteRebuildEntry', label => "刪除迴響/通告並重建文章項目"},
                {value => 'deleteRebuildEntryIndexes', label => "刪除迴響/通告並重建文章項目/索引頁面      "});
        my $despamAction_default = $_cache->{config}->{'despamAction'} || 0;
    
        $html .= $q->p({-class=>'question'},'請問妳想怎麼清垃圾?');
		$html .= $q->p({-class=>'description'},'妳可以在底下設定從迴響通知書的鏈結使用清垃圾模式時, 所要執行的預設動作.  這個組態設定祇是決定預設值而已, 到時候妳還是可以在清垃圾頁面裡視情況需要而加以變更.');
        $html .= '<p><select name="despamAction">';
        foreach (@despamAction_labels) {
            my $selected = ($_->{'value'} eq $despamAction_default) ? 'selected="selected" ' : '';
            $html .= '<option '.$selected.'value="'.$_->{'value'}.'">'.$_->{'label'}.'</option>'; 
        }
        $html .= '</select></p>';


        $html .= $q->p({-class=>'question'},
        	'應該搜尋多深? (迴響/通告的數量) &nbsp; &nbsp; ',
            $q->textfield(-class=>'text', 
            				-name => 'spamSearchDepth', 
            				-size=>4, 
            				-value => $_cache->{config}->{'spamSearchDepth'})
        );
		$html .= $q->p({-class=>'description'},'垃圾搜尋模式 (按下工具列的清垃圾按鈕或從清垃圾結果頁面而來的) 會從妳最近的這些迴響/通告比對黑名單.  妳可以在此設定預設要往回搜尋多少迴響/通告.  要搜尋的迴響/通告越多, 就會花去越多時間; 所以也許設少一點會是比較好的選擇.  不過, 妳也可以針對狂猛垃圾攻擊而指定更高的數值.');
    
	}
	
	
	
	# Only users who can create blogs can fiddle with file writing
    if ($app->{user}->can_edit_publishloc) {

        $html .= $q->h3('自動黑名單發佈功能');
        $html .= $q->p({-class=>'question'},'請問妳想要在每次有所變動後, 發佈站台上的黑名單嗎?',
                        $q->radio_group(-name => 'autoPublish',
                                    '-values' => [1,0],
                                    '-default' =>$_cache->{config}->{'autoPublish'},
                                    -labels => {1=>'要', 0=>'不要'})
                ).
                $q->p({-class=>'description'},'如果妳 (如我們所強烈建議地) 選擇了要, 那麼每當妳做出變更後就會自動發佈妳站台的黑名單. 這可以幫助其他人幾乎也能及時抵禦由你所發現的垃圾攻擊.   基於標準化的好處, 我們建議妳把這個檔案取名叫 <em>blacklist.txt</em> 並存放於網站根目錄.  日後可能會累積起來的網路效應可是相當有趣的呦. ');
    
        $html .= $q->p(
            $q->strong('如果妳要這麼做的話, 請在底下輸入完整的路徑及檔案名稱:').
            $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 => '儲存組態設定')
	);
    $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("還原至預設的設定"),
            $q->p('當妳有需要還原到預設的設定情況時, 就可以使用下面的按鈕.  這將會刪除妳整份黑名單, 並將妳的組態設定還原至出廠預設值.'),   
            $q->start_form('post',$app->uri),
            $q->p(
                $q->hidden('__mode'),
                $q->checkbox(-name=>'yesImSure',
                                   -value=>'1',
                                   -label=>'我真的不想保留我的黑名單跟組態設定資料了.')
            ),
            $q->p(
                $q->submit(-name => '', -value => '還原至預設的組態設定及黑名單')
            ),
            $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("妳未經授權以執行這個動作.");
    }
	
	my $idiotCheck = $q->param('yesImSure') || $newInstall;
	unless ($idiotCheck == 1) {
	    return $app->redirect($app->uri); 
	} 
		
	$app->_saveBlacklist([{string => ''}]);											

	my $docroot = './';
	$docroot =~ s#/?$#/#;

	my $config_data = {
				pluginVersion => $VERSION,
				blacklistActive => 0,
#                 publishLoc => $docroot.'blacklist.txt',  # L10N Bug?!
                publishLoc => 'blacklist.txt',
                autoPublish => (-d $docroot ? 1 : 0),
                updateNotifyURLs => [],
				logDenials => 1,
				despamAction => 'deleteRebuildEntryIndexes',
				spamSearchDepth => 25,
				denyResponse => '妳所輸入的「 __TYPE__ 」因為含有「 __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('清除妳網誌上的垃圾','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("無法載入 $type 來清垃圾. (".
	         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("無法載入 $type 來清垃圾. (".
             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("不正確的物件型別: ".$type);
	} 


	$html.= $q->h2("清除 Movable Type 的垃圾");
	$html .= $q->p('在底下的問句裡，妳將看到從',
                $type eq 'comment' ?
					$q->a({-href=>$comment_mt_url},'迴響') :
                	'通告',
			'中所擷取出的網址.  在這個頁面, 妳可以把這些網址加進
			 妳的黑名單, 然後刪除迴響再重建.  所有的事情就祇需要
			 一個步驟!'
	);

	my @unique = $app->_extractURLs(@extract_strings);

	$html .= $q->start_form('post',$app->uri);
	
	$html .=  $q->h3('找到的網址');

	if (@unique) {
        $html .=  $q->p({-class=>'description'},"下列的網址片段都是從 $type 中找到的.").
        	$q->p({-class=>'description'},
        	"請從中選擇妳想要擋掉的, 並如同妳在",
            $q->a({-href=>$app->uri.'?__mode=add'},'加入頁面'),'中那樣予以編輯 (也就是加入註解等資訊).  如果妳想要擋掉的字串沒被擷取出來的話, 也可以從實際送出的'.$type.$q->a({-href=>'#rawsource'},'原始資料').'中複製出來並貼上: ');
 
        my $rows = scalar(@unique) > 20 ? 20 : scalar(@unique);
        $rows = $rows < 5 ? 5 : $rows;   
		$html .= '<p><textarea name="foundURLs" rows="'.$rows.'" cols="80" class="text">'.join("\n",@unique).'</textarea></p>';
	} else {
        $html .=  $q->p('找不到任何網址片段.');
	}



	$html .=  $q->h3('把垃圾拿開').
			$q->p({-class=>'description'},"高興的話, 妳可以選擇刪除 $type ",
			'然後重建文章項目.',$q->hidden('id'));

	my @despamAction_labels = (
			{value => 0, label => "別刪除 $type 也不要重建"},
			{value => 'delete', label => "刪除 $type"},
			{value => 'deleteRebuildEntry', label => "刪除 $type 並重建文章項目"},
			{value => 'deleteRebuildEntryIndexes', label => "刪除 $type 並重建文章項目/索引頁面"});
	my $despamAction_default = $_cache->{config}->{'despamAction'} || 0;

	$html .= '<p><select name="despamAction">';
	foreach (@despamAction_labels) {
		my $selected = ($_->{'value'} eq $despamAction_default) ? 'selected="selected" ' : '';
		$html .= '<option '.$selected.'value="'.$_->{'value'}.'">'.$_->{'label'}.'</option>'; 
	}
	$html .= '</select></p>';


	$q->param('__mode','despam_confirm');	
    $html .= $q->p({-class=>'right'}, $q->hidden('__mode'),$q->hidden('_type'),
$q->submit(-name => '', -value => '現在馬上執行我的指令!')).$q->hr;




    $html.= $q->h3({-id=>'rawsource'},"原始的 $type 內容:");
    my $rows = () = $raw_source =~ /\n/g; ;
    $rows = $rows > 20 ? 20 : $rows;
    $rows = $rows < 5 ? 5 : $rows;   
	$html .= $q->p('<textarea readonly="readonly" rows="'.$rows.'" cols="80" name="rawsource" class="text">'.$raw_source.'</textarea>');
    $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('清垃圾結果','despampage');
	$html .= $q->h2('清垃圾結果');

	# 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 = '使用者 \''.$app->{user}->name.'\' 加入了 '.
                (@{$results{'success'}} == 1 ? '\''.${$results{'success'}}[0].'\'' :
                    (@{$results{'success'}}).' 則項目').
                    '到 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("無法載入 $type 來刪除 (ID: $id):") unless $object ;


        $findMoreLink = $q->p('尋找其他吻合<a href="'.
                $app->uri.'?__mode=search&amp;_type='.$type.'">妳的黑名單</a>或<a href="'.
                $app->uri.'?__mode=search&amp;_type='.$type.'&amp;ip='.
                $object->ip.'&amp;matchType=ip">這個使用者的 IP 地址</a>的'.$type.'.');

        if ($app->_deleteObject($object)) {
            $results{'obj_removal'} = $q->strong({-class=>'msg_success'},'成功啦');
			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'},'成功啦');
            } elsif (exists($results{'entry_rebuild'})) { 
                $results{'entry_rebuild'} =
                    $q->strong({-class=>'msg_failure'},'失敗了 - '.
                    	($app->errstr || '未知的錯誤'));
			} else { 
                     $results{'entry_rebuild'} = $q->strong({-class=>'msg_info'},'未請求');
            }
        } else {
            $results{'obj_removal'} = $q->strong({-class=>'msg_error'},'失敗了'.
            	($app->errstr || 'Unknown error'));
            $results{'entry_rebuild'} = $q->strong({-class=>'msg_info'},'沒完成');
        }		
    } else {
        $results{'entry_rebuild'} = $results{'obj_removal'} = 
        	$q->strong({-class=>'msg_info'},'未請求');
    }


	$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(["啥事也沒做."]))
		);
		return $html;
	}


	$html .= $q->div({-class=>'msg_info'},
		$q->ul($q->li([ucfirst($type).' 移除: '.$results{'obj_removal'},
			'重建文章項目: '.$results{'entry_rebuild'} ]))
	);


	if (@{$results{'dupes'}}) {
		$html .= $q->div({-class=>'msg_warning'},
			$q->p("下列的項目業已重複或者祇是既有子字串的較長版本而已, 因此不會被加入:"),
			$q->ul($q->li($results{'dupes'})));
	}

	return $html unless @{$results{'success'}};
	

	if ($results{'blacklist_saved'}) {
        $html .= $q->div({-class=>'msg_success'},
            $q->p("下列的項目已經成功地被加進黑名單裡了:"),
            $q->ul($q->li($results{'success'})));
	} else {
		$html .= $q->div({-class=>'msg_failure'},
			$q->p("加入妳的項目時出錯了. 請按下「上一頁」按鈕, 取消「刪除 $type 」的核選框然後再試一次"));
	}
	$html .= $findMoreLink || '';
	$html;

}



sub despam_multi {

	
    my $app = shift;
	my $q = $app->{query};
	my $type = $q->param('_type');

    my $html = $app->_navigation_html('清垃圾結果','despampage');
	$html .= $q->h2('清垃圾結果');

    my @objects = $q->param('deleteObject');
    my $rebuild = $q->param('rebuildEntries');

    unless (@objects) {
        my $html = $app->_navigation_html('清垃圾結果','despampage');
        $html .= $q->h2('清垃圾結果');
        $html .= $q->div({-class=>'msg_info'},
            $q->ul($q->li([ '啥事也沒做.' ]))
        );
        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, '妳沒有編輯 '.$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, "無法刪除 $type (ID: $id):". 
    	    		($object->errstr || '未知的錯誤'));
	        }
        } else {
    	    push(@errors, "無法載入 $type 來刪除 (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, '重建文章項目 ID '.$entry->id. '時出錯了');
             }
        }
	}
	
	
	$html .= $q->div({-class=>'msg_info'}, scalar(@objects).' 個 '.$type.' 中共有 '.$q->ul($q->li([scalar(@deleted).' 個已成功地被刪除了.'])));

	if (@extracted_urls) {
	    $q->param('__mode', 'add');
	    $html .= $q->start_form('post',$app->uri);
		$html .= $q->div({-class=>'msg_info'}, 
				$q->p('從被刪除的迴響中找到了網址.  ',
				$q->hidden('__mode'),
				(map { $q->hidden('url',$_) } @extracted_urls),
				$q->submit(-name=>'',-value=>'按此以檢視並加入')));
	    $html .= $q->end_form;
	}
	if (@errors) {
		$html .= $q->div({-class=>'msg_failure'}, 
				$q->p('遇到了下列的錯誤:'),
				$q->ul($q->li([@errors])));
	}
	$html;
}





sub search {

    my $app = shift;
	my $q = $app->{query};

	# Set search type
	my $type = $q->param('_type') || 'comment';
	my $matchType = $q->param('matchType') || 'blacklist';

	# Set extra search criteria if applicable
	my $ip = $q->param('ip') if ($matchType eq 'ip');
	my $text = $q->param('text') if ($matchType eq 'text');


	# Set miscellaneous search options

	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();
	}

	# Get objects the user has edit priviliges for
	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('無法載入網誌 ID '.$obj->blog_id);
            $blognames{$obj->blog_id} = $blog->name;
        }

        my ($rc,$blacklist_string,$matched_string);
    
		# IP address matching  
		if (($matchType eq 'ip') && $ip && $obj->ip && ($obj->ip eq $ip)) {
		
			push(@filtered_objects, {class => 'ip_address',
									blog => $blognames{$obj->blog_id},
									entry => $e,
									object => $obj,
									matched => $ip});

		# Blacklist matching
		} elsif (($matchType eq 'blacklist') && (($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});

		# Text/Regexp matching
		} elsif (($matchType eq 'text') && $text && ($matched_string = _matchArbitraryText($text, $string))) { 
            push(@filtered_objects, {class => 'text',
                                        blog => $blognames{$obj->blog_id},
                                        entry => $e,
                                        object => $obj,
                                        blacklist_string => $blacklist_string,
                                        matched_string => $matched_string});
                                        
		# LastN comments/trackbacks (no filtering, match all)
		} elsif ($matchType eq 'all') { 
             push(@filtered_objects, {class => 'matchall',
                                        blog => $blognames{$obj->blog_id},
                                        entry => $e,
                                        object => $obj,
                                        blacklist_string => $blacklist_string,
                                        matched_string => $matched_string});
       }
    }


    my $html = $app->_navigation_html('搜尋迴響/引用','searchpage');
    my %defaults = (all => '', blacklist => '', ip => '', text => '', comment => '', ping => '');
    $defaults{$matchType} = 'checked="checked" ';
    $defaults{$type} = ' selected="selected" ';
    $html .= $q->start_form('get',$app->uri);
    $html .= $q->div({-class => 'searchbox'},
		$q->p("從最後 ",
    '<input type="hidden" name="__mode" value="search" /> ',
    '<input type="text" name="n" value="'.$searchDepth.'" size="4" /> 則',
    '<select name="_type"><option value="comment"'.$defaults{'comment'}.'>迴響   </option>'.
		'<option value="ping"'.$defaults{'ping'}.'>引用通告   </option></select> 搜尋'),
    	$q->p({-style => 'margin: 0px auto; text-align: left;width:300px'},
		'<input type="radio" name="matchType" value="blacklist" '.$defaults{'blacklist'}.'/> 吻合於黑名單的內容<br />'.
	    '<input type="radio" name="matchType" value="text" '.$defaults{'text'}.'/> 文字/正規表示式'.
		'<input type="text" name="text" value="'.($text || '').'" size="25" /><br />',
    '<input type="radio" name="matchType" value="ip" '.$defaults{'ip'}.'/> IP 地址'.
	    '<input type="text" name="ip" value="'.($ip || '').'" size="25" /><br />',
 	   '<input type="radio" name="matchType" value="all" '.$defaults{'all'}.'/> 全部 (亦即不另設限)<br />'),
	$q->p('<input type="submit" name="re-search" value="搜尋" />')
    );
    $html .= $q->end_form;
    				
    				
    				
	$html .= '搜尋'.$q->h2(ucfirst($type).'垃圾');

# 	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 <a href="'.$toggle_type_link.';_type='.$type_toggle.'">'.$type_toggle. 's</a>');


	if (! @filtered_objects) {
		$html .= $q->p('在最後 '.$searchDepth.' 個 '.$type.' 中, 沒有任何妳能編輯的網誌裏, 有任何項目吻合妳的搜尋條件.');
#		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);
    $html .= $q->p({-class=>'description'},$q->hidden('__mode'), $q->hidden('_type'), 
    		'最新的 ',$searchDepth,' 則',$type.'中, 共有 '.scalar(@filtered_objects).
        ' 則'.$type.'吻合妳先前所指定的'.
        "搜尋條件. ".
        '請從底下選擇妳要刪除的.  妳高興的話也可以'.
        '<a href="'.$spamTextToggleURL.'">切換</a>'.$type.'文字.');
    $html .= '<table width="95%">';

	my @headings;
    if ($type eq 'comment') {
	    @headings = ('垃圾','作者','IP 地址','電子郵件','網址');
    } else {
	    @headings = ('垃圾','網誌','標題','來源網址');
    }		
    $html .= '<tr><td>'.join('</td><td>', @headings).'</td></tr>';

    require MT::Util; 
    
    my $count = 0;
    foreach (@filtered_objects) {
    	my $obj = $_->{object};
		my $alternate = ($count++ % 2 == 1) ? 'odd' : 'even'; 
        if ($type eq 'comment') {
            $html .= '<tr class="'.$alternate.
                    '"><td class="noline">'.
                    '<input type="checkbox" name="deleteObject" checked="checked" value="'.
                    $obj->id.
                    '" /></td><td class="noline">'.
                    $obj->author.'</td><td class="noline">'.
                    $obj->ip.'</td><td class="noline">'.
                    $obj->email.'</td><td class="noline">'.
                    $obj->url.'</td></tr>';
        } else {
            $html .= '<tr class="'.$alternate.
                    '"><td class="noline">'.
                    '<input type="checkbox" name="deleteObject" checked="checked" value="'.
                    $obj->id.
                    '" /></td><td class="noline">'.
                    $obj->blog_name.'</td><td class="noline">'.
                    $obj->title.'</td><td class="noline">'.
                    $obj->source_url.'</td></tr>';
        }		
		if ($matchType eq 'blacklist') {
            $html .= '<tr class="'.$alternate.
                    '"><td colspan="'.scalar(@headings).
                    '" class="noline" align="center">吻合的黑名單項目: '.
                    $_->{blacklist_string}.'</td></tr>';
        }
		if ($_cache->{config}->{showSpamText}) {
			my $text = $type eq 'comment' ? MT::Util::first_n_words(MT::Util::encode_html($obj->text),10) : $obj->excerpt;
            $html .= '<tr class="'.$alternate.
                    '"><td colspan="'.scalar(@headings).'" class="noline">'.
                    $text.'</td></tr>';
        }

		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 .= '<tr class="'.$alternate.
    			'"><td colspan="5" align="center"><strong>'. ($comment_mt_url ? '<a href="'.$comment_mt_url.
    			'">' : '').'發表至 '.$_->{blog}.' 的文章項目 "'.
    			$_->{entry}->title.'"'. ($comment_mt_url ? '</a>' : '').'</strong></td></tr>';
    }
    $html .= '</table>';

    $html .= $q->p('<input type="checkbox" name="rebuildEntries" value="1" checked="checked" />'.
    		'刪除'.$type.'之後重建相關的文章項目');
    $html .= $q->p($q->submit(-name => '', -value => '刪除已標記的'.$type ));
    $html .= $q->end_form;
    $html;




}

sub debug_app {
	my $app = shift;

    $app->{user}->can_create_blog or return $app->error("妳未經授權以檢視此頁面");
    my $html = $app->_navigation_html('除錯模式','debugpage');
	$app->_debug_status(1);
	$html .= '<h1>ENV OBJECT</h1>'.$app->_debug(\%ENV);
	$html .= '<h1>APP OBJECT</h1>'.$app->_debug($app);
	$html .= '<h1>CACHE OBJECT</h1>'.$app->_debug($_cache);
	$html;

}


sub debug_perms {
	my $app = shift;

    my $blogs = _getBlogs(); 
	my $html;
    require Data::Dumper;
$app->_debug_status(1);
    for (values %{$blogs}) {
		my $perms = $app->_getBlogPermissions($_->id);
		$html .= '<h1>'.$_->id .' and '.$app->{user}->id.'</h1>';
		$html .= $app->_debug($perms);
	}

	$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} ||= 
		'妳的「 __TYPE__ 」因為含有「 __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();

}


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 = '使用者 \''.$app->{user}->name.'\' 加入了 '.
			(@{$results{'unique'}} == 1 ? '\''.${$results{'unique'}}[0].'\'' :
				(@{$results{'unique'}}).' 則項目').
				'到 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 安裝';
	} 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 = '使用者 \''.$app->{user}->name.'\' 從 MT-Blacklist 裡刪除了 '.
		($diff == 1 ? '\''.$delete_strings[0].'\'' :
			(scalar(@{ $_cache->{blacklist} })-scalar(@pared_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("未提供要刪除的物件");
	
	$object->remove or 
		$app->error("無法移除物件: ".($object->errstr || '未知的錯誤'));
}



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} && ($data->[0]->{string} eq '')) { shift(@{$data}); }
		@$data = sort { $a->{string} cmp $b->{string} } @$data;
   	} 

	my $rc;
    if ($blacklist_config_directory && ($blacklist_config_directory ne '')) {
    	my $file = $blacklist_config_directory."/$key.yaml";
    	$file =~ s#//#/#g;
		unless (-d $blacklist_config_directory) {
        	die "妳所指定的黑名單組態目錄 ($blacklist_config_directory) 並不存在.";
        }
		require Yaml; 
		$rc = YAML::DumpFile($file, $data);
		unless ($rc) {
			my $mode = (stat($blacklist_config_directory))[2];
			my $perm = sprintf "%04o", $mode & 07777;
            my $debuginfo = {
            'directory' => $blacklist_config_directory,
            'mode' => $mode,
            'perm' => $perm
            };
			die "<p>無法儲存妳的 $key 資料. 這可能表示妳的黑名單組態目錄的權限有問題.</p><p>系統傳回來的錯誤是: ". ($@ || '未知的錯誤') ."</p><p>這裡是一些可以讓妳回報的除錯資訊:</p><pre>\n".Dumper($debuginfo).'</pre>'; 
		}
    } else {
        my $blObj = _getPluginDataObject($key);
        if (!$blObj) {
			require MT::PluginData;
            $blObj = MT::PluginData->new;
            $blObj->plugin ('Blacklist');
            $blObj->key ($key);
        }
        
        $blObj->data ($data);
        $rc = $blObj->save or die "無法儲存妳的 $key 資料: ". 
        	($blObj->errstr || '未知的錯誤');
    }

	# 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,"沒有設定黑名單發佈位置.") unless $blPath;

	# Create blacklist file content
	my $data = _getBlacklistMasthead();	

	my $num_entries = 0;
	if ($_cache->{blacklist}) {
		$num_entries = scalar(@{ $_cache->{blacklist} }); 
		foreach (@{ $_cache->{blacklist} }) {

			my $len = length($_->{'string'}) > 35 ? length($_->{'string'}) : 35;
			$data .= sprintf("%-".$len."s ", $_->{'string'});
			$data .= $_->{'comment'} ? '# '.$_->{'comment'}."\n" : "\n";
		}
	} 
	$data =~ s/\{NUM_ENTRIES\}/$num_entries/;

	my $ts = _getTimestamp();
	$ts =~ s#(\d{4})(\d{2})(\d{2})(\d{2})(\d{2})(\d{2})#$1/$2/$3 $4:$5:$6#;
	$data =~ s/\{TIMESTAMP\}/$ts/;

	# Write file
	require 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} }));
	
}


#
# _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 _matchArbitraryText {

    my (@strings, @blacklisted_strings, $str, $deny);
	my $needle = shift;
	my $haystack = shift;

    # Sanitize text
    $haystack = _sanitizeInput($haystack);
    return unless $haystack =~ /\S/;

    # Match against pattern in one expression
    return unless $haystack =~ m#$needle#io;
	return $&;

}






sub _canEditObject {
	my $app = shift;
	my ($type,$object) = @_;
	my $rc; 

	_mywarn("在 $type ID 為 ".$object->id." 的 canEditObject");

	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 "canEditObject 中未知的物件型別";
	}
	_mywarn("從 canEditObject 傳回了一個 $type ID 為 ".$object->id." 的 $rc");

	$rc;
}


sub _canEditComment {
	my $app = shift;
	my $c = shift or return $app->error("沒有要檢查權限的特定迴響.");

	_mywarn("在迴響 ID 為 ".$c->id." 的 canEditComment");

	my $entry = _getEntry($c->entry_id);

	$app->_canEditEntry($entry);
}


sub _canEditPing {
	my $app = shift;
	my $p = shift or return $app->error("沒有要檢查權限的特定通告.");

	_mywarn("在通告 ID 為 ".$p->id." 的 canEditPing");

	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("沒有要檢查權限的特定文章項目.");

	_mywarn("在文章項目 ID 為 ".$e->id." 的 canEditEntry");

	if ($app->_canEditAllEntries($e->blog_id)) {
		_mywarn("從 canEditEntry 傳回了 1 (CAN_EDIT_ALL_BLOGS)");
		$_cache->{perms}->{canEditEntry}{$e->id} = 1;
		return $_cache->{perms}->{canEditEntry}{$e->id} 

    } elsif (exists($_cache->{perms}->{canEditEntry}{$e->id})) {
		_mywarn("(CACHE) 從 canEditEntry 傳回了 ".
				$_cache->{perms}->{canEditEntry}{$e->id});
    	return $_cache->{perms}->{canEditEntry}{$e->id};

    } elsif (! $app->_getBlogPermissions($e->blog_id)) {
		_mywarn("(CACHE) 從 canEditEntry 傳回了 0 (沒有網誌的權限)");
    	$_cache->{perms}->{canEditEntry}{$e->id} = 0;
    	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) 從 canEditEntry 傳回了 ".
		$_cache->{perms}->{canEditEntry}{$e->id});

	$_cache->{perms}->{canEditEntry}{$e->id};
		
}


sub _canEditAllEntries {
	my $app = shift;
	my $b_id = shift or return $app->error("沒有要檢查權限的特定網誌 ID.");

	_mywarn("在網誌 ID 為 ".$b_id." 的 canEdirAllEntries");

	if (exists($_cache->{perms}->{canEditAllEntries}{$b_id})) {
	_mywarn("(CACHE) 從 canEdiAllEntries 傳回了 ".$_cache->{perms}->{canEditAllEntries}{$b_id});
		return $_cache->{perms}->{canEditAllEntries}{$b_id};
    }
		
	if (defined($_cache->{perms}->{blog}{$b_id})) {
		_mywarn("(CACHE) 正在從 _canEditAllEntries 傳回網誌 ID 為 ".$b_id." 的權限");
		return $_cache->{perms}->{blog}{$b_id};
	} else {
		_mywarn("(DB) 正在從 _canEditAllEntries 傳回網誌 ID 為 ".$b_id." 的權限");
	    my $perm = $app->_getBlogPermissions($b_id);
	    if ($perm && ref($perm)) {
	        $_cache->{perms}->{canEditAllEntries}{$b_id} = 
 	           $perm->can_edit_all_posts;
		} elsif (defined($perm)) {
			return $perm;
		} else {
			warn "警告：無法取回網誌 ID 為 $b_id 中、使用者 ID 為 ".$app->{user}->id." 的權限";	
			0;    
		}
	}
}


sub _getBlogPermissions {
	my $app = shift;
	my $id = shift;

	_mywarn("在網誌 ID 為 $id 的 getBlogPermissions 中 ");

	if (my $p = $_cache->{perms}->{blog}{$id}) {
		_mywarn("(CACHE) 正在傳回網誌 ID 為 $id 的權限");
		return $p;
	} else {
		_mywarn("(DB) 正在載入網誌 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({}, { 'sort' => 'created_on', direction => 'descend', limit => $searchDepth});

	} else {
		require MT::TBPing;
		$iter = MT::TBPing->load_iter({}, { 'sort' => 'created_on', direction => 'descend', limit => $searchDepth});
	}

	my $count = 0;
	my @objects;
	while (my $obj = $iter->()) {
		if ($app->_canEditObject($type,$obj)) {
			_mywarn("(DB) 已載入 $type ID ".$obj->id);
			push(@objects, $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 _stringBreak {

    my $app = shift;
    my ($string,$length) = @_;
    
    return $string unless $length;
    
    
    my @lines = ();
    if (length($string) > $length) {
    	my @chars = split //, $string;
     	while (@chars) {
	     	push(@lines, join('',splice(@chars,0,$length-1)));
     	}
     	$string = join(' ',@lines,join('',@chars));
    }
    
	return $string;
}



sub _navigation_html {

	my $app = shift;
	my ($tagline,$pageclass) = @_;
	my $q = $app->{query};

	$tagline = "- $tagline" if $tagline;
	
	my $styles = _blacklistAppCSS();
 	my $html = <<EOD;
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
        "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
<head>
<title>MT-Blacklist $tagline</title>
$styles
</head>
<body class="$pageclass">
<div id="header">
EOD


    $html .= $q->h1('MT-Blacklist',($_cache->{config}->{'blacklistActive'} ? '' : '目前 '.$q->a({-href => '?__mode=config', -style=>'msg_warning'}, '尚未啟用')));

    my %menu = (
            '清單' => {
                'link' => $app->uri,
                title => '列出所有的黑名單項目'
            },
            '加入' => {
                'link' => '?__mode=add',
                title => '加入或匯入黑名單項目'
            },
            '發佈' => {
                'link' => '?__mode=publish',
                title => '發佈妳的黑名單'
            },
            '組態' => {
                'link' => '?__mode=config',
                title => '組態 MT-Blacklist'
            },
            '清垃圾' => {
                'link' => '?__mode=search',
                title => '從妳的 Movable Type 裡清垃圾'
            },
            '關於' => {
                'link' => 'http://www.jayallen.org/comment_spam/',
                title => '關於 MT-Blacklist'
            },
            '還原' => {
                'link' => '?__mode=restore&yesImSure=1',
                title => '清除並還原 MT-Blacklist'
            },
            '除錯' => {
                'link' => '?__mode=debug',
                title => '除錯 MT-Blacklist'
            }
    );
	delete($menu{'發佈'}) if $_cache->{config}->{'autoPublish'}; 
	delete($menu{'除錯'}) unless $app->_debug_status() && $app->{user}->can_create_blog; 
	delete($menu{'還原'}) unless $app->_debug_status() && $app->{user}->can_create_blog; 

	my $menulist;
	foreach ('清單','加入','清垃圾','發佈','組態','關於','還原','除錯') {
		next unless exists($menu{$_});
		push(@$menulist, $q->a({href=>$menu{$_}{'link'},
							title=>$menu{$_}{'title'}},
							$_));
	}
		
	$html .= $q->div({id => $menu{'發佈'} ? 'navcontainer' : 'navcontainer_nopublish'},
		$q->ul({id => 'navlist'},$q->li($menulist)));
	$html .= '</div><div id="content">';
	$html;

}

 
sub _debug {

	my $app = shift;	
	return '' unless $app->_debug_status() && $app->{user}->can_create_blog; #JAYBO
 
	unless (@_) {
		my ($package, $filename, $line) = caller(); 
		@_ = ('沒有可用於 '.$package.', 第 '.$line.' 列的除錯資訊');
	}
		
	if ((@_ > 1) || ref($_[0])){
	    require Data::Dumper;
        return '<pre style="background: #eee;border:1px solid #ccc;text-align:left;">'.Data::Dumper::Dumper(@_).'</pre>';
    } else {
		return '<p style="background: #eee;border:1px solid #ccc;text-align:left;">'.(shift).'</p>';
    }
}

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
#
#	MT-Blacklist Version $VERSION
#
#   Last update:        {TIMESTAMP}
#   Number of entries:  {NUM_ENTRIES}
#
#   You can find out more about this file at:
#    http://www.jayallen.org/comment_spam/
#
#   You can find out more about MT-Blacklist at
#   http://www.jayallen.org/projects/mt-blacklist
#

EOD
}

sub _blacklistAppCSS {
return <<EOD;

<style type="text/css" media="screen">
/* <![CDATA[ */

/* 
	Menu bar with List-o-matic
	http://www.accessify.com/tools-and-wizards/list-o-matic
	
	General styles lovingly stolen 
	from Dean Allen's Refer.
	http://www.textism.com/tools/refer/
*/

body {
	text-align: center;
	background: #fff;
	color: #000;
	}
h1,h2,h3,h4,h5,h6  {
	font-family: Verdana, Trebuchet;
	margin-bottom: 0px;
	}
h1 { margin-top: 10px; }
h1 a { background: inherit; color: #f00;}
td {
	padding: 0 10px 0 0;
	color: black;
	font-family: Verdana;
	font-size: 10px;
	line-height: 20px;
	border-bottom: #dddddd;
	border-width: 0 0 1px 0;
	border-style: none none solid none;
	}
td.noline {
	border: 0;
	}
thead { font-weight: bold; }

p {
	font-family: Verdana, Trebuchet;
	font-size: small;
	line-height: 150%;
	}
code { font-size: medium; }

a {
	color: olive;
	text-decoration: none;
	}
a:hover { text-decoration: underline; }

a.helplink {
	font-size: 90%;
	font-weight: bold;
	}
input.text,textarea.text {
	border-color: gray white white gray;
	padding: 2px;
	}
textarea, select {
	font-family: Verdana;
	font-size: 10px;
	background-color: #eee;
	border-color: white gray gray white;
	border-width: 1px;
	border-style: solid;
	padding: 5px;
	}
input.white {
	background-color: #fff;
	border: 0;
	}
.right { text-align:right; }
.even { background: #ccc; }
.odd { background: #eee; }

/* ***************
 * SPECIFIC STYLES
 * ************* */

div#header {
	text-align: center;
	padding-bottom: 20px;
	}
div#content {
	clear: both;
	text-align: left;
	width: 500px;
	margin: 0 auto;
	}
div#versiontag {
	font-size:11px;
	color: #aaa;
	background: inherit;
	text-align: center;
	}	
/* MSG STYLES */
.msg_success {color: #0a0;}
.msg_failure {color: #f00;}
.msg_warning {color: #f90;}
.msg_info {color: #069;}

/* MSG BOXES */
p.msg_success, div.msg_success {
	border: 1px solid #cfc;
	background: inherit;
	}
p.msg_failure, div.msg_failure {
	border: 1px solid #fcc;
	background: inherit;
	}
p.msg_warning, div.msg_warning {
	border: 1px solid #fc9;
	background: inherit;
	}
p.msg_info, div.msg_info {
	border: 1px solid #369;
	background: inherit;
	}
	
	
/*LIST PAGE - Default view */
body.listpage div#content {
	text-align: center;
	width: 600px;
	}
body.listpage table {
	width: 600px;
	margin: 0px auto;
	text-align:left;
	}
body.listpage table th {
	text-align:left;
	}
body.listpage table td {
	vertical-align: top;
	text-align:left;
	}
body.listpage table td.right  {
	text-align:right;
	}
body.listpage table td.highlight {
	background:#ccf;
	}

/* VIEW PAGE */
body.viewpage div#content {
	text-align: center;
	}
body.viewpage table {
	margin: 20px auto;
	}
body.viewpage th {
	text-align: left;
	font-weight: bold;
	}
body.viewpage th.prevlink {
	text-align: left;
	font-weight: normal;
	}
body.viewpage th.nextlink {
	text-align: right;
	font-weight: normal;
	}
body.viewpage th.entryString {
	text-align: center;
	background: #ccc;
	color: #000;
	}
body.viewpage form {
	text-align: center;
	}

/* CONFIG PAGE */
body.configpage {
	text-align: center;
	}
body.configpage div#restoreform {
	padding: 0 10px;
	border: 1px solid #000;
	background: #c66;
	color: #000;
	}
body.configpage p.question {
	font-weight: bold;
	margin-bottom: 0px;
	}
body.configpage table#actiontable {
	width: 95%;
	}
/*
body.configpage table#actiontable tr.topheader {

	}
body.configpage table#actiontable tr.bottomheader {

	}
*/
body.configpage table#actiontable th.blognames {
	text-align:left;
	}
body.configpage table#actiontable td {
	text-align:center;
	}
.description {
	margin-top: 0px;
	font-size: x-small;
	background: inherit;
	color: #666;
	}

/* DESPAM PAGE */
body.despampage div.msg_info,
body.despampage div.msg_warning,
body.despampage div.msg_success,
body.despampage div.msg_failure {
	text-align: center;
	padding: 0 40px;
	}
body.despampage ul {
    margin: 10px 40px;
    padding: 0;
    border: none;
    text-align:left;
	}
	
/* PUBLISH PAGE */
body.publishpage form {
	text-align: center;
	}
	
body.searchpage div.searchbox {
	margin-top: 20px;
	background: #eee;
	color: inherit;
	border: 1px solid #999;
	padding: 5px;
	text-align: center;
	}	

/* *************
 * NAV MENU
 * *********** */

div#navcontainer {
    width: 395px; 
    margin: 0 auto;
    padding: 0;
    text-align:center;
	}
div#navcontainer_nopublish {
    width: 325px;
    margin: 0 auto;
    padding: 0;
    text-align:center;
	}
ul#navlist {
    list-style: none;
    margin: 0;
    padding: 0;
    border: none;
	}
ul#navlist li {
    display: block;
    margin: 0;
    padding: 0;
    float: left;
    width: auto;
	}
ul#navlist a {
    color: #444;
    display: block;
    width: auto;
    text-decoration: none;
    background: #DDDDDD;
    margin: 0;
    padding: 2px 10px;
    border-left: 1px solid #fff;
    border-top: 1px solid #fff;
    border-right: 1px solid #aaa;
	}

ul#navlist a:hover, ul#navlist a:active { background: #BBBBBB; }

ul#navlist a.active:link, ul#navlist a.active:visited {
    position: relative;
    z-index: 102;
    background: #BBBBBB;
    font-weight: bold;
	}


/* ]]> */
</style>

EOD
}











# #####################################
#
#
#  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} && ($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;

    if ($blacklist_config_directory && ($blacklist_config_directory ne '')) {
    	my $file = $blacklist_config_directory."/$key.yaml";
    	$file =~ s#//#/#g;
		require Yaml; 
		return unless -e $file;
		$_cache->{$key} = YAML::LoadFile($file)
			or return; 
    } else {
        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("在 $key 的 getPluginDataObject");
	if ($_cache->{plugindataobject}{$key}) {
		_mywarn("(CACHE) 正在從 _getPluginDataObject $key 傳回 PluginDataObject");
		$_cache->{plugindataobject}{$key};
    } else {
		require MT::PluginData;
		_mywarn("(DB) 正在從 _getPluginDataObject $key 傳回 PluginDataObject");
        $_cache->{plugindataobject}{$key} = MT::PluginData->load ({ plugin => 'Blacklist', 
                                            key => $key });
    }
}



sub _getEntry {
    my $id = shift;
	_mywarn("在文章項目 ID 為 $id 的 _getEntry 中 ");

    if (my $e = $_cache->{entry}->{$id}) {
		_mywarn("(CACHE) 正在從 _getEntry 傳回了 ID 為 $id 的文章項目\n");
    	return $e ;
    }
		_mywarn("(DB) 正在從 _getEntry 讀取 ID 為 $id 的文章項目\n");
	require MT::Entry;
    $_cache->{entry}->{$id} = MT::Entry->load($id);
}

sub _getTrackback {
    my $id = shift;
	_mywarn("在從調用者引用 ID 為 $id 的 loadTrackback 中 ");

    if (my $trackback = $_cache->{trackback}->{$id}) {
		_mywarn("(CACHE) 正在傳回引用 ID $id\n");
    	return $trackback ;
    }
	_mywarn("(DB) 正在讀取引用 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 "_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("在網誌 ID 為 ".$id." 的 loadBlog");

	if (my $b = $_cache->{blog}->{$id}) {
		_mywarn("(CACHE) 正在傳回網誌 ID $id");
	    return $b;
	} else {
		_mywarn("(DB) 正在傳回網誌 ID $id");
		require MT::Blog;
	    $_cache->{blog}->{$id} = MT::Blog->load($id);
    }
}

sub _getBlogs {
	require MT::Blog;

	_mywarn("在 loadBlogs 中 ");
	if ($_cache->{blogs_loaded}) {
		_mywarn("(CACHE) 正在傳回網誌");
		return $_cache->{blog};
	} else {
		_mywarn("(DB) 正在傳回網誌");
	    my @blogs = MT::Blog->load();
		$_cache->{blogs_loaded}++;
	    %{$_cache->{blog}} = map { $_->id, $_ } @blogs;
    	$_cache->{blog};
	}
}















# #####################################
#
#
#  START OF MT-BLACKLIST PLUGIN METHODS
#
#
# #####################################


#
# _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();

	my $query = $ENV{QUERY_STRING} || $ENV{REQUEST_URI} || '';
    unless ($query =~ 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...
	return;
}






#
# 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 HTML comments in the form of <! ... >
	$str =~ s/\x3c\!.+?\x3e//g;

	#
	#THANKS to Stepan Riha for the next three
    # Convert decimal entities (&#112; => p)
    $str =~ s/&#(\d{1,3});/chr($1)/eg;

    # Convert hex entities (&#x70; => p)
    $str =~ s/&#x(\d{2});/chr(hex($1))/eg;

    # Convert URL encodings (%70 => p)
    $str =~ s/\%([0-9A-Z]{2})/chr(hex($1))/eig;

	# 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; 

	return $str;
}



sub _mywarn {

    my $warning = shift;
    
    push(@_warnings, $warning);
    
}

sub _dumpwarn {
my $app = shift;
    $app->_debug_status(1);
    return $app->_debug(join("<br />\n", @_warnings)); 
}


1;
