mirror of
https://gerrit.wikimedia.org/r/mediawiki/extensions/AbuseFilter.git
synced 2024-12-01 09:06:22 +00:00
ed5b83e6ac
Recommit of r66254 to trunk. This was just find extensions phase3 -iname '*.php' \! -iname '*.i18n.php' \! -iname 'Messages*.php' \! -iname '*_Messages.php' -exec sed -i 's/ /\ /g;s/—/―/g;s/•/•/g;s/á/á/g;s/´/´/g;s/à/à/g;s/α/α/g;s/ä/ä/g;s/ç/ç/g;s/©/©/g;s/↓/↓/g;s/°/°/g;s/é/é/g;s/ê/ê/g;s/ë/ë/g;s/è/è/g;s/€/€/g;s/↔//g;s/…/…/g;s/í/í/g;s/ì/ì/g;s/←/←/g;s/“/“/g;s/·/·/g;s/−/−/g;s/–/–/g;s/ó/ó/g;s/ô/ô/g;s/œ/œ/g;s/ò/ò/g;s/õ/õ/g;s/ö/ö/g;s/£/£/g;s/′/′/g;s/″/″/g;s/»/»/g;s/→/→/g;s/”/”/g;s/Σ/Σ/g;s/×/×/g;s/ú/ú/g;s/↑/↑/g;s/ü/ü/g;s/¥/¥/g' {} + followed by reading over every single line of the resulting diff and fixing a whole bunch of false positives. The reason for this change is given in <http://lists.wikimedia.org/pipermail/wikitech-l/2010-April/047617.html>. I cleared it with Tim and Brion on IRC before committing. It might cause a few problems, but I tried to be careful; please report any issues. I skipped all messages files. I plan to make a follow-up commit that alters wfMsgExt() with 'escapenoentities' to sanitize all the entities. That way, the only messages that will be problems will be ones that output raw HTML, and we want to get rid of those anyway. This should get rid of all named entities everywhere except messages. I skipped a few things like   that I noticed in manual inspection, because they weren't well-formed XML anyway. Also, to everyone who uses non-breaking spaces when they could use a normal space, or nothing at all, or CSS padding: I still hate you. Die.
1740 lines
48 KiB
PHP
1740 lines
48 KiB
PHP
<?php
|
|
if ( !defined( 'MEDIAWIKI' ) )
|
|
die();
|
|
|
|
class AbuseFilter {
|
|
public static $statsStoragePeriod = 86400;
|
|
public static $tokenCache = array();
|
|
public static $modifyCache = array();
|
|
public static $condLimitEnabled = true;
|
|
public static $condCount = 0;
|
|
public static $filters = array();
|
|
public static $tagsToSet = array();
|
|
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'
|
|
);
|
|
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'
|
|
),
|
|
'misc' => array(
|
|
'in' => 'in',
|
|
'contains' => 'contains',
|
|
'like' => 'like',
|
|
'""' => 'stringlit',
|
|
'rlike' => 'rlike',
|
|
'irlike' => 'irlike',
|
|
'cond ? iftrue : iffalse' => 'tern',
|
|
'if cond then iftrue else iffalse end' => 'cond',
|
|
),
|
|
'funcs' => array(
|
|
'length(string)' => 'length',
|
|
'lcase(string)' => 'lcase',
|
|
'ccnorm(string)' => 'ccnorm',
|
|
'rmdoubles(string)' => 'rmdoubles',
|
|
'specialratio(string)' => 'specialratio',
|
|
'norm(string)' => 'norm',
|
|
'count(needle,haystack)' => 'count',
|
|
'rcount(needle,haystack)' => 'rcount',
|
|
'rmwhitespace(text)' => 'rmwhitespace',
|
|
'rmspecials(text)' => 'rmspecials',
|
|
'ip_in_range(ip, range)' => 'ip_in_range',
|
|
'contains_any(haystack,needle1,needle2,needle3)' => 'contains-any',
|
|
'substr(subject, offset, length)' => 'substr',
|
|
'strpos(haystack, needle)' => 'strpos',
|
|
'str_replace(subject, search, replace)' => 'str_replace',
|
|
'set_var(var,value)' => 'set_var',
|
|
),
|
|
'vars' => array(
|
|
'timestamp' => 'timestamp',
|
|
'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',
|
|
# 'old_text' => 'old-text-stripped', ## Disabled, performance
|
|
# 'old_html' => 'old-html', ## Disabled, performance
|
|
'old_links' => 'old-links',
|
|
'minor_edit' => 'minor-edit',
|
|
'file_sha1' => 'file-sha1',
|
|
),
|
|
);
|
|
|
|
public static function addNavigationLinks( $out, $sk, $pageType ) {
|
|
global $wgLang, $wgUser;
|
|
$linkDefs = array(
|
|
'home' => 'Special:AbuseFilter',
|
|
'recentchanges' => 'Special:AbuseFilter/history',
|
|
'test' => 'Special:AbuseFilter/test',
|
|
'examine' => 'Special:AbuseFilter/examine',
|
|
'log' => 'Special:AbuseLog',
|
|
);
|
|
|
|
if ( $wgUser->isAllowed( 'abusefilter-modify' ) ) {
|
|
$linkDefs = array_merge( $linkDefs, array(
|
|
'tools' => 'Special:AbuseFilter/tools',
|
|
'import' => 'Special:AbuseFilter/import',
|
|
) );
|
|
}
|
|
|
|
// Save some translator work
|
|
$msgOverrides = array(
|
|
'recentchanges' => 'abusefilter-filter-log',
|
|
);
|
|
|
|
$links = array();
|
|
|
|
foreach ( $linkDefs as $name => $page ) {
|
|
$msgName = "abusefilter-topnav-$name";
|
|
|
|
if ( isset( $msgOverrides[$name] ) )
|
|
$msgName = $msgOverrides[$name];
|
|
|
|
$msg = wfMsgExt( $msgName, 'parseinline' );
|
|
$title = Title::newFromText( $page );
|
|
|
|
if ( $name == $pageType ) {
|
|
$links[] = Xml::tags( 'strong', null, $msg );
|
|
} else {
|
|
$links[] = $sk->link( $title, $msg );
|
|
}
|
|
}
|
|
|
|
$linkStr = '(' . $wgLang->pipeList( $links ) . ')';
|
|
$linkStr = wfMsgExt( 'abusefilter-topnav', 'parseinline' ) . " $linkStr";
|
|
|
|
$linkStr = Xml::tags( 'div', array( 'class' => 'mw-abusefilter-navigation' ), $linkStr );
|
|
|
|
$out->addHTML( $linkStr );
|
|
}
|
|
|
|
public static function generateUserVars( $user ) {
|
|
$vars = new AbuseFilterVariableHolder;
|
|
|
|
$vars->setLazyLoadVar( 'user_editcount', 'simple-user-accessor',
|
|
array( 'user' => $user->getName(), 'method' => 'getEditCount' ) );
|
|
$vars->setVar( 'user_name', $user->getName() );
|
|
$vars->setLazyLoadVar( 'user_emailconfirm', 'simple-user-accessor',
|
|
array( 'user' => $user->getName(), 'method' => 'getEmailAuthenticationTimestamp' ) );
|
|
|
|
$vars->setLazyLoadVar( 'user_age', 'user-age',
|
|
array( 'user' => $user->getName(), 'asof' => wfTimestampNow() ) );
|
|
$vars->setLazyLoadVar( 'user_groups', 'user-groups', array( 'user' => $user->getName() ) );
|
|
|
|
return $vars;
|
|
}
|
|
|
|
public static function getBuilderValues() {
|
|
static $realValues = null;
|
|
|
|
if ( $realValues ) return $realValues;
|
|
|
|
$realValues = self::$builderValues;
|
|
wfRunHooks( 'AbuseFilter-builder', array( &$realValues ) );
|
|
|
|
return $realValues;
|
|
}
|
|
|
|
public static function ajaxCheckSyntax( $filter ) {
|
|
wfLoadExtensionMessages( 'AbuseFilter' );
|
|
|
|
global $wgUser;
|
|
if ( !$wgUser->isAllowed( 'abusefilter-modify' ) ) {
|
|
return false;
|
|
}
|
|
|
|
$result = self::checkSyntax( $filter );
|
|
|
|
$ok = ( $result === true );
|
|
|
|
if ( $ok ) {
|
|
return 'OK';
|
|
} else {
|
|
return 'ERR: ' . json_encode( $result );
|
|
}
|
|
}
|
|
|
|
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 );
|
|
}
|
|
|
|
public static function filterHidden( $filter ) {
|
|
$dbr = wfGetDB( DB_SLAVE );
|
|
$hidden = $dbr->selectField(
|
|
'abuse_filter',
|
|
'af_hidden',
|
|
array( 'af_id' => $filter ),
|
|
__METHOD__
|
|
);
|
|
return $hidden ? true : false;
|
|
}
|
|
|
|
public static function ajaxCheckFilterWithVars( $filter, $vars ) {
|
|
global $wgUser;
|
|
|
|
// Anti-DoS
|
|
if ( !$wgUser->isAllowed( 'abusefilter-modify' ) ) {
|
|
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';
|
|
}
|
|
|
|
public static function triggerLimiter( $val = 1 ) {
|
|
self::$condCount += $val;
|
|
|
|
global $wgAbuseFilterConditionLimit;
|
|
|
|
if ( self::$condCount > $wgAbuseFilterConditionLimit ) {
|
|
throw new MWException( 'Condition limit reached.' );
|
|
}
|
|
}
|
|
|
|
public static function disableConditionLimit() {
|
|
// For use in batch scripts and the like
|
|
self::$condLimitEnabled = false;
|
|
}
|
|
|
|
public static function generateTitleVars( $title, $prefix ) {
|
|
$vars = new AbuseFilterVariableHolder;
|
|
|
|
if ( !$title )
|
|
return new AbuseFilterVariableHolder;
|
|
|
|
$vars->setVar( $prefix . '_ARTICLEID', $title->getArticleId() );
|
|
$vars->setVar( $prefix . '_NAMESPACE', $title->getNamespace() );
|
|
$vars->setVar( $prefix . '_TEXT', $title->getText() );
|
|
$vars->setVar( $prefix . '_PREFIXEDTEXT', $title->getPrefixedText() );
|
|
|
|
// Use restrictions.
|
|
global $wgRestrictionTypes;
|
|
foreach ( $wgRestrictionTypes as $action ) {
|
|
$vars->setLazyLoadVar( "{$prefix}_restrictions_$action", 'get-page-restrictions',
|
|
array( 'title' => $title->getText(),
|
|
'namespace' => $title->getNamespace(),
|
|
'action' => $action
|
|
)
|
|
);
|
|
}
|
|
|
|
$vars->setLazyLoadVar( "{$prefix}_recent_contributors", 'load-recent-authors',
|
|
array(
|
|
'cutoff' => wfTimestampNow(),
|
|
'title' => $title->getText(),
|
|
'namespace' => $title->getNamespace()
|
|
) );
|
|
|
|
return $vars;
|
|
}
|
|
|
|
public static function checkSyntax( $filter ) {
|
|
global $wgAbuseFilterParserClass;
|
|
|
|
$parser = new $wgAbuseFilterParserClass;
|
|
|
|
return $parser->checkSyntax( $filter );
|
|
}
|
|
|
|
public static function evaluateExpression( $expr, $vars = array() ) {
|
|
wfLoadExtensionMessages( 'AbuseFilter' );
|
|
global $wgAbuseFilterParserClass;
|
|
|
|
if ( self::checkSyntax( $expr ) !== true ) {
|
|
return 'BADSYNTAX';
|
|
}
|
|
|
|
$parser = new $wgAbuseFilterParserClass;
|
|
|
|
$parser->setVars( $vars );
|
|
|
|
return $parser->evaluateExpression( $expr );
|
|
}
|
|
|
|
public static function ajaxReAutoconfirm( $username ) {
|
|
global $wgUser;
|
|
wfLoadExtensionMessages( 'AbuseFilter' );
|
|
|
|
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 wfMsgExt( 'abusefilter-reautoconfirm-none', array( 'parsemag' ), $username );
|
|
}
|
|
|
|
$wgMemc->delete( $k );
|
|
|
|
return wfMsgExt( 'abusefilter-reautoconfirm-done', array( 'parsemag' ), $username );
|
|
}
|
|
|
|
public static function ajaxEvaluateExpression( $expr ) {
|
|
global $wgUser;
|
|
if ( !$wgUser->isAllowed( 'abusefilter-modify' ) ) {
|
|
return false;
|
|
}
|
|
return htmlspecialchars( self::evaluateExpression( $expr ) );
|
|
}
|
|
|
|
public static function checkConditions( $conds, $vars, $ignoreError = true,
|
|
$keepVars = 'resetvars' ) {
|
|
global $wgAbuseFilterParserClass;
|
|
|
|
static $parser;
|
|
|
|
wfProfileIn( __METHOD__ );
|
|
|
|
if ( is_null( $parser ) || $keepVars == 'resetvars' ) {
|
|
$parser = new $wgAbuseFilterParserClass;
|
|
|
|
$parser->setVars( $vars );
|
|
}
|
|
|
|
try {
|
|
$result = $parser->parse( $conds, self::$condCount );
|
|
} catch ( Exception $excep ) {
|
|
// Sigh.
|
|
$result = false;
|
|
|
|
wfDebugLog( 'AbuseFilter', 'AbuseFilter parser error: ' . $excep->getMessage() . "\n" );
|
|
|
|
if ( !$ignoreError ) {
|
|
throw $excep;
|
|
}
|
|
}
|
|
|
|
wfProfileOut( __METHOD__ );
|
|
|
|
return $result;
|
|
}
|
|
|
|
/** Returns an associative array of filters which were tripped */
|
|
public static function checkAllFilters( $vars ) {
|
|
// Fetch from the database.
|
|
wfProfileIn( __METHOD__ );
|
|
|
|
$filter_matched = array();
|
|
|
|
$dbr = wfGetDB( DB_SLAVE );
|
|
$res = $dbr->select( 'abuse_filter', '*', array( 'af_enabled' => 1, 'af_deleted' => 0 ), __METHOD__ );
|
|
|
|
while ( $row = $dbr->fetchObject( $res ) ) {
|
|
$filter_matched[$row->af_id] = self::checkFilter( $row, $vars, true );
|
|
}
|
|
|
|
global $wgAbuseFilterCentralDB, $wgAbuseFilterIsCentral;
|
|
if ( $wgAbuseFilterCentralDB && !$wgAbuseFilterIsCentral ) {
|
|
// Global filters
|
|
$fdb = wfGetDB( DB_SLAVE, array(), $wgAbuseFilterCentralDB );
|
|
$res = $fdb->select(
|
|
'abuse_filter',
|
|
'*',
|
|
array(
|
|
'af_enabled' => 1,
|
|
'af_deleted' => 0,
|
|
'af_global' => 1
|
|
),
|
|
__METHOD__
|
|
);
|
|
|
|
while ( $row = $fdb->fetchObject( $res ) ) {
|
|
$filter_matched['global-' . $row->af_id] =
|
|
self::checkFilter( $row, $vars, true, 'global-' );
|
|
}
|
|
}
|
|
|
|
// Update statistics, and disable filters which are over-blocking.
|
|
self::recordStats( $filter_matched );
|
|
|
|
wfProfileOut( __METHOD__ );
|
|
|
|
return $filter_matched;
|
|
}
|
|
|
|
public static function checkFilter( $row, $vars, $profile = false, $prefix = '' ) {
|
|
$filterID = $prefix . $row->af_id;
|
|
|
|
if ( $profile ) {
|
|
$startConds = self::$condCount;
|
|
$startTime = microtime( true );
|
|
}
|
|
|
|
// Store the row somewhere convenient
|
|
self::$filters[$filterID] = $row;
|
|
|
|
// Check conditions...
|
|
$pattern = trim( $row->af_pattern );
|
|
if ( self::checkConditions( $pattern, $vars, true /* ignore errors */,
|
|
'keepvars' ) ) {
|
|
// Record match.
|
|
$result = true;
|
|
} else {
|
|
// Record non-match.
|
|
$result = false;
|
|
}
|
|
|
|
if ( $profile ) {
|
|
$endTime = microtime( true );
|
|
$endConds = self::$condCount;
|
|
|
|
$timeTaken = $endTime - $startTime;
|
|
$condsUsed = $endConds - $startConds;
|
|
|
|
self::recordProfilingResult( $row->af_id, $timeTaken, $condsUsed );
|
|
}
|
|
|
|
return $result;
|
|
}
|
|
|
|
public static function resetFilterProfile( $filter ) {
|
|
global $wgMemc;
|
|
$countKey = wfMemcKey( 'abusefilter', 'profile', $filter, 'count' );
|
|
$totalKey = wfMemcKey( 'abusefilter', 'profile', $filter, 'total' );
|
|
|
|
$wgMemc->delete( $countKey );
|
|
$wgMemc->delete( $totalKey );
|
|
}
|
|
|
|
public static function recordProfilingResult( $filter, $time, $conds ) {
|
|
global $wgMemc;
|
|
|
|
$countKey = wfMemcKey( 'abusefilter', 'profile', $filter, 'count' );
|
|
$totalKey = wfMemcKey( 'abusefilter', 'profile', $filter, 'total' );
|
|
$totalCondKey = wfMemcKey( 'abusefilter', 'profile-conds', 'total' );
|
|
|
|
$curCount = $wgMemc->get( $countKey );
|
|
$curTotal = $wgMemc->get( $totalKey );
|
|
$curTotalConds = $wgMemc->get( $totalCondKey );
|
|
|
|
if ( $curCount ) {
|
|
$wgMemc->set( $totalCondKey, $curTotalConds + $conds, 3600 );
|
|
$wgMemc->set( $totalKey, $curTotal + $time, 3600 );
|
|
$wgMemc->incr( $countKey );
|
|
} else {
|
|
$wgMemc->set( $countKey, 1, 3600 );
|
|
$wgMemc->set( $totalKey, $time, 3600 );
|
|
$wgMemc->set( $totalCondKey, $conds, 3600 );
|
|
}
|
|
}
|
|
|
|
public static function getFilterProfile( $filter ) {
|
|
global $wgMemc;
|
|
|
|
$countKey = wfMemcKey( 'abusefilter', 'profile', $filter, 'count' );
|
|
$totalKey = wfMemcKey( 'abusefilter', 'profile', $filter, 'total' );
|
|
$totalCondKey = wfMemcKey( 'abusefilter', 'profile-conds', 'total' );
|
|
|
|
$curCount = $wgMemc->get( $countKey );
|
|
$curTotal = $wgMemc->get( $totalKey );
|
|
$curTotalConds = $wgMemc->get( $totalCondKey );
|
|
|
|
if ( !$curCount )
|
|
return array( 0, 0 );
|
|
|
|
$timeProfile = ( $curTotal / $curCount ) * 1000; // 1000 ms in a sec
|
|
$timeProfile = round( $timeProfile, 2 ); // Return in ms, rounded to 2dp
|
|
|
|
$condProfile = ( $curTotalConds / $curCount );
|
|
$condProfile = round( $condProfile, 0 );
|
|
|
|
return array( $timeProfile, $condProfile );
|
|
}
|
|
|
|
/** Utility function to decode global-$index to $index. Returns false if not global */
|
|
public static function decodeGlobalName( $filter ) {
|
|
if ( strpos( $filter, 'global-' ) == 0 ) {
|
|
return substr( $filter, strlen( 'global-' ) );
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
public static function getConsequencesForFilters( $filters ) {
|
|
$globalFilters = array();
|
|
$localFilters = array();
|
|
|
|
foreach ( $filters as $filter ) {
|
|
$globalIndex = self::decodeGlobalName( $filter );
|
|
|
|
if ( $globalIndex )
|
|
$globalFilters[] = $globalIndex;
|
|
else
|
|
$localFilters[] = $filter;
|
|
}
|
|
|
|
global $wgAbuseFilterCentralDB;
|
|
// Load local filter info
|
|
$dbr = wfGetDB( DB_SLAVE );
|
|
// Retrieve the consequences.
|
|
$consequences = array();
|
|
|
|
if ( count( $localFilters ) ) {
|
|
$consequences = self::loadConsequencesFromDB( $dbr, $localFilters );
|
|
}
|
|
|
|
if ( count( $globalFilters ) ) {
|
|
$fdb = wfGetDB( DB_SLAVE, array(), $wgAbuseFilterCentralDB );
|
|
$consequences = array_merge( $consequences,
|
|
self::loadConsequencesFromDB( $fdb, $globalFilters, 'global-' ) );
|
|
}
|
|
|
|
return $consequences;
|
|
}
|
|
|
|
public static function loadConsequencesFromDB( $dbr, $filters, $prefix = '' ) {
|
|
$actionsByFilter = array();
|
|
foreach ( $filters as $filter ) {
|
|
$actionsByFilter[$prefix . $filter] = array();
|
|
}
|
|
|
|
$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' ) )
|
|
);
|
|
|
|
// Categorise consequences by filter.
|
|
global $wgAbuseFilterRestrictedActions;
|
|
while ( $row = $dbr->fetchObject( $res ) ) {
|
|
if ( $row->af_throttled
|
|
&& in_array( $row->afa_consequence, $wgAbuseFilterRestrictedActions ) )
|
|
{
|
|
# # Don't do the action
|
|
} elseif ( $row->afa_filter != $row->af_id ) {
|
|
// We probably got a NULL, as it's a LEFT JOIN.
|
|
// Don't add it.
|
|
} else {
|
|
$actionsByFilter[$prefix . $row->afa_filter][$row->afa_consequence] = array(
|
|
'action' => $row->afa_consequence,
|
|
'parameters' => explode( "\n", $row->afa_parameters )
|
|
);
|
|
}
|
|
}
|
|
|
|
return $actionsByFilter;
|
|
}
|
|
|
|
/** Returns an array [ list of actions taken by filter, error message to display, if any ] */
|
|
public static function executeFilterActions( $filters, $title, $vars ) {
|
|
wfProfileIn( __METHOD__ );
|
|
static $blockingActions = array(
|
|
'block',
|
|
'rangeblock',
|
|
'degroup',
|
|
'blockautopromote'
|
|
);
|
|
|
|
$actionsByFilter = self::getConsequencesForFilters( $filters );
|
|
$actionsTaken = array_fill_keys( $filters, array() );
|
|
|
|
wfLoadExtensionMessages( 'AbuseFilter' );
|
|
|
|
$messages = array();
|
|
|
|
foreach ( $actionsByFilter as $filter => $actions ) {
|
|
// Special-case handling for warnings.
|
|
global $wgOut;
|
|
$parsed_public_comments = $wgOut->parseInline(
|
|
self::$filters[$filter]->af_public_comments );
|
|
|
|
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;
|
|
}
|
|
}
|
|
|
|
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
|
|
if ( !empty( $parameters[0] ) && strlen( $parameters[0] ) ) {
|
|
$msg = $parameters[0];
|
|
} else {
|
|
$msg = 'abusefilter-warning';
|
|
}
|
|
$messages[] = wfMsgExt( $msg, 'parseinline',
|
|
array( $parsed_public_comments, $filter ) ) . "<br />\n";
|
|
|
|
$actionsTaken[$filter][] = 'warn';
|
|
|
|
continue; // Don't do anything else.
|
|
} else {
|
|
// We already warned them
|
|
$_SESSION[$warnKey] = false;
|
|
}
|
|
|
|
unset( $actions['warn'] );
|
|
}
|
|
|
|
// prevent double warnings
|
|
if ( count( array_intersect( array_keys( $actions ), $blockingActions ) ) > 0 &&
|
|
!empty( $actions['disallow'] ) ) {
|
|
unset( $actions['disallow'] );
|
|
}
|
|
|
|
// Do the rest of the actions
|
|
foreach ( $actions as $action => $info ) {
|
|
$newMsg = self::takeConsequenceAction(
|
|
$action, $info['parameters'], $title, $vars,
|
|
self::$filters[$filter]->af_public_comments
|
|
);
|
|
|
|
if ( $newMsg )
|
|
$messages[] = $newMsg;
|
|
$actionsTaken[$filter][] = $action;
|
|
}
|
|
}
|
|
|
|
wfProfileOut( __METHOD__ );
|
|
return array( $actionsTaken, implode( "\n", $messages ) );
|
|
}
|
|
|
|
public static function filterAction( $vars, $title ) {
|
|
global $wgUser, $wgMemc, $wgTitle;
|
|
|
|
wfProfileIn( __METHOD__ );
|
|
|
|
if ( !$wgTitle )
|
|
$wgTitle = SpecialPage::getTitleFor( 'AbuseFilter' );
|
|
|
|
// Add vars from extensions
|
|
wfRunHooks( 'AbuseFilter-filterAction', array( &$vars, $title ) );
|
|
|
|
// Set context
|
|
$vars->setVar( 'context', 'filter' );
|
|
$vars->setVar( 'timestamp', time() );
|
|
|
|
$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 ) {
|
|
wfProfileOut( __METHOD__ );
|
|
return true;
|
|
}
|
|
|
|
wfProfileIn( __METHOD__ . '-block' );
|
|
|
|
list( $actions_taken, $error_msg ) = self::executeFilterActions(
|
|
array_keys( array_filter( $filter_matched ) ), $title, $vars );
|
|
|
|
$action = $vars->getVar( 'ACTION' )->toString();
|
|
|
|
// Create a template
|
|
$log_template = array(
|
|
'afl_user' => $wgUser->getId(),
|
|
'afl_user_text' => $wgUser->getName(),
|
|
'afl_timestamp' => $dbr->timestamp( wfTimestampNow() ),
|
|
'afl_namespace' => $title->getNamespace(),
|
|
'afl_title' => $title->getDBkey(),
|
|
'afl_ip' => wfGetIP()
|
|
);
|
|
|
|
// Hack to avoid revealing IPs of people creating accounts
|
|
if ( !$wgUser->getId() && $action == 'createaccount' ) {
|
|
$log_template['afl_user_text'] = $vars->getVar( 'accountname' )->toString();
|
|
}
|
|
|
|
self::addLogEntries( $actions_taken, $log_template, $action, $vars );
|
|
|
|
$error_msg = $error_msg == '' ? true : $error_msg;
|
|
|
|
wfProfileOut( __METHOD__ . '-block' );
|
|
|
|
wfProfileOut( __METHOD__ );
|
|
|
|
return $error_msg;
|
|
}
|
|
|
|
public static function addLogEntries( $actions_taken, $log_template, $action, $vars ) {
|
|
wfProfileIn( __METHOD__ );
|
|
$dbw = wfGetDB( DB_MASTER );
|
|
|
|
$central_log_template = array(
|
|
'afl_wiki' => wfWikiID(),
|
|
);
|
|
|
|
$log_rows = array();
|
|
$central_log_rows = array();
|
|
$logged_local_filters = array();
|
|
$logged_global_filters = array();
|
|
|
|
foreach ( $actions_taken as $filter => $actions ) {
|
|
$globalIndex = self::decodeGlobalName( $filter );
|
|
$thisLog = $log_template;
|
|
$thisLog['afl_filter'] = $filter;
|
|
$thisLog['afl_action'] = $action;
|
|
$thisLog['afl_actions'] = implode( ',', $actions );
|
|
|
|
// Don't log if we were only throttling.
|
|
if ( $thisLog['afl_actions'] != 'throttle' ) {
|
|
$log_rows[] = $thisLog;
|
|
|
|
if ( !$globalIndex )
|
|
$logged_local_filters[] = $filter;
|
|
|
|
// Global logging
|
|
if ( $globalIndex ) {
|
|
$title = Title::makeTitle( $thisLog['afl_namespace'],
|
|
$thisLog['afl_title'] );
|
|
$centralLog = $thisLog + $central_log_template;
|
|
$centralLog['afl_filter'] = $globalIndex;
|
|
$centralLog['afl_title'] = $title->getPrefixedText();
|
|
$centralLog['afl_namespace'] = 0;
|
|
|
|
$central_log_rows[] = $centralLog;
|
|
$logged_global_filters[] = $globalIndex;
|
|
}
|
|
}
|
|
}
|
|
|
|
if ( !count( $log_rows ) ) {
|
|
wfProfileOut( __METHOD__ );
|
|
return;
|
|
}
|
|
|
|
// Only store the var dump if we're actually going to add log rows.
|
|
$var_dump = self::storeVarDump( $vars );
|
|
$var_dump = "stored-text:$var_dump"; // To distinguish from stuff stored directly
|
|
|
|
foreach ( $log_rows as $index => $data ) {
|
|
$log_rows[$index]['afl_var_dump'] = $var_dump;
|
|
}
|
|
|
|
wfProfileIn( __METHOD__ . '-hitstats' );
|
|
|
|
global $wgMemc;
|
|
|
|
// Increment trigger counter
|
|
$wgMemc->incr( self::filterMatchesKey() );
|
|
|
|
$dbw->insert( 'abuse_filter_log', $log_rows, __METHOD__ );
|
|
|
|
if ( count( $logged_local_filters ) ) {
|
|
// Update hit-counter.
|
|
$dbw->update( 'abuse_filter',
|
|
array( 'af_hit_count=af_hit_count+1' ),
|
|
array( 'af_id' => $logged_local_filters ),
|
|
__METHOD__
|
|
);
|
|
}
|
|
|
|
// Global stuff
|
|
if ( count( $logged_global_filters ) ) {
|
|
$vars->computeDBVars();
|
|
$global_var_dump = self::storeVarDump( $vars, 'global' );
|
|
$global_var_dump = "stored-text:$global_var_dump";
|
|
foreach ( $central_log_rows as $index => $data ) {
|
|
$central_log_rows[$index]['afl_var_dump'] = $global_var_dump;
|
|
}
|
|
|
|
global $wgAbuseFilterCentralDB;
|
|
$fdb = wfGetDB( DB_MASTER, array(), $wgAbuseFilterCentralDB );
|
|
|
|
$fdb->insert( 'abuse_filter_log', $central_log_rows, __METHOD__ );
|
|
|
|
$fdb->update( 'abuse_filter',
|
|
array( 'af_hit_count=af_hit_count+1' ),
|
|
array( 'af_id' => $logged_global_filters ),
|
|
__METHOD__
|
|
);
|
|
}
|
|
|
|
// Check for emergency disabling.
|
|
$total = $wgMemc->get( AbuseFilter::filterUsedKey() );
|
|
self::checkEmergencyDisable( $logged_local_filters, $total );
|
|
|
|
wfProfileOut( __METHOD__ . '-hitstats' );
|
|
|
|
wfProfileOut( __METHOD__ );
|
|
}
|
|
|
|
/**
|
|
* Store a var dump to External Storage or the text table
|
|
* Some of this code is stolen from Revision::insertOn and friends
|
|
*/
|
|
public static function storeVarDump( $vars, $global = false ) {
|
|
wfProfileIn( __METHOD__ );
|
|
|
|
global $wgCompressRevisions;
|
|
|
|
if ( is_array( $vars ) || is_object( $vars ) )
|
|
$text = serialize( $vars );
|
|
else $text = $vars;
|
|
|
|
$flags = array();
|
|
|
|
if ( $wgCompressRevisions ) {
|
|
if ( function_exists( 'gzdeflate' ) ) {
|
|
$text = gzdeflate( $text );
|
|
$flags[] = 'gzip';
|
|
}
|
|
}
|
|
|
|
// Store to ES if applicable
|
|
global $wgDefaultExternalStore, $wgAbuseFilterCentralDB;
|
|
if ( $wgDefaultExternalStore ) {
|
|
if ( $global )
|
|
$text = ExternalStore::insertToForeignDefault( $text, $wgAbuseFilterCentralDB );
|
|
else
|
|
$text = ExternalStore::insertToDefault( $text );
|
|
$flags[] = 'external';
|
|
|
|
if ( !$text ) {
|
|
// Not mission-critical, just return nothing
|
|
wfProfileOut( __METHOD__ );
|
|
return null;
|
|
}
|
|
}
|
|
|
|
// Store to text table
|
|
if ( $global )
|
|
$dbw = wfGetDB( DB_MASTER, array(), $wgAbuseFilterCentralDB );
|
|
else
|
|
$dbw = wfGetDB( DB_MASTER );
|
|
$old_id = $dbw->nextSequenceValue( 'text_old_id_seq' );
|
|
$dbw->insert( 'text',
|
|
array(
|
|
'old_id' => $old_id,
|
|
'old_text' => $text,
|
|
'old_flags' => implode( ',', $flags ),
|
|
), __METHOD__
|
|
);
|
|
$text_id = $dbw->insertId();
|
|
wfProfileOut( __METHOD__ );
|
|
|
|
return $text_id;
|
|
}
|
|
|
|
/**
|
|
* Retrieve a var dump from External Storage or the text table
|
|
* Some of this code is stolen from Revision::loadText et al
|
|
*/
|
|
public static function loadVarDump( $stored_dump ) {
|
|
wfProfileIn( __METHOD__ );
|
|
|
|
// Back-compat
|
|
if ( strpos( $stored_dump, 'stored-text:' ) === false ) {
|
|
wfProfileOut( __METHOD__ );
|
|
return unserialize( $stored_dump );
|
|
}
|
|
|
|
$text_id = substr( $stored_dump, strlen( 'stored-text:' ) );
|
|
|
|
$dbr = wfGetDB( DB_SLAVE );
|
|
|
|
$text_row = $dbr->selectRow(
|
|
'text',
|
|
array( 'old_text', 'old_flags' ),
|
|
array( 'old_id' => $text_id ),
|
|
__METHOD__
|
|
);
|
|
|
|
if ( !$text_row ) {
|
|
wfProfileOut( __METHOD__ );
|
|
return new AbuseFilterVariableHolder;
|
|
}
|
|
|
|
$flags = explode( ',', $text_row->old_flags );
|
|
$text = $text_row->old_text;
|
|
|
|
if ( in_array( 'external', $flags ) ) {
|
|
$text = ExternalStore::fetchFromURL( $text );
|
|
}
|
|
|
|
if ( in_array( 'gzip', $flags ) ) {
|
|
$text = gzinflate( $text );
|
|
}
|
|
|
|
$obj = unserialize( $text );
|
|
|
|
wfProfileOut( __METHOD__ );
|
|
return $obj;
|
|
}
|
|
|
|
public static function takeConsequenceAction( $action, $parameters, $title,
|
|
$vars, $rule_desc )
|
|
{
|
|
wfLoadExtensionMessages( 'AbuseFilter' );
|
|
$display = '';
|
|
switch ( $action ) {
|
|
case 'disallow':
|
|
if ( strlen( $parameters[0] ) ) {
|
|
$display .= wfMsgExt( $parameters[0], 'parseinline', array( $rule_desc ) ) . "\n";
|
|
} else {
|
|
// Generic message.
|
|
$display .= wfMsgExt( 'abusefilter-disallowed', 'parseinline',
|
|
array( $rule_desc ) ) . "<br />\n";
|
|
}
|
|
break;
|
|
|
|
case 'block':
|
|
global $wgUser, $wgAbuseFilterBlockDuration;
|
|
$filterUser = AbuseFilter::getFilterUser();
|
|
|
|
// Create a block.
|
|
$block = new Block;
|
|
$block->mAddress = $wgUser->getName();
|
|
$block->mUser = $wgUser->getId();
|
|
$block->mBy = $filterUser->getId();
|
|
$block->mByName = $filterUser->getName();
|
|
$block->mReason = wfMsgForContent( 'abusefilter-blockreason', $rule_desc );
|
|
$block->mTimestamp = wfTimestampNow();
|
|
$block->mAnonOnly = 1;
|
|
$block->mCreateAccount = 1;
|
|
$block->mExpiry = Block::parseExpiryInput( $wgAbuseFilterBlockDuration );
|
|
|
|
$block->insert();
|
|
|
|
// Log it
|
|
# Prepare log parameters
|
|
$logParams = array();
|
|
if ( $block->mExpiry == 'infinity' ) {
|
|
$logParams[] = 'indefinite';
|
|
} else {
|
|
$logParams[] = $wgAbuseFilterBlockDuration;
|
|
}
|
|
$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()
|
|
);
|
|
|
|
$display .= wfMsgExt( 'abusefilter-blocked-display', 'parseinline',
|
|
array( $rule_desc ) ) . "<br />\n";
|
|
break;
|
|
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' );
|
|
$log->addEntry( 'block', Title::makeTitle( NS_USER, $range ),
|
|
wfMsgForContent( 'abusefilter-blockreason', $rule_desc ),
|
|
$logParams, self::getFilterUser()
|
|
);
|
|
|
|
$display .= wfMsgExt( 'abusefilter-blocked-display', 'parseinline',
|
|
$rule_desc ) . "<br />\n";
|
|
break;
|
|
case 'degroup':
|
|
global $wgUser;
|
|
if ( !$wgUser->isAnon() ) {
|
|
// Remove all groups from the user. Ouch.
|
|
$groups = $wgUser->getGroups();
|
|
|
|
foreach ( $groups as $group ) {
|
|
$wgUser->removeGroup( $group );
|
|
}
|
|
|
|
$display .= wfMsgExt( 'abusefilter-degrouped', 'parseinline',
|
|
array( $rule_desc ) ) . "<br />\n";
|
|
|
|
// Don't log it if there aren't any groups being removed!
|
|
if ( !count( $groups ) ) {
|
|
break;
|
|
}
|
|
|
|
// Log it.
|
|
$log = new LogPage( 'rights' );
|
|
|
|
$log->addEntry( 'rights',
|
|
$wgUser->getUserPage(),
|
|
wfMsgForContent( 'abusefilter-degroupreason', $rule_desc ),
|
|
array(
|
|
implode( ', ', $groups ),
|
|
wfMsgForContent( 'rightsnone' )
|
|
),
|
|
self::getFilterUser()
|
|
);
|
|
}
|
|
|
|
break;
|
|
case 'blockautopromote':
|
|
global $wgUser, $wgMemc;
|
|
if ( !$wgUser->isAnon() ) {
|
|
$blockPeriod = (int)mt_rand( 3 * 86400, 7 * 86400 ); // Block for 3-7 days.
|
|
$wgMemc->set( self::autoPromoteBlockKey( $wgUser ), true, $blockPeriod );
|
|
|
|
$display .= wfMsgExt( 'abusefilter-autopromote-blocked', 'parseinline',
|
|
array( $rule_desc ) ) . "<br />\n";
|
|
}
|
|
break;
|
|
|
|
case 'flag':
|
|
// Do nothing. Here for completeness.
|
|
break;
|
|
|
|
case 'tag':
|
|
// Mark with a tag on recentchanges.
|
|
global $wgUser;
|
|
|
|
$actionID = implode( '-', array(
|
|
$title->getPrefixedText(), $wgUser->getName(),
|
|
$vars->getVar( 'ACTION' )->toString()
|
|
) );
|
|
|
|
AbuseFilter::$tagsToSet[$actionID] = $parameters;
|
|
break;
|
|
default:
|
|
wfDebugLog( 'AbuseFilter', "Unrecognised action $action" );
|
|
}
|
|
|
|
return $display;
|
|
}
|
|
|
|
public static function isThrottled( $throttleId, $types, $title, $rateCount, $ratePeriod ) {
|
|
global $wgMemc;
|
|
|
|
$key = self::throttleKey( $throttleId, $types, $title );
|
|
$count = intval( $wgMemc->get( $key ) );
|
|
|
|
wfDebugLog( 'AbuseFilter', "Got value $count for throttle key $key\n" );
|
|
|
|
if ( $count > 0 ) {
|
|
$wgMemc->incr( $key );
|
|
$count++;
|
|
wfDebugLog( 'AbuseFilter', "Incremented throttle key $key" );
|
|
} else {
|
|
wfDebugLog( 'AbuseFilter', "Added throttle key $key with value 1" );
|
|
$wgMemc->add( $key, 1, $ratePeriod );
|
|
$count = 1;
|
|
}
|
|
|
|
if ( $count > $rateCount ) {
|
|
wfDebugLog( 'AbuseFilter', "Throttle $key hit value $count -- maximum is $rateCount." );
|
|
return true; // THROTTLED
|
|
}
|
|
|
|
wfDebugLog( 'AbuseFilter', "Throttle $key not hit!" );
|
|
|
|
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() );
|
|
}
|
|
|
|
public static function recordStats( $filters ) {
|
|
global $wgAbuseFilterConditionLimit, $wgMemc;
|
|
|
|
wfProfileIn( __METHOD__ );
|
|
|
|
$blocking_filters = array_keys( array_filter( $filters ) );
|
|
|
|
// Figure out if we've triggered overflows and blocks.
|
|
$overflow_triggered = ( self::$condCount > $wgAbuseFilterConditionLimit );
|
|
$filter_triggered = count( $blocking_filters ) > 0;
|
|
|
|
// Store some keys...
|
|
$overflow_key = self::filterLimitReachedKey();
|
|
$total_key = self::filterUsedKey();
|
|
|
|
$total = $wgMemc->get( $total_key );
|
|
|
|
$storage_period = self::$statsStoragePeriod;
|
|
|
|
if ( !$total || $total > 10000 ) {
|
|
// 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 );
|
|
|
|
foreach ( $filters as $filter => $matched ) {
|
|
$wgMemc->set( self::filterMatchesKey( $filter ), 0, $storage_period );
|
|
}
|
|
$wgMemc->set( self::filterMatchesKey(), 0, $storage_period );
|
|
}
|
|
|
|
// Increment total
|
|
$wgMemc->incr( $total_key );
|
|
|
|
// Increment overflow counter, if our condition limit overflowed
|
|
if ( $overflow_triggered ) {
|
|
$wgMemc->incr( $overflow_key );
|
|
}
|
|
wfProfileOut( __METHOD__ );
|
|
}
|
|
|
|
public static function checkEmergencyDisable( $filters, $total ) {
|
|
global $wgAbuseFilterEmergencyDisableThreshold, $wgAbuseFilterEmergencyDisableCount,
|
|
$wgAbuseFilterEmergencyDisableAge, $wgMemc;
|
|
|
|
foreach ( $filters as $filter ) {
|
|
// Increment counter
|
|
$matchCount = $wgMemc->get( self::filterMatchesKey( $filter ) );
|
|
|
|
// Handle missing keys...
|
|
if ( !$matchCount ) {
|
|
$wgMemc->set( self::filterMatchesKey( $filter ), 1, self::$statsStoragePeriod );
|
|
} else {
|
|
$wgMemc->incr( self::filterMatchesKey( $filter ) );
|
|
}
|
|
$matchCount++;
|
|
|
|
// Figure out if the filter is subject to being deleted.
|
|
$filter_age = wfTimestamp( TS_UNIX,
|
|
self::$filters[$filter]->af_timestamp );
|
|
$throttle_exempt_time = $filter_age + $wgAbuseFilterEmergencyDisableAge;
|
|
|
|
if ( $total && $throttle_exempt_time > time()
|
|
&& $matchCount > $wgAbuseFilterEmergencyDisableCount
|
|
&& ( $matchCount / $total ) > $wgAbuseFilterEmergencyDisableThreshold )
|
|
{
|
|
// More than $wgAbuseFilterEmergencyDisableCount matches,
|
|
// constituting more than $wgAbuseFilterEmergencyDisableThreshold
|
|
// (a fraction) of last few edits. Disable it.
|
|
$dbw = wfGetDB( DB_MASTER );
|
|
$dbw->update( 'abuse_filter',
|
|
array( 'af_throttled' => 1 ),
|
|
array( 'af_id' => $filter ),
|
|
__METHOD__
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
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 );
|
|
}
|
|
|
|
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() ) {
|
|
print 'Trying to create account -- user id is ' . $user->getId();
|
|
$user->addToDatabase();
|
|
$user->saveSettings();
|
|
# Increment site_stats.ss_users
|
|
$ssu = new SiteStatsUpdate( 0, 0, 0, 0, 1 );
|
|
$ssu->doUpdate();
|
|
} 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;
|
|
}
|
|
|
|
static function buildEditBox( $rules, $textName = 'wpFilterRules', $addResultDiv = true,
|
|
$canEdit = true ) {
|
|
global $wgOut;
|
|
|
|
$readOnlyAttrib = array();
|
|
if ( !$canEdit )
|
|
$readOnlyAttrib['readonly'] = 'readonly';
|
|
|
|
global $wgUser;
|
|
$noTestAttrib = array();
|
|
if ( !$wgUser->isAllowed( 'abusefilter-modify' ) ) {
|
|
$noTestAttrib['disabled'] = 'disabled';
|
|
$addResultDiv = false;
|
|
}
|
|
|
|
$rules = rtrim( $rules ) . "\n";
|
|
$rules = Xml::textarea( $textName, $rules, 40, 5, $readOnlyAttrib );
|
|
|
|
$dropDown = self::getBuilderValues();
|
|
// 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";
|
|
}
|
|
|
|
$rules .=
|
|
Xml::tags(
|
|
'select',
|
|
array( 'id' => 'wpFilterBuilder', 'onchange' => 'addText();' ),
|
|
$builder
|
|
) . ' ';
|
|
|
|
// Add syntax checking
|
|
$rules .= Xml::element( 'input',
|
|
array(
|
|
'type' => 'button',
|
|
'onclick' => 'doSyntaxCheck()',
|
|
'value' => wfMsg( 'abusefilter-edit-check' ),
|
|
'id' => 'mw-abusefilter-syntaxcheck'
|
|
) + $noTestAttrib );
|
|
|
|
if ( $addResultDiv )
|
|
$rules .= Xml::element( 'div',
|
|
array( 'id' => 'mw-abusefilter-syntaxresult', 'style' => 'display: none;' ),
|
|
' ' );
|
|
|
|
// Add script
|
|
$editScript = file_get_contents( dirname( __FILE__ ) . '/edit.js' );
|
|
$editScript = "var wgFilterBoxName = " . Xml::encodeJSVar( $textName ) . ";\n$editScript";
|
|
|
|
// Import localisation.
|
|
$importMessages = array( 'abusefilter-edit-syntaxok', 'abusefilter-edit-syntaxerr' );
|
|
$msgData = array();
|
|
foreach ( $importMessages as $msg ) {
|
|
$msgData[$msg] = wfMsg( $msg );
|
|
}
|
|
$editScript .= "\nvar wgAbuseFilterMessages = " . json_encode( $msgData ) . ";\n";
|
|
|
|
$wgOut->addInlineScript( $editScript );
|
|
|
|
return $rules;
|
|
}
|
|
|
|
/**
|
|
* 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',
|
|
'af_global',
|
|
);
|
|
$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 );
|
|
}
|
|
|
|
static function getActionDisplay( $action ) {
|
|
$display = wfMsg( "abusefilter-action-$action" );
|
|
$display = wfEmptyMsg( "abusefilter-action-$action", $display ) ? $action : $display;
|
|
return $display;
|
|
}
|
|
|
|
public static function getVarsFromRCRow( $row ) {
|
|
if ( $row->rc_this_oldid ) {
|
|
// It's an edit.
|
|
$vars = self::getEditVarsFromRCRow( $row );
|
|
} elseif ( $row->rc_log_type == 'move' ) {
|
|
$vars = self::getMoveVarsFromRCRow( $row );
|
|
} elseif ( $row->rc_log_type == 'newusers' ) {
|
|
$vars = self::getCreateVarsFromRCRow( $row );
|
|
} else {
|
|
return null;
|
|
}
|
|
if ( $vars ) {
|
|
$vars->setVar( 'context', 'generated' );
|
|
$vars->setVar( 'timestamp', wfTimestamp( TS_UNIX, $row->rc_timestamp ) );
|
|
}
|
|
|
|
return $vars;
|
|
}
|
|
|
|
public static function getCreateVarsFromRCRow( $row ) {
|
|
$vars = new AbuseFilterVariableHolder;
|
|
|
|
$vars->setVar( 'ACTION', 'createaccount' );
|
|
|
|
$name = Title::makeTitle( $row->rc_namespace, $row->rc_title )->getText();
|
|
$vars->setVar( 'user_name', $name );
|
|
$vars->setVar( 'accountname', $name );
|
|
return $vars;
|
|
}
|
|
|
|
public static function getEditVarsFromRCRow( $row ) {
|
|
$vars = new AbuseFilterVariableHolder;
|
|
$title = Title::makeTitle( $row->rc_namespace, $row->rc_title );
|
|
|
|
if ( $row->rc_user )
|
|
$user = User::newFromName( $row->rc_user_text );
|
|
else {
|
|
$user = new User;
|
|
$user->setName( $row->rc_user_text );
|
|
}
|
|
|
|
$vars->addHolder( self::generateUserVars( $user ) );
|
|
|
|
$vars->addHolder( self::generateTitleVars( $title, 'ARTICLE' ) );
|
|
$vars->setVar( 'ACTION', 'edit' );
|
|
$vars->setVar( 'SUMMARY', $row->rc_comment );
|
|
$vars->setVar( 'minor_edit', $row->rc_minor );
|
|
|
|
$vars->setLazyLoadVar( 'new_wikitext', 'revision-text-by-id',
|
|
array( 'revid' => $row->rc_this_oldid ) );
|
|
|
|
if ( $row->rc_last_oldid ) {
|
|
$vars->setLazyLoadVar( 'old_wikitext', 'revision-text-by-id',
|
|
array( 'revid' => $row->rc_last_oldid ) );
|
|
} else {
|
|
$vars->setVar( 'old_wikitext', '' );
|
|
}
|
|
|
|
$vars->addHolder( self::getEditVars(
|
|
$title, $row->rc_this_oldid, $row->rc_last_oldid ) );
|
|
|
|
return $vars;
|
|
}
|
|
|
|
public static function getMoveVarsFromRCRow( $row ) {
|
|
$vars = new AbuseFilterVariableHolder;
|
|
|
|
if ( $row->rc_user )
|
|
$user = User::newFromId( $row->rc_user );
|
|
else {
|
|
$user = new User;
|
|
$user->setName( $row->rc_user_text );
|
|
}
|
|
|
|
$params = explode( "\n", trim( $row->rc_params ) );
|
|
|
|
$oldTitle = Title::makeTitle( $row->rc_namespace, $row->rc_title );
|
|
$newTitle = Title::newFromText( $params[0] );
|
|
|
|
$vars = AbuseFilterVariableHolder::merge(
|
|
$vars,
|
|
AbuseFilter::generateUserVars( $user ),
|
|
AbuseFilter::generateTitleVars( $oldTitle, 'MOVED_FROM' ),
|
|
AbuseFilter::generateTitleVars( $newTitle, 'MOVED_TO' )
|
|
);
|
|
|
|
$vars->setVar( 'SUMMARY', $row->rc_comment );
|
|
$vars->setVar( 'ACTION', 'move' );
|
|
|
|
return $vars;
|
|
}
|
|
|
|
public static function getEditVars( $title ) {
|
|
$vars = new AbuseFilterVariableHolder;
|
|
$article = new Article( $title );
|
|
|
|
$vars->setLazyLoadVar( 'edit_diff', 'diff',
|
|
array( 'oldtext-var' => 'old_wikitext', 'newtext-var' => 'new_wikitext' ) );
|
|
$vars->setLazyLoadVar( 'new_size', 'length', array( 'length-var' => 'new_wikitext' ) );
|
|
$vars->setLazyLoadVar( 'old_size', 'length', array( 'length-var' => 'old_wikitext' ) );
|
|
$vars->setLazyLoadVar( 'edit_delta', 'subtract',
|
|
array( 'val1-var' => 'new_size', 'val2-var' => 'old_size' ) );
|
|
|
|
// Some more specific/useful details about the changes.
|
|
$vars->setLazyLoadVar( 'added_lines', 'diff-split',
|
|
array( 'diff-var' => 'edit_diff', 'line-prefix' => '+' ) );
|
|
$vars->setLazyLoadVar( 'removed_lines', 'diff-split',
|
|
array( 'diff-var' => 'edit_diff', 'line-prefix' => '-' ) );
|
|
|
|
// Links
|
|
$vars->setLazyLoadVar( 'all_links', 'links-from-wikitext',
|
|
array(
|
|
'namespace' => $title->getNamespace(),
|
|
'title' => $title->getText(),
|
|
'text-var' => 'new_wikitext'
|
|
) );
|
|
$vars->setLazyLoadVar( 'old_links', 'links-from-wikitext-or-database',
|
|
array(
|
|
'namespace' => $title->getNamespace(),
|
|
'title' => $title->getText(),
|
|
'text-var' => 'old_wikitext'
|
|
) );
|
|
$vars->setLazyLoadVar( 'added_links', 'link-diff-added',
|
|
array( 'oldlink-var' => 'old_links', 'newlink-var' => 'all_links' ) );
|
|
$vars->setLazyLoadVar( 'removed_links', 'link-diff-removed',
|
|
array( 'oldlink-var' => 'old_links', 'newlink-var' => 'all_links' ) );
|
|
|
|
$vars->setLazyLoadVar( 'new_html', 'parse-wikitext',
|
|
array(
|
|
'namespace' => $title->getNamespace(),
|
|
'title' => $title->getText(),
|
|
'wikitext-var' => 'new_wikitext'
|
|
) );
|
|
$vars->setLazyLoadVar( 'new_text', 'strip-html',
|
|
array( 'html-var' => 'new_html' ) );
|
|
$vars->setLazyLoadVar( 'old_html', 'parse-wikitext-nonedit',
|
|
array(
|
|
'namespace' => $title->getNamespace(),
|
|
'title' => $title->getText(),
|
|
'wikitext-var' => 'old_wikitext'
|
|
) );
|
|
$vars->setLazyLoadVar( 'old_text', 'strip-html',
|
|
array( 'html-var' => 'old_html' ) );
|
|
|
|
return $vars;
|
|
}
|
|
|
|
public static function buildVarDumpTable( $vars ) {
|
|
// Export all values
|
|
if ( $vars instanceof AbuseFilterVariableHolder )
|
|
$vars = $vars->exportAllVars();
|
|
|
|
$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::getBuilderValues();
|
|
$variableMessageMappings = $variableMessageMappings['vars'];
|
|
|
|
$output .=
|
|
Xml::openElement( 'table', array( 'class' => 'mw-abuselog-details' ) ) .
|
|
Xml::openElement( 'tbody' ) .
|
|
"\n";
|
|
|
|
$header =
|
|
Xml::element( 'th', null, wfMsg( 'abusefilter-log-details-var' ) ) .
|
|
Xml::element( 'th', null, wfMsg( 'abusefilter-log-details-val' ) );
|
|
$output .= Xml::tags( 'tr', null, $header ) . "\n";
|
|
|
|
// Now, build the body of the table.
|
|
foreach ( $vars as $key => $value ) {
|
|
$key = strtolower( $key );
|
|
|
|
if ( !empty( $variableMessageMappings[$key] ) ) {
|
|
$mapping = $variableMessageMappings[$key];
|
|
$keyDisplay = wfMsgExt( "abusefilter-edit-builder-vars-$mapping", 'parseinline' ) .
|
|
' ' . Xml::element( 'tt', null, wfMsg( 'parentheses', $key ) );
|
|
} else {
|
|
$keyDisplay = Xml::element( 'tt', null, $key );
|
|
}
|
|
|
|
if ( is_null( $value ) )
|
|
$value = '';
|
|
$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 );
|
|
$output .=
|
|
Xml::tags( 'tr',
|
|
array( 'class' => "mw-abuselog-details-$key mw-abuselog-value" ), $trow
|
|
) . "\n";
|
|
}
|
|
|
|
$output .= Xml::closeElement( 'tbody' ) . Xml::closeElement( 'table' );
|
|
return $output;
|
|
}
|
|
|
|
static function modifyActionText( $page, $type, $title, $sk, $args ) {
|
|
list( $history_id, $filter_id ) = $args;
|
|
|
|
$filter_link = $sk ? $sk->link( $title ) : $title->getFullURL();
|
|
|
|
$details_title = SpecialPage::getTitleFor( 'AbuseFilter', "history/$filter_id/diff/prev/$history_id" );
|
|
$details_text = wfMsgExt( 'abusefilter-log-detailslink', 'parseinline' );
|
|
$details_link =
|
|
$sk ? $sk->link( $details_title, $details_text ) : $details_title->getFullURL();
|
|
|
|
return wfMsgExt( 'abusefilter-log-entry-modify',
|
|
array( 'parseinline', 'replaceafter' ), array( $filter_link, $details_link ) );
|
|
}
|
|
|
|
static function formatAction( $action, $parameters ) {
|
|
global $wgLang;
|
|
if ( count( $parameters ) == 0 ) {
|
|
$displayAction = AbuseFilter::getActionDisplay( $action );
|
|
} else {
|
|
$displayAction = AbuseFilter::getActionDisplay( $action ) .
|
|
wfMsgExt( 'colon-separator', 'escapenoentities' ) .
|
|
$wgLang->semicolonList( $parameters );
|
|
}
|
|
return $displayAction;
|
|
}
|
|
|
|
static function formatFlags( $value ) {
|
|
global $wgLang;
|
|
$flags = array_filter( explode( ',', $value ) );
|
|
$flags_display = array();
|
|
foreach ( $flags as $flag ) {
|
|
$flags_display[] = wfMsg( "abusefilter-history-$flag" );
|
|
}
|
|
return $wgLang->commaList( $flags_display );
|
|
}
|
|
|
|
static function sendToUDP( $data ) {
|
|
global $wgAbuseFilterUDPPrefix, $wgAbuseFilterUDPAddress, $wgAbuseFilterUDPPort;
|
|
|
|
RecentChange::sendToUDP( $data, $wgAbuseFilterUDPAddress, $wgAbuseFilterUDPPrefix,
|
|
$wgAbuseFilterUDPPort );
|
|
}
|
|
|
|
static function getGlobalFilterDescription( $filterID ) {
|
|
global $wgAbuseFilterCentralDB;
|
|
|
|
if ( !$wgAbuseFilterCentralDB ) return;
|
|
|
|
$fdb = wfGetDB( DB_SLAVE, array(), $wgAbuseFilterCentralDB );
|
|
|
|
return $fdb->selectField(
|
|
'abuse_filter',
|
|
'af_public_comments',
|
|
array( 'af_id' => $filterID ),
|
|
__METHOD__
|
|
);
|
|
}
|
|
}
|