Abuse Filter changes designed for testing filters against real data:

* Add searching and filtering functionality to the existing 'test' interface.
* Add an 'examine' interface designed for testing filters against a previous change, selectable through the search interface in either the 'test' or the 'examine' view.
* Minor enabling change in ChangesList core, to allow subclassing.
This commit is contained in:
Andrew Garrett 2009-01-29 22:44:31 +00:00
parent e9957027f9
commit f9c9c07ccf
10 changed files with 285 additions and 39 deletions

View file

@ -68,6 +68,25 @@ class AbuseFilter {
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;
@ -669,7 +688,7 @@ class AbuseFilter {
return $user;
}
static function buildEditBox( $rules, $textName = 'wpFilterRules' ) {
static function buildEditBox( $rules, $textName = 'wpFilterRules', $addResultDiv = true ) {
global $wgOut;
$rules = Xml::textarea( $textName, ( isset( $rules ) ? $rules."\n" : "\n" ) );
@ -695,7 +714,9 @@ class AbuseFilter {
// Add syntax checking
$rules .= Xml::element( 'input', array( 'type' => 'button', 'onclick' => 'doSyntaxCheck()', 'value' => wfMsg( 'abusefilter-edit-check' ), 'id' => 'mw-abusefilter-syntaxcheck' ) );
$rules .= Xml::element( 'div', array( 'id' => 'mw-abusefilter-syntaxresult', 'style' => 'display: none;' ), ' ' );
if ($addResultDiv)
$rules .= Xml::element( 'div', array( 'id' => 'mw-abusefilter-syntaxresult', 'style' => 'display: none;' ), ' ' );
// Add script
$editScript = file_get_contents(dirname(__FILE__)."/edit.js");
@ -882,4 +903,36 @@ class AbuseFilter {
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' );
$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 );
// 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 );
}
$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 );
}
$output .= Xml::closeElement( 'tbody' ) . Xml::closeElement( 'table' );
return $output;
}
}

View file

@ -316,6 +316,7 @@ Please check them carefully, and click "confirm" to confirm your selection.',
Reason given: $2',
'abusefilter-revert-reasonfield' => 'Reason for revert:',
// These messages are for batch checking
'abusefilter-test' => 'Test a filter against previous edits',
'abusefilter-test-intro' => 'This page allows you to check a filter entered in the box below against the last $1 changes.
To load an existing filter, type its filter ID into the box below the edit textbox, and click the "Load" button.',
@ -323,6 +324,25 @@ To load an existing filter, type its filter ID into the box below the edit textb
'abusefilter-test-load-filter' => 'Load filter ID:',
'abusefilter-test-submit' => 'Test',
'abusefilter-test-load' => 'Load',
'abusefilter-test-user' => 'Changes by user:',
'abusefilter-test-period-start' => 'Changes made after:',
'abusefilter-test-period-end' => 'Changes made before:',
'abusefilter-changeslist-examine' => 'examine',
// And these messages are for examining specific actions.
'abusefilter-examine' => 'Examine individual changes',
'abusefilter-examine-intro' => 'This page allows you to examine the variables generated by the Abuse Filter for an individual change, and test it against filters.',
'abusefilter-examine-legend' => 'Select changes',
'abusefilter-examine-diff' => 'Diff URL:',
'abusefilter-examine-user' => 'User:',
'abusefilter-examine-title' => 'Page title:',
'abusefilter-examine-submit' => 'Search',
'abusefilter-examine-vars' => 'Variables generated for this change',
'abusefilter-examine-test' => 'Test this change against a filter',
'abusefilter-examine-test-button' => 'Test filter',
'abusefilter-examine-match' => 'The filter matched this change.',
'abusefilter-examine-nomatch' => 'The filter did not match this change.',
'abusefilter-examine-syntaxerror' => 'The filter has invalid syntax',
);
/** Message documentation (Message documentation)

View file

@ -39,7 +39,9 @@ $wgAutoloadClasses['AbuseFilterViewEdit'] = "$dir/Views/AbuseFilterViewEdit.php"
$wgAutoloadClasses['AbuseFilterViewTools'] = "$dir/Views/AbuseFilterViewTools.php";
$wgAutoloadClasses['AbuseFilterViewHistory'] = "$dir/Views/AbuseFilterViewHistory.php";
$wgAutoloadClasses['AbuseFilterViewRevert'] = "$dir/Views/AbuseFilterViewRevert.php";
$wgAutoloadClasses['AbuseFilterViewTest'] = "$dir/Views/AbuseFilterViewTest.php";
$wgAutoloadClasses['AbuseFilterViewTestBatch'] = "$dir/Views/AbuseFilterViewTestBatch.php";
$wgAutoloadClasses['AbuseFilterViewExamine'] = "$dir/Views/AbuseFilterViewExamine.php";
$wgAutoloadClasses['AbuseFilterChangesList'] = "$dir/Views/AbuseFilterViewExamine.php";
$wgSpecialPages['AbuseLog'] = 'SpecialAbuseLog';
$wgSpecialPages['AbuseFilter'] = 'SpecialAbuseFilter';
@ -84,6 +86,7 @@ $wgAjaxExportList[] = 'AbuseFilter::ajaxCheckSyntax';
$wgAjaxExportList[] = 'AbuseFilter::ajaxEvaluateExpression';
$wgAjaxExportList[] = 'AbuseFilter::ajaxReAutoconfirm';
$wgAjaxExportList[] = 'AbuseFilter::ajaxGetFilter';
$wgAjaxExportList[] = 'AbuseFilter::ajaxCheckFilterWithVars';
// Bump the version number every time you change any of the .css/.js files
$wgAbuseFilterStyleVersion = 3;

View file

@ -50,8 +50,12 @@ class SpecialAbuseFilter extends SpecialPage {
$view = 'AbuseFilterViewRevert';
}
if ( !empty($params[0]) && $params[0] == 'test' ) {
$view = 'AbuseFilterViewTest';
if ( $subpage == 'test' ) {
$view = 'AbuseFilterViewTestBatch';
}
if ( count($params) && $params[0] == 'examine' ) {
$view = 'AbuseFilterViewExamine';
}
if (!empty($params[0]) && ($params[0] == 'history' || $params[0] == 'log') ) {

View file

@ -101,11 +101,6 @@ class SpecialAbuseLog extends SpecialPage {
if (!$this->canSeeDetails()) {
return;
}
// 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 = AbuseFilter::$builderValues['vars'];
$dbr = wfGetDB( DB_SLAVE );
@ -123,27 +118,7 @@ class SpecialAbuseLog extends SpecialPage {
// Build a table.
$vars = unserialize( $row->afl_var_dump );
$output .= Xml::openElement( 'table', array( 'class' => 'mw-abuselog-details' ) ) . Xml::openElement( 'tbody' );
$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 );
// 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 );
}
$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 );
}
$output .= Xml::closeElement( 'tbody' ) . Xml::closeElement( 'table' );
$output .= AbuseFilter::buildVarDumpTable( $vars );
if ($this->canSeePrivate()) {
// Private stuff, like IPs.

View file

@ -25,4 +25,16 @@ abstract class AbuseFilterView {
return $canEdit;
}
}
class AbuseFilterChangesList extends OldChangesList {
protected function insertExtra( &$s, &$rc, &$classes ) {
## Empty, used for subclassers to add anything special.
$sk = $this->skin;
$title = SpecialPage::getTitleFor( 'AbuseFilter', "examine/".$rc->mAttribs['rc_id'] );
$examineLink = $sk->link( $title, wfMsgExt( 'abusefilter-changeslist-examine', 'parseinline' ) );
$s .= " ($examineLink)";
}
}

View file

@ -0,0 +1,134 @@
<?php
if (!defined( 'MEDIAWIKI' ))
die();
class AbuseFilterViewExamine extends AbuseFilterView {
function show( ) {
global $wgOut, $wgUser;
$wgOut->setPageTitle( wfMsg( 'abusefilter-examine' ) );
$wgOut->addWikiMsg( 'abusefilter-examine-intro' );
$this->loadParameters();
// Check if we've got a subpage
if ( count($this->mParams)>1 && is_numeric($this->mParams[1]) ) {
$this->showExaminer( $this->mParams[1] );
} else {
$this->showSearch();
}
}
function showSearch() {
global $wgUser, $wgOut;
// Add selector
$selector = '';
$selectFields = array(); ## Same fields as in Test
$selectFields['abusefilter-test-user'] = wfInput( 'wpSearchUser', 45, $this->mSearchUser );
$selectFields['abusefilter-test-period-start'] = wfInput( 'wpSearchPeriodStart', 45, $this->mSearchPeriodStart );
$selectFields['abusefilter-test-period-end'] = wfInput( 'wpSearchPeriodEnd', 45, $this->mSearchPeriodEnd );
$selector .= Xml::buildForm( $selectFields, 'abusefilter-examine-submit' );
$selector .= Xml::hidden( 'submit', 1 );
$selector .= Xml::hidden( 'title', $this->getTitle( 'examine' )->getPrefixedText() );
$selector = Xml::tags( 'form', array( 'action' => $this->getTitle( 'examine' )->getLocalURL(), 'method' => 'GET' ), $selector );
$selector = Xml::fieldset( wfMsg( 'abusefilter-examine-legend' ), $selector );
$wgOut->addHTML( $selector );
if ($this->mSubmit) {
$this->showResults();
}
}
function showResults() {
global $wgUser, $wgOut;
$dbr = wfGetDB( DB_SLAVE );
$conds = array( 'rc_user_text' => $this->mSearchUser );
if ( $startTS = strtotime($this->mSearchPeriodStart) ) {
$conds[] = 'rc_timestamp>=' . $dbr->addQuotes( $dbr->timestamp( $startTS ) );
}
if ( $endTS = strtotime($this->mSearchPeriodEnd) ) {
$conds[] = 'rc_timestamp<=' . $dbr->addQuotes( $dbr->timestamp( $endTS ) );
}
$res = $dbr->select( 'recentchanges', '*', array_filter($conds), __METHOD__, array( 'ORDER BY' => 'rc_timestamp DESC', 'LIMIT' => '500' ) );
$changesList = new AbuseFilterChangesList( $wgUser->getSkin() );
$output = $changesList->beginRecentChangesList();
$counter = 1;
while ( $row = $dbr->fetchObject( $res ) ) {
$rc = RecentChange::newFromRow( $row );
$rc->counter = $counter++;
$output .= $changesList->recentChangesLine( $rc, false );
}
$output .= $changesList->endRecentChangesList();
$wgOut->addHTML( $output );
}
function showExaminer( $rcid ) {
global $wgOut, $wgUser;
// Get data
$dbr = wfGetDB( DB_SLAVE );
$row = $dbr->selectRow( 'recentchanges', '*', array( 'rc_id' => $rcid ), __METHOD__ );
if (!$row) {
$wgOut->addWikiMsg( 'abusefilter-examine-notfound' );
return;
}
$vars = AbuseFilter::getVarsFromRCRow( $row );
if (!$vars) {
$wgOut->addWikiMsg( 'abusefilter-examine-incompatible' );
return;
}
$output = '';
// Send armoured as JSON -- I totally give up on trying to send it as a proper object.
$wgOut->addInlineScript( "var wgExamineVars = ". Xml::encodeJsVar( json_encode( $vars ) ) .";" );
$wgOut->addInlineScript( file_get_contents( dirname( __FILE__ ) . "/examine.js" ) );
// Add messages
$msg = array();
$msg['match'] = wfMsg( 'abusefilter-examine-match' );
$msg['nomatch'] = wfMsg( 'abusefilter-examine-nomatch' );
$msg['syntaxerror'] = wfMsg( 'abusefilter-examine-syntaxerror' );
$wgOut->addInlineScript( "var wgMessageMatch = ".Xml::encodeJsVar( $msg['match'] ) . ";\n".
"var wgMessageNomatch = ".Xml::encodeJsVar( $msg['nomatch'] ) . ";\n".
"var wgMessageError = ".Xml::encodeJsVar( $msg['syntaxerror'] ) . ";\n" );
// Add test bit
$tester = Xml::tags( 'h2', null, wfMsgExt( 'abusefilter-examine-test', 'parseinline' ) );
$tester .= AbuseFilter::buildEditBox( '', 'wpTestFilter', false );
$tester .= "\n" . Xml::inputLabel( wfMsg( 'abusefilter-test-load-filter' ), 'wpInsertFilter', 'mw-abusefilter-load-filter', 10, '' ) . '&nbsp;' .
Xml::element( 'input', array( 'type' => 'button', 'value' => wfMsg( 'abusefilter-test-load' ), 'id' => 'mw-abusefilter-load' ) );
$output .= Xml::tags( 'div', array( 'id' => 'mw-abusefilter-examine-editor' ), $tester );
$output .= Xml::tags( 'p', null, Xml::element( 'input', array( 'type' => 'button', 'value' => wfMsg( 'abusefilter-examine-test-button' ), 'id' => 'mw-abusefilter-examine-test' ) ) .
Xml::element( 'div', array( 'id' => 'mw-abusefilter-syntaxresult', 'style' => 'display: none;' ), '&nbsp;' ) );
// Variable dump
$output .= Xml::tags( 'h2', null, wfMsgExt( 'abusefilter-examine-vars', 'parseinline' ) );
$output .= AbuseFilter::buildVarDumpTable( $vars );
$wgOut->addHTML( $output );
}
function loadParameters() {
global $wgRequest;
$this->mSearchUser = $wgRequest->getText( 'wpSearchUser' );
$this->mSearchPeriodStart = $wgRequest->getText( 'wpSearchPeriodStart' );
$this->mSearchPeriodEnd = $wgRequest->getText( 'wpSearchPeriodEnd' );
$this->mSubmit = $wgRequest->getCheck( 'submit' );
}
}

View file

@ -14,13 +14,14 @@ class AbuseFilterViewList extends AbuseFilterView {
// Quick links
$wgOut->addWikiMsg( 'abusefilter-links' );
$lists = array( 'tools', 'test' );
$lists = array( 'tools', 'test', 'examine' );
if ($this->canEdit())
$lists[] = 'new';
$links = '';
$sk = $wgUser->getSkin();
foreach( $lists as $list ) {
$title = $this->getTitle( $list );
$list = strtr( $list, '/', '-' );
$link = $sk->link( $title, wfMsg( "abusefilter-$list" ) );
$links .= Xml::tags( 'li', null, $link ) . "\n";

View file

@ -3,7 +3,7 @@
if (!defined( 'MEDIAWIKI' ))
die();
class AbuseFilterViewTest extends AbuseFilterView {
class AbuseFilterViewTestBatch extends AbuseFilterView {
// Hard-coded for now.
static $mChangeLimit = 100;
@ -25,7 +25,14 @@ class AbuseFilterViewTest extends AbuseFilterView {
// Removed until I can distinguish between positives and negatives :)
// $output .= Xml::tags( 'p', null, Xml::checkLabel( wfMsg( 'abusefilter-test-shownegative' ), 'wpShowNegative', 'wpShowNegative', $this->mShowNegative ) );
$output .= Xml::tags( 'p', null, Xml::submitButton( wfMsg( 'abusefilter-test-submit' ) ) );
// Selectory stuff
$selectFields = array();
$selectFields['abusefilter-test-user'] = wfInput( 'wpTestUser', 45, $this->mTestUser );
$selectFields['abusefilter-test-period-start'] = wfInput( 'wpTestPeriodStart', 45, $this->mTestPeriodStart );
$selectFields['abusefilter-test-period-end'] = wfInput( 'wpTestPeriodEnd', 45, $this->mTestPeriodEnd );
$output .= Xml::buildForm( $selectFields, 'abusefilter-test-submit' );
$output .= Xml::hidden( 'title', $this->getTitle("test")->getPrefixedText() );
$output = Xml::tags( 'form', array( 'action' => $this->getTitle("test")->getLocalURL(), 'method' => 'POST' ), $output );
@ -40,18 +47,27 @@ class AbuseFilterViewTest extends AbuseFilterView {
function doTest() {
// Quick syntax check.
global $wgUser, $wgOut;
if ( ($result = AbuseFilter::checkSyntax( $this->mFilter )) !== true ) {
$wgOut->addWikiMsg( 'abusefilter-test-syntaxerr' );
return;
}
$dbr = wfGetDB( DB_SLAVE );
$conds = array( 'rc_user_text' => $this->mTestUser );
if ($this->mTestPeriodStart) {
$conds[] = 'rc_timestamp>='.$dbr->addQuotes( $dbr->timestamp( strtotime( $this->mTestPeriodStart ) ) );
}
if ($this->mTestPeriodEnd) {
$conds[] = 'rc_timestamp<='.$dbr->addQuotes( $dbr->timestamp( strtotime( $this->mTestPeriodEnd ) ) );
}
// Get our ChangesList
global $wgUser, $wgOut;
$changesList = ChangesList::newFromUser( $wgUser );
$changesList = new AbuseFilterChangesList( $wgUser->getSkin() );
$output = $changesList->beginRecentChangesList();
$dbr = wfGetDB( DB_SLAVE );
$res = $dbr->select( 'recentchanges', '*', array(), __METHOD__, array( 'LIMIT' => self::$mChangeLimit, 'ORDER BY' => 'rc_timestamp asc' ) );
$res = $dbr->select( 'recentchanges', '*', array_filter( $conds ), __METHOD__, array( 'LIMIT' => self::$mChangeLimit, 'ORDER BY' => 'rc_timestamp desc' ) );
$counter = 1;
@ -80,5 +96,8 @@ class AbuseFilterViewTest extends AbuseFilterView {
$this->mFilter = $wgRequest->getText( 'wpTestFilter' );
$this->mShowNegative = $wgRequest->getBool( 'wpShowNegative' );
$this->mTestUser = $wgRequest->getText( 'wpTestUser' );
$this->mTestPeriodEnd = $wgRequest->getText( 'wpTestPeriodEnd' );
$this->mTestPeriodStart = $wgRequest->getText( 'wpTestPeriodStart' );
}
}
}

25
Views/examine.js Normal file
View file

@ -0,0 +1,25 @@
/** Scripts for Examiner */
function examinerCheckFilter() {
var filter = document.getElementById( 'wpTestFilter' ).value;
sajax_do_call( 'AbuseFilter::ajaxCheckFilterWithVars', [filter, wgExamineVars], function(request) {
var response = request.responseText;
var el = document.getElementById( 'mw-abusefilter-syntaxresult' );
el.style.display = 'block';
if (response == 'MATCH') {
changeText( el, wgMessageMatch );
} else if (response == 'NOMATCH') {
changeText( el, wgMessageNomatch );
} else if (response == 'SYNTAXERROR' ) {
changeText( el, wgMessageError );
}
} );
}
addOnloadHook( function() {
var el = document.getElementById( 'mw-abusefilter-examine-test' );
addHandler( el, 'click', examinerCheckFilter );
} );