'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', 'like' => 'like', '""' => 'stringlit', 'rlike' => 'rlike' ), 'funcs' => array( 'length(string)' => 'length', 'lcase(string)' => 'lcase', 'ccnorm(string)' => 'ccnorm', 'rmdoubles(string)' => 'rmdoubles', 'specialratio(string)' => 'specialratio', 'norm(string)' => 'norm', 'count(needle,haystack)' => 'count' ), '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',), ); 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; } public static function ajaxCheckSyntax( $filter ) { wfLoadExtensionMessages( 'AbuseFilter' ); $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 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'; } 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 = array(); if (!$title) return array(); $vars[$prefix."_ARTICLEID"] = $title->getArticleId(); $vars[$prefix."_NAMESPACE"] = $title->getNamespace(); $vars[$prefix."_TEXT"] = $title->getText(); $vars[$prefix."_PREFIXEDTEXT"] = $title->getPrefixedText(); // Use restrictions. 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 ); } } // 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]; } $vars[$prefix."_RECENT_CONTRIBUTORS"] = implode(',', $users); 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 ''; } $parser = new $wgAbuseFilterParserClass; $parser->setVars( $vars ); return $parser->evaluateExpression( $expr ); } 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 ); } public static function ajaxEvaluateExpression( $expr ) { return self::evaluateExpression( $expr ); } public static function checkConditions( $conds, $vars, $ignoreError = true ) { global $wgAbuseFilterParserClass; wfProfileIn( __METHOD__ ); try { $parser = new $wgAbuseFilterParserClass; $parser->setVars( $vars ); $result = $parser->parse( $conds, self::$condCount ); } catch (Exception $excep) { // Sigh. $result = false; 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. $dbr = wfGetDB( DB_SLAVE ); $res = $dbr->select( 'abuse_filter', '*', array( 'af_enabled' => 1, 'af_deleted' => 0 ) ); $filter_matched = array(); while ( $row = $dbr->fetchObject( $res ) ) { // Store the row somewhere convenient self::$filters[$row->af_id] = $row; // Check conditions... $pattern = trim($row->af_pattern); if ( self::checkConditions( $pattern, $vars ) ) { // Record match. $filter_matched[$row->af_id] = true; } else { // Record non-match. $filter_matched[$row->af_id] = false; } } // Update statistics, and disable filters which are over-blocking. self::recordStats( $filter_matched ); 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 ); // Retrieve the consequences. $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') ) ); $actionsByFilter = array_fill_keys( $filters, array() ); $actionsTaken = array_fill_keys( $filters, array() ); // 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 } else { $actionsByFilter[$row->afa_filter][$row->afa_consequence] = array( 'action' => $row->afa_consequence, 'parameters' => explode( "\n", $row->afa_parameters ) ); } } 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 $msg = ( !empty($parameters[0]) && strlen($parameters[0]) ) ? $parameters[0] : 'abusefilter-warning'; $messages[] = wfMsgExt( $msg, 'parseinline', array( $parsed_public_comments) ) . "
\n"; $actionsTaken[$filter][] = 'warn'; continue; // Don't do anything else. } else { // We already warned them $_SESSION[$warnKey] = false; } unset( $actions['warn'] ); } if ( count($actions)>1 && !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; } } 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'] ); $error_msg = $error_msg == '' ? true : $error_msg; return $error_msg; } public static function addLogEntries( $actions_taken, $log_template, $action ) { $dbw = wfGetDB( DB_MASTER ); $log_rows = array(); foreach( $actions_taken as $filter => $actions ) { $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 (!count($log_rows)) { return; } $dbw->insert( 'abuse_filter_log', $log_rows, __METHOD__ ); } 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) ) ."
\n"; } break; case 'block': global $wgUser; $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 = 'infinity'; $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, $wgUser->getName() ), wfMsgForContent( 'abusefilter-blockreason', $rule_desc ), $logParams, self::getFilterUser() ); $display .= wfMsgExt( 'abusefilter-blocked-display', 'parseinline', array($rule_desc) ) ."
\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 ) ."
\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) ) ."
\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) ) ."
\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['ACTION'] ) ); AbuseFilter::$tagsToSet[$actionID] = $parameters; break; default: throw new MWException( "Unrecognised action $action" ); } return $display; } 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) { 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() ); } public static function recordStats( $filters ) { global $wgAbuseFilterConditionLimit,$wgMemc; $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 > 1000) { // 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 ); } if (!$filter_triggered) { return; // The rest will only apply if a filter was triggered. } self::checkEmergencyDisable( $filters, $total ); // 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__ ); } public static function checkEmergencyDisable( $filters, $total ) { global $wgAbuseFilterEmergencyDisableThreshold, $wgAbuseFilterEmergencyDisableCount, $wgAbuseFilterEmergencyDisableAge, $wgMemc; foreach( $filters as $filter => $matched ) { if ($matched) { // 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 ) { global $wgOut; $rules = Xml::textarea( $textName, ( isset( $rules ) ? $rules."\n" : "\n" ) ); $dropDown = self::$builderValues; // 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' ) ); 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"; $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' ); $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. 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 ) ) ); $vars = array_merge( $vars, self::generateTitleVars( $title, 'ARTICLE' ) ); $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... $editInfo = $article->prepareTextForEdit( $new_text, $revid ); $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. $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*$/si', '', $newHTML ); $vars['NEW_HTML'] = $newHTML; $newText = $vars['NEW_TEXT'] = preg_replace( '/<[^>]+>/', '', $newHTML ); return $vars; } 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']; $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 ) { 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 ); } 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; } }