SpamFerret/Special

Code
<?php /* NAME: SpecialSpamFerret PURPOSE: Special page for administering the SpamFerret database REQUIRES: SpamFerret (for now...) AUTHOR: Woozle (Nick) Staddon VERSION: 2009-08-04 0.0 (Wzl) Started writing 2009-10-01 0.1 (Wzl) incremental improvements; clsMenu now a separate file 2009-10-06 0.2 (Wzl) text-check now shows if matching filters are deactivated 2010-02-23 0.3 (Wzl) finally gave regular users some brief summary text to look at	2010-08-17 0.31 (Wzl) a bit of debugging display in the regex checker $wgSpecialPages['SpamFerret'] = 'SpecialSpamFerret'; # Let MediaWiki know about your new special page. $wgExtensionCredits['other'][] = array(       'name' => 'Special:SpamFerret',	'url' => 'http://htyp.org/SpamFerret',        'description' => 'special page for SpamFerret administration',        'author' => 'Woozle (Nick) Staddon',	'version' => '0.31 2010-08-17 alpha' ); define('KS_CHAR_URL_ASSIGN',':');	// character used for encoding values in wiki-internal URLs

function wfSpecialSpamFerret { // This registers the page's class. I think. global $wgRequest;

$app = new SpecialSpamFerret($wgRequest); }

require_once( $wgScriptPath.'includes/SpecialPage.php' ); if (!defined('LIBMGR')) { require('libmgr.php'); } clsLibMgr::Add('menus',		KFP_MW_PLUGINS.'/menu.php',__FILE__,__LINE__); clsLibMgr::Load('menus'		,__FILE__,__LINE__);

class SpecialSpamFerret extends SpecialPage { //======= // STATIC static private $objDB;

static public function Setting($iName) { global $wgSpamFerretSettings;

return $wgSpamFerretSettings[$iName]; }   static public function DB { if (!isset(self::$objDB)) { self::$objDB = new clsDatabase(self::Setting('dbspec')); self::$objDB->Open; }	return self::$objDB; }

//======= // DYNAMIC

protected $args;

public function __construct { global $wgMessageCache;

parent::__construct( 'SpamFerret' ); $this->includable( false ); $wgMessageCache->addMessage('spamferret', 'SpamFerret administration'); } function execute( $par ) { global $wgUser;

$this->setHeaders; $this->GetArgs($par);

if ($wgUser->isAllowed('editinterface')) { $this->doAdmin; } else { $this->doUser; } }  private function GetArgs($par) {

/* PURPOSE: Parses variable arguments from the URL The URL is formatted as a series of arguments /arg=val/arg=val/..., so that we can always refer directly to any particular item as a wiki page title while also not worrying about hierarchy/order. $args_raw = split('/',$par); foreach($args_raw as $arg_raw) { if (strpos($arg_raw,KS_CHAR_URL_ASSIGN) !== FALSE) { list($key,$val) = split(KS_CHAR_URL_ASSIGN,$arg_raw); $this->args[$key] = $val; }   }  }  public function doAdmin { global $wgOut; /*	PURPOSE: do stuff that only admins are allowed to do	if (isset($this->args['page'])) { $page = $this->args['page']; } else { $page = NULL; } // display menu $wtSelf = 'Special:'.$this->name; $objMenu = new clsMenu($wtSelf); $objMenu->Add($objRow = new clsMenuRow('Utilities','menu.util')); $objRow->Add(new clsMenuItem('check expression','ckexpr')); $objRow->Add(new clsMenuItem('check text','cktext')); $objMenu->Add($objRow = new clsMenuRow('Inspect','menu.insp')); $objRow->Add(new clsMenuItem('recent edits','edits')); $objRow->Add(new clsMenuItem('filters','filters')); $out = $objMenu->WikiText($page); $out .= $objMenu->Execute;

$wgOut->addHTML(' ');

if (!is_null($page)) { $id = isset($this->args['id'])?$this->args['id']:NULL; switch ($page) { case 'ckexpr': $this->doRegexChecker; break; case 'cktext': $this->doTextChecker; break; case 'edits': $this->doInspectEdits; break; case 'filters': $this->doInspectFilters; break; case 'filter': $this->doEditFilter($id); }	} }  public function doUser { global $wgOut; /*	PURPOSE: do only stuff that regular users are allowed to do //$wgOut->AddWikiText('Hello regular user! I haven\'t written anything for you yet, but eventually.'); $sql = 'SELECT MIN(`When`) AS WhenEarly, COUNT(ID) AS Count, didAllow FROM attempt GROUP BY didAllow'; $objRows = self::DB->DataSet($sql); if ($objRows->HasRows) { $utOldest = NULL; while ($objRows->NextRow) { $intCount = $objRows->Row['Count']; $utWhen = strtotime($objRows->Row['WhenEarly']); if (is_null($utOldest) || ($utWhen < $utOldest)) { $utOldest = $utWhen; }		if ($objRows->didAllow) { $outAllowed = $intCount.' edit'.Pluralize($intCount).' allowed'; } else { $outRejected = $intCount.' attempted spam'.Pluralize($intCount).' rejected'; }	   }	    $out = $outRejected.' and '.$outAllowed.' since '.date('F j, Y',$utOldest).'.'; } else { $out = 'No edit attempts recorded by SpamFerret yet!'; }	$wgOut->AddHTML($out); // URL needs to be broken up in order not to get filtered as spam >.< $wgOut->AddHTML(' See HTYP for all available documentation.'); } // individual admin functions public function doInspectEdits { global $wgOut,$wgRequest; $arOpts['filt'] = $wgRequest->getVal('sqlFilt',''); $arOpts['sort'] = $wgRequest->getVal('sqlSort','ID DESC'); $arOpts['rows'] = $wgRequest->getVal('sqlRows',50); // LATER: display form for changing these defaults

//$objTbl = //$objRows->

}   public function doEditFilter($iID) { global $wgOut;

$dbSP = self::DB; $tblFilt = new clsTable($dbSP); $tblFilt->Name('patterns'); $tblFilt->KeyName('ID'); $objRows = $tblFilt->GetItem($iID); if ($objRows->hasRows) { $htPattern = htmlspecialchars($objRows->Pattern); $out = ' '; $wgOut->AddHTML($out);	$out = ''; }   }    public function doInspectFilters { global $wgOut;

$dbSP = self::DB; $tblFilt = new clsTable($dbSP); $tblFilt->Name('patterns'); $tblFilt->KeyName('ID'); $objRows = $tblFilt->GetData; if ($objRows->hasRows) { $out = "{| class=sortable\n|-\n! ID || Pattern || A? || U? || R? || D? || Added || Tried || Count"; $isOdd = TRUE; while ($objRows->NextRow) { $wtStyle = $isOdd?'background:#ffffff;':'background:#eeeeee;'; $isOdd = !$isOdd;

$id = $objRows->ID; $wtID = SpIDSelfLink('filter','id',$id,$id); $strPatt = ' '.$objRows->Pattern.' '; $isActive = $objRows->isActive; $isURL = $objRows->isURL; $isRegex = $objRows->isRegex; $isDiff = $objRows->isDiff; $wtAdded = TimeStamp_HideTime($objRows->WhenAdded); $wtTried = TimeStamp_HideTime($objRows->WhenTried); $intCount = $objRows->Count;

$out .= "\n|- style=\"$wtStyle\"\n|$wtID||$strPatt||$isActive||$isURL||$isRegex||$isDiff||$wtAdded||$wtTried||align=right|$intCount"; }	   $out .= "\n|}\n"; $wgOut->AddWikiText($out); } else { $wgOut->AddHTML('No filters have been defined.'); if (!$dbSP->isOk) { $wgOut->AddHTML(' Database Error: '.$dbSP->getError); }	}   }    public function doRegexChecker { global $wgOut,$wgRequest;

$inExpr = $wgRequest->getVal('expr'); $inText = $wgRequest->getVal('sample');

$wgOut->AddHTML(''); $wgOut->AddHTML('Expression: '); $wgOut->AddHTML(' Text to check: '.htmlspecialchars($inText).' '); $wgOut->AddHTML(''); $wgOut->AddHTML(' '); $wgOut->AddWikiText('==Results=='); $wgOut->AddWikiText("checked: ".htmlspecialchars($inExpr),TRUE).' '; /*	// prefix any '/' characters with an escape ('\') because we are using the format which requires '/' at either end $chDelim = '/'; $strPattCk = str_replace($chDelim,'\\'.$chDelim,$inExpr); $isMatch = @preg_match('/'.$strPattCk.'/',$inText,$matches); // first 2 lines are a kluge until CheckRegex is static, returning results in array $objSF = new SpamFerret;

global $gRegexMatches,$strDbg; $isMatch = $objSF->CheckRegex($inExpr,$inText); $wgOut->AddWikiText($strDbg,TRUE); $matches = $gRegexMatches; if (isset($php_errormsg)) { $outErr .= "===Error===\n$php_errormsg"; $wgOut->AddWikiText($outErr); } else { $wgOut->AddWikiText("===Matches==="); $wgOut->AddHTML(' '.htmlspecialchars(var_export($matches,TRUE)).' '); }   }    public function doTextChecker { global $wgOut,$wgRequest; global $gFilterMatches,$gFilterCount,$gFilterRows; global $debug;

$inText = $wgRequest->getVal('sample'); $doEcho = $wgRequest->getVal('doShowText'); $htDoEcho = $doEcho?' checked':'';

$wgOut->AddHTML('Check sample text against all defined filters.'); $wgOut->AddHTML(''); $wgOut->AddHTML(' Text to check: '.$inText.' '); $wgOut->AddHTML(''); $wgOut->AddHTML('Echo input text'); $wgOut->AddHTML(' '); if ($inText != '') { $wgOut->AddWikiText('==Results==');

$objSF = new SpamFerret; $objSF->OpenDatabase; $objSF->txtEditRaw = $inText; $arArgs['doAll'] = TRUE; // LATER: allow user to enter title of existing page for generating diff $arArgs['diff'] = '!!NEW: '.$inText; if ($doEcho) { $out = "* checked:"; $out .= "\n** plain: ".htmlspecialchars($inText); $out .= "\n** diff: ".htmlspecialchars($arArgs['diff']); $wgOut->AddWikiText($out,TRUE); }	   $objSF->CheckFilters($arArgs); $out = "\n* ".$gFilterRows.' filter'.Pluralize($gFilterRows).' defined'. "\n* ".$gFilterCount.' filter'.Pluralize($gFilterCount).' checked'; $wgOut->AddWikiText($out); if (is_array($gFilterMatches)) { $out = "{|\n|-\n! ID || length || filter"; $isOdd = FALSE; foreach ($gFilterMatches as $id=>$text) {

$wtStyle = $isOdd?'background:#ffffff;':'background:#eeeeee;'; $isOdd = !$isOdd;

$objFilt = $objSF->FiltTbl->GetItem($id);

$isActive = $objFilt->isActive; if (!$isActive) { $wtStyle .= ' color: #888888;'; $wtStyle .= ' text-decoration: line-through;'; }

$out .= "\n|- style=\"$wtStyle\"\n| $id || ".strlen($text)." || {$objFilt->Pattern} "; }		$out .= "\n|}"; } else { $out = "* No filter matches"; }	   $wgOut->AddWikiText($out); //$wgOut->AddWikiText('* DEBUG: '.$debug); }   } /* LATER: for debugging why a particular string doesn't seem to be triggering the filter it should trigger public function CheckFiltersLocal($iCheckAll) { global $gRegexMatches,$gFilterMatches,$gFilterRows,$gFilterCount; global $debug;

$this->PatternTbl = new clsTable($this->dbSpam); $this->PatternTbl->Name('patterns'); $this->PatternTbl->KeyName('ID');

$this->PatternRows = $this->PatternTbl->GetData; $objRow = $this->PatternRows;

$strTextEdit = strtolower($this->txtEditRaw); $this->txtEditChk = $strTextEdit;	// text after being massaged for checking $this->isMatch = FALSE; $gFilterCount = 0; $gFilterRows = $this->PatternRows->RowCount; //$objDataPatterns->StartRows; while($objRow->NextRow && (!$this->isMatch || $iCheckAll)) { $isMatch = FALSE; if ($objRow->isDiff) { if (isset($this->txtDiff)) { $strTextCk = $this->txtDiff; } else { $strTextCk = NULL; }	   } else { $strTextCk = $strTextEdit; }	   if (!is_null($strTextCk)) { $gFilterCount++; $strPattern = $objRow->Pattern; $isRegex = $objRow->isRegex; $this->idPattern = $objRow->ID; if ($isRegex) { $isMatch = $this->CheckRegex($strPattern,$strTextCk);

if (isset($php_errormsg)) { $this->AddErrorLine('Filter #'.$this->idPattern.' generated error "'.$php_errormsg);		   }

if ($isMatch) { $this->strMatch = $gRegexMatches[0]; }		} else { if (empty($strPattern)) { $isMatch = FALSE; } else { $this->strMatch = stristr($strTextCk,$strPattern); $isMatch = ($this->strMatch != ''); }		}		if ($isMatch) { $this->isMatch = TRUE; if ($iCheckAll) { $gFilterMatches[$this->idPattern] = $this->strMatch; }		}	   }	}    } } class clsAttempts extends clsTable { public function __construct($iDB) { parent::__construct($iDB); $this->Name('attempt'); $this->KeyName('ID'); $this->ClassSng('clsAttempt'); }   public function GetRecent(array $iarOpts) { if (empty($iarOpts['filt'])) { $sqlFilt = ''; } else { $sqlFilt = ' WHERE '.$iarOpts['filt']; }

if (empty($iarOpts['sort'])) { $sqlSort = ''; } else { $sqlSort = ' ORDER BY '.$iarOpts['sort']; }

if (empty($iarOpts['rows'])) { $sqlRows = ''; } else { $sqlRows = ' LIMIT '.$iarOpts['rows']; }	$sql = 'SELECT * FROM attempts'.$sqlFilt.$sqlSort.$sqlRows; $objRows = $this->DataSet($sql); return $objRows; } } class clsAttempt extends clsDataSet { }

// UTILITY FUNCTIONS // function SpIDSelfLink($iDo,$iKey,$iVal,$iText) { //   return .$iText.; return .$iText.; } /* function SelfLink($iPage,$iKey,$iVal,$iText) { return .$iText.; } function ShowFlag($iName,$iVal,$iText) { $out = ''.$iText; return $out; }

if (!function_exists('TimeStamp_HideTime')) { //-- function TimeStamp_HideTime($iStamp) {

if (is_string($iStamp)) { $intStamp = strtotime($iStamp); } else if (is_int($iStamp)) { $intStamp = $iStamp; } else { $intStamp = NULL; }   if (!is_null($intStamp)) { return date('Y-m-d',$intStamp); } else { return NULL; } } //-- }