User:Suffusion of Yellow/batchtest-plus-core.js

/* * Adds a "Test against past hits" button to Special:AbuseFilter/test. * Useful for testing your changes to a filter without tediously checking * each old hit with Special:AbuseFilter/examine. * * Only the "user", "page", "before", and "after" fields are respected. */

// jshint esnext: false, esversion: 8 // (function {	/* globals $, mw, OO */	'use strict';

// If forking, PLEASE change this line. const API_USER_AGENT = "batchtest-plus/0.5 (https://en.wikipedia.org/wiki/User:Suffusion_of_Yellow/batchtest-plus.js)";

const DEFAULT_CONFIG = { "default" : { batchSize: 100, // Same as Special:AbuseFilter/test maxConcurrentRequests: 10, // Too many seems to cause random HTTP timeouts falsePositveTestFilter: false, // Filter at your wiki matching a random sample of edits enableFalseNegativeTest: false },		"en.wikipedia.org" : { falsePositiveTestFilter: 1201, enableFalseNegativeTest: true }	};

let config = { }, api;

function handleApiError(code, details) { if (typeof code != 'string') throw code; // Something went very wrong

if (code == "http" && details.textStatus == "abort") return { aborted: true }; // Aborted by user, not an error

return { error : (code == "http") ? "HTTP error: " + details.textStatus : "API returned error \"" + code + "\": " + details.error.info };	}

// Make API abuselog entry into something human-readable. function formatLogEntry(log) { let link = (target, text) => $('', {				href: mw.util.getUrl(target),				text: text			}); let sclink = (params, text) => $('', {				href: new mw.Uri(mw.config.get('wgScript')).extend(params),				text: text			}); let monthNamesShort = [ "", "Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec" ];

let t = log.timestamp; let date = parseInt(t.slice(8, 10)) + " " + monthNamesShort[parseInt(t.slice(5, 7))] + " " + t.slice(0, 4); let time = t.slice(11, 19);

let $li = $('').append(			"(", link("Special:AbuseLog/" + log.id, "details"), " | ",			link("Special:AbuseFilter/examine/log/" + log.id, "examine"), " | ",			log.revid ? link("Special:Diff/" + log.revid, "diff") : "diff", ") . . ",			link("Special:AbuseFilter/" + log.filter_id, log.filter_id).attr("title", log.filter),			" (", mw.html.escape(log.action), " -> ",			$(' ', {				text : log.result || "none",				class : "filter-highlighter-" + (log.result || "noaction")			}), ") . . ",			link(log.title, log.title),			" (", log.title.indexOf("Special:") !== 0 ? sclink( { title : log.title,						 action : "history" }, "hist") : "hist", " | ",			sclink( { title: "Special:AbuseLog",					 wpSearchTitle : log.title }, "log"), "); ",			mw.html.escape(time),			" . . ",			link("Special:Contributions/" + log.user, log.user),			" (", link("User talk:" + log.user, "talk"), " | ",			sclink( { title: "Special:AbuseLog",					 wpSearchUser : log.user }, "log"), ")"		);

$li.addClass("btp-maybematch");

return [date, $li]; }

async function doTest(abuselog, filter, stats, cb) { let pending = [];

for(let log of abuselog) { let idx = pending.length < config.maxConcurrentRequests ? pending.length : await Promise.race(pending);

if (idx === undefined) break; // Something went wrong with the last request

pending[idx] = api.post({				action : 'abusefiltercheckmatch',				filter : filter,				logid : log.id			}).catch(handleApiError) .then(response => {					let result;

if (response.aborted) return;

stats.tested++;

if (!response || !response.abusefiltercheckmatch) { stats.errors++; result = null; } else { result = response.abusefiltercheckmatch.result; }

if (result) stats.matches++;

if (cb) cb(log.id, result, response.error);

return idx; });		}

await Promise.all(pending);

return stats; }

async function testAtTestPage(filters, query, testFilter, action, stats) { if (!query) { // We got here because the user clicked "Test". // If they had clicked "continue", query would be defined.

stats = { tested : 0, errors: 0, matches : 0 };

query = { action : "query", list : "abuselog", aflprop : "ids|filter|user|title|action|result|timestamp|revid", afllimit : config.batchSize };

let user = $('[name="wpTestUser"]').val; let title = $('[name="wpTestPage"]').val; let after = $('[name="wpTestPeriodStart"]').val; let before = $('[name="wpTestPeriodEnd"]').val; testFilter = $('[name="wpFilterRules"]').val; action = $('[name="wpTestAction"]').val;

if (filters.length) query.aflfilter = filters; if (user.length) query.afluser = user; if (title.length) query.afltitle = title; if (before.length) query.aflstart = before; if (after.length) query.aflend = after;

// Cleanup last run, or the normal /test results $('.mw-changeslist, .btp-results, .btp-progress').remove;

mw.util.$content.append(' '); mw.util.$content.append(' '); }

let response = await api.get(query).catch(handleApiError);

if (!response || !response.query || !response.query.abuselog) { if (response.error) mw.notify(response.error); return; }

let abuselog = response.query.abuselog .filter((log) => (action === "0" || log.action.includes(action)));

let $results = $(' '), $loglines = {}; let prev = $(".btp-results").find('h4').last.text;

let $ul = $(''); $results.append($ul);

for(let log of abuselog) { let [date, $li] = formatLogEntry(log);

$loglines[log.id] = $li;

if (date != prev ) { prev = date;

$ul = $(''); $results.append($(' ', { text: date }), $ul); }

$ul.append($li); }

$('.btp-results').append($results);

await doTest(abuselog, testFilter, stats, (id, result, err) => {			$loglines[id].removeClass('btp-maybematch').removeAttr("title");

if (result === null) { $loglines[id].addClass('btp-error').attr("title", err); } else if (result === true) { $loglines[id].addClass('btp-match'); } else { $loglines[id].addClass('btp-nomatch'); }		});

let $summary = $(' ').append(            $('  ', { text: stats.matches + "/" + stats.tested + " match, " + stats.errors + " error(s)" })		);

if (response.continue) { $summary.append(				", ",               $('', { text: "continue?" }).click( => { $summary.remove;

query.aflstart = response.continue.aflstart; testAtTestPage(filters, query, testFilter, action, stats); })			);       }        $results.append($summary);

// For popups/markblocked/filter-highlighter/etc. mw.hook('wikipage.content').fire($results); }

async function testAtFilterEditor(filterRules, id) { let stats = { tested : 0, errors: 0, matches : 0 };

let query = { action : "query", list : "abuselog", aflprop : "ids|filter", afllimit : config.batchSize, aflfilter : id		};

$('.btp-progress').text("Fetching filter log...");

let response = await api.get(query).catch(handleApiError);

if (!response || !response.query || !response.query.abuselog || !response.query.abuselog.length) { $('.btp-progress').text("Failed to fetch filter log"); return; }

await doTest(response.query.abuselog, filterRules, stats, => {			$('.btp-progress').text( stats.matches + "/" + stats.tested + " match, " + stats.errors + " error(s) (Filter rule: "					+ response.query.abuselog[0].filter + ")" );		});	}

function setupFilterEditor { let $form = $("#mw-abusefilter-editing-form"); let $saveButton = $form.find("input[type=submit]");

let FPButton, FNButton;

if (config.falsePositiveTestFilter) { FPButton = new OO.ui.ButtonWidget({				label: 'FP check',				title: 'Check for false positives'			}).on("click", async => {				api.abort;				testAtFilterEditor($("#wpFilterRules").val, config.falsePositiveTestFilter);			}); }

let id = $form.attr("action") && $form.attr("action").match(/\d+$/);

if (config.enableFalseNegativeTest && id) { FNButton = new OO.ui.ButtonWidget({				label: 'FN check',				title: 'Check for false negatives'			}).on("click", => {				api.abort;				testAtFilterEditor($("#wpFilterRules").val, id[0]);			}); }

$saveButton.parent.after(			FPButton && FPButton.$element,			FNButton && FNButton.$element,			$(' ').html('What\'s this?')		);

$form.append($(' ')); }

function setupTestPage { let filterId = mw.config.get('wgPageName').match(/\/(\d+)$/); let filters = new OO.ui.TextInputWidget({			placeholder: "Filter IDs (separate with pipes)",			value: filterId && filterId[1]		});

let test = new OO.ui.ButtonWidget({			label: "Test"		}).on("click", => {			api.abort;			testAtTestPage(filters.getValue);		});

let cancel = new OO.ui.ButtonWidget({			label: "Cancel"		}).on("click", => {			api.abort;		});

let fieldset = new OO.ui.FieldsetLayout({			label: "Test against past hits"		}).addItems([new OO.ui.HorizontalLayout({ items: [filters, test, cancel] })]);

$('#wpFilterForm').append(fieldset.$element); }

function setup { Object.assign(config, DEFAULT_CONFIG['default']); Object.assign(config, DEFAULT_CONFIG[mw.config.get('wgServerName')]); if(window.batchTestPlusConfig) { Object.assign(config, window.batchTestPlusConfig['default']); Object.assign(config, window.batchTestPlusConfig[mw.config.get('wgServerName')]); }

api = new mw.Api( {			ajax: {				headers: {					'Api-User-Agent' : API_USER_AGENT				}			}		});

if (/\/test(\/\d+)?$/.test(mw.config.get('wgPageName'))) setupTestPage; else if ($('#mw-abusefilter-editing-form') &&				 (config.falsePositveTestFilter || config.enableFalseNegativeCheck)); setupFilterEditor; }

if (mw.config.get('wgCanonicalSpecialPageName') === 'AbuseFilter') { $.when($.ready,			  mw.loader.load("https://en.wikipedia.org/w/index.php?action=raw&title=User:Suffusion_of_Yellow/batchtest-plus.css&ctype=text/css", "text/css"),			   mw.loader.using( ['mediawiki.util', 'mediawiki.api', 'mediawiki.Uri', 'oojs-ui-core'])).then(setup); } }); //