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-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 ) {
$result = self :: checkSyntax ( $filter );
$ok = ( $result === true );
if ( $ok ) {
return " OK " ;
} else {
return " ERR: $result " ;
}
}
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 ();
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 ();
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 );
}
}
2008-08-02 11:10:42 +00:00
// Find last 5 authors
$dbr = wfGetDB ( DB_SLAVE );
$res = $dbr -> select ( 'revision' , 'distinct rev_user_text' , array ( 'rev_page' => $title -> getArticleId () ), __METHOD__ , array ( 'order by' => 'rev_timestamp desc' , 'limit' => 10 ) );
$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 () ) {
global $wgAbuseFilterParserClass ;
$parser = new $wgAbuseFilterParserClass ;
$parser -> setVars ( $vars );
return $parser -> evaluateExpression ( $expr );
}
public static function ajaxEvaluateExpression ( $expr ) {
return self :: evaluateExpression ( $expr );
}
2008-07-18 02:18:58 +00:00
2008-08-02 11:10:42 +00:00
public static function checkConditions ( $conds , $vars ) {
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 ;
}
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
}
2008-06-27 06:18:51 +00:00
public static function filterAction ( $vars , $title ) {
2008-07-18 08:30:25 +00:00
global $wgUser , $wgMemc ;
2008-06-27 06:18:51 +00:00
// Fetch from the database.
$dbr = wfGetDB ( DB_SLAVE );
2008-06-27 07:55:13 +00:00
$res = $dbr -> select ( 'abuse_filter' , '*' , array ( 'af_enabled' => 1 ) );
2008-06-27 06:18:51 +00:00
$blocking_filters = array ();
$log_entries = array ();
$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 () );
$doneActionsByFilter = array ();
2008-07-17 02:43:45 +00:00
$filter_matched = array ();
2008-06-27 06:18:51 +00:00
while ( $row = $dbr -> fetchObject ( $res ) ) {
if ( self :: checkConditions ( $row -> af_pattern , $vars ) ) {
$blocking_filters [ $row -> af_id ] = $row ;
$newLog = $log_template ;
$newLog [ 'afl_filter' ] = $row -> af_id ;
$newLog [ 'afl_action' ] = $vars [ 'ACTION' ];
$log_entries [] = $newLog ;
$doneActionsByFilter [ $row -> af_id ] = array ();
2008-07-17 02:43:45 +00:00
$filter_matched [ $row -> af_id ] = true ;
} else {
$filter_matched [ $row -> af_id ] = false ;
2008-06-27 06:18:51 +00:00
}
}
2008-07-18 08:30:25 +00:00
//// Clean up from checking all the filters
2008-07-18 02:18:58 +00:00
// Don't store stats if the cond limit is disabled.
2008-07-18 08:30:25 +00:00
// It's probably a batch process or similar.
2008-07-18 02:18:58 +00:00
if ( ! self :: $condLimitEnabled )
self :: recordStats ( $filter_matched );
2008-07-17 02:43:45 +00:00
2008-06-27 06:18:51 +00:00
if ( count ( $blocking_filters ) == 0 ) {
// No problems.
return true ;
}
// Retrieve the consequences.
2008-06-29 14:00:39 +00:00
$res = $dbr -> select ( 'abuse_filter_action' , '*' , array ( 'afa_filter' => array_keys ( $blocking_filters ) ), __METHOD__ , array ( " ORDER BY " => " (afa_consequence in ('throttle','warn'))-(afa_consequence in ('disallow')) desc " ) );
// We want throttles, warnings first, as they have a bit of a special treatment. We want disallow last.
2008-06-27 06:18:51 +00:00
$actions_done = array ();
$throttled_filters = array ();
$display = '' ;
while ( $row = $dbr -> fetchObject ( $res ) ) {
2008-06-29 14:00:39 +00:00
// Don't do the same action-parameters twice
2008-06-27 06:18:51 +00:00
$action_key = md5 ( $row -> afa_consequence . $row -> afa_parameters );
2008-06-29 14:00:39 +00:00
// Skip if we've already done this action-parameter, or a passive action has sufficed.
$skipAction = ( in_array ( $action_key , $actions_done ) || in_array ( $row -> afa_filter , $throttled_filters ) );
// Don't disallow if we've already done something active. It produces two messages, where one would suffice.
if ( $row -> afa_consequence == 'disallow' && ! $skipAction ) {
$doneActiveActions = array_diff ( $doneActionsByFilter [ $row -> afa_filter ], array ( 'throttle' , 'warn' /* passive actions */ ) );
$skipAction = ( bool ) count ( $doneActiveActions );
}
if ( ! $skipAction ) {
// Unpack parameters
2008-06-27 06:18:51 +00:00
$parameters = explode ( " \n " , $row -> afa_parameters );
2008-06-29 14:00:39 +00:00
// Take the action.
2008-08-02 13:51:29 +00:00
$result = self :: takeConsequenceAction ( $row -> afa_consequence , $parameters , $title , $vars , $display , $continue , $blocking_filters [ $row -> afa_filter ] -> af_public_comments );
2008-06-29 14:00:39 +00:00
// Don't do it twice.
2008-06-27 06:18:51 +00:00
$doneActionsByFilter [ $row -> afa_filter ][] = $row -> afa_consequence ;
2008-06-29 14:00:39 +00:00
$actions_done [] = $action_key ;
// Only execute other actions for a filter if that filter's rate limiter has been tripped.
2008-06-27 06:18:51 +00:00
if ( ! $result ) {
2008-06-29 14:00:39 +00:00
$throttled_filters [] = $row -> afa_filter ;
2008-06-27 06:18:51 +00:00
}
} else {
// Ignore it, until we hit the rate limit.
}
}
$dbw = wfGetDB ( DB_MASTER );
// Log it
foreach ( $log_entries as $index => $entry ) {
$log_entries [ $index ][ 'afl_actions' ] = implode ( ',' , $doneActionsByFilter [ $entry [ 'afl_filter' ]] );
// Increment the hit counter
$dbw -> update ( 'abuse_filter' , array ( 'af_hit_count=af_hit_count+1' ), array ( 'af_id' => $entry [ 'afl_filter' ] ), __METHOD__ );
}
$dbw -> insert ( 'abuse_filter_log' , $log_entries , __METHOD__ );
return $display ;
}
2008-06-29 14:00:39 +00:00
public static function takeConsequenceAction ( $action , $parameters , $title , $vars , & $display , & $continue , $rule_desc ) {
2008-06-27 06:18:51 +00:00
switch ( $action ) {
case 'warn' :
wfLoadExtensionMessages ( 'AbuseFilter' );
if ( ! $_SESSION [ 'abusefilter-warned' ]) {
$_SESSION [ 'abusefilter-warned' ] = true ;
// Threaten them a little bit
if ( strlen ( $parameters [ 0 ])) {
2008-06-29 20:10:42 +00:00
$display .= call_user_func_array ( 'wfMsgNoTrans' , $parameters ) . " \n " ;
2008-06-27 06:18:51 +00:00
} else {
// Generic message.
2008-06-29 20:10:42 +00:00
$display .= wfMsgNoTrans ( 'abusefilter-warning' , $rule_desc ) . " <br /> \n " ;
2008-06-27 06:18:51 +00:00
}
return false ; // Don't apply the other stuff yet.
} else {
// We already warned them
$_SESSION [ 'abusefilter-warned' ] = false ;
}
break ;
case 'disallow' :
wfLoadExtensionMessages ( 'AbuseFilter' );
// Don't let them do it
if ( strlen ( $parameters [ 0 ])) {
2008-06-29 20:10:42 +00:00
$display .= call_user_func_array ( 'wfMsgNoTrans' , $parameters ) . " \n " ;
2008-06-27 06:18:51 +00:00
} else {
// Generic message.
2008-06-29 20:10:42 +00:00
$display .= wfMsgNoTrans ( 'abusefilter-disallowed' , $rule_desc ) . " <br /> \n " ;
2008-06-27 06:18:51 +00:00
}
break ;
case 'block' :
wfLoadExtensionMessages ( 'AbuseFilter' );
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-06-27 09:38:54 +00:00
$block -> mBy = User :: idFromName ( wfMsgForContent ( 'abusefilter-blocker' ) ); // Let's say the site owner blocked them
2008-06-27 06:18:51 +00:00
$block -> mByName = wfMsgForContent ( 'abusefilter-blocker' );
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 ();
$block -> mEnableAutoblock = 1 ;
$block -> mAngryAutoblock = 1 ; // Block lots of IPs
$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 () );
2008-06-29 20:10:42 +00:00
$display .= wfMsgNoTrans ( 'abusefilter-blocked-display' , $rule_desc ) . " <br /> \n " ;
2008-06-27 06:18:51 +00:00
break ;
case 'throttle' :
$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 );
}
return $hitThrottle ;
break ;
case 'degroup' :
wfLoadExtensionMessages ( 'AbuseFilter' );
global $wgUser ;
2008-08-02 13:51:29 +00:00
if ( ! $wgUser -> isAnon ()) {
// Remove all groups from the user. Ouch.
$groups = $wgUser -> getGroups ();
foreach ( $groups as $group ) {
$wgUser -> removeGroup ( $group );
}
$display .= wfMsgNoTrans ( 'abusefilter-degrouped' , $rule_desc ) . " <br /> \n " ;
// Log it.
$log = new LogPage ( 'rights' );
$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
}
break ;
case 'blockautopromote' :
global $wgUser , $wgMemc ;
2008-08-02 13:51:29 +00:00
if ( ! $wgUser -> isAnon ()) {
wfLoadExtensionMessages ( 'AbuseFilter' );
$blockPeriod = ( int ) mt_rand ( 3 * 86400 , 7 * 86400 ); // Block for 3-7 days.
$wgMemc -> set ( self :: autoPromoteBlockKey ( $wgUser ), true , $blockPeriod );
$display .= wfMsgNoTrans ( 'abusefilter-autopromote-blocked' , $rule_desc ) . " <br /> \n " ;
}
2008-06-27 06:18:51 +00:00
break ;
case 'flag' :
// Do nothing. Here for completeness.
break ;
}
return true ;
}
public static function isThrottled ( $throttleId , $types , $title , $rateCount , $ratePeriod ) {
global $wgMemc ;
$key = self :: throttleKey ( $throttleId , $types , $title );
$count = $wgMemc -> get ( $key );
if ( $count > 0 ) {
$wgMemc -> incr ( $key );
if ( $count > $rateCount ) {
//die( "Hit rate limiter: $count actions, against limit of $rateCount actions in $ratePeriod seconds (key is $key).\n" );
$wgMemc -> delete ( $key );
return true ; // THROTTLED
}
} else {
$wgMemc -> add ( $key , 1 , $ratePeriod );
}
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 ;
$overflow_triggered = ( self :: $condCount > $wgAbuseFilterConditionLimit );
$filter_triggered = count ( $blocking_filters );
$overflow_key = self :: filterLimitReachedKey ();
$total_key = self :: filterUsedKey ();
$total = $wgMemc -> get ( $total_key );
$storage_period = self :: $statsStoragePeriod ; // One day.
if ( ! $total || $total > 1000 ) {
$wgMemc -> set ( $total_key , 1 , $storage_period );
if ( $overflow_triggered ) {
$wgMemc -> set ( $overflow_key , 1 , $storage_period );
} else {
$wgMemc -> set ( $overflow_key , 0 , $storage_period );
}
$anyMatch = false ;
foreach ( $filters as $filter => $matched ) {
$filter_key = self :: filterMatchesKey ( $filter );
if ( $matched ) {
$anyMatch = true ;
$wgMemc -> set ( $filter_key , 1 , $storage_period );
} else {
$wgMemc -> set ( $filter_key , 0 , $storage_period );
}
}
if ( $anyMatch ) {
$wgMemc -> set ( self :: filterMatchesKey (), 1 , $storage_period );
} else {
$wgMemc -> set ( self :: filterMatchesKey (), 0 , $storage_period );
}
return ;
}
$wgMemc -> incr ( $total_key );
if ( $overflow_triggered ) {
$wgMemc -> incr ( $overflow_key );
}
$anyMatch = false ;
global $wgAbuseFilterEmergencyDisableThreshold , $wgAbuseFilterEmergencyDisableCount ;
foreach ( $filters as $filter => $matched ) {
if ( $matched ) {
$anyMatch = true ;
$match_count = $wgMemc -> get ( self :: filterMatchesKey ( $filter ) );
if ( $match_count > 0 ) {
$wgMemc -> incr ( self :: filterMatchesKey ( $filter ) );
} else {
$wgMemc -> set ( self :: filterMatchesKey ( $filter ), 1 , self :: $statsStoragePeriod );
}
if ( $match_count > $wgAbuseFilterEmergencyDisableCount && ( $match_count / $total ) > $wgAbuseFilterEmergencyDisableThreshold ) {
// More than X matches, constituting more than Y% of last Z edits. Disable it.
$dbw = wfGetDB ( DB_MASTER );
$dbw -> update ( 'abuse_filter' , array ( 'af_enabled' => 0 , 'af_throttled' => 1 ), array ( 'af_id' => $filter ), __METHOD__ );
}
}
}
if ( $anyMatch ) {
$wgMemc -> incr ( self :: filterMatchesKey () );
}
}
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 ()) {
$user -> addToDatabase ();
$user -> saveSettings ();
} 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' );
# Increment site_stats.ss_users
$ssu = new SiteStatsUpdate ( 0 , 0 , 0 , 0 , 1 );
$ssu -> doUpdate ();
return $user ;
}
2008-06-27 06:18:51 +00:00
}