r36691 MediaWiki - Code Review archive

Repository:MediaWiki
Revision:r36690‎ | r36691 | r36692 >
Date:06:18, 27 June 2008
Author:werdna
Status:old
Tags:
Comment:
Add AbuseFilter extension
Modified paths:
  • /trunk/extensions/AbuseFilter (added) (history)
  • /trunk/extensions/AbuseFilter/AbuseFilter.class.php (added) (history)
  • /trunk/extensions/AbuseFilter/AbuseFilter.hooks.php (added) (history)
  • /trunk/extensions/AbuseFilter/AbuseFilter.i18n.php (added) (history)
  • /trunk/extensions/AbuseFilter/AbuseFilter.php (added) (history)
  • /trunk/extensions/AbuseFilter/SpecialAbuseFilter.php (added) (history)
  • /trunk/extensions/AbuseFilter/SpecialAbuseLog.php (added) (history)
  • /trunk/extensions/AbuseFilter/abusefilter.tables.sql (added) (history)
  • /trunk/extensions/AbuseFilter/testTitles.php (added) (history)

Diff [purge]

Index: trunk/extensions/AbuseFilter/AbuseFilter.class.php
@@ -0,0 +1,496 @@
 2+<?php
 3+if ( ! defined( 'MEDIAWIKI' ) )
 4+ die();
 5+
 6+class AbuseFilter {
 7+
 8+ public static function generateUserVars( $user ) {
 9+ $vars = array();
 10+
 11+ // Load all the data we want.
 12+ $user->load();
 13+
 14+ $vars['USER_EDITCOUNT'] = $user->getEditCount();
 15+ $vars['USER_AGE'] = time() - wfTimestampOrNull( TS_UNIX, $user->getRegistration() );
 16+ $vars['USER_NAME'] = $user->getName();
 17+ $vars['USER_GROUPS'] = implode(',', $user->getEffectiveGroups() );
 18+ $vars['USER_EMAILCONFIRM'] = $user->getEmailAuthenticationTimestamp();
 19+
 20+ // More to come
 21+
 22+ return $vars;
 23+ }
 24+
 25+ public static function generateTitleVars( $title, $prefix ) {
 26+ $vars = array();
 27+
 28+ $vars[$prefix."_NAMESPACE"] = $title->getNamespace();
 29+ $vars[$prefix."_TEXT"] = $title->getText();
 30+ $vars[$prefix."_PREFIXEDTEXT"] = $title->getPrefixedText();
 31+
 32+ if ($title->mRestrictionsLoaded) {
 33+ // Don't bother if they're unloaded
 34+ foreach( $title->mRestrictions as $action => $rights ) {
 35+ $rights = count($rights) ? $rights : array();
 36+ $vars[$prefix."_RESTRICTIONS_".$action] = implode(',', $rights );
 37+ }
 38+ }
 39+
 40+ return $vars;
 41+ }
 42+
 43+ public static function checkConditions( $conds, $vars ) {
 44+ $modifierWords = array( 'norm', 'supernorm', 'lcase', 'length' );
 45+ $operatorWords = array( 'eq', 'neq', 'gt', 'lt', 'regex' );
 46+ $validJoinConditions = array( '!', '|', '&' );
 47+
 48+ // Remove leading/trailing spaces
 49+ $conds = trim($conds);
 50+
 51+ // Is it a set?
 52+ if (substr( $conds, 0, 1 ) == '(' && substr( $conds, -1, 1 ) == ')' ) {
 53+ // We should have a set here.
 54+ $setInternal = substr($conds,1,-1);
 55+
 56+ // Get the join condition ( &, | or ! )
 57+ list($setJoinCondition,$conditionList) = explode(':', $setInternal, 2 );
 58+ $setJoinCondition = trim($setJoinCondition);
 59+
 60+ if (!in_array( $setJoinCondition, $validJoinConditions )) {
 61+ // Bad join condition
 62+ return false;
 63+ }
 64+
 65+ // Tokenise.
 66+ $allConditions = self::tokeniseList( $conditionList );
 67+
 68+ foreach( $allConditions as $thisCond ) {
 69+
 70+ if (trim($thisCond)=='') {
 71+ // Ignore it
 72+ $result = true;
 73+ } else {
 74+ $result = self::checkConditions( $thisCond, $vars );
 75+ }
 76+
 77+ // Need we short-circuit?
 78+ if ($setJoinCondition == '|' && $result) {
 79+ // Definite yes.
 80+ return true;
 81+ } elseif ($setJoinCondition == '&' && !$result) {
 82+ // Definite no.
 83+ return false;
 84+ } elseif ($setJoinCondition == '!' && $result) {
 85+ // Definite no.
 86+ return false;
 87+ }
 88+ }
 89+
 90+ // Return the default result.
 91+ return ($setJoinCondition != '|'); // Only OR returns false after checking all conditions.
 92+ }
 93+
 94+ // Grab the first word.
 95+ list ($thisWord) = explode( ' ', $conds );
 96+ $wordNum = 0;
 97+
 98+ // Check for modifiers
 99+ $modifier = '';
 100+ if (in_array( $thisWord, $modifierWords ) ) {
 101+ $modifier = $thisWord;
 102+ $wordNum++;
 103+ $thisWord = explode( ' ', $conds );
 104+ $thisWord = $thisWord[$wordNum];
 105+ }
 106+
 107+ if ( in_array( $thisWord, array_keys($vars ) ) ) {
 108+
 109+ $value = $vars[$thisWord];
 110+ if ($modifier) {
 111+ $value = self::modifyValue( $modifier, $value );
 112+ }
 113+
 114+ // We have a variable. Now read the next word to see what we're doing with it.
 115+ $wordNum++;
 116+ $thisWord = explode( ' ', $conds );
 117+ $thisWord = $thisWord[$wordNum];
 118+
 119+ if ( in_array( $thisWord, $operatorWords ) ) {
 120+ // Get the rest of the string after the operator.
 121+ $parameters = explode( ' ', $conds, $wordNum+2);
 122+ $parameters = $parameters[$wordNum+1];
 123+
 124+ return self::checkOperator( $thisWord, $value, $parameters );
 125+ }
 126+ } else {
 127+ print "Word $thisWord is not a variable (".implode(',', array_keys($vars)).")\n";
 128+ }
 129+ }
 130+
 131+ public static function tokeniseList( $list ) {
 132+ // Parse it, character by character.
 133+ $escapeNext = false;
 134+ $listLevel = 0;
 135+ $thisToken = '';
 136+ $allTokens = array();
 137+ for( $i=0;$i<strlen($list);$i++ ) {
 138+ $char = substr( $list, $i, 1 );
 139+
 140+ $suppressAdd = false;
 141+
 142+ // We don't care about semicolons and so on unless it's
 143+ if ($listLevel == 0) {
 144+ if ($char == "\\") {
 145+ if ($escapeNext) { // Escaped backslash
 146+ $escapeNext = false;
 147+ } else {
 148+ $escapeNext = true;
 149+ $suppressAdd = true;
 150+ }
 151+ } elseif ($char == ';') {
 152+ if ($escapeNext) {
 153+ $escapeNext = false; // Escaped semicolon
 154+ } else { // Next token, plz
 155+ $escapeNext = false;
 156+ $allTokens[] = $thisToken;
 157+ $thisToken = '';
 158+ $suppressAdd = true;
 159+ }
 160+ } elseif ($escapeNext) {
 161+ $escapeNext = false;
 162+ $thisToken .= "\\"; // The backslash wasn't intended to escape.
 163+ }
 164+ }
 165+
 166+ if ($char == '(' && $lastChar == ';') {
 167+ // A list!
 168+ $listLevel++;
 169+ } elseif ($char == ')' && ($lastChar == ';' || $lastChar == ')' || $lastChar = '') ) {
 170+ $listLevel--; // End of a list.
 171+ }
 172+
 173+ if (!$suppressAdd) {
 174+ $thisToken .= $char;
 175+ }
 176+
 177+ // Ignore whitespace.
 178+ if ($char != ' ') {
 179+ $lastChar = $char;
 180+ }
 181+ }
 182+
 183+ // Put any leftovers in
 184+ $allTokens[] = $thisToken;
 185+
 186+ return $allTokens;
 187+ }
 188+
 189+ public static function modifyValue( $modifier, $value ) {
 190+ if ($modifier == 'norm') {
 191+ return self::normalise( $value );
 192+ } elseif ($modifier == 'supernorm') {
 193+ return self::superNormalise( $value );
 194+ } elseif ($modifier == 'lcase') {
 195+ return strtolower($value);
 196+ } elseif ($modifier == 'length') {
 197+ return strlen($value);
 198+ } elseif ($modifier == 'specialratio') {
 199+ $specialsonly = preg_replace('/\w/', '', $value );
 200+ return (strlen($specialsonly) / strlen($value));
 201+ }
 202+ }
 203+
 204+ public static function checkOperator( $operator, $value, $parameters ) {
 205+ if ($operator == 'eq') {
 206+ return $value == $parameters;
 207+ } elseif ($operator == 'neq') {
 208+ return $value != $parameters;
 209+ } elseif ($operator == 'gt') {
 210+ return $value > $parameters;
 211+ } elseif ($operator == 'lt') {
 212+ return $value < $parameters;
 213+ } elseif ($operator == 'regex') {
 214+ return preg_match( $parameters, $value );
 215+ } else {
 216+ return false;
 217+ }
 218+ }
 219+
 220+ public static function superNormalise( $text ) {
 221+ $text = self::normalise( $text );
 222+ $text = preg_split( '//', $text, -1, PREG_SPLIT_NO_EMPTY ); // Split to a char array.
 223+ sort($text);
 224+ $text = array_unique( $text ); // Remove duplicate characters.
 225+ $text = implode( '', $text );
 226+
 227+ return $text;
 228+ }
 229+
 230+ public static function normalise( $text ) {
 231+ $old_text = $text;
 232+ $text = preg_replace( '/\W/', '', $text ); // Remove any special characters.
 233+ $text = strtolower($text);
 234+ $text = preg_split( '//', $text, -1, PREG_SPLIT_NO_EMPTY ); // Split to a char array.
 235+ $text = AntiSpoof::equivString( $text ); // Normalise
 236+
 237+ // Remove repeated characters, but not all duplicates.
 238+ $oldText = $text;
 239+ $text = array($oldText[0]);
 240+ for ($i=1;$i<count($oldText);$i++) {
 241+ if ($oldText[$i] != $oldText[$i-1]) {
 242+ $text[] = $oldText[$i];
 243+ }
 244+ }
 245+
 246+ $text = implode('', $text ); // Sort in alphabetical order, put back as it was.
 247+
 248+ return $text;
 249+ }
 250+
 251+ public static function filterAction( $vars, $title ) {
 252+ global $wgUser;
 253+
 254+ // Fetch from the database.
 255+ $dbr = wfGetDB( DB_SLAVE );
 256+ $res = $dbr->select( 'abuse_filter', '*', array( ) );
 257+
 258+ $blocking_filters = array();
 259+ $log_entries = array();
 260+ $log_template = array( 'afl_user' => $wgUser->getId(), 'afl_user_text' => $wgUser->getName(),
 261+ 'afl_var_dump' => serialize( $vars ), 'afl_timestamp' => $dbr->timestamp(wfTimestampNow()),
 262+ 'afl_namespace' => $title->getNamespace(), 'afl_title' => $title->getDbKey(), 'afl_ip' => wfGetIp() );
 263+ $doneActionsByFilter = array();
 264+
 265+ while ( $row = $dbr->fetchObject( $res ) ) {
 266+ if ( self::checkConditions( $row->af_pattern, $vars ) ) {
 267+ $blocking_filters[$row->af_id] = $row;
 268+
 269+ $newLog = $log_template;
 270+ $newLog['afl_filter'] = $row->af_id;
 271+ $newLog['afl_action'] = $vars['ACTION'];
 272+ $log_entries[] = $newLog;
 273+
 274+ $doneActionsByFilter[$row->af_id] = array();
 275+ }
 276+ }
 277+
 278+ if (count($blocking_filters) == 0 ) {
 279+ // No problems.
 280+ return true;
 281+ }
 282+
 283+ // Retrieve the consequences.
 284+ $res = $dbr->select( 'abuse_filter_action', '*', array( 'afa_filter' => array_keys( $blocking_filters ) ), __METHOD__, array( "ORDER BY" => " (afa_consequence in ('throttle','warn')) desc" ) );
 285+ // We want throttles, warnings first, as they have a bit of a special treatment.
 286+
 287+ $actions_done = array();
 288+ $throttled_filters = array();
 289+
 290+ $display = '';
 291+
 292+ while ( $row = $dbr->fetchObject( $res ) ) {
 293+ $action_key = md5( $row->afa_consequence . $row->afa_parameters );
 294+ if (!in_array( $action_key, $actions_done ) && !in_array( $row->afa_filter, $throttled_filters ) ) {
 295+ $parameters = explode( "\n", $row->afa_parameters );
 296+ $result = self::takeConsequenceAction( $row->afa_consequence, $parameters, $title, $vars, &$display, &$continue );
 297+ $doneActionsByFilter[$row->afa_filter][] = $row->afa_consequence;
 298+ if (!$result) {
 299+ $throttled_filters[] = $row->afa_filter; // Only execute other actions for a filter if that filter's rate limiter has been tripped.
 300+ }
 301+ $actions_done[] = $action_key;
 302+ } else {
 303+ // Ignore it, until we hit the rate limit.
 304+ }
 305+ }
 306+
 307+ $dbw = wfGetDB( DB_MASTER );
 308+
 309+ // Log it
 310+ foreach( $log_entries as $index => $entry ) {
 311+ $log_entries[$index]['afl_actions'] = implode( ',', $doneActionsByFilter[$entry['afl_filter']] );
 312+
 313+ // Increment the hit counter
 314+ $dbw->update( 'abuse_filter', array( 'af_hit_count=af_hit_count+1' ), array( 'af_id' => $entry['afl_filter'] ), __METHOD__ );
 315+ }
 316+
 317+ $dbw->insert( 'abuse_filter_log', $log_entries, __METHOD__ );
 318+
 319+ return $display;
 320+ }
 321+
 322+ public static function takeConsequenceAction( $action, $parameters, $title, $vars, &$display, &$continue ) {
 323+ switch ($action) {
 324+ case 'warn':
 325+ wfLoadExtensionMessages( 'AbuseFilter' );
 326+
 327+ if (!$_SESSION['abusefilter-warned']) {
 328+ $_SESSION['abusefilter-warned'] = true;
 329+
 330+ // Threaten them a little bit
 331+ if (strlen($parameters[0])) {
 332+ $display .= call_user_func_array( 'wfMsg', $parameters ) . "\n";
 333+ } else {
 334+ // Generic message.
 335+ $display .= wfMsg( 'abusefilter-warning' );
 336+ }
 337+
 338+ return false; // Don't apply the other stuff yet.
 339+ } else {
 340+ // We already warned them
 341+ $_SESSION['abusefilter-warned'] = false;
 342+ }
 343+ break;
 344+
 345+ case 'disallow':
 346+ wfLoadExtensionMessages( 'AbuseFilter' );
 347+
 348+ // Don't let them do it
 349+ if (strlen($parameters[0])) {
 350+ $display .= call_user_func_array( 'wfMsg', $parameters ) . "\n";
 351+ } else {
 352+ // Generic message.
 353+ $display .= wfMsg( 'abusefilter-disallowed' );
 354+ }
 355+ break;
 356+
 357+ case 'block':
 358+ wfLoadExtensionMessages( 'AbuseFilter' );
 359+
 360+ global $wgUser;
 361+
 362+ // Create a block.
 363+ $block = new Block;
 364+ $block->mAddress = $wgUser->getName();
 365+ $block->mUser = $wgUser->getId();
 366+ $block->mByName = wfMsgForContent( 'abusefilter-blocker' );
 367+ $block->mReason = wfMsgForContent( 'abusefilter-blockreason' );
 368+ $block->mTimestamp = wfTimestampNow();
 369+ $block->mEnableAutoblock = 1;
 370+ $block->mAngryAutoblock = 1; // Block lots of IPs
 371+ $block->mCreateAccount = 1;
 372+ $block->mExpiry = 'infinity';
 373+
 374+ $block->insert();
 375+
 376+ $display .= wfMsg( 'abusefilter-blocked-display' );
 377+ break;
 378+ case 'throttle':
 379+ $throttleId = array_shift( $parameters );
 380+ list( $rateCount, $ratePeriod ) = explode( ',', array_shift( $parameters ) );
 381+
 382+ $hitThrottle = false;
 383+
 384+ // The rest are throttle-types.
 385+ foreach( $parameters as $throttleType ) {
 386+ $hitThrottle = $hitThrottle || self::isThrottled( $throttleId, $throttleType, $title, $rateCount, $ratePeriod );
 387+ }
 388+
 389+ return $hitThrottle;
 390+ break;
 391+ case 'degroup':
 392+ wfLoadExtensionMessages( 'AbuseFilter' );
 393+
 394+ global $wgUser;
 395+
 396+ // Remove all groups from the user. Ouch.
 397+ $groups = $wgUser->getGroups();
 398+
 399+ foreach( $groups as $group ) {
 400+ $wgUser->removeGroup( $group );
 401+ }
 402+
 403+ $display = wfMsg( 'abusefilter-degrouped' );
 404+
 405+ break;
 406+ case 'blockautopromote':
 407+ wfLoadExtensionMessages( 'AbuseFilter' );
 408+
 409+ global $wgUser, $wgMemc;
 410+
 411+ $blockPeriod = (int)mt_rand( 3*86400, 7*86400 ); // Block for 3-7 days.
 412+ $wgMemc->set( self::autoPromoteBlockKey( $wgUser ), true, $blockPeriod );
 413+
 414+ $display = wfMsg( 'abusefilter-autopromote-blocked' );
 415+
 416+ break;
 417+
 418+ case 'flag':
 419+ // Do nothing. Here for completeness.
 420+ break;
 421+ }
 422+
 423+ return true;
 424+ }
 425+
 426+ public static function isThrottled( $throttleId, $types, $title, $rateCount, $ratePeriod ) {
 427+ global $wgMemc;
 428+
 429+ $key = self::throttleKey( $throttleId, $types, $title );
 430+ $count = $wgMemc->get( $key );
 431+
 432+ if ($count > 0) {
 433+ $wgMemc->incr( $key );
 434+ if ($count > $rateCount) {
 435+ //die( "Hit rate limiter: $count actions, against limit of $rateCount actions in $ratePeriod seconds (key is $key).\n" );
 436+ $wgMemc->delete( $key );
 437+ return true; // THROTTLED
 438+ }
 439+ } else {
 440+ $wgMemc->add( $key, 1, $ratePeriod );
 441+ }
 442+
 443+ return false; // NOT THROTTLED
 444+ }
 445+
 446+ public static function throttleIdentifier( $type, $title ) {
 447+ global $wgUser;
 448+
 449+ switch ($type) {
 450+ case 'ip':
 451+ $identifier = wfGetIp();
 452+ break;
 453+ case 'user':
 454+ $identifier = $wgUser->getId();
 455+ break;
 456+ case 'range':
 457+ $identifier = substr(IP::toHex(wfGetIp()),0,4);
 458+ break;
 459+ case 'creationdate':
 460+ $reg = $wgUser->getRegistration();
 461+ $identifier = $reg - ($reg % 86400);
 462+ break;
 463+ case 'editcount':
 464+ // Hack for detecting different single-purpose accounts.
 465+ $identifier = $wgUser->getEditCount();
 466+ break;
 467+ case 'site':
 468+ return 1;
 469+ break;
 470+ case 'page':
 471+ return $title->getPrefixedText();
 472+ break;
 473+ }
 474+
 475+ return $identifier;
 476+ }
 477+
 478+ public static function throttleKey( $throttleId, $type, $title ) {
 479+ $identifier = '';
 480+
 481+ $types = explode(',', $type);
 482+
 483+ $identifiers = array();
 484+
 485+ foreach( $types as $subtype ) {
 486+ $identifiers[] = self::throttleIdentifier( $subtype, $title );
 487+ }
 488+
 489+ $identifier = implode( ':', $identifiers );
 490+
 491+ return wfMemcKey( 'abusefilter', 'throttle', $throttleId, $type, $identifier );
 492+ }
 493+
 494+ public static function autoPromoteBlockKey( $user ) {
 495+ return wfMemcKey( 'abusefilter', 'block-autopromote', $user->getId() );
 496+ }
 497+}
Property changes on: trunk/extensions/AbuseFilter/AbuseFilter.class.php
___________________________________________________________________
Name: svn:eol-style
1498 + native
Index: trunk/extensions/AbuseFilter/SpecialAbuseLog.php
@@ -0,0 +1,220 @@
 2+<?php
 3+if ( ! defined( 'MEDIAWIKI' ) )
 4+ die();
 5+
 6+class SpecialAbuseLog extends SpecialPage {
 7+
 8+ function __construct() {
 9+ wfLoadExtensionMessages('AbuseFilter');
 10+ parent::__construct( 'AbuseLog', 'abusefilter-log' );
 11+ }
 12+
 13+ function execute( $parameter ) {
 14+ global $wgUser,$wgOut,$wgRequest;
 15+
 16+ $this->setHeaders();
 17+ $this->loadParameters();
 18+
 19+ $wgOut->setPageTitle( wfMsg( 'abusefilter-log' ) );
 20+ $wgOut->setRobotpolicy( "noindex,nofollow" );
 21+ $wgOut->setArticleRelated( false );
 22+ $wgOut->enableClientCache( false );
 23+
 24+ // Are we allowed?
 25+ if ( count( $errors = $this->getTitle()->getUserPermissionsErrors( 'abusefilter-log', $wgUser, true, array( 'ns-specialprotected' ) ) ) ) {
 26+ // Go away.
 27+ $wgOut->showPermissionsErrorPage( $errors, 'abusefilter-log' );
 28+ return;
 29+ }
 30+
 31+ // Show the search form.
 32+ $this->searchForm();
 33+
 34+ // Show the log itself.
 35+ $this->showList();
 36+ }
 37+
 38+ function loadParameters() {
 39+ global $wgRequest;
 40+
 41+ $this->mSearchUser = $wgRequest->getText( 'wpSearchUser' );
 42+ $this->mSearchTitle = $wgRequest->getText( 'wpSearchTitle' );
 43+ if ($this->canSeeDetails())
 44+ $this->mSearchFilter = $wgRequest->getIntOrNull( 'wpSearchFilter' );
 45+
 46+ $detailsid = $wgRequest->getIntOrNull( 'details' );
 47+
 48+ if ($detailsid) {
 49+ $this->showDetails( $detailsid );
 50+ }
 51+ }
 52+
 53+ function searchForm() {
 54+ global $wgOut, $wgUser;
 55+
 56+ $output = Xml::element( 'legend', null, wfMsg( 'abusefilter-log-search' ) );
 57+ $fields = array();
 58+
 59+ // Search conditions
 60+ $fields['abusefilter-log-search-user'] = wfInput( 'wpSearchUser', 45, $this->mSearchUser );
 61+ if ($this->canSeeDetails())
 62+ $fields['abusefilter-log-search-filter'] = wfInput( 'wpSearchFilter', 45, $this->mSearchFilter );
 63+ $fields['abusefilter-log-search-title'] = wfInput( 'wpSearchTitle', 45, $this->mSearchTitle );
 64+
 65+ $form = Xml::hidden( 'title', $this->getTitle()->getPrefixedText() );
 66+
 67+ $form .= Xml::buildForm( $fields, 'abusefilter-log-search-submit' );
 68+ $output .= Xml::tags( 'form', array( 'method' => 'GET', 'action' => $this->getTitle()->getLocalURL() ), $form );
 69+ $output = Xml::tags( 'fieldset', null, $output );
 70+
 71+ $wgOut->addHtml( $output );
 72+ }
 73+
 74+ function showList() {
 75+ global $wgOut;
 76+
 77+ // Generate conditions list.
 78+ $conds = array();
 79+
 80+ if ($this->mSearchUser)
 81+ $conds['afl_user_text'] = $this->mSearchUser;
 82+ if ($this->mSearchFilter)
 83+ $conds['afl_filter'] = $this->mSearchFilter;
 84+
 85+ $searchTitle = Title::newFromText( $this->mSearchTitle );
 86+ if ($this->mSearchTitle && $searchTitle) {
 87+ $conds['afl_namespace'] = $searchTitle->getNamespace();
 88+ $conds['afl_title'] = $searchTitle->getDbKey();
 89+ }
 90+
 91+ $pager = new AbuseLogPager( $this, $conds );
 92+
 93+ $wgOut->addHtml( $pager->getNavigationBar() .
 94+ Xml::tags( 'ul', null, $pager->getBody() ) .
 95+ $pager->getNavigationBar() );
 96+ }
 97+
 98+ function showDetails( $id ) {
 99+ if (!$this->canSeeDetails()) {
 100+ return;
 101+ }
 102+
 103+ $dbr = wfGetDB( DB_SLAVE );
 104+
 105+ $row = $dbr->selectRow( array('abuse_filter_log','abuse_filter'), '*', array( 'afl_id' => $id, 'af_id=afl_filter' ), __METHOD__ );
 106+
 107+ if (!$row)
 108+ return;
 109+
 110+ $output = '';
 111+
 112+ $output .= Xml::element( 'legend', null, wfMsg( 'abusefilter-log-details-legend', $id ) );
 113+
 114+ $output .= Xml::tags( 'p', null, $this->formatRow( $row ) );
 115+
 116+ $output .= Xml::element( 'h3', null, wfMsg( 'abusefilter-log-details-vars' ) );
 117+
 118+ // Build a table.
 119+ $vars = unserialize( $row->afl_var_dump );
 120+
 121+ $output .= Xml::openElement( 'table', array( 'class' => 'wikitable' ) ) . Xml::openElement( 'tbody' );
 122+
 123+ $header = Xml::element( 'th', null, wfMsg( 'abusefilter-log-details-var' ) ) . Xml::element( 'th', null, wfMsg( 'abusefilter-log-details-val' ) );
 124+ $output .= Xml::tags( 'tr', null, $header );
 125+
 126+ // Now, build the body of the table.
 127+
 128+ foreach( $vars as $key => $value ) {
 129+ $trow = Xml::element( 'td', null, $key ) . Xml::element( 'td', null, $value );
 130+ $output .= Xml::tags( 'tr', null, $trow );
 131+ }
 132+
 133+ $output .= Xml::closeElement( 'tbody' ) . Xml::closeElement( 'table' );
 134+
 135+ // Private stuff, like IPs.
 136+ $output .= Xml::element( 'h3', null, wfMsg( 'abusefilter-log-details-private' ) );
 137+ $output .= Xml::openElement( 'table', array( 'class' => 'wikitable' ) ) . Xml::openElement( 'tbody' );
 138+ $output .= $header;
 139+
 140+ // IP address
 141+ $output .= Xml::tags( 'tr', null, Xml::element( 'td', null, wfMsg('abusefilter-log-details-ip' ) ) . Xml::element( 'td', null, $row->afl_ip ) );
 142+
 143+ $output .= Xml::closeElement( 'tbody' ) . Xml::closeElement( 'table' );
 144+
 145+ $output = Xml::tags( 'fieldset', null, $output );
 146+
 147+ global $wgOut;
 148+ $wgOut->addHTML( $output );
 149+ }
 150+
 151+ function canSeeDetails() {
 152+ global $wgUser;
 153+ return !count($this->getTitle()->getUserPermissionsErrors( 'abusefilter-log-detail', $wgUser, true, array( 'ns-specialprotected' ) ));
 154+ }
 155+
 156+ function formatRow( $row ) {
 157+ global $wgLang,$wgUser;
 158+
 159+ ## One-time setup
 160+ static $sk=null;
 161+
 162+ if (is_null($sk)) {
 163+ $sk = $wgUser->getSkin();
 164+ }
 165+
 166+ $title = Title::makeTitle( $row->afl_namespace, $row->afl_title );
 167+
 168+ $user = $sk->userLink( $row->afl_user, $row->afl_user_text ) .
 169+ $sk->userToolLinks( $row->afl_user, $row->afl_user_text );
 170+
 171+ $description = '';
 172+
 173+ $timestamp = $wgLang->timeanddate( $row->afl_timestamp );
 174+
 175+ $actions_taken = $row->afl_actions;
 176+ if (!strlen(trim($actions_taken))) {
 177+ $actions_taken = wfMsg( 'abusefilter-log-noactions' );
 178+ }
 179+
 180+ if ($this->canSeeDetails()) {
 181+ $detailsLink = $sk->makeKnownLinkObj( $this->getTitle( ), wfMsg( 'abusefilter-log-detailslink' ), 'details='.$row->afl_id );
 182+
 183+ $description = wfMsg( 'abusefilter-log-detailedentry', $timestamp, $user, $row->afl_filter, $row->afl_action, $sk->makeKnownLinkObj( $title ), $actions_taken, $row->af_public_comments, $detailsLink );
 184+ } else {
 185+ $description = wfMsg( 'abusefilter-log-entry', $timestamp, $user, $row->afl_action, $sk->makeKnownLinkObj( $title ), $actions_taken, $row->af_public_comments );
 186+ }
 187+
 188+ return Xml::tags( 'li', null, $description );
 189+ }
 190+
 191+}
 192+
 193+class AbuseLogPager extends ReverseChronologicalPager {
 194+ public $mForm, $mConds;
 195+
 196+ function __construct( $form, $conds = array(), $details = false ) {
 197+ $this->mForm = $form;
 198+ $this->mConds = $conds;
 199+ parent::__construct();
 200+ }
 201+
 202+ function formatRow( $row ) {
 203+ return $this->mForm->formatRow( $row );
 204+ }
 205+
 206+ function getQueryInfo() {
 207+ $conds = $this->mConds;
 208+
 209+ $conds[] = 'af_id=afl_filter';
 210+
 211+ return array(
 212+ 'tables' => array('abuse_filter_log','abuse_filter'),
 213+ 'fields' => '*',
 214+ 'conds' => $conds,
 215+ );
 216+ }
 217+
 218+ function getIndexField() {
 219+ return 'afl_timestamp';
 220+ }
 221+}
\ No newline at end of file
Property changes on: trunk/extensions/AbuseFilter/SpecialAbuseLog.php
___________________________________________________________________
Name: svn:eol-style
1222 + native
Index: trunk/extensions/AbuseFilter/AbuseFilter.i18n.php
@@ -0,0 +1,94 @@
 2+<?php
 3+/**
 4+ * Internationalisation file for extension AbuseFilter.
 5+ *
 6+ * @addtogroup Extensions
 7+ */
 8+
 9+$messages = array();
 10+
 11+/** English
 12+ * @author Andrew Garrett
 13+ */
 14+$messages['en'] = array(
 15+ // Hooks
 16+ 'abusefilter-warning' => "<big>'''Warning'''</big>: This action has been automatically identified as harmful. Unconstructive edits will be quickly reverted, and egregious or repeated unconstructive editing will result in your account or computer being blocked. If you believe this edit to be constructive, you may click Submit again to confirm it.",
 17+ 'abusefilter-disallowed' => "This action has been automatically identified as harmful, and therefore disallowed. If you believe your edit was constructive, please contact an administrator, and inform them of what you were trying to do.\n",
 18+ 'abusefilter-blocked-display' => "This action has been automatically identified as harmful, and you have been prevented from executing it. In addition, to protect {{SITENAME}}, your user account and all associated IP addresses have been blocked from editing. If this has occurred in error, please contact an administrator.",
 19+ 'abusefilter-degrouped' => "This action has been automatically identified as harmful. Consequently, it has been disallowed, and, since your account is suspected of being compromised, all rights have been revoked. If you believe this to have been in error, please contact a bureaucrat with an explanation of this action, and your rights may be restored.",
 20+ 'abusefilter-autopromote-blocked' => "This action has been automatically identified as harmful, and it has been disallowed. In addition, as a security measure, some privileges routinely granted to established accounts have been temporarily revoked from your account.",
 21+ 'abusefilter-blocker' => 'Abuse Filter',
 22+ 'abusefilter-blockreason' => 'Automatically blocked for attempting to make edits identified as harmful.',
 23+
 24+ // Permissions
 25+ 'right-abusefilter-modify' => 'Modify abuse filters',
 26+ 'right-abusefilter-view' => 'View abuse filters',
 27+ 'right-abusefilter-log' => 'View the abuse log',
 28+ 'right-abusefilter-log-detail' => 'View detailed abuse log entries',
 29+ 'right-abusefilter-private' => 'View private data in the abuse log',
 30+
 31+ // Abuse Log
 32+ 'abusefilter-log' => 'Abuse Filter Log',
 33+ 'abusefilter-log-search' => 'Search the abuse log',
 34+ 'abusefilter-log-search-user' => 'User:',
 35+ 'abusefilter-log-search-filter' => 'Filter ID:',
 36+ 'abusefilter-log-search-title' => 'Title:',
 37+ 'abusefilter-log-search-submit' => 'Search',
 38+ 'abusefilter-log-entry' => '$1: $2 triggered an abuse filter, making a $3 on $4. Actions taken: $5; Filter description: $6',
 39+ 'abusefilter-log-detailedentry' => '$1: $2 triggered filter $3, making a $4 on $5. Actions taken: $6; Filter description: $7 ($8)',
 40+ 'abusefilter-log-detailslink' => 'details',
 41+ 'abusefilter-log-details-legend' => 'Details for log entry $1',
 42+ 'abusefilter-log-details-var' => 'Variable',
 43+ 'abusefilter-log-details-val' => 'Value',
 44+ 'abusefilter-log-details-vars' => 'Action parameters',
 45+ 'abusefilter-log-details-private' => 'Private data',
 46+ 'abusefilter-log-details-ip' => 'Originating IP address',
 47+ 'abusefilter-log-noactions' => 'none',
 48+
 49+ // Abuse filter management
 50+ 'abusefilter-management' => 'Abuse Filter Management',
 51+ 'abusefilter-list' => 'All filters',
 52+ 'abusefilter-list-id' => 'Filter ID',
 53+ 'abusefilter-list-status' => 'Status',
 54+ 'abusefilter-list-public' => 'Public description',
 55+ 'abusefilter-list-consequences' => 'Consequences',
 56+ 'abusefilter-list-visibility' => 'Visibility',
 57+ 'abusefilter-list-hitcount' => 'Hit count',
 58+ 'abusefilter-list-edit' => 'Edit',
 59+ 'abusefilter-list-details' => 'Details',
 60+ 'abusefilter-hidden' => 'Private',
 61+ 'abusefilter-unhidden' => 'Public',
 62+ 'abusefilter-enabled' => 'Enabled',
 63+ 'abusefilter-disabled' => 'Disabled',
 64+ 'abusefilter-hitcount' => '$1 {{PLURAL:$1|hit|hits}}',
 65+
 66+ // The edit screen
 67+ 'abusefilter-edit-subtitle' => 'Editing filter $1',
 68+ 'abusefilter-edit-save' => 'Save Filter',
 69+ 'abusefilter-edit-id' => 'Filter ID:',
 70+ 'abusefilter-edit-description' => "Description:\n:''(publicly viewable)''",
 71+ 'abusefilter-edit-flags' => 'Flags:',
 72+ 'abusefilter-edit-enabled' => 'Enable this filter',
 73+ 'abusefilter-edit-hidden' => 'Hide details of this filter from public view',
 74+ 'abusefilter-edit-rules' => 'Ruleset:',
 75+ 'abusefilter-edit-notes' => "Notes:\n:''(private)",
 76+ 'abusefilter-edit-lastmod' => 'Filter last modified:',
 77+ 'abusefilter-edit-lastuser' => 'Last user to modify this filter:',
 78+ 'abusefilter-edit-hitcount' => 'Filter hits:',
 79+ 'abusefilter-edit-consequences' => 'Actions taken on hit',
 80+ 'abusefilter-edit-action-warn' => 'Trigger these actions after giving the user a warning',
 81+ 'abusefilter-edit-action-disallow' => 'Disallow the action',
 82+ 'abusefilter-edit-action-flag' => 'Flag the edit in the abuse log',
 83+ 'abusefilter-edit-action-blockautopromote' => "Revoke the users' autoconfirmed status",
 84+ 'abusefilter-edit-action-degroup' => 'Remove all privileged groups from the user',
 85+ 'abusefilter-edit-action-block' => 'Block the user from editing',
 86+ 'abusefilter-edit-action-throttle' => 'Trigger actions only if the user trips a rate limit',
 87+ 'abusefilter-edit-throttle-count' => 'Number of actions to allow:',
 88+ 'abusefilter-edit-throttle-period' => 'Period of time:',
 89+ 'abusefilter-edit-throttle-seconds' => '$1 seconds',
 90+ 'abusefilter-edit-throttle-groups' => "Group throttle by:\n:''(one per line, combine with commas)''",
 91+ 'abusefilter-edit-denied' => "You may not view details of this filter, because it is hidden from public view",
 92+ 'abusefilter-edit-main' => 'Filter parameters',
 93+ 'abusefilter-edit-done-subtitle' => 'Filter edited',
 94+ 'abusefilter-edit-done' => "You have successfully saved your changes to the filter.\n\n[[Special:AbuseFilter|Return]]",
 95+);
\ No newline at end of file
Property changes on: trunk/extensions/AbuseFilter/AbuseFilter.i18n.php
___________________________________________________________________
Name: svn:eol-style
196 + native
Index: trunk/extensions/AbuseFilter/testTitles.php
@@ -0,0 +1,12 @@
 2+<?
 3+
 4+require( '/home/andrew/mediawiki/maintenance/commandLine.inc' );
 5+
 6+while ( ( $line = readconsole( '> ' ) ) !== false ) {
 7+ $line = trim($line);
 8+
 9+ print "Testing $line...\n";
 10+ $result = AbuseFilter::checkTitleText( $line );
 11+ print "-Result: ";
 12+ var_dump( $result );
 13+}
\ No newline at end of file
Property changes on: trunk/extensions/AbuseFilter/testTitles.php
___________________________________________________________________
Name: svn:eol-style
114 + native
Index: trunk/extensions/AbuseFilter/AbuseFilter.php
@@ -0,0 +1,49 @@
 2+<?php
 3+if ( ! defined( 'MEDIAWIKI' ) )
 4+ die();
 5+
 6+/**#@+
 7+ * Automatically applies heuristics to edits.
 8+ * @addtogroup Extensions
 9+ *
 10+ * @link http://www.mediawiki.org/wiki/Extension:AbuseFilter Documentation
 11+ *
 12+ *
 13+ * @author Andrew Garrett <andrew@epstone.net>
 14+ * @license http://www.gnu.org/copyleft/gpl.html GNU General Public License 2.0 or later
 15+ */
 16+
 17+$dir = dirname(__FILE__);
 18+$wgExtensionCredits['other'][] = array(
 19+ 'name' => 'Abuse Filter',
 20+ 'author' => 'Andrew Garrett',
 21+ 'svn-date' => '$LastChangedDate: 2008-06-08 20:48:19 +1000 (Sun, 08 Jun 2008) $',
 22+ 'svn-revision' => '$LastChangedRevision: 36018 $',
 23+ 'description' => 'Applies automatic heuristics to edits.',
 24+ 'descriptionmsg' => 'abusefilter-desc',
 25+ 'url' => 'http://www.mediawiki.org/wiki/Extension:AbuseFilter',
 26+);
 27+
 28+$wgExtensionMessagesFiles['AbuseFilter'] = "$dir/AbuseFilter.i18n.php";
 29+
 30+$wgAutoloadClasses[ 'AbuseFilter' ] = "$dir/AbuseFilter.class.php";
 31+$wgAutoloadClasses[ 'AbuseFilterHooks' ] = "$dir/AbuseFilter.hooks.php";
 32+$wgAutoloadClasses['SpecialAbuseLog'] = "$dir/SpecialAbuseLog.php";
 33+$wgAutoloadClasses['SpecialAbuseFilter'] = "$dir/SpecialAbuseFilter.php";
 34+
 35+$wgSpecialPages['AbuseLog'] = 'SpecialAbuseLog';
 36+$wgSpecialPages['AbuseFilter'] = 'SpecialAbuseFilter';
 37+
 38+$wgHooks['EditFilter'][] = 'AbuseFilterHooks::onEditFilter';
 39+$wgHooks['GetAutoPromoteGroups'][] = 'AbuseFilterHooks::onGetAutoPromoteGroups';
 40+$wgHooks['AbortMove'][] = 'AbuseFilterHooks::onAbortMove';
 41+$wgHooks['AbortNewAccount'][] = 'AbuseFilterHooks::onAbortNewAccount';
 42+$wgHooks['ArticleDelete'][] = 'AbuseFilterHooks::onArticleDelete';
 43+
 44+$wgAvailableRights[] = 'abusefilter-modify';
 45+$wgAvailableRights[] = 'abusefilter-log-detail';
 46+$wgAvailableRights[] = 'abusefilter-view';
 47+$wgAvailableRights[] = 'abusefilter-log';
 48+$wgAvailableRights[] = 'abusefilter-private';
 49+
 50+$wgAbuseFilterAvailableActions = array( 'flag', 'throttle', 'warn', 'disallow', 'blockautopromote', 'block', 'degroup' );
\ No newline at end of file
Property changes on: trunk/extensions/AbuseFilter/AbuseFilter.php
___________________________________________________________________
Name: svn:eol-style
151 + native
Index: trunk/extensions/AbuseFilter/SpecialAbuseFilter.php
@@ -0,0 +1,355 @@
 2+<?php
 3+if ( ! defined( 'MEDIAWIKI' ) )
 4+ die();
 5+
 6+class SpecialAbuseFilter extends SpecialPage {
 7+
 8+ var $mSkin;
 9+
 10+ function __construct() {
 11+ wfLoadExtensionMessages('AbuseFilter');
 12+ parent::__construct( 'AbuseFilter', 'abusefilter-view' );
 13+ }
 14+
 15+ function execute( $subpage ) {
 16+ global $wgUser,$wgOut,$wgRequest;
 17+
 18+ $this->setHeaders();
 19+
 20+ $this->loadParameters( $subpage );
 21+ $wgOut->setPageTitle( wfMsg( 'abusefilter-management' ) );
 22+ $wgOut->setRobotpolicy( "noindex,nofollow" );
 23+ $wgOut->setArticleRelated( false );
 24+ $wgOut->enableClientCache( false );
 25+
 26+ // Are we allowed?
 27+ if ( count( $errors = $this->getTitle()->getUserPermissionsErrors( 'abusefilter-view', $wgUser, true, array( 'ns-specialprotected' ) ) ) ) {
 28+ // Go away.
 29+ $wgOut->showPermissionsErrorPage( $errors, 'abusefilter-view' );
 30+ return;
 31+ }
 32+
 33+ $this->mSkin = $wgUser->getSkin();
 34+
 35+ if ($output = $this->doEdit( )) {
 36+ $wgOut->addHtml( $output );
 37+ } else {
 38+ // Show list of filters.
 39+ $this->showList();
 40+ }
 41+ }
 42+
 43+ function doEdit() {
 44+ global $wgRequest, $wgUser;
 45+
 46+ $filter = $this->mFilter;
 47+
 48+ $editToken = $wgRequest->getVal( 'wpEditToken' );
 49+ $didEdit = $filter && $this->canEdit() && $wgUser->matchEditToken( $editToken, array( 'abusefilter', $filter ) );
 50+
 51+ if ($didEdit) {
 52+ $dbw = wfGetDB( DB_MASTER );
 53+ $newRow = array();
 54+
 55+ // Load the toggles which go straight into the DB
 56+ $dbToggles = array( 'enabled', 'hidden' );
 57+ foreach( $dbToggles as $toggle ) {
 58+ $newRow['af_'.$toggle] = $wgRequest->getBool( 'wpFilter'.ucfirst($toggle) );
 59+ }
 60+
 61+ // Text which can go straight into the DB
 62+ $dbText = array( 'wpFilterDescription' => 'af_public_comments', 'wpFilterRules' => 'af_pattern', 'wpFilterNotes' => 'af_comments' );
 63+ foreach( $dbText as $key => $value ) {
 64+ $newRow[$value] = trim($wgRequest->getText( $key ));
 65+ }
 66+
 67+ // Last modified details
 68+ $newRow['af_timestamp'] = $dbw->timestamp( wfTimestampNow() );
 69+ $newRow['af_user'] = $wgUser->getId();
 70+ $newRow['af_user_text'] = $wgUser->getName();
 71+
 72+ // Actions
 73+ global $wgAbuseFilterAvailableActions;
 74+ $enabledActions = array();
 75+ $deadActions = array();
 76+ $actionsRows = array();
 77+ foreach( $wgAbuseFilterAvailableActions as $action ) {
 78+ // Check if it's set
 79+ $enabled = $wgRequest->getBool( 'wpFilterAction'.ucfirst($action) );
 80+
 81+ if (!$enabled) {
 82+ $deadActions[] = $action;
 83+ }
 84+
 85+ if ($enabled) {
 86+ $parameters = array();
 87+
 88+ if ($action == 'throttle') {
 89+ // Grumble grumble.
 90+ // We need to load the parameters
 91+ $throttleCount = $wgRequest->getIntOrNull( 'wpFilterThrottleCount' );
 92+ $throttlePeriod = $wgRequest->getIntOrNull( 'wpFilterThrottlePeriod' );
 93+ $throttleGroups = explode("\n", trim( $wgRequest->getText( 'wpFilterThrottleGroups' ) ) );
 94+
 95+ $parameters[0] = $filter; // For now, anyway
 96+ $parameters[1] = "$throttleCount,$throttlePeriod";
 97+ $parameters = array_merge( $parameters, $throttleGroups );
 98+ }
 99+
 100+ $thisRow = array( 'afa_filter' => $filter, 'afa_consequence' => $action, 'afa_parameters' => implode( "\n", $parameters ) );
 101+ $actionsRows[] = $thisRow;
 102+ }
 103+ }
 104+
 105+ // Do the update
 106+
 107+ $dbw->begin();
 108+ $dbw->update( 'abuse_filter', $newRow, array( 'af_id' => $filter ), __METHOD__ );
 109+ $dbw->delete( 'abuse_filter_action', array( 'afa_filter' => $filter, 'afa_consequence' => $deadActions ), __METHOD__ );
 110+ $dbw->replace( 'abuse_filter_action', array( array( 'afa_filter', 'afa_consequence' ) ), $actionsRows, __METHOD__ );
 111+ $dbw->commit();
 112+
 113+ global $wgOut;
 114+
 115+ $wgOut->setSubtitle( wfMsg('abusefilter-edit-done-subtitle' ) );
 116+ return wfMsgExt( 'abusefilter-edit-done', array( 'parse' ) );
 117+ } else {
 118+ return $this->buildFilterEditor();
 119+ }
 120+ }
 121+
 122+ function buildFilterEditor( ) {
 123+ if (!is_numeric($this->mFilter) || $this->mFilter<=0) {
 124+ return false;
 125+ }
 126+
 127+ // Build the edit form
 128+ global $wgOut,$wgLang,$wgUser;
 129+ $sk = $this->mSkin;
 130+ $wgOut->setSubtitle( wfMsg( 'abusefilter-edit-subtitle', $this->mFilter ) );
 131+
 132+ list ($row, $actions) = $this->loadFilterData();
 133+
 134+ if ($row->af_hidden && !$this->canEdit()) {
 135+ return wfMsg( 'abusefilter-edit-hidden' );
 136+ }
 137+
 138+ $output = '';
 139+ $fields = array();
 140+
 141+ $fields['abusefilter-edit-id'] = $this->mFilter;
 142+ $fields['abusefilter-edit-description'] = Xml::input( 'wpFilterDescription', 20, $row->af_public_comments );
 143+
 144+ // Hit count display
 145+ $count = $row->af_hit_count;
 146+ $count_display = wfMsgExt( 'abusefilter-hitcount', array( 'parseinline' ), array( $count ) );
 147+ $hitCount = $sk->makeKnownLinkObj( SpecialPage::getTitleFor( 'AbuseLog' ), $count_display, 'wpSearchFilter='.$row->af_id );
 148+
 149+ $fields['abusefilter-edit-hitcount'] = $hitCount;
 150+
 151+ $fields['abusefilter-edit-rules'] = Xml::textarea( 'wpFilterRules', $row->af_pattern . "\n" );
 152+ $fields['abusefilter-edit-notes'] = Xml::textarea( 'wpFilterNotes', $row->af_comments ."\n" );
 153+
 154+
 155+ // Build checkboxen
 156+ $checkboxes = array( 'hidden', 'enabled' );
 157+ $flags = '';
 158+ foreach( $checkboxes as $checkboxId ) {
 159+ $message = "abusefilter-edit-$checkboxId";
 160+ $dbField = "af_$checkboxId";
 161+ $postVar = "wpFilter".ucfirst($checkboxId);
 162+
 163+ $checkbox = Xml::checkLabel( wfMsg( $message ), $postVar, $postVar, $row->$dbField );
 164+ $checkbox = Xml::tags( 'p', null, $checkbox );
 165+ $flags .= $checkbox;
 166+ }
 167+ $fields['abusefilter-edit-flags'] = $flags;
 168+
 169+ // Last modification details
 170+ $fields['abusefilter-edit-lastmod'] = $wgLang->timeanddate( $row->af_timestamp );
 171+ $fields['abusefilter-edit-lastuser'] = $sk->userLink( $row->af_user, $row->af_user_text ) . $sk->userToolLinks( $row->af_user, $row->af_user_text );
 172+
 173+ $form = Xml::buildForm( $fields );
 174+ $form = Xml::fieldset( wfMsg( 'abusefilter-edit-main' ), $form );
 175+ $form .= Xml::fieldset( wfMsg( 'abusefilter-edit-consequences' ), $this->buildConsequenceEditor( $row, $actions ) );
 176+
 177+ if ($this->canEdit()) {
 178+ $form .= Xml::submitButton( wfMsg( 'abusefilter-edit-save' ) );
 179+ $form .= Xml::hidden( 'wpEditToken', $wgUser->editToken( array( 'abusefilter', $this->mFilter )) );
 180+ }
 181+
 182+ $form = Xml::tags( 'form', array( 'action' => $this->getTitle( $this->mFilter )->getFullURL(), 'method' => 'POST' ), $form );
 183+
 184+ $output .= $form;
 185+
 186+ return $output;
 187+ }
 188+
 189+ function buildConsequenceEditor( $row, $actions ) {
 190+ global $wgAbuseFilterAvailableActions;
 191+ $setActions = array();
 192+ foreach( $wgAbuseFilterAvailableActions as $action ) {
 193+ $setActions[$action] = in_array( $action, array_keys( $actions ) );
 194+ }
 195+
 196+ $output = '';
 197+
 198+ // Special case: flagging - always on.
 199+ $checkbox = Xml::checkLabel( wfMsg( 'abusefilter-edit-action-flag' ), 'wpFilterActionFlag', 'wpFilterActionFlag', true, array( 'disabled' => '1' ) );
 200+ $output .= Xml::tags( 'p', null, $checkbox );
 201+
 202+ // Special case: throttling
 203+ $throttleSettings = Xml::checkLabel( wfMsg( 'abusefilter-edit-action-throttle' ), 'wpFilterActionThrottle', 'wpFilterActionThrottle', $setActions['throttle'] );
 204+ $throttleFields = array();
 205+
 206+ if ($setActions['throttle']) {
 207+ $throttleRate = explode(',',$actions['throttle']['parameters'][0]);
 208+ $throttleCount = $throttleRate[0];
 209+ $throttlePeriod = $throttleRate[1];
 210+
 211+ $throttleGroups = implode("\n", array_slice($actions['throttle']['parameters'], 1 ) );
 212+ } else {
 213+ $throttleCount = 3;
 214+ $throttlePeriod = 60;
 215+
 216+ $throttleGroups = "user\n";
 217+ }
 218+
 219+ $throttleFields['abusefilter-edit-throttle-count'] = Xml::input( 'wpFilterThrottleCount', 20, $throttleCount );
 220+ $throttleFields['abusefilter-edit-throttle-period'] = wfMsgExt( 'abusefilter-edit-throttle-seconds', array( 'parseinline', 'replaceafter' ), array(Xml::input( 'wpFilterThrottlePeriod', 20, $throttlePeriod ) ) );
 221+ $throttleFields['abusefilter-edit-throttle-groups'] = Xml::textarea( 'wpFilterThrottleGroups', $throttleGroups."\n" );
 222+ $throttleSettings .= Xml::buildForm( $throttleFields );
 223+ $output .= Xml::tags( 'p', null, $throttleSettings );
 224+
 225+ // The remainder are just toggles
 226+ $remainingActions = array_diff( $wgAbuseFilterAvailableActions, array( 'flag', 'throttle' ) );
 227+
 228+ foreach( $remainingActions as $action ) {
 229+ $message = 'abusefilter-edit-action-'.$action;
 230+ $form_field = 'wpFilterAction' . ucfirst($action);
 231+ $status = $setActions[$action];
 232+
 233+ $thisAction = Xml::checkLabel( wfMsg( $message ), $form_field, $form_field, $status );
 234+ $thisAction = Xml::tags( 'p', null, $thisAction );
 235+
 236+ $output .= $thisAction;
 237+ }
 238+
 239+ return $output;
 240+ }
 241+
 242+ function loadFilterData() {
 243+ $id = $this->mFilter;
 244+
 245+ $dbr = wfGetDB( DB_SLAVE );
 246+
 247+ // Load the main row
 248+ $row = $dbr->selectRow( 'abuse_filter', '*', array( 'af_id' => $id ), __METHOD__ );
 249+
 250+ // Load the actions
 251+ $actions = array();
 252+ $res = $dbr->select( 'abuse_filter_action', '*', array( 'afa_filter' => $id), __METHOD__ );
 253+ while ( $actionRow = $dbr->fetchObject( $res ) ) {
 254+ $thisAction = array();
 255+ $thisAction['action'] = $actionRow->afa_consequence;
 256+ $thisAction['parameters'] = explode( "\n", $actionRow->afa_parameters );
 257+
 258+ $actions[$actionRow->afa_consequence] = $thisAction;
 259+ }
 260+
 261+ return array( $row, $actions );
 262+ }
 263+
 264+ function loadParameters( $subpage ) {
 265+ global $wgRequest,$wgUser;
 266+
 267+ $filter = $subpage;
 268+
 269+ if (!is_numeric($filter)) {
 270+ $filter = $wgRequest->getIntOrNull( 'wpFilter' );
 271+
 272+ if (!$filter) {
 273+ return;
 274+ }
 275+ }
 276+ $this->mFilter = $filter;
 277+ }
 278+
 279+ function canEdit() {
 280+ global $wgUser;
 281+ static $canEdit = 'unset';
 282+
 283+ if ($canEdit == 'unset') {
 284+ $canEdit = !count( $errors = $this->getTitle()->getUserPermissionsErrors( 'abusefilter-modify', $wgUser, true, array( 'ns-specialprotected' ) ) );
 285+ }
 286+
 287+ return $canEdit;
 288+ }
 289+
 290+ function showList() {
 291+ global $wgOut,$wgUser;;
 292+
 293+ $this->mSkin = $wgUser->getSkin();
 294+
 295+ $output = '';
 296+
 297+ $output .= Xml::element( 'h2', null, wfMsgExt( 'abusefilter-list', array( 'parseinline' ) ) );
 298+
 299+ // We shouldn't have more than 100 filters, so don't bother paging.
 300+ $dbr = wfGetDB( DB_SLAVE );
 301+ $res = $dbr->select( array('abuse_filter','abuse_filter_action'), '*,group_concat(afa_consequence) AS consequences', array( 'afa_filter=af_id' ), __METHOD__, array( 'LIMIT' => 100, 'GROUP BY' => 'af_id' ) );
 302+ $list = '';
 303+ $editLabel = $this->canEdit() ? 'abusefilter-list-edit' : 'abusefilter-list-details';
 304+
 305+ // Build in a table
 306+ $headers = array( 'abusefilter-list-id', 'abusefilter-list-public', 'abusefilter-list-consequences', 'abusefilter-list-status', 'abusefilter-list-visibility', 'abusefilter-list-hitcount', $editLabel );
 307+ $header_row = '';
 308+ foreach( $headers as $header ) {
 309+ $header_row .= Xml::element( 'th', null, wfMsgExt( $header, array( 'parseinline' ) ) );
 310+ }
 311+
 312+ $list .= Xml::tags( 'tr', null, $header_row );
 313+
 314+ while ($row = $dbr->fetchObject( $res ) ) {
 315+ $list .= $this->shortFormatFilter( $row );
 316+ }
 317+
 318+ $output .= Xml::tags( 'table', array( 'class' => 'wikitable' ), Xml::tags( 'tbody', null, $list ) );
 319+
 320+ $wgOut->addHTML( $output );
 321+ }
 322+
 323+ function shortFormatFilter ( $row ) {
 324+ global $wgOut;
 325+
 326+ $sk = $this->mSkin;
 327+
 328+ $editLabel = $this->canEdit() ? 'abusefilter-list-edit' : 'abusefilter-list-details';
 329+
 330+ // Build a table row
 331+ $trow = '';
 332+
 333+ $status = $row->af_enabled ? 'abusefilter-enabled' : 'abusefilter-disabled';
 334+ $status = wfMsgExt( $status, array( 'parseinline' ) );
 335+
 336+ $visibility = $row->af_hidden ? 'abusefilter-hidden' : 'abusefilter-unhidden';
 337+ $visibility = wfMsgExt( $visibility, array( 'parseinline' ) );
 338+
 339+ // Hit count
 340+ $count = $row->af_hit_count;
 341+ $count_display = wfMsgExt( 'abusefilter-hitcount', array( 'parseinline' ), array( $count ) );
 342+ $hitCount = $sk->makeKnownLinkObj( SpecialPage::getTitleFor( 'AbuseLog' ), $count_display, 'wpSearchFilter='.$row->af_id );
 343+
 344+ $editLink = $sk->makeKnownLinkObj( $this->getTitle( $row->af_id ), wfMsg( $editLabel ) );
 345+
 346+ $values = array( $row->af_id, $wgOut->parse($row->af_public_comments), wfEscapeWikitext($row->consequences), $status, $visibility, $hitCount, $editLink );
 347+
 348+ foreach( $values as $value ) {
 349+ $trow .= Xml::tags( 'td', null, $value );
 350+ }
 351+
 352+ $trow = Xml::tags( 'tr', null, $trow );
 353+
 354+ return $trow;
 355+ }
 356+}
\ No newline at end of file
Property changes on: trunk/extensions/AbuseFilter/SpecialAbuseFilter.php
___________________________________________________________________
Name: svn:eol-style
1357 + native
Index: trunk/extensions/AbuseFilter/abusefilter.tables.sql
@@ -0,0 +1,46 @@
 2+-- SQL tables for AbuseFilter extension
 3+
 4+CREATE TABLE /*$wgDBprefix*/abuse_filter (
 5+ af_id BIGINT unsigned NOT NULL AUTO_INCREMENT,
 6+ af_pattern BLOB NOT NULL,
 7+ af_user BIGINT unsigned NOT NULL,
 8+ af_user_text varchar(255) binary NOT NULL,
 9+ af_timestamp binary(14) NOT NULL,
 10+ af_enabled tinyint(1) not null default 1,
 11+ af_comments BLOB,
 12+ af_public_comments TINYBLOB,
 13+ af_hidden tinyint(1) not null default 0,
 14+ af_hit_count bigint not null default 0,
 15+
 16+ PRIMARY KEY (af_id),
 17+ KEY (af_user)
 18+) /*$wgDBTableOptions*/;
 19+
 20+CREATE TABLE /*$wgDBprefix*/abuse_filter_action (
 21+ afa_filter BIGINT unsigned NOT NULL,
 22+ afa_consequence varchar(255) NOT NULL,
 23+ afa_parameters TINYBLOB NOT NULL,
 24+
 25+ PRIMARY KEY (afa_filter),
 26+ KEY (afa_consequence)
 27+) /*$wgDBTableOptions*/;
 28+
 29+CREATE TABLE /*$wgDBprefix*/abuse_filter_log (
 30+ afl_id BIGINT unsigned NOT NULL AUTO_INCREMENT,
 31+ afl_filter BIGINT unsigned NOT NULL,
 32+ afl_user BIGINT unsigned NOT NULL,
 33+ afl_user_text varchar(255) binary NOT NULL,
 34+ afl_ip varchar(255) not null,
 35+ afl_action varbinary(255) not null,
 36+ afl_var_dump BLOB NOT NULL,
 37+ afl_timestamp binary(14) NOT NULL,
 38+ afl_namespace tinyint NOT NULL,
 39+ afl_title varchar(255) binary NOT NULL,
 40+
 41+ PRIMARY KEY (afl_id),
 42+ KEY (afl_filter),
 43+ KEY (afl_user),
 44+ KEY (afl_timestamp),
 45+ KEY (afl_namespace, afl_title)
 46+ KEY (afl_ip),
 47+) /*$wgDBTableOptions*/;
\ No newline at end of file
Property changes on: trunk/extensions/AbuseFilter/abusefilter.tables.sql
___________________________________________________________________
Name: svn:eol-style
148 + native
Index: trunk/extensions/AbuseFilter/AbuseFilter.hooks.php
@@ -0,0 +1,84 @@
 2+<?php
 3+if ( ! defined( 'MEDIAWIKI' ) )
 4+ die();
 5+
 6+class AbuseFilterHooks {
 7+
 8+ public static function onEditFilter($editor, $text, $section, &$error, $summary) {
 9+ // Load vars
 10+ $vars = array();
 11+
 12+ global $wgUser;
 13+ $vars = array_merge( $vars, AbuseFilter::generateUserVars( $wgUser ) );
 14+ $vars = array_merge( $vars, AbuseFilter::generateTitleVars( $editor->mTitle , 'ARTICLE' ));
 15+ $vars['ACTION'] = 'edit';
 16+ $vars['SUMMARY'] = $summary;
 17+
 18+ // TODO:
 19+// // Include added/removed lines in the vars.
 20+
 21+ $filter_result = AbuseFilter::filterAction( $vars, $editor->mTitle );
 22+
 23+ $error = $filter_result;
 24+
 25+ return true;
 26+ }
 27+
 28+ public static function onGetAutoPromoteGroups( $user, &$promote ) {
 29+ global $wgMemc;
 30+
 31+ $key = AbuseFilter::autoPromoteBlockKey( $user );
 32+
 33+ if ($wgMemc->get( $key ) ) {
 34+ $promote = array();
 35+ }
 36+
 37+ return true;
 38+ }
 39+
 40+ function onAbortMove( $oldTitle, $newTitle, $user, &$error, $reason ) {
 41+ $vars = array();
 42+
 43+ global $wgUser;
 44+ $vars = array_merge( $vars, AbuseFilter::generateUserVars( $wgUser ),
 45+ AbuseFilter::generateTitleVars( $oldTitle, 'MOVED_FROM' ),
 46+ AbuseFilter::generateTitleVars( $newTitle, 'MOVED_TO' ) );
 47+ $vars['SUMMARY'] = $reason;
 48+ $vars['ACTION'] = 'move';
 49+
 50+ $filter_result = AbuseFilter::filterAction( $vars, $oldTitle );
 51+
 52+ $error = "BLAH\n$filter_result";
 53+
 54+ return $filter_result == '';
 55+ }
 56+
 57+ function onArticleDelete( &$article, &$user, &$reason, &$error ) {
 58+ $vars = array();
 59+
 60+ global $wgUser;
 61+ $vars = array_merge( $vars, AbuseFilter::generateUserVars( $wgUser ),
 62+ AbuseFilter::generateTitleVars( $article->mTitle, 'ARTICLE' ) );
 63+ $vars['SUMMARY'] = $reason;
 64+ $vars['ACTION'] = 'delete';
 65+
 66+ $filter_result = AbuseFilter::filterAction( $vars, $user );
 67+
 68+ $error = "BLAH\n$filter_result";
 69+
 70+ return $filter_result == '';
 71+ }
 72+
 73+ function onAbortNewAccount( $username, &$message ) {
 74+ $vars = array();
 75+
 76+ $vars['ACTION'] = 'createaccount';
 77+ $vars['ACCOUNTNAME'] = $username;
 78+
 79+ $filter_result = AbuseFilter::filterAction( $vars, $user );
 80+
 81+ $error = "BLAH\n$filter_result";
 82+
 83+ return $filter_result == '';
 84+ }
 85+}
Property changes on: trunk/extensions/AbuseFilter/AbuseFilter.hooks.php
___________________________________________________________________
Name: svn:eol-style
186 + native

Status & tagging log