2008-06-27 06:18:51 +00:00
< ? php
if ( ! defined ( 'MEDIAWIKI' ) )
die ();
class AbuseFilter {
2008-07-17 02:43:45 +00:00
public static $statsStoragePeriod = 86400 ;
2008-07-18 02:18:58 +00:00
public static $tokenCache = array ();
public static $modifyCache = array ();
2008-08-02 11:10:42 +00:00
public static $condLimitEnabled = true ;
public static $condCount = 0 ;
2008-09-18 13:01:50 +00:00
public static $filters = array ();
2009-01-23 19:23:19 +00:00
public static $tagsToSet = array ();
2009-01-26 22:31:02 +00:00
public static $history_mappings = array ( 'af_pattern' => 'afh_pattern' , 'af_user' => 'afh_user' , 'af_user_text' => 'afh_user_text' , 'af_timestamp' => 'afh_timestamp' , 'af_comments' => 'afh_comments' , 'af_public_comments' => 'afh_public_comments' , 'af_deleted' => 'afh_deleted' , 'af_id' => 'afh_filter' );
2009-01-27 01:31:42 +00:00
public static $builderValues = array (
'op-arithmetic' => array ( '+' => 'addition' , '-' => 'subtraction' , '*' => 'multiplication' , '/' => 'divide' , '%' => 'modulo' , '**' => 'pow' ),
'op-comparison' => array ( '==' => 'equal' , '!=' => 'notequal' , '<' => 'lt' , '>' => 'gt' , '<=' => 'lte' , '>=' => 'gte' ),
'op-bool' => array ( '!' => 'not' , '&' => 'and' , '|' => 'or' , '^' => 'xor' ),
2009-01-29 23:28:59 +00:00
'misc' => array ( 'in' => 'in' , 'like' => 'like' , '""' => 'stringlit' , 'rlike' => 'rlike' ),
2009-01-27 01:31:42 +00:00
'funcs' => array ( 'length(string)' => 'length' , 'lcase(string)' => 'lcase' , 'ccnorm(string)' => 'ccnorm' , 'rmdoubles(string)' => 'rmdoubles' , 'specialratio(string)' => 'specialratio' , 'norm(string)' => 'norm' , 'count(needle,haystack)' => 'count' ),
2009-01-29 23:36:09 +00:00
'vars' => array ( 'ACCOUNTNAME' => 'accountname' , 'ACTION' => 'action' , 'ADDED_LINES' => 'addedlines' , 'EDIT_DELTA' => 'delta' , 'EDIT_DIFF' => 'diff' , 'NEW_SIZE' => 'newsize' , 'OLD_SIZE' => 'oldsize' , 'REMOVED_LINES' => 'removedlines' , 'SUMMARY' => 'summary' , 'ARTICLE_ARTICLEID' => 'article-id' , 'ARTICLE_NAMESPACE' => 'article-ns' , 'ARTICLE_TEXT' => 'article-text' , 'ARTICLE_PREFIXEDTEXT' => 'article-prefixedtext' , 'MOVED_FROM_ARTICLEID' => 'movedfrom-id' , 'MOVED_FROM_NAMESPACE' => 'movedfrom-ns' , 'MOVED_FROM_TEXT' => 'movedfrom-text' , 'MOVED_FROM_PREFIXEDTEXT' => 'movedfrom-prefixedtext' , 'MOVED_TO_ARTICLEID' => 'movedto-id' , 'MOVED_TO_NAMESPACE' => 'movedto-ns' , 'MOVED_TO_TEXT' => 'movedto-text' , 'MOVED_TO_PREFIXEDTEXT' => 'movedto-prefixedtext' , 'USER_EDITCOUNT' => 'user-editcount' , 'USER_AGE' => 'user-age' , 'USER_NAME' => 'user-name' , 'USER_GROUPS' => 'user-groups' , 'USER_EMAILCONFIRM' => 'user-emailconfirm' , 'OLD_WIKITEXT' => 'old-text' , 'NEW_WIKITEXT' => 'new-text' , 'ADDED_LINKS' => 'added-links' , 'REMOVED_LINKS' => 'removed-links' , 'ALL_LINKS' => 'all-links' , 'NEW_TEXT' => 'new-text-stripped' , 'NEW_HTML' => 'new-html' , 'ARTICLE_RESTRICTIONS_edit' => 'restrictions-edit' , 'ARTICLE_RESTRICTIONS_move' => 'restrictions-move' , 'ARTICLE_RECENT_CONTRIBUTORS' => 'recent-contributors' ,),
2009-01-27 01:31:42 +00:00
);
2008-07-15 08:46:17 +00:00
2008-06-27 06:18:51 +00:00
public static function generateUserVars ( $user ) {
$vars = array ();
// Load all the data we want.
$user -> load ();
$vars [ 'USER_EDITCOUNT' ] = $user -> getEditCount ();
$vars [ 'USER_AGE' ] = time () - wfTimestampOrNull ( TS_UNIX , $user -> getRegistration () );
$vars [ 'USER_NAME' ] = $user -> getName ();
$vars [ 'USER_GROUPS' ] = implode ( ',' , $user -> getEffectiveGroups () );
$vars [ 'USER_EMAILCONFIRM' ] = $user -> getEmailAuthenticationTimestamp ();
// More to come
return $vars ;
}
2008-08-04 12:15:14 +00:00
public static function ajaxCheckSyntax ( $filter ) {
2009-01-26 23:32:46 +00:00
wfLoadExtensionMessages ( 'AbuseFilter' );
2008-08-04 12:15:14 +00:00
$result = self :: checkSyntax ( $filter );
$ok = ( $result === true );
if ( $ok ) {
return " OK " ;
} else {
2009-01-26 23:32:46 +00:00
return " ERR: " . json_encode ( $result );
2008-08-04 12:15:14 +00:00
}
}
2008-07-18 02:18:58 +00:00
2009-01-28 23:54:41 +00:00
public static function ajaxGetFilter ( $filter ) {
global $wgUser ;
if ( ! $wgUser -> isAllowed ( 'abusefilter-view' ) ) {
return false ;
}
$dbr = wfGetDB ( DB_SLAVE );
$row = $dbr -> selectRow ( 'abuse_filter' , '*' , array ( 'af_id' => $filter ), __METHOD__ );
if ( $row -> af_hidden && ! $wgUser -> isAllowed ( 'abusefilter-modify' ) ) {
return false ;
}
return strval ( $row -> af_pattern );
}
2009-01-29 22:44:31 +00:00
public static function ajaxCheckFilterWithVars ( $filter , $vars ) {
global $wgUser ;
// Anti-DoS
if ( ! $wgUser -> isAllowed ( 'abusefilter-view' ) ) {
return false ;
}
// If we have a syntax error.
if ( self :: checkSyntax ( $filter ) !== true ) {
return 'SYNTAXERROR' ;
}
$vars = json_decode ( $vars , true );
$result = self :: checkConditions ( $filter , $vars );
return $result ? 'MATCH' : 'NOMATCH' ;
}
2009-01-23 19:23:19 +00:00
public static function triggerLimiter ( $val = 1 ) {
self :: $condCount += $val ;
global $wgAbuseFilterConditionLimit ;
if ( self :: $condCount > $wgAbuseFilterConditionLimit ) {
throw new MWException ( " Condition limit reached. " );
}
}
2008-07-18 02:18:58 +00:00
public static function disableConditionLimit () {
// For use in batch scripts and the like
self :: $condLimitEnabled = false ;
}
2008-06-27 06:18:51 +00:00
public static function generateTitleVars ( $title , $prefix ) {
$vars = array ();
2009-02-03 18:48:16 +00:00
if ( ! $title )
return array ();
2008-06-27 06:18:51 +00:00
2008-08-02 13:51:29 +00:00
$vars [ $prefix . " _ARTICLEID " ] = $title -> getArticleId ();
2008-06-27 06:18:51 +00:00
$vars [ $prefix . " _NAMESPACE " ] = $title -> getNamespace ();
$vars [ $prefix . " _TEXT " ] = $title -> getText ();
$vars [ $prefix . " _PREFIXEDTEXT " ] = $title -> getPrefixedText ();
2009-01-23 19:23:19 +00:00
// Use restrictions.
2008-06-27 06:18:51 +00:00
if ( $title -> mRestrictionsLoaded ) {
// Don't bother if they're unloaded
foreach ( $title -> mRestrictions as $action => $rights ) {
$rights = count ( $rights ) ? $rights : array ();
$vars [ $prefix . " _RESTRICTIONS_ " . $action ] = implode ( ',' , $rights );
}
}
2009-01-23 19:23:19 +00:00
// Find last 5 authors.
2008-08-02 11:10:42 +00:00
$dbr = wfGetDB ( DB_SLAVE );
2009-01-28 06:29:59 +00:00
$res = $dbr -> select ( 'revision' , 'distinct rev_user_text' , array ( 'rev_page' => $title -> getArticleId () ), __METHOD__ , array ( 'ORDER BY' => 'rev_timestamp DESC' , 'LIMIT' => 10 ) );
2008-08-02 11:10:42 +00:00
$users = array ();
while ( $user = $dbr -> fetchRow ( $res )) {
$users [] = $user [ 0 ];
2008-07-18 08:30:25 +00:00
}
2008-08-02 11:10:42 +00:00
$vars [ $prefix . " _RECENT_CONTRIBUTORS " ] = implode ( ',' , $users );
2008-06-27 06:18:51 +00:00
2008-08-02 11:10:42 +00:00
return $vars ;
2008-06-27 06:18:51 +00:00
}
2008-08-03 14:04:26 +00:00
public static function checkSyntax ( $filter ) {
global $wgAbuseFilterParserClass ;
$parser = new $wgAbuseFilterParserClass ;
return $parser -> checkSyntax ( $filter );
}
2008-08-04 14:27:48 +00:00
public static function evaluateExpression ( $expr , $vars = array () ) {
2009-01-30 23:23:52 +00:00
wfLoadExtensionMessages ( 'AbuseFilter' );
2008-08-04 14:27:48 +00:00
global $wgAbuseFilterParserClass ;
2009-01-30 23:23:52 +00:00
if ( self :: checkSyntax ( $expr ) !== true ) {
return '' ;
}
2008-08-04 14:27:48 +00:00
$parser = new $wgAbuseFilterParserClass ;
$parser -> setVars ( $vars );
return $parser -> evaluateExpression ( $expr );
}
2008-09-29 13:30:11 +00:00
public static function ajaxReAutoconfirm ( $username ) {
if ( ! $wgUser -> isAllowed ( 'abusefilter-modify' )) {
// Don't allow it.
return wfMsg ( 'abusefilter-reautoconfirm-notallowed' );
}
$u = User :: newFromName ( $username );
global $wgMemc ;
$k = AbuseFilter :: autoPromoteBlockKey ( $u );
if ( ! $wgMemc -> get ( $k ) ) {
return wfMsg ( 'abusefilter-reautoconfirm-none' );
}
$wgMemc -> delete ( $k );
}
2008-08-04 14:27:48 +00:00
public static function ajaxEvaluateExpression ( $expr ) {
return self :: evaluateExpression ( $expr );
}
2008-07-18 02:18:58 +00:00
2009-01-23 19:23:19 +00:00
public static function checkConditions ( $conds , $vars , $ignoreError = true ) {
2008-08-02 11:10:42 +00:00
global $wgAbuseFilterParserClass ;
2008-07-18 08:30:25 +00:00
wfProfileIn ( __METHOD__ );
2008-06-27 06:18:51 +00:00
2008-08-03 14:04:26 +00:00
try {
$parser = new $wgAbuseFilterParserClass ;
$parser -> setVars ( $vars );
$result = $parser -> parse ( $conds , self :: $condCount );
} catch ( Exception $excep ) {
// Sigh.
$result = false ;
2009-01-23 19:23:19 +00:00
if ( ! $ignoreError ) {
throw $excep ;
}
2008-08-03 14:04:26 +00:00
}
2008-06-27 06:18:51 +00:00
2008-07-18 08:30:25 +00:00
wfProfileOut ( __METHOD__ );
2008-08-02 11:10:42 +00:00
return $result ;
2008-07-18 08:30:25 +00:00
}
2009-01-23 19:23:19 +00:00
/** Returns an associative array of filters which were tripped */
public static function checkAllFilters ( $vars ) {
2008-06-27 06:18:51 +00:00
// Fetch from the database.
$dbr = wfGetDB ( DB_SLAVE );
2008-09-05 14:27:18 +00:00
$res = $dbr -> select ( 'abuse_filter' , '*' , array ( 'af_enabled' => 1 , 'af_deleted' => 0 ) );
2009-01-23 19:23:19 +00:00
2009-02-02 17:57:06 +00:00
$filter_matched = array ();
2008-06-27 06:18:51 +00:00
while ( $row = $dbr -> fetchObject ( $res ) ) {
2009-01-23 19:23:19 +00:00
// Store the row somewhere convenient
2008-09-18 13:01:50 +00:00
self :: $filters [ $row -> af_id ] = $row ;
2009-01-23 19:23:19 +00:00
// Check conditions...
2008-10-24 08:58:32 +00:00
$pattern = trim ( $row -> af_pattern );
if ( self :: checkConditions ( $pattern , $vars ) ) {
2009-01-23 19:23:19 +00:00
// Record match.
2008-07-17 02:43:45 +00:00
$filter_matched [ $row -> af_id ] = true ;
} else {
2009-01-23 19:23:19 +00:00
// Record non-match.
2008-07-17 02:43:45 +00:00
$filter_matched [ $row -> af_id ] = false ;
2008-06-27 06:18:51 +00:00
}
}
2009-01-23 19:23:19 +00:00
// Update statistics, and disable filters which are over-blocking.
2008-10-24 08:58:32 +00:00
self :: recordStats ( $filter_matched );
2009-01-23 19:23:19 +00:00
return $filter_matched ;
}
/** Returns an array [ list of actions taken by filter, error message to display, if any ] */
public static function executeFilterActions ( $filters , $title , $vars ) {
$dbr = wfGetDB ( DB_SLAVE );
2008-06-27 06:18:51 +00:00
// Retrieve the consequences.
2009-01-27 20:18:58 +00:00
$res = $dbr -> select ( array ( 'abuse_filter_action' , 'abuse_filter' ), '*' , array ( 'af_id' => $filters ), __METHOD__ , array (), array ( 'abuse_filter_action' => array ( 'LEFT JOIN' , 'afa_filter=af_id' ) ) );
2009-01-23 19:23:19 +00:00
$actionsByFilter = array_fill_keys ( $filters , array () );
$actionsTaken = array_fill_keys ( $filters , array () );
// Categorise consequences by filter.
2009-01-27 20:18:58 +00:00
global $wgAbuseFilterRestrictedActions ;
2008-06-27 06:18:51 +00:00
while ( $row = $dbr -> fetchObject ( $res ) ) {
2009-01-27 20:18:58 +00:00
if ( $row -> af_throttled && in_array ( $row -> afa_consequence , $wgAbuseFilterRestrictedActions ) ) {
## Don't do the action
} else {
$actionsByFilter [ $row -> afa_filter ][ $row -> afa_consequence ] = array ( 'action' => $row -> afa_consequence , 'parameters' => explode ( " \n " , $row -> afa_parameters ) );
}
2009-01-23 19:23:19 +00:00
}
wfLoadExtensionMessages ( 'AbuseFilter' );
$messages = array ();
foreach ( $actionsByFilter as $filter => $actions ) {
// Special-case handling for warnings.
2009-01-31 01:59:13 +00:00
global $wgOut ;
$parsed_public_comments = $wgOut -> parseInline ( self :: $filters [ $filter ] -> af_public_comments );
2009-01-23 19:23:44 +00:00
if ( ! empty ( $actions [ 'throttle' ] ) ) {
$parameters = $actions [ 'throttle' ][ 'parameters' ];
$throttleId = array_shift ( $parameters );
list ( $rateCount , $ratePeriod ) = explode ( ',' , array_shift ( $parameters ) );
$hitThrottle = false ;
// The rest are throttle-types.
foreach ( $parameters as $throttleType ) {
$hitThrottle = $hitThrottle || self :: isThrottled ( $throttleId , $throttleType , $title , $rateCount , $ratePeriod );
}
unset ( $actions [ 'throttle' ] );
if ( ! $hitThrottle ) {
$actionsTaken [ $filter ][] = 'throttle' ;
continue ;
}
}
2009-01-23 19:23:19 +00:00
if ( ! empty ( $actions [ 'warn' ] ) ) {
$parameters = $actions [ 'warn' ][ 'parameters' ];
$warnKey = 'abusefilter-warned-' . $title -> getPrefixedText ();
if ( ! isset ( $_SESSION [ $warnKey ]) || ! $_SESSION [ $warnKey ]) {
$_SESSION [ $warnKey ] = true ;
// Threaten them a little bit
$msg = ( ! empty ( $parameters [ 0 ]) && strlen ( $parameters [ 0 ]) ) ? $parameters [ 0 ] : 'abusefilter-warning' ;
2009-01-31 01:59:13 +00:00
$messages [] = wfMsgExt ( $msg , 'parseinline' , array ( $parsed_public_comments ) ) . " <br /> \n " ;
2009-01-23 19:23:19 +00:00
2009-01-23 19:23:44 +00:00
$actionsTaken [ $filter ][] = 'warn' ;
2009-01-23 19:23:19 +00:00
continue ; // Don't do anything else.
} else {
// We already warned them
$_SESSION [ $warnKey ] = false ;
}
2008-06-29 14:00:39 +00:00
2009-01-23 19:23:19 +00:00
unset ( $actions [ 'warn' ] );
}
2009-01-29 23:24:24 +00:00
if ( count ( $actions ) > 1 && ! empty ( $actions [ 'disallow' ] ) ) {
unset ( $actions [ 'disallow' ] );
}
2009-01-23 19:23:19 +00:00
// Do the rest of the actions
foreach ( $actions as $action => $info ) {
2009-01-31 01:59:13 +00:00
$newMsg = self :: takeConsequenceAction ( $action , $info [ 'parameters' ], $title , $vars , self :: $filters [ $filter ] -> af_public_comments );
2009-01-23 19:23:19 +00:00
if ( $newMsg )
$messages [] = $newMsg ;
$actionsTaken [ $filter ][] = $action ;
2008-06-27 06:18:51 +00:00
}
}
2009-01-23 19:23:19 +00:00
return array ( $actionsTaken , implode ( " \n " , $messages ) );
}
public static function filterAction ( $vars , $title ) {
global $wgUser , $wgMemc ;
$dbr = wfGetDB ( DB_SLAVE );
$filter_matched = self :: checkAllFilters ( $vars );
// Short-cut any remaining code if no filters were hit.
if ( count ( array_filter ( $filter_matched ) ) == 0 ) {
return true ;
}
list ( $actions_taken , $error_msg ) = self :: executeFilterActions ( array_keys ( array_filter ( $filter_matched ) ), $title , $vars );
// Create a template
$log_template = array ( 'afl_user' => $wgUser -> getId (), 'afl_user_text' => $wgUser -> getName (),
'afl_var_dump' => serialize ( $vars ), 'afl_timestamp' => $dbr -> timestamp ( wfTimestampNow ()),
'afl_namespace' => $title -> getNamespace (), 'afl_title' => $title -> getDBKey (), 'afl_ip' => wfGetIp () );
self :: addLogEntries ( $actions_taken , $log_template , $vars [ 'ACTION' ] );
2009-02-02 23:30:48 +00:00
$error_msg = $error_msg == '' ? true : $error_msg ;
2008-06-27 06:18:51 +00:00
2009-01-23 19:23:19 +00:00
return $error_msg ;
}
public static function addLogEntries ( $actions_taken , $log_template , $action ) {
2008-06-27 06:18:51 +00:00
$dbw = wfGetDB ( DB_MASTER );
2009-01-23 19:23:19 +00:00
$log_rows = array ();
foreach ( $actions_taken as $filter => $actions ) {
$thisLog = $log_template ;
$thisLog [ 'afl_filter' ] = $filter ;
$thisLog [ 'afl_action' ] = $action ;
2009-01-23 19:23:44 +00:00
$thisLog [ 'afl_actions' ] = implode ( ',' , $actions );
2009-01-23 19:23:19 +00:00
// Don't log if we were only throttling.
if ( $thisLog [ 'afl_actions' ] != 'throttle' ) {
$log_rows [] = $thisLog ;
2008-09-21 13:08:10 +00:00
}
2008-06-27 06:18:51 +00:00
}
2009-01-23 19:23:19 +00:00
if ( ! count ( $log_rows )) {
2008-09-21 13:08:10 +00:00
return ;
}
2009-01-23 19:23:19 +00:00
$dbw -> insert ( 'abuse_filter_log' , $log_rows , __METHOD__ );
2008-06-27 06:18:51 +00:00
}
2009-01-23 19:23:19 +00:00
public static function takeConsequenceAction ( $action , $parameters , $title , $vars , $rule_desc ) {
wfLoadExtensionMessages ( 'AbuseFilter' );
$display = '' ;
2008-06-27 06:18:51 +00:00
switch ( $action ) {
case 'disallow' :
if ( strlen ( $parameters [ 0 ])) {
2009-01-31 01:59:13 +00:00
$display .= wfMsgExt ( $parameters [ 0 ], 'parseinline' , array ( $rule_desc ) ) . " \n " ;
2008-06-27 06:18:51 +00:00
} else {
// Generic message.
2009-01-31 01:59:13 +00:00
$display .= wfMsgExt ( 'abusefilter-disallowed' , 'parseinline' , array ( $rule_desc ) ) . " <br /> \n " ;
2008-06-27 06:18:51 +00:00
}
break ;
case 'block' :
global $wgUser ;
2008-07-09 07:02:13 +00:00
$filterUser = AbuseFilter :: getFilterUser ();
2008-06-27 06:18:51 +00:00
// Create a block.
$block = new Block ;
$block -> mAddress = $wgUser -> getName ();
$block -> mUser = $wgUser -> getId ();
2008-09-18 13:01:50 +00:00
$block -> mBy = $filterUser -> getId ();
$block -> mByName = $filterUser -> getName ();
2008-06-29 14:00:39 +00:00
$block -> mReason = wfMsgForContent ( 'abusefilter-blockreason' , $rule_desc );
2008-06-27 06:18:51 +00:00
$block -> mTimestamp = wfTimestampNow ();
2008-09-18 13:01:50 +00:00
$block -> mAnonOnly = 1 ;
2008-06-27 06:18:51 +00:00
$block -> mCreateAccount = 1 ;
$block -> mExpiry = 'infinity' ;
$block -> insert ();
2008-07-09 07:02:13 +00:00
// Log it
# Prepare log parameters
$logParams = array ();
$logParams [] = 'indefinite' ;
$logParams [] = 'nocreate, angry-autoblock' ;
$log = new LogPage ( 'block' );
$log -> addEntry ( 'block' , Title :: makeTitle ( NS_USER , $wgUser -> getName () ),
wfMsgForContent ( 'abusefilter-blockreason' , $rule_desc ), $logParams , self :: getFilterUser () );
2009-01-31 01:59:13 +00:00
$display .= wfMsgExt ( 'abusefilter-blocked-display' , 'parseinline' , array ( $rule_desc ) ) . " <br /> \n " ;
2008-06-27 06:18:51 +00:00
break ;
2008-09-18 13:01:50 +00:00
case 'rangeblock' :
global $wgUser ;
$filterUser = AbuseFilter :: getFilterUser ();
$range = IP :: toHex ( wfGetIP () );
$range = substr ( $range , 0 , 4 ) . '0000' ;
$range = long2ip ( hexdec ( $range ) );
$range .= " /16 " ;
$range = Block :: normaliseRange ( $range );
// Create a block.
$block = new Block ;
$block -> mAddress = $range ;
$block -> mUser = 0 ;
$block -> mBy = $filterUser -> getId ();
$block -> mByName = $filterUser -> getName ();
$block -> mReason = wfMsgForContent ( 'abusefilter-blockreason' , $rule_desc );
$block -> mTimestamp = wfTimestampNow ();
$block -> mAnonOnly = 0 ;
$block -> mCreateAccount = 1 ;
$block -> mExpiry = Block :: parseExpiryInput ( '1 week' );
$block -> insert ();
// Log it
# Prepare log parameters
$logParams = array ();
$logParams [] = 'indefinite' ;
$logParams [] = 'nocreate, angry-autoblock' ;
$log = new LogPage ( 'block' );
2008-09-18 13:33:39 +00:00
$log -> addEntry ( 'block' , Title :: makeTitle ( NS_USER , $range ),
2008-09-18 13:01:50 +00:00
wfMsgForContent ( 'abusefilter-blockreason' , $rule_desc ), $logParams , self :: getFilterUser () );
2009-01-31 01:59:13 +00:00
$display .= wfMsgExt ( 'abusefilter-blocked-display' , 'parseinline' , $rule_desc ) . " <br /> \n " ;
2008-09-18 13:01:50 +00:00
break ;
2008-06-27 06:18:51 +00:00
case 'degroup' :
global $wgUser ;
2008-08-02 13:51:29 +00:00
if ( ! $wgUser -> isAnon ()) {
// Remove all groups from the user. Ouch.
$groups = $wgUser -> getGroups ();
2009-01-23 19:23:19 +00:00
2008-08-02 13:51:29 +00:00
foreach ( $groups as $group ) {
$wgUser -> removeGroup ( $group );
}
2009-01-23 19:23:19 +00:00
2009-01-31 01:59:13 +00:00
$display .= wfMsgExt ( 'abusefilter-degrouped' , 'parseinline' , array ( $rule_desc ) ) . " <br /> \n " ;
2009-01-23 19:23:19 +00:00
2009-01-26 18:52:41 +00:00
// Don't log it if there aren't any groups being removed!
if ( ! count ( $groups )) {
break ;
}
2008-08-02 13:51:29 +00:00
// Log it.
$log = new LogPage ( 'rights' );
2009-01-23 19:23:19 +00:00
2008-08-02 13:51:29 +00:00
$log -> addEntry ( 'rights' ,
$wgUser -> getUserPage (),
wfMsgForContent ( 'abusefilter-degroupreason' , $rule_desc ),
array (
implode ( ', ' , $groups ),
wfMsgForContent ( 'rightsnone' )
)
, self :: getFilterUser () );
2008-06-27 06:18:51 +00:00
}
2009-01-23 19:23:19 +00:00
2008-06-27 06:18:51 +00:00
break ;
case 'blockautopromote' :
global $wgUser , $wgMemc ;
2008-08-02 13:51:29 +00:00
if ( ! $wgUser -> isAnon ()) {
$blockPeriod = ( int ) mt_rand ( 3 * 86400 , 7 * 86400 ); // Block for 3-7 days.
$wgMemc -> set ( self :: autoPromoteBlockKey ( $wgUser ), true , $blockPeriod );
2009-01-23 19:23:19 +00:00
2009-01-31 01:59:13 +00:00
$display .= wfMsgExt ( 'abusefilter-autopromote-blocked' , 'parseinline' , array ( $rule_desc ) ) . " <br /> \n " ;
2008-08-02 13:51:29 +00:00
}
2008-06-27 06:18:51 +00:00
break ;
case 'flag' :
// Do nothing. Here for completeness.
break ;
2009-01-23 19:23:19 +00:00
2009-01-28 19:08:18 +00:00
case 'tag' :
// Mark with a tag on recentchanges.
global $wgUser ;
$actionID = implode ( '-' , array (
$title -> getPrefixedText (), $wgUser -> getName (), $vars [ 'ACTION' ]
) );
AbuseFilter :: $tagsToSet [ $actionID ] = $parameters ;
break ;
default :
throw new MWException ( " Unrecognised action $action " );
2008-06-27 06:18:51 +00:00
}
2009-01-23 19:23:19 +00:00
return $display ;
2008-06-27 06:18:51 +00:00
}
public static function isThrottled ( $throttleId , $types , $title , $rateCount , $ratePeriod ) {
global $wgMemc ;
$key = self :: throttleKey ( $throttleId , $types , $title );
2009-02-03 23:44:47 +00:00
$count = intval ( $wgMemc -> get ( $key ) );
wfDebugLog ( 'AbuseFilter' , " Got value $count for throttle key $key\n " );
2008-06-27 06:18:51 +00:00
if ( $count > 0 ) {
$wgMemc -> incr ( $key );
2009-02-03 23:44:47 +00:00
$count ++ ;
wfDebugLog ( 'AbuseFilter' , " Incremented throttle key $key " );
2008-06-27 06:18:51 +00:00
} else {
2009-02-03 23:44:47 +00:00
wfDebugLog ( 'AbuseFilter' , " Added throttle key $key with value 1 " );
2008-06-27 06:18:51 +00:00
$wgMemc -> add ( $key , 1 , $ratePeriod );
2009-02-03 23:44:47 +00:00
$count = 1 ;
2008-06-27 06:18:51 +00:00
}
2009-02-03 23:44:47 +00:00
if ( $count > $rateCount ) {
wfDebugLog ( 'AbuseFilter' , " Throttle $key hit value $count -- maximum is $rateCount . " );
return true ; // THROTTLED
}
wfDebugLog ( 'AbuseFilter' , " Throttle $key not hit! " );
2008-06-27 06:18:51 +00:00
return false ; // NOT THROTTLED
}
public static function throttleIdentifier ( $type , $title ) {
global $wgUser ;
switch ( $type ) {
case 'ip' :
$identifier = wfGetIp ();
break ;
case 'user' :
$identifier = $wgUser -> getId ();
break ;
case 'range' :
$identifier = substr ( IP :: toHex ( wfGetIp ()), 0 , 4 );
break ;
case 'creationdate' :
$reg = $wgUser -> getRegistration ();
$identifier = $reg - ( $reg % 86400 );
break ;
case 'editcount' :
// Hack for detecting different single-purpose accounts.
$identifier = $wgUser -> getEditCount ();
break ;
case 'site' :
return 1 ;
break ;
case 'page' :
return $title -> getPrefixedText ();
break ;
}
return $identifier ;
}
public static function throttleKey ( $throttleId , $type , $title ) {
$identifier = '' ;
$types = explode ( ',' , $type );
$identifiers = array ();
foreach ( $types as $subtype ) {
$identifiers [] = self :: throttleIdentifier ( $subtype , $title );
}
$identifier = implode ( ':' , $identifiers );
return wfMemcKey ( 'abusefilter' , 'throttle' , $throttleId , $type , $identifier );
}
public static function autoPromoteBlockKey ( $user ) {
return wfMemcKey ( 'abusefilter' , 'block-autopromote' , $user -> getId () );
}
2008-07-09 07:02:13 +00:00
2008-07-17 02:43:45 +00:00
public static function recordStats ( $filters ) {
global $wgAbuseFilterConditionLimit , $wgMemc ;
2008-10-24 08:58:32 +00:00
$blocking_filters = array_keys ( array_filter ( $filters ) );
2009-01-23 19:23:19 +00:00
// Figure out if we've triggered overflows and blocks.
2008-07-17 02:43:45 +00:00
$overflow_triggered = ( self :: $condCount > $wgAbuseFilterConditionLimit );
2009-01-23 19:23:19 +00:00
$filter_triggered = count ( $blocking_filters ) > 0 ;
// Store some keys...
2008-07-17 02:43:45 +00:00
$overflow_key = self :: filterLimitReachedKey ();
$total_key = self :: filterUsedKey ();
2009-01-23 19:23:19 +00:00
2008-07-17 02:43:45 +00:00
$total = $wgMemc -> get ( $total_key );
2009-01-23 19:23:19 +00:00
$storage_period = self :: $statsStoragePeriod ;
2008-07-17 02:43:45 +00:00
if ( ! $total || $total > 1000 ) {
2009-01-23 19:23:19 +00:00
// This is for if the total doesn't exist, or has gone past 1000.
// Recreate all the keys at the same time, so they expire together.
$wgMemc -> set ( $total_key , 0 , $storage_period );
$wgMemc -> set ( $overflow_key , 0 , $storage_period );
2008-07-17 02:43:45 +00:00
foreach ( $filters as $filter => $matched ) {
2009-01-23 19:23:19 +00:00
$wgMemc -> set ( self :: filterMatchesKey ( $filter ), 0 , $storage_period );
2008-07-17 02:43:45 +00:00
}
2009-01-23 19:23:19 +00:00
$wgMemc -> set ( self :: filterMatchesKey (), 0 , $storage_period );
2008-07-17 02:43:45 +00:00
}
2009-01-23 19:23:19 +00:00
// Increment total
2008-07-17 02:43:45 +00:00
$wgMemc -> incr ( $total_key );
2009-01-23 19:23:19 +00:00
// Increment overflow counter, if our condition limit overflowed
2008-07-17 02:43:45 +00:00
if ( $overflow_triggered ) {
$wgMemc -> incr ( $overflow_key );
}
2009-01-23 19:23:19 +00:00
2009-01-27 05:22:24 +00:00
if ( ! $filter_triggered ) {
return ; // The rest will only apply if a filter was triggered.
}
2009-01-27 20:18:58 +00:00
self :: checkEmergencyDisable ( $filters , $total );
2009-01-23 19:23:19 +00:00
// Increment trigger counter
if ( $filter_triggered ) {
$wgMemc -> incr ( self :: filterMatchesKey () );
}
$dbw = wfGetDB ( DB_MASTER );
// Update hit-counter.
$dbw -> update ( 'abuse_filter' , array ( 'af_hit_count=af_hit_count+1' ), array ( 'af_id' => array_keys ( array_filter ( $filters ) ) ), __METHOD__ );
}
2009-01-27 20:18:58 +00:00
public static function checkEmergencyDisable ( $filters , $total ) {
2009-01-23 19:23:19 +00:00
global $wgAbuseFilterEmergencyDisableThreshold , $wgAbuseFilterEmergencyDisableCount , $wgAbuseFilterEmergencyDisableAge , $wgMemc ;
2008-07-17 02:43:45 +00:00
foreach ( $filters as $filter => $matched ) {
if ( $matched ) {
2009-01-23 19:23:19 +00:00
// Increment counter
$matchCount = $wgMemc -> get ( self :: filterMatchesKey ( $filter ) );
// Handle missing keys...
if ( ! $matchCount ) {
2008-07-17 02:43:45 +00:00
$wgMemc -> set ( self :: filterMatchesKey ( $filter ), 1 , self :: $statsStoragePeriod );
2009-01-23 19:23:19 +00:00
} else {
$wgMemc -> incr ( self :: filterMatchesKey ( $filter ) );
2008-07-17 02:43:45 +00:00
}
2009-01-23 19:23:19 +00:00
$matchCount ++ ;
// Figure out if the filter is subject to being deleted.
2008-10-24 08:58:32 +00:00
$filter_age = wfTimestamp ( TS_UNIX , self :: $filters [ $filter ] -> af_timestamp );
2008-09-18 13:01:50 +00:00
$throttle_exempt_time = $filter_age + $wgAbuseFilterEmergencyDisableAge ;
2009-01-23 19:23:19 +00:00
2009-01-27 20:18:58 +00:00
if ( $total && $throttle_exempt_time > time () && $matchCount > $wgAbuseFilterEmergencyDisableCount && ( $matchCount / $total ) > $wgAbuseFilterEmergencyDisableThreshold ) {
2009-01-23 19:23:19 +00:00
// More than $wgAbuseFilterEmergencyDisableCount matches, constituting more than $wgAbuseFilterEmergencyDisableThreshold (a fraction) of last few edits. Disable it.
2008-07-17 02:43:45 +00:00
$dbw = wfGetDB ( DB_MASTER );
2009-01-27 20:18:58 +00:00
$dbw -> update ( 'abuse_filter' , array ( 'af_throttled' => 1 ), array ( 'af_id' => $filter ), __METHOD__ );
2008-07-17 02:43:45 +00:00
}
}
}
}
public static function filterLimitReachedKey () {
return wfMemcKey ( 'abusefilter' , 'stats' , 'overflow' );
}
public static function filterUsedKey () {
return wfMemcKey ( 'abusefilter' , 'stats' , 'total' );
}
public static function filterMatchesKey ( $filter = null ) {
return wfMemcKey ( 'abusefilter' , 'stats' , 'matches' , $filter );
}
2008-07-09 07:02:13 +00:00
public static function getFilterUser () {
wfLoadExtensionMessages ( 'AbuseFilter' );
$user = User :: newFromName ( wfMsgForContent ( 'abusefilter-blocker' ) );
$user -> load ();
if ( $user -> getId () && $user -> mPassword == '' ) {
// Already set up.
return $user ;
}
// Not set up. Create it.
if ( ! $user -> getId ()) {
2009-01-28 00:10:35 +00:00
print " Trying to create account -- user id is " . $user -> getId ();
2008-07-09 07:02:13 +00:00
$user -> addToDatabase ();
$user -> saveSettings ();
2009-01-28 00:10:35 +00:00
# Increment site_stats.ss_users
$ssu = new SiteStatsUpdate ( 0 , 0 , 0 , 0 , 1 );
$ssu -> doUpdate ();
2008-07-09 07:02:13 +00:00
} else {
// Take over the account
$user -> setPassword ( null );
$user -> setEmail ( null );
$user -> saveSettings ();
}
# Promote user so it doesn't look too crazy.
$user -> addGroup ( 'sysop' );
return $user ;
}
2009-01-23 22:49:13 +00:00
2009-01-29 22:44:31 +00:00
static function buildEditBox ( $rules , $textName = 'wpFilterRules' , $addResultDiv = true ) {
2009-01-23 22:49:13 +00:00
global $wgOut ;
$rules = Xml :: textarea ( $textName , ( isset ( $rules ) ? $rules . " \n " : " \n " ) );
2009-01-27 01:31:42 +00:00
$dropDown = self :: $builderValues ;
2009-01-23 22:49:13 +00:00
// Generate builder drop-down
$builder = '' ;
$builder .= Xml :: option ( wfMsg ( " abusefilter-edit-builder-select " ) );
foreach ( $dropDown as $group => $values ) {
$builder .= Xml :: openElement ( 'optgroup' , array ( 'label' => wfMsg ( " abusefilter-edit-builder-group- $group " ) ) ) . " \n " ;
foreach ( $values as $content => $name ) {
$builder .= Xml :: option ( wfMsg ( " abusefilter-edit-builder- $group - $name " ), $content ) . " \n " ;
}
$builder .= Xml :: closeElement ( 'optgroup' ) . " \n " ;
}
2009-01-27 17:32:30 +00:00
$rules .= Xml :: tags ( 'select' , array ( 'id' => 'wpFilterBuilder' , 'onchange' => 'addText();' ), $builder ) . ' ' ;
2009-01-23 22:49:13 +00:00
// Add syntax checking
$rules .= Xml :: element ( 'input' , array ( 'type' => 'button' , 'onclick' => 'doSyntaxCheck()' , 'value' => wfMsg ( 'abusefilter-edit-check' ), 'id' => 'mw-abusefilter-syntaxcheck' ) );
2009-01-29 22:44:31 +00:00
if ( $addResultDiv )
$rules .= Xml :: element ( 'div' , array ( 'id' => 'mw-abusefilter-syntaxresult' , 'style' => 'display: none;' ), ' ' );
2009-01-23 22:49:13 +00:00
// Add script
$editScript = file_get_contents ( dirname ( __FILE__ ) . " /edit.js " );
$editScript = " var wgFilterBoxName = " . Xml :: encodeJSVar ( $textName ) . " ; \n $editScript " ;
$wgOut -> addInlineScript ( $editScript );
return $rules ;
}
2009-01-26 22:31:02 +00:00
/** Each version is expected to be an array ( $row , $actions )
Returns an array of fields that are different .*/
static function compareVersions ( $version_1 , $version_2 ) {
$compareFields = array ( 'af_public_comments' , 'af_pattern' , 'af_comments' , 'af_deleted' , 'af_enabled' , 'af_hidden' );
$differences = array ();
list ( $row1 , $actions1 ) = $version_1 ;
list ( $row2 , $actions2 ) = $version_2 ;
foreach ( $compareFields as $field ) {
if ( $row1 -> $field != $row2 -> $field ) {
$differences [] = $field ;
}
}
global $wgAbuseFilterAvailableActions ;
foreach ( $wgAbuseFilterAvailableActions as $action ) {
if ( ! isset ( $actions1 [ $action ]) && ! isset ( $actions2 [ $action ] ) ) {
// They're both unset
} elseif ( isset ( $actions1 [ $action ]) && isset ( $actions2 [ $action ] ) ) {
// They're both set.
if ( array_diff ( $actions1 [ $action ][ 'parameters' ], $actions2 [ $action ][ 'parameters' ] ) ) {
// Different parameters
$differences [] = 'actions' ;
}
} else {
// One's unset, one's set.
$differences [] = 'actions' ;
}
}
return array_unique ( $differences );
}
static function translateFromHistory ( $row ) {
## Translate into an abuse_filter row with some black magic. This is ever so slightly evil!
$af_row = new StdClass ;
foreach ( self :: $history_mappings as $af_col => $afh_col ) {
$af_row -> $af_col = $row -> $afh_col ;
}
## Process flags
$af_row -> af_deleted = 0 ;
$af_row -> af_hidden = 0 ;
$af_row -> af_enabled = 0 ;
$flags = explode ( ',' , $row -> afh_flags );
foreach ( $flags as $flag ) {
$col_name = " af_ $flag " ;
$af_row -> $col_name = 1 ;
}
## Process actions
$actions_raw = unserialize ( $row -> afh_actions );
$actions_output = array ();
foreach ( $actions_raw as $action => $parameters ) {
$actions_output [ $action ] = array ( 'action' => $action , 'parameters' => $parameters );
}
return array ( $af_row , $actions_output );
}
2009-01-28 01:26:38 +00:00
static function getActionDisplay ( $action ) {
$display = wfMsg ( " abusefilter-action- $action " );
$display = wfEmptyMsg ( " abusefilter-action- $action " , $display ) ? $action : $display ;
return $display ;
}
2009-01-28 23:54:41 +00:00
public static function getVarsFromRCRow ( $row ) {
if ( $row -> rc_this_oldid ) {
// It's an edit.
return self :: getEditVarsFromRCRow ( $row );
} elseif ( $row -> rc_log_type == 'move' ) {
return self :: getMoveVarsFromRCRow ( $row );
} elseif ( $row -> rc_log_type == 'newusers' ) {
return self :: getCreateVarsFromRCRow ( $row );
}
}
public static function getCreateVarsFromRCRow ( $row ) {
$vars = array ( 'ACTION' => 'createaccount' );
$vars [ 'USER_NAME' ] = $vars [ 'ACCOUNTNAME' ] = Title :: makeTitle ( $row -> rc_namespace , $row -> rc_title ) -> getText ();
return $vars ;
}
public static function getEditVarsFromRCRow ( $row ) {
$vars = array ();
$title = Title :: makeTitle ( $row -> rc_namespace , $row -> rc_title );
$vars = array_merge ( $vars , self :: generateUserVars ( User :: newFromId ( $row -> rc_user ) ) );
2009-02-03 00:15:12 +00:00
$vars = array_merge ( $vars , self :: generateTitleVars ( $title , 'ARTICLE' ) );
2009-01-28 23:54:41 +00:00
$vars [ 'ACTION' ] = 'edit' ;
$vars [ 'SUMMARY' ] = $row -> rc_comment ;
$newRev = Revision :: newFromId ( $row -> rc_this_oldid );
$new_text = $newRev -> getText ();
if ( $row -> rc_last_oldid ) {
$oldRev = Revision :: newFromId ( $row -> rc_last_oldid );
$old_text = $oldRev -> getText ();
} else {
$old_text = '' ;
}
$vars = array_merge ( $vars , self :: getEditVars ( $title , $old_text , $new_text , null , $row -> rc_this_oldid , $row -> rc_last_oldid ) );
return $vars ;
}
public static function getMoveVarsFromRCRow ( $row ) {
$vars = array ();
$user = User :: newFromId ( $row -> rc_user );
$oldTitle = Title :: makeTitle ( $row -> rc_namespace , $row -> rc_title );
$newTitle = Title :: newFromText ( trim ( $row -> rc_params ) );
$vars = array_merge ( $vars , AbuseFilter :: generateUserVars ( $user ),
AbuseFilter :: generateTitleVars ( $oldTitle , 'MOVED_FROM' ),
AbuseFilter :: generateTitleVars ( $newTitle , 'MOVED_TO' ) );
$vars [ 'SUMMARY' ] = $row -> rc_comment ;
$vars [ 'ACTION' ] = 'move' ;
return $vars ;
}
public static function getEditVars ( $title , $old_text , $new_text , $oldLinks = null , $revid = null , $oldid = null ) {
$vars = array ();
$article = new Article ( $title );
$vars [ 'EDIT_DELTA' ] = strlen ( $new_text ) - strlen ( $old_text );
$vars [ 'OLD_SIZE' ] = strlen ( $old_text );
$diff = wfDiff ( $old_text , $new_text );
$diff = trim ( str_replace ( '\No newline at end of file' , '' , $diff ) );
$vars [ 'EDIT_DIFF' ] = $diff ;
$vars [ 'NEW_SIZE' ] = strlen ( $new_text );
$vars [ 'OLD_WIKITEXT' ] = $old_text ;
$vars [ 'NEW_WIKITEXT' ] = $new_text ;
// Some more specific/useful details about the changes.
$diff_lines = explode ( " \n " , $diff );
$added_lines = array ();
$removed_lines = array ();
foreach ( $diff_lines as $line ) {
if ( strpos ( $line , '-' ) === 0 ) {
$removed_lines [] = substr ( $line , 1 );
} elseif ( strpos ( $line , '+' ) === 0 ) {
$added_lines [] = substr ( $line , 1 );
}
}
$vars [ 'ADDED_LINES' ] = implode ( " \n " , $added_lines );
$vars [ 'REMOVED_LINES' ] = implode ( " \n " , $removed_lines );
if ( $oldLinks === null && $oldid ) {
$oldInfo = $article -> prepareTextForEdit ( $old_text , $oldid );
$oldLinks = $oldInfo -> output -> getExternalLinks ();
} elseif ( $oldLinks === null ) {
$oldLinks = array ();
}
// Added links...
2009-01-30 23:31:31 +00:00
$editInfo = $article -> prepareTextForEdit ( $new_text , $revid );
2009-01-28 23:54:41 +00:00
$newLinks = array_keys ( $editInfo -> output -> getExternalLinks () );
$vars [ 'ALL_LINKS' ] = implode ( " \n " , $newLinks );
$vars [ 'ADDED_LINKS' ] = implode ( " \n " , array_diff ( $newLinks , array_intersect ( $newLinks , $oldLinks ) ) );
$vars [ 'REMOVED_LINKS' ] = implode ( " \n " , array_diff ( $oldLinks , array_intersect ( $newLinks , $oldLinks ) ) );
// Pull other useful stuff from $editInfo.
2009-01-29 23:46:19 +00:00
$newHTML = $editInfo -> output -> getText ();
// Kill the PP limit comments. Ideally we'd just remove these by not setting the parser option, but then we
// can't share a parse operation with the edit, which is bad.
$newHTML = preg_replace ( '/<!--\s*NewPP limit report[^>]*-->\s*$/si' , '' , $newHTML );
$vars [ 'NEW_HTML' ] = $newHTML ;
2009-01-28 23:54:41 +00:00
$newText = $vars [ 'NEW_TEXT' ] = preg_replace ( '/<[^>]+>/' , '' , $newHTML );
return $vars ;
}
2009-01-29 22:44:31 +00:00
public static function buildVarDumpTable ( $vars ) {
$output = '' ;
// I don't want to change the names of the pre-existing messages
// describing the variables, nor do I want to rewrite them, so I'm just
// mapping the variable names to builder messages with a pre-existing array.
$variableMessageMappings = self :: $builderValues [ 'vars' ];
2009-01-30 15:40:59 +00:00
$output .= Xml :: openElement ( 'table' , array ( 'class' => 'mw-abuselog-details' ) ) . Xml :: openElement ( 'tbody' ) . " \n " ;
2009-01-29 22:44:31 +00:00
$header = Xml :: element ( 'th' , null , wfMsg ( 'abusefilter-log-details-var' ) ) . Xml :: element ( 'th' , null , wfMsg ( 'abusefilter-log-details-val' ) );
2009-01-30 15:40:59 +00:00
$output .= Xml :: tags ( 'tr' , null , $header ) . " \n " ;
2009-01-29 22:44:31 +00:00
// Now, build the body of the table.
foreach ( $vars as $key => $value ) {
if ( ! empty ( $variableMessageMappings [ $key ]) ) {
$mapping = $variableMessageMappings [ $key ];
$keyDisplay = wfMsgExt ( " abusefilter-edit-builder-vars- $mapping " , 'parseinline' ) . ' (' . Xml :: element ( 'tt' , null , $key ) . ')' ;
} else {
$keyDisplay = Xml :: element ( 'tt' , null , $key );
}
2009-01-30 15:40:59 +00:00
if ( is_null ( $value ) )
$value = '' ;
2009-01-29 22:44:31 +00:00
$value = Xml :: element ( 'div' , array ( 'class' => 'mw-abuselog-var-value' ), $value );
$trow = Xml :: tags ( 'td' , array ( 'class' => 'mw-abuselog-var' ), $keyDisplay ) . Xml :: tags ( 'td' , array ( 'class' => 'mw-abuselog-var-value' ), $value );
2009-01-30 15:40:59 +00:00
$output .= Xml :: tags ( 'tr' , array ( 'class' => " mw-abuselog-details- $key mw-abuselog-value " ), $trow ) . " \n " ;
2009-01-29 22:44:31 +00:00
}
$output .= Xml :: closeElement ( 'tbody' ) . Xml :: closeElement ( 'table' );
return $output ;
}
2008-06-27 06:18:51 +00:00
}