User:Arcticocean/cufilter.js

mw.loader.using(['mediawiki.util', 'jquery', 'jquery.ui'], function{	if (mw.config.get("wgNamespaceNumber")!==-1 || mw.config.get("wgCanonicalSpecialPageName")!=="CheckUser") return;	var $ = jQuery;	// We only want to do our thing on a CU edits page.	// Page source of CU results isn't particularly rich in semantic information; we use existance of an h4 for now ...	if ($("#checkuserresults > h4").length===0) return;	if (typeof CuHelperConfig === "undefined") CuHelperConfig = {};	var filterFormExpanded = typeof CuHelperConfig.filterFormExpanded !== "undefined" ? CuHelperConfig.filterFormExpanded : false;	var reasonToDefaultFilterFunction = CuHelperConfig.reasonToDefaultFilterFunction || function(reason){		var match = /\[\[\s*(?:WP|Wikipedia)\s*:\s*Sockpuppet investigations\/([^\/\]]*)(?:\/Archive)?\s*\]\]/.exec(reason);		if (match) return "mark username "+match[1];	};	var IpBlock = function(oc1,oc2,oc3,oc4,cidrbits) {		this.ip = ((oc1*256+oc2)*256+oc3)*256+oc4; this.mask = Math.pow(2, 32-cidrbits) - 1; }	IpBlock.prototype.Contains = function(ipstring){ var parser = new Parser(ipstring); parser.EatWsp; var ipblock = parser.EatIpBlock; if (ipblock===false) return false; //not an ip. should we do anythin here? parser.EatWsp; if (!parser.EOF) return false; if (ipblock.mask>this.mask) return false; //given range is larger than this return (this.ip|this.mask)==(ipblock.ip|this.mask); }	var cssRuleCounter = 0; var cssRules = {}; //tool-static container of css rules we created // quick and dirty parser of our filter grammar var Parser = function(text){ this.i = 0; this.text = text; this.l = text.length; }	Parser.prototype.EOF = function{return this.i>=this.l;} Parser.prototype.EatChar = function(c){ if (this.EOF) return false; var eaten = c === this.text.charAt(this.i); if (eaten) this.i++; return eaten; }	Parser.prototype.EatString = function(s){ var sl = s.length; if (this.i+sl-1 >= this.l) return false; //eof if (this.text.substring(this.i,this.i+sl)!==s) return false; this.i += sl; return true; }	Parser.prototype.EatStringDelimited = function(s){ var iold = this.i;		if (this.EatString(s)) {			if (this.EOF || this.text.charAt(this.i)==='\r' || this.text.charAt(this.i)==='\n' || this.text.charAt(this.i)===' ' || this.text.charAt(this.i)==='\t') return true; //we don't actually eat the EOL here. }		this.i = iold; return false; }	Parser.prototype.EatEOL = function{return this.EatChar('\r') || this.EatChar('\n');} Parser.prototype.EatStringIncluding = function(delimiters){return this.EatStringUntil(delimiters, true); } Parser.prototype.EatStringUntil = function(delimiters, including){ var iold = this.i;		if (typeof delimiters === "string") delimiters = [delimiters]; while (!this.EOF) {			var inow = this.i;			for (var j=0; j='0' && this.text.charAt(this.i)<='9') this.i++; if (this.i==iold) return false; return parseInt(this.text.substring(iold,this.i)); }	Parser.prototype.EatAction = function{ var simpleresult = false; var parser = this; $.each({"mark":"border-left:3px solid red;background-color:#fff0f0", "hide":"display:none", "show":"display:block", "red":"background-color:lightpink", "green":"background-color:lightgreen", "blue":"background-color:lightblue", "cyan":"background-color:lightcyan", "magenta":"background-color:#ffe0ff", "yellow":"background-color:#ff8"}, function(simpleaction,actioncss){			if (!parser.EatStringDelimited(simpleaction)) return; //continue			simpleresult = {action:"css", actionData: actioncss};			return false;		}); if (simpleresult!==false) return simpleresult;

var iold = this.i;		if (this.EatString("css")) {			this.EatWsp; if (this.EatChar('{')){ var css = this.EatStringUntil(['}', '\r', '\n']); if (this.EatChar('}') && this.EatWsp) return {action:"css", actionData: css}; }			this.i = iold; }		return false; }	Parser.prototype.EatRecursor = function{ if (this.EatStringDelimited("acs of") || this.EatStringDelimited("accounts of") || this.EatStringDelimited("users of") || this.EatStringDelimited("usernames of")) return "ac"; if (this.EatStringDelimited("ess of") || this.EatStringDelimited("summaries of") || this.EatStringDelimited("edit summaries of") || this.EatStringDelimited("editsummaries of")) return "es"; // probably not useful if (this.EatStringDelimited("ips of") || this.EatStringDelimited("ipaddresses of") || this.EatStringDelimited("ip addresses of")) return "ip"; if (this.EatStringDelimited("pgs of") || this.EatStringDelimited("pages of") || this.EatStringDelimited("pagenames of") || this.EatStringDelimited("page names of")) return "pg"; if (this.EatStringDelimited("uas of") || this.EatStringDelimited("agents of") || this.EatStringDelimited("useragents of") || this.EatStringDelimited("user agents of")) return "ua"; return false; }	Parser.prototype.EatMatchtype = function{ var matchtype; if (this.EatStringDelimited("all")) matchtype='all'; else if (this.EatStringDelimited("anon")) matchtype='anon'; else if (this.EatStringDelimited("blocked")) matchtype='blocked'; else if (this.EatStringDelimited("previously blocked") || this.EatStringDelimited("prevblocked")) matchtype='prevblocked'; else if (this.EatStringDelimited("ac") || this.EatStringDelimited("account") || this.EatStringDelimited("user") || this.EatStringDelimited("username")) matchtype='ac'; else if (this.EatStringDelimited("es") || this.EatStringDelimited("summary") || this.EatStringDelimited("edit summary") || this.EatStringDelimited("editsummary")) matchtype='es'; else if (this.EatStringDelimited("ip") || this.EatStringDelimited("ipaddress") || this.EatStringDelimited("ip address")) matchtype='ip'; else if (this.EatStringDelimited("pg") || this.EatStringDelimited("page") || this.EatStringDelimited("pagename") || this.EatStringDelimited("page name")) matchtype='pg'; else if (this.EatStringDelimited("ua") || this.EatStringDelimited("agent") || this.EatStringDelimited("useragent") || this.EatStringDelimited("user agent")) matchtype='ua'; //else if (this.EatStringDelimited("log")) matchtype='log'; //TODO: Log! else return false; this.EatWsp; var matchmodifier; if (this.EatStringDelimited("is")) matchmodifier = "is"; else if (this.EatStringDelimited("contains")) matchmodifier = "contains"; else if (this.EatStringDelimited("matches")||this.EatStringDelimited("regex")||this.EatStringDelimited("regexp")) matchmodifier = "matches"; else matchmodifier = "is"; return { type: matchtype, modifier: matchmodifier }; }	Parser.prototype.EatIpBlock = function{ //atm we only accept a single IP or a CIDR range var iold = this.i;		var b1,b2,b3,b4,cidrbits; b1 = this.EatNaturalNumber; if (b1===false || b1<0 || b1>255 || !this.EatChar('.')) { this.i=iold; return false; } b2 = this.EatNaturalNumber; if (b2===false || b2<0 || b2>255 || !this.EatChar('.')) { this.i=iold; return false; } b3 = this.EatNaturalNumber; if (b3===false || b3<0 || b3>255 || !this.EatChar('.')) { this.i=iold; return false; } b4 = this.EatNaturalNumber; if (b4===false || b4<0 || b4>255) { this.i=iold; return false; } if (this.EatChar('/')) cidrbits = this.EatNaturalNumber; else cidrbits = 32; if (cidrbits===false || cidrbits<0 || cidrbits>32) { this.i=iold; return false; } return new IpBlock(b1,b2,b3,b4,cidrbits); }	var Parse = function(text) {		if (text.length==0 || (text.substring(text.length-1, text.length)!='\r' && text.substring(text.length-1, text.length)!='\n')) text = text + '\n'; //simplify parsing: each significant line ends on linebreak var parser = new Parser(text); var results = []; var errors = []; var line = 1; while (!parser.EOF) {			while (true) {				var any = parser.EatWsp; if (parser.EatEOL) { line++; any = true; } if (!any) break; }			if (parser.EOF) break; if (parser.EatChar('#') || parser.EatChar('//')) { parser.EatStringIncluding(['\r', '\n']); line++; continue; } //comments

var action = parser.EatAction; if (action===false) { errors.push("Could not parse action in line "+line); parser.EatStringIncluding(['\r', '\n']); line++; continue; } var recursors = []; while (true) {				parser.EatWsp; var recursor = parser.EatRecursor; if (recursor===false) break; recursors.push(recursor); }			parser.EatWsp; var matchtype = parser.EatMatchtype; if (matchtype===false) { errors.push("Could not parse match type in line "+line); parser.EatStringIncluding(['\r', '\n']); line++; continue; } if (matchtype.type!=='all' && matchtype.type!=='anon' && matchtype.type!=='prevblocked' && matchtype.type!=='blocked') {				parser.EatWsp; var matchdata; if (matchtype.type=="ip") //ips are always in the same format. No regexp matches here, just plain IPs, CIDRS, and plain IP ranges. {					matchdata = parser.EatIpBlock; //TODO: Should also accept ranges, several CIDRs, comma separated, ... 					var reminder = $.trim(parser.EatStringUntil(['\r', '\n'])); if (reminder!=="") { errors.push("Invalid CIDR ip in line "+line); parser.EatStringIncluding(['\r', '\n']); line++; continue; } }				else if (matchtype.modifier==="matches") {					var regexpliteral = parser.EatStringUntil(['\r', '\n']); try { matchdata = new RegExp(regexpliteral); } catch(excp) { errors.push("Invalid regex in line "+line); parser.EatStringIncluding(['\r', '\n']); line++; continue; } }				else {					matchdata = parser.EatStringUntil(['\r', '\n']); }			}			results.push( {action:action, matchdata:matchdata, matchtype: matchtype, recursors:recursors} ); parser.EatEOL; line++; }		return { results: results, errors: errors };	};

//functions to gather the respective data from one checkuser result list item var gatherers = { //matchdata filters "ac":function($this){return $this.find("a.mw-userlink:first").text;}, "es":function($this){var es = $this.find(".comment"); if (es.length){ var s = es.text; return $.trim(s.substring(1, s.length-1)); } return "";}, "ip":function($this){return $this.find(">small>a:first").text;}, //TODO: XFF! Won't currently match. What logic makes sense here? Probably needs a separate filter for that ... "pg":function($this){return $this.find(">a:first").attr("title");}, "ua":function($this){return $this.find(".mw-checkuser-agent").text;}, //non-matchdata filters "all":function{return true;}, "anon":function($this){var ac = $this.find("a.mw-userlink:first").text; if (ac.indexOf(".")===-1) return false; return /^\d{1,3}\.\d{1,3}\.\d{1,3}\.(?:xxx|\d{1,3})$/.test(ac); }, "blocked":function($this){ return $this.find("strong > a").text === "Blocked"; }, "prevblocked":function($this){ return $this.find("strong > a").text === "Previously blocked"; } };

var HitTest = function($this, matchparas) {		var s = gatherers[matchparas.matchtype.type]($this); //non-matchdata filters if (s===true) return true; if (s===false) return false; //matchdata filters if (matchparas.matchtype.type==="ip") {			switch(matchparas.matchtype.modifier) {				case "isany": return matchparas.matchdata[s]===true; default: return matchparas.matchdata.Contains(s); }		}		else {			switch(matchparas.matchtype.modifier) {				case "is": return s === matchparas.matchdata; case "isany": return matchparas.matchdata[s]===true; case "contains": return s.indexOf(matchparas.matchdata)>-1; case "matches": return matchparas.matchdata.test(s); }		}	}	var DoMatch = function($this, matchparas, result, recursor, nextmatchparas) {		if (HitTest($this, matchparas)) {			if (typeof recursor === "undefined") {				result.push($this); }			else {				nextmatchparas.matchdata[gatherers[recursor]($this)]=true; }		}	}	var Execute = function(parsed) {		var $all = $("#checkuserresults > ul > li"); $all.removeClass; for (var lineindex=0; lineindex<parsed.length; lineindex++) {			var line = parsed[lineindex]; //Algorithm: //* iterate all CU results //* test each against matchtype and matchdata //* if it's a match, collect it. peek into recursor to know which result we need: if we plan to recurse, build a new match. Else collect the nodes //* While we have recursors, recurse //* Apply the action against all nodes we eventually collected var matchparas = line; var recursors = line.recursors; var result = []; do {				var recursor = recursors.pop; if (typeof recursor !== "undefined") {					var nextmatchparas = { matchtype:{ type:recursor, modifier:"isany" },						matchdata:{}, };				}				//Iterate all CU result items, and collect the matches -- either as nodes in result, or as matchdata in nextmatchparas $.each($all, function{ DoMatch($(this), matchparas, result, recursor, nextmatchparas); }); if (typeof recursor !== "undefined") matchparas = nextmatchparas; }			while (typeof recursor !== "undefined"); //Find or create class to use. // We try to force css specifity here so that later rules override earlier ones. // We use a hack for that (not sure if that works in all browsers): We specifiy the same class name multiple times to make it appear to be more highly specified. // E.g., a class corresponding with the second filter line might get the selector '#checkuserresults .cufilteraction2.cufilteraction2' and will thus // have higher specifity than '#checkuserresults .cufilteraction1' var classname = cssRules["{"+lineindex+"}"+line.action.actionData]; if (typeof classname === "undefined") {				classname = "cufilteraction"+cssRuleCounter++; mw.util.addCSS("#checkuserresults "+(new Array(lineindex+2).join("."+classname))+"{"+line.action.actionData+"}"); cssRules["{"+lineindex+"}"+line.action.actionData] = classname; }			//apply $.each(result, function{				$(this).addClass(classname);			}); }		//show only those headings that have visible children $("#checkuserresults > h4").each(function{$(this).toggle( $(this).next("ul").children("li:visible").length>0 ) }); }	var $filterform = $(" Result filter Filter (Ctrl+Enter) Apply filter on changes  "); var accordionParams = { autoHeight:false, clearStyle:true, collapsible: true, change: function(event, ui) { if(ui.newHeader.length > 0) $filterform.find("textarea").focus; } };	if (!filterFormExpanded) accordionParams.active = false; //start collapsed $filterform.accordion(accordionParams); var filterFunction = function{ text = $filterform.find("textarea").val; var parsedInput = Parse(text); $filterform.find("#cufiltererrors").empty; if (parsedInput.errors.length) $filterform.find("#cufiltererrors").text(parsedInput.errors.join('\n')); Execute(parsedInput.results); }	$filterform.find("textarea").keydown(function (e) { if (e.keyCode===10 || (e.ctrlKey && e.keyCode===13)) filterFunction; });

$filterform.find("button").click(function{ filterFunction; });

$filterform.data('timeout', null); $filterform.find("textarea").keyup(function{		if (!$filterform.find("#cufilterautoupdate:checked").val) return;		clearTimeout($(this).data('timeout'));		$(this).data('timeout', setTimeout(filterFunction, 300));	}); $filterform.insertAfter("#checkuserform"); try {		var defaultFilter = reasonToDefaultFilterFunction($("#checkuserform #checkreason").val); $filterform.find("textarea").val(defaultFilter); filterFunction; }	catch(e){}//ignore errors });