User:Suffusion of Yellow/fdb-core.js
Appearance
Code that you insert on this page could contain malicious content capable of compromising your account. If you import a script from another page with "importScript", "mw.loader.load", "iusc", or "lusc", take note that this causes you to dynamically load a remote script, which could be changed by others. Editors are responsible for all edits and actions they perform, including by scripts. User scripts are not centrally supported and may malfunction or become inoperable due to software changes. A guide to help you find broken scripts is available. If you are unsure whether code you are adding to this page is safe, you can ask at the appropriate village pump. This code will be executed when previewing this page. |
![]() | Documentation for this user script can be added at User:Suffusion of Yellow/fdb-core. |
//<nowiki>
/* jshint esversion: 11, esnext: false */
/******/ (() => { // webpackBootstrap
/******/ "use strict";
/******/ // The require scope
/******/ var __webpack_require__ = {};
/******/
/************************************************************************/
/******/ /* webpack/runtime/define property getters */
/******/ (() => {
/******/ // define getter functions for harmony exports
/******/ __webpack_require__.d = (exports, definition) => {
/******/ for(var key in definition) {
/******/ if(__webpack_require__.o(definition, key) && !__webpack_require__.o(exports, key)) {
/******/ Object.defineProperty(exports, key, { enumerable: true, get: definition[key] });
/******/ }
/******/ }
/******/ };
/******/ })();
/******/
/******/ /* webpack/runtime/hasOwnProperty shorthand */
/******/ (() => {
/******/ __webpack_require__.o = (obj, prop) => (Object.prototype.hasOwnProperty.call(obj, prop))
/******/ })();
/******/
/******/ /* webpack/runtime/make namespace object */
/******/ (() => {
/******/ // define __esModule on exports
/******/ __webpack_require__.r = (exports) => {
/******/ if(typeof Symbol !== 'undefined' && Symbol.toStringTag) {
/******/ Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
/******/ }
/******/ Object.defineProperty(exports, '__esModule', { value: true });
/******/ };
/******/ })();
/******/
/************************************************************************/
var __webpack_exports__ = {};
// ESM COMPAT FLAG
__webpack_require__.r(__webpack_exports__);
// EXPORTS
__webpack_require__.d(__webpack_exports__, {
setup: () => (/* binding */ setup)
});
;// CONCATENATED MODULE: ./src/filter.js
class FilterEvaluator {
constructor(options) {
let blob = new Blob(['importScripts("https://en.wikipedia.org/w/index.php?title=User:Suffusion_of_Yellow/fdb-worker.js&action=raw&ctype=text/javascript");'], { type: "text/javascript" });
this.version = 0;
this.uid = 0;
this.callbacks = {};
this.status = options.status || (() => null);
this.workers = [];
this.threads = Math.min(Math.max(options.threads || 1, 1), 16);
this.status("Starting workers...");
let channels = [];
for (let i = 0; i < this.threads - 1; i++)
channels.push(new MessageChannel());
for (let i = 0; i < this.threads; i++) {
this.workers[i] = new Worker(URL.createObjectURL(blob), { type: 'classic' });
this.workers[i].onmessage = (event) => {
if (this.status && event.data.status)
this.status(event.data.status);
if (event.data.uid && this.callbacks[event.data.uid]) {
this.callbacks[event.data.uid](event.data);
delete this.callbacks[event.data.uid];
}
};
if (i == 0) {
if (this.threads > 1)
this.workers[i].postMessage({
action: "setsecondaries",
ports: channels.map(c => c.port1)
}, channels.map(c => c.port1));
} else {
this.workers[i].postMessage({
action: "setprimary",
port: channels[i - 1].port2
}, [channels[i - 1].port2]);
}
}
}
work(data, i = 0) {
return new Promise((resolve) => {
data.uid = ++this.uid;
this.callbacks[this.uid] = (data) => resolve(data);
this.workers[i].postMessage(data);
});
}
terminate() {
this.workers.forEach(w => w.terminate());
}
async getBatch(params) {
for (let i = 0; i < this.threads; i++)
this.work({
action: "clearallvardumps",
}, i);
let response = (await this.work({
action: "getbatch",
params: params,
stash: true
}));
this.batch = response.batch || [];
this.owners = response.owners;
return this.batch;
}
async getVar(name, id) {
let response = await this.work({
action: "getvar",
name: name,
vardump_id: id
}, this.owners[id]);
return response.vardump;
}
async getDiff(id) {
let response = await this.work({
action: "diff",
vardump_id: id
}, this.owners[id]);
return response.diff;
}
async createDownload(fileHandle, compress = true) {
let encoder = new TextEncoderStream() ;
let writer = encoder.writable.getWriter();
(async() => {
await writer.write("[\n");
for (let i = 0; i < this.batch.length; i++) {
let entry = {
...this.batch[i],
...{
details: await this.getVar("*", this.batch[i].id)
}
};
this.status(`Writing entries... (${i}/${this.batch.length})`);
await writer.write(JSON.stringify(entry, null, 2).replace(/^/gm, " "));
await writer.write(i == this.batch.length - 1 ? "\n]\n" : ",\n");
}
await writer.close();
})();
let output = encoder.readable;
if (compress)
output = output.pipeThrough(new CompressionStream("gzip"));
if (fileHandle) {
await output.pipeTo(await fileHandle.createWritable());
this.status(`Created ${(await fileHandle.getFile()).size} byte file`);
} else {
let compressed = await (new Response(output).blob());
this.status(`Created ${compressed.size} byte file`);
return URL.createObjectURL(compressed);
}
}
async evalBatch(text, scmode) {
if (!this.batch)
return [];
let version = ++this.version;
text = text.replaceAll("\r\n", "\n");
for (let i = 1; i < this.threads; i++)
this.work({
action: "setfilter",
filter_id: 1,
filter: text,
}, i);
let response = await this.work({
action: "setfilter",
filter_id: 1,
filter: text,
}, 0);
// Leftover response from last batch
if (this.version != version)
return [];
if (response.error)
throw response;
let promises = [], tasks = Array(this.threads).fill().map(() => []);
for (let entry of this.batch) {
let task = { entry };
promises.push(new Promise((resolve) => task.callback = resolve));
tasks[this.owners[entry.id]].push(task);
}
for (let i = 0; i < this.threads; i++)
(async() => {
for (let task of tasks[i]) {
let response = await this.work({
action: "evaluate",
filter_id: 1,
vardump_id: task.entry.id,
scmode: scmode
}, i);
if (this.version != version)
return;
task.callback(response);
}
})();
return promises;
}
}
;// CONCATENATED MODULE: ./src/parserdata.js
const parserData = {
functions: "bool|ccnorm_contains_all|ccnorm_contains_any|ccnorm|contains_all|contains_any|count|equals_to_any|float|get_matches|int|ip_in_range|ip_in_ranges|lcase|length|norm|rcount|rescape|rmdoubles|rmspecials|rmwhitespace|sanitize|set|set_var|specialratio|string|strlen|strpos|str_replace|str_replace_regexp|substr|ucase",
operators: "==?=?|!==?|!=|\\+|-|/|%|\\*\\*?|<=?|>=?|\\(|\\)|\\[|\\]|&|\\||\\^|!|:=?|\\?|;|,",
keywords: "contains|in|irlike|like|matches|regex|rlike|if|then|else|end",
variables: "accountname|action|added_lines|added_lines_pst|added_links|all_links|edit_delta|edit_diff|edit_diff_pst|file_bits_per_channel|file_height|file_mediatype|file_mime|file_sha1|file_size|file_width|global_user_editcount|global_user_groups|moved_from_age|moved_from_first_contributor|moved_from_id|moved_from_last_edit_age|moved_from_namespace|moved_from_prefixedtitle|moved_from_recent_contributors|moved_from_restrictions_create|moved_from_restrictions_edit|moved_from_restrictions_move|moved_from_restrictions_upload|moved_from_title|moved_to_age|moved_to_first_contributor|moved_to_id|moved_to_last_edit_age|moved_to_namespace|moved_to_prefixedtitle|moved_to_recent_contributors|moved_to_restrictions_create|moved_to_restrictions_edit|moved_to_restrictions_move|moved_to_restrictions_upload|moved_to_title|new_content_model|new_html|new_pst|new_size|new_text|new_wikitext|oauth_consumer|old_content_model|old_links|old_size|old_wikitext|page_age|page_first_contributor|page_id|page_last_edit_age|page_namespace|page_prefixedtitle|page_recent_contributors|page_restrictions_create|page_restrictions_edit|page_restrictions_move|page_restrictions_upload|page_title|removed_lines|removed_links|summary|timestamp|tor_exit_node|user_age|user_app|user_blocked|user_editcount|user_emailconfirm|user_groups|user_mobile|user_name|user_rights|user_type|wiki_language|wiki_name",
deprecated: "article_articleid|article_first_contributor|article_namespace|article_prefixedtext|article_recent_contributors|article_restrictions_create|article_restrictions_edit|article_restrictions_move|article_restrictions_upload|article_text|moved_from_articleid|moved_from_prefixedtext|moved_from_text|moved_to_articleid|moved_to_prefixedtext|moved_to_text",
disabled: "minor_edit|old_html|old_text"
};
;// CONCATENATED MODULE: ./src/Hit.js
/* globals mw */
function sanitizedSpan(text, classList) {
let span = document.createElement('span');
span.textContent = text;
if (classList)
span.classList = classList;
return span.outerHTML;
}
// @vue/component
/* harmony default export */ const Hit = ({
inject: ["shared"],
props: {
entry: {
type: Object,
required: true
},
type: {
type: String,
required: true
},
matchContext: {
type: Number,
default: 10
},
diffContext: {
type: Number,
default: 25
},
header: Boolean
},
data() {
return {
vars: {},
diff: []
};
},
computed: {
id() {
return this.entry.id;
},
difflink() {
return this.entry.filter_id == 0 ?
mw.util.getUrl("Special:Diff/" + this.entry.revid) :
mw.util.getUrl("Special:AbuseLog/" + this.entry.id);
},
userlink() {
return this.entry.filter_id == 0 ?
mw.util.getUrl("Special:Contribs/" + mw.util.wikiUrlencode(this.entry.user)) :
new mw.Uri(mw.config.get('wgScript')).extend({
title: "Special:AbuseLog",
wpSearchUser: this.entry.user
});
},
pagelink() {
return this.entry.filter_id == 0 ?
mw.util.getUrl("Special:PageHistory/" + mw.util.wikiUrlencode(this.entry.title)) :
new mw.Uri(mw.config.get('wgScript')).extend({
title: "Special:AbuseLog",
wpSearchTitle: this.entry.title
});
},
result() {
return JSON.stringify(this.entry.testresult.result, null, 2);
},
vardump() {
return JSON.stringify(this.vars || null, null, 2);
},
vartext() {
return JSON.stringify(this.vars?.[this.type.slice(4)] ?? null, null, 2);
},
matches() {
let html = "";
for (let log of this.entry.testresult.log || []) {
for (let matchinfo of log.details?.matches ?? []) {
let input = log.details.inputs[matchinfo.arg_haystack];
let start = Math.max(matchinfo.match[0] - this.matchContext, 0);
let end = Math.min(matchinfo.match[1] + this.matchContext, input.length);
let pre = (start == 0 ? "" : "...") + input.slice(start, matchinfo.match[0]);
let post = input.slice(matchinfo.match[1], end) + (end == input.length ? "" : "...");
let match = input.slice(matchinfo.match[0], matchinfo.match[1]);
html += '<div class="fdb-matchresult">' +
sanitizedSpan(pre) +
sanitizedSpan(match, "fdb-matchedtext") +
sanitizedSpan(post) +
'</div>';
}
}
return html;
},
prettydiff() {
let html = '<div class="fdb-diff">';
for (let i = 0; i < this.diff.length; i++) {
let hunk = this.diff[i];
if (hunk[0] == -1)
html += sanitizedSpan(hunk[1], "fdb-removed");
else if (hunk[0] == 1)
html += sanitizedSpan(hunk[1], "fdb-added");
else {
let common = hunk[1];
if (i == 0) {
if (common.length > this.diffContext)
common = "..." + common.slice(-this.diffContext);
} else if (i == this.diff.length - 1) {
if (common.length > this.diffContext)
common = common.slice(0, this.diffContext) + "...";
} else {
if (common.length > this.diffContext * 2)
common = common.slice(0, this.diffContext) + "..." + common.slice(-this.diffContext);
}
html += sanitizedSpan(common);
}
}
html += "</div>";
return html;
},
cls() {
if (!this.header)
return "";
if (this.entry.testresult === undefined)
return 'fdb-undef';
if (this.entry.testresult.error)
return 'fdb-error';
if (this.entry.testresult.result)
return 'fdb-match';
return 'fdb-nonmatch';
}
},
watch: {
id: {
handler() {
this.getAsyncData();
},
immediate: true
},
type: {
handler() {
this.getAsyncData();
},
immediate: true
}
},
methods: {
async getAsyncData() {
if (this.type == "vardump")
this.vars = await this.shared.evaluator.getVar("*", this.entry.id);
else if (this.type.slice(0, 4) == "var-")
this.vars = await this.shared.evaluator.getVar(this.type.slice(4), this.entry.id);
else {
this.vars = {};
if (this.type == "diff")
this.diff = await this.shared.evaluator.getDiff(this.entry.id);
else
this.diff = "";
}
}
},
template: `
<div class="fdb-hit" :class="cls">
<div v-if="header"><a :href="difflink">{{entry.time}}</a> | <a :href="userlink">{{entry.user}}</a> | <a :href="pagelink">{{entry.title}}</a></div><div v-if="entry.testresult && entry.testresult.error && (type == 'result' || type == 'matches')">{{entry.testresult.error}}</div>
<div v-else-if="entry.testresult && type == 'result'">{{result}}</div>
<div v-else-if="entry.testresult && type == 'matches'" v-html="matches"></div>
<div v-else-if="type == 'diff'" v-html="prettydiff"></div>
<div v-else-if="type == 'vardump'">{{vardump}}</div>
<div v-else-if="type != 'none' && type != 'matches' && type != 'result'">{{vartext}}</div>
</div>`
});
;// CONCATENATED MODULE: ./src/Batch.js
// @vue/component
/* harmony default export */ const Batch = ({
components: { Hit: Hit },
props: {
batch: {
type: Array,
required: true
},
dategroups: {
type: Array,
required: true
},
type: {
type: String,
required: true
}
},
emits: ['selecthit'],
data() {
return {
selectedHit: 0
};
},
methods: {
selectHit(hit) {
this.selectedHit = hit;
this.$refs["idx-" + this.selectedHit][0].$el.focus();
this.$emit('selecthit', this.selectedHit);
},
nextHit() {
this.selectHit((this.selectedHit + 1) % this.batch.length);
},
prevHit() {
this.selectHit((this.selectedHit - 1 + this.batch.length) % this.batch.length);
}
},
template: `
<div v-for="dategroup of dategroups" class="fdb-dategroup">
<div class="fdb-dateheader">{{dategroup.date}}</div>
<hit v-for="entry of dategroup.batch" tabindex="-1" @focus="selectHit(entry)" @keydown.arrow-down.prevent="nextHit" @keydown.arrow-up.prevent="prevHit" :key="batch[entry].id" :ref="'idx-' + entry" :entry="batch[entry]" :type="type" header></hit>
</div>
</div>
`
});
;// CONCATENATED MODULE: ./src/Editor.js
/* globals mw, ace */
// @vue/component
/* harmony default export */ const Editor = ({
props: {
wrap: Boolean,
ace: Boolean
},
emits: ["textchange"],
data() {
return {
session: null,
timeout: 0,
text: ""
};
},
watch: {
wrap() {
this.session.setOption("wrap", this.wrap);
},
ace() {
if (this.ace)
this.session.setValue(this.text);
else
this.text = this.session.getValue();
},
text() {
clearTimeout(this.timeout);
this.timeout = setTimeout(() => this.$emit('textchange', this.text), 50);
}
},
async mounted() {
let config = { ...parserData, aceReadOnly: false };
mw.config.set("aceConfig", config);
ace.config.set('basePath', mw.config.get('wgExtensionAssetsPath') + "/CodeEditor/modules/lib/ace");
let editor = ace.edit(this.$refs.aceEditor);
this.session = editor.getSession();
this.session.setMode("ace/mode/abusefilter");
this.session.setUseWorker(false);
ace.require('ace/range');
let observer = new ResizeObserver(() => editor.resize());
observer.observe(this.$refs.aceEditor);
this.session.setValue(this.text);
this.session.on("change", () => this.text = this.session.getValue());
},
methods: {
async loadFilter(id, revision, overwrite = true, status) {
if (!overwrite && this.text.trim() !== "")
return;
let filterText = "";
if (/^[0-9]+$/.test(id) && /^[0-9]+$/.test(revision)) {
try {
// Why isn't this possible through the API?
let title = `Special:AbuseFilter/history/${id}/item/${revision}?safemode=1&useskin=fallback&uselang=qqx`;
let url = mw.config.get('wgArticlePath').replace("$1", title);
let response = await fetch(url);
let text = await response.text();
let html = (new DOMParser()).parseFromString(text, "text/html");
let exported = html.querySelector('#mw-abusefilter-export textarea').value;
let parsed = JSON.parse(exported);
filterText = parsed.data.rules;
} catch (error) {
status(`Failed to fetch revision ${revision} of filter ${id}`);
return false;
}
} else {
try {
let filter = await (new mw.Api()).get({
action: "query",
list: "abusefilters",
abfstartid: id,
abflimit: 1,
abfprop: "pattern"
});
filterText = filter.query.abusefilters[0].pattern;
} catch (error) {
status(`Failed to fetch filter ${id}`);
return false;
}
}
this.text = filterText;
if (this.session)
this.session.setValue(this.text);
return true;
},
getPos(index) {
let len, pos = { row: 0, column: 0 };
while (index > (len = this.session.getLine(pos.row).length)) {
index -= len + 1;
pos.row++;
}
pos.column = index;
return pos;
},
clearAllMarkers() {
let markers = this.session.getMarkers();
for (let id of Object.keys(markers))
if (markers[id].clazz.includes("fdb-"))
this.session.removeMarker(id);
},
markRange(start, end, cls) {
let startPos = this.getPos(start);
let endPos = this.getPos(end);
let range = new ace.Range(startPos.row, startPos.column, endPos.row, endPos.column);
this.session.addMarker(range, cls, "text");
},
markRanges(batch) {
let ranges = {};
for (let hit of batch) {
for (let log of hit.testresult?.log ?? []) {
let key = `${log.start} ${log.end}`;
if (!ranges[key])
ranges[key] = {
start: log.start,
end: log.end,
total: 0,
tested: 0,
matches: 0,
errors: 0
};
ranges[key].total++;
if (log.error)
ranges[key].errors++;
else if (log.result !== undefined)
ranges[key].tested++;
if (log.result)
ranges[key].matches++;
for (let match of log.details?.matches ?? []) {
for (let regexRange of match.ranges ?? []) {
let key = `${regexRange.start} ${regexRange.end}`;
if (!ranges[key])
ranges[key] = {
start: regexRange.start,
end: regexRange.end,
regexmatch: true
};
}
}
}
}
this.clearAllMarkers();
for (let range of Object.values(ranges)) {
let cls = "";
if (range.regexmatch)
cls = "fdb-regexmatch";
else if (range.errors > 0)
cls = "fdb-evalerror";
else if (range.tested == 0)
cls = "fdb-undef";
else if (range.matches == range.tested)
cls = "fdb-match";
else if (range.matches > 0)
cls = "fdb-match1";
else
cls = "fdb-nonmatch";
this.markRange(range.start, range.end, "fdb-ace-marker " + cls);
}
},
markParseError(error) {
this.markRange(error.start, error.end, "fdb-ace-marker fdb-parseerror");
}
},
template: `
<div class="fdb-ace-editor mw-abusefilter-editor" v-show="ace" ref="aceEditor"></div>
<textarea class="fdb-textbox-editor" v-show="!ace" v-model="text"></textarea>
`
});
;// CONCATENATED MODULE: ./src/Main.js
/* globals mw, Vue */
const validURLParams = ["mode", "logid", "revids", "filter", "limit", "user",
"title", "start", "end", "namespace", "tag", "show"];
const validParams = [...validURLParams, "expensive", "file"];
// @vue/component
/* harmony default export */ const Main = ({
components: { Hit: Hit, Editor: Editor, Batch: Batch },
inject: ["shared"],
provide() {
return {
shared: this.shared
};
},
data() {
let state = {
ace: true,
wrap: false,
loadableFilter: "",
mode: "recentchanges",
logid: "",
revids: "",
filter: "",
limit: "",
user: "",
title: "",
start: "",
end: "",
namespace: "",
tag: "",
show: "",
file: null,
expensive: false,
shortCircuit: true,
showMatches: true,
showNonMatches: true,
showErrors: true,
showUndef: true,
markAll: true,
showAdvanced: false,
threads: navigator.hardwareConcurrency || 2,
fullscreen: false,
topSelect: "diff",
bottomSelect: "matches",
varnames: [],
text: "",
timeout: null,
batch: [],
dategroups: [],
selectedHit: 0,
status: "",
statusTimeout: null,
filterRevisions: [],
filterRevision: "",
shared: Vue.shallowRef({ })
};
return { ...state, ...this.getParams() };
},
watch: {
fullscreen() {
if (this.fullscreen)
this.$refs.wrapper.requestFullscreen();
else if (document.fullscreenElement)
document.exitFullscreen();
},
markAll() {
this.markRanges();
},
shortCircuit() {
this.updateText();
},
async loadableFilter() {
let response = await (new mw.Api()).get({
action: "query",
list: "logevents",
letype: "abusefilter",
letitle: `Special:AbuseFilter/${this.loadableFilter}`,
leprop: "user|timestamp|details",
lelimit: 500
});
this.filterRevisions = (response?.query?.logevents ?? []).map(item => ({
timestamp: item.timestamp,
user: item.user,
id: item.params.historyId ?? item.params[0]
}));
}
},
beforeMount() {
this.startEvaluator();
},
async mounted() {
this.varnames = parserData.variables.split("|");
this.getBatch();
addEventListener("popstate", () => {
Object.assign(this, this.getParams());
this.getBatch();
});
document.addEventListener("fullscreenchange", () => {
this.fullscreen = !!document.fullscreenElement;
});
},
methods: {
getParams() {
let params = {}, rest = mw.config.get('wgPageName').split('/');
for (let i = 2; i < rest.length - 1; i += 2)
if (validURLParams.includes(rest[i]))
params[rest[i]] = rest[i + 1];
for (let [param, value] of (new URL(window.location)).searchParams)
if (validURLParams.includes(param))
params[param] = value;
if (!params.mode) {
if (params.filter || params.logid)
params.mode = "abuselog";
else if (params.revid || params.title || params.user)
params.mode = "revisions";
else if (Object.keys(params).length > 0)
params.mode = "recentchanges";
else {
// Nothing requested, just show a quick "demo"
params.mode = "abuselog";
params.limit = 10;
}
}
return params;
},
getURL(params) {
let url = mw.config.get("wgArticlePath").replace("$1", "Special:BlankPage/FilterDebug");
for (let param of validURLParams)
if (params[param] !== undefined) {
let encoded = mw.util.wikiUrlencode(params[param]).replaceAll("/", "%2F");
url += `/${param}/${encoded}`;
}
return url;
},
async getCacheSize() {
let size = 1000;
if (typeof window.FilterDebuggerCacheSize == 'number')
size = window.FilterDebuggerCacheSize;
// Storing "too much data" migh cause the browser to decide that this site is
// "abusing" resources and delete EVERYTHING, including data stored by other scripts
if (size > 5000 && !(await navigator.storage.persist()))
size = 5000;
return size;
},
async getBatch() {
let params = {};
for (let param of validParams) {
let val = this[param];
if (val === undefined || val === "")
continue;
params[param] = val;
}
params.cacheSize = await this.getCacheSize();
if (this.getURL(params) != this.getURL(this.getParams()))
window.history.pushState(params, "", this.getURL(params));
if (params.filter && params.filter.match(/^[0-9]+$/))
this.$refs.editor.loadFilter(params.filter, null, false, this.updateStatus);
let batch = await this.shared.evaluator.getBatch(params);
this.batch = [];
this.dategroups = [];
for (let i = 0; i < batch.length; i++) {
let d = new Date(batch[i].timestamp);
let date = `${d.getUTCDate()} ${mw.language.months.names[d.getUTCMonth()]} ${d.getUTCFullYear()}`;
let time = `${("" + d.getUTCHours()).padStart(2, "0")}:${("" + d.getUTCMinutes()).padStart(2, "0")}`;
let entry = { ...batch[i], date, time };
if (this.dategroups.length == 0 || date != this.dategroups[this.dategroups.length - 1].date) {
this.dategroups.push({
date,
batch: [i]
});
} else {
this.dategroups[this.dategroups.length - 1].batch.push(i);
}
this.batch.push(entry);
}
if (params.logid && this.batch.length)
this.$refs.editor.loadFilter(this.batch[0].filter_id, null, false, this.updateStatus);
this.updateText();
},
loadFilter() {
this.$refs.editor.loadFilter(this.loadableFilter, this.filterRevision, true, this.updateStatus);
},
startEvaluator() {
if (this.shared.evaluator)
this.shared.evaluator.terminate();
this.shared.evaluator = new FilterEvaluator({
threads: this.threads,
status: this.updateStatus
});
},
updateStatus(status) {
this.status = status;
if (this.statusTimeout === null)
this.statusTimeout = setTimeout(() => {
this.statusTimeout = null;
// Vue takes takes waaaay too long to update a simple line of text...
this.$refs.status.textContent = this.status;
}, 50);
},
async restart() {
this.startEvaluator();
await this.getBatch();
this.updateText();
},
async clearCache() {
try {
await window.caches.delete("filter-debugger");
this.updateStatus("Cache cleared");
} catch (e) {
this.updateStatus("No cache found");
}
},
selectHit(hit) {
this.selectedHit = hit;
this.markAll = false;
this.markRanges();
},
markRanges() {
this.$refs.editor.markRanges(
this.markAll ?
this.batch :
this.batch.slice(this.selectedHit, this.selectedHit + 1));
},
async updateText(text) {
if (text !== undefined)
this.text = text;
this.$refs.editor.clearAllMarkers();
let promises = [];
let startTime = performance.now();
let evaluated = 0;
let matches = 0;
let errors = 0;
try {
promises = await this.shared.evaluator
.evalBatch(this.text, this.shortCircuit ? "blank" : "allpaths");
} catch (error) {
if (typeof error.start == 'number' && typeof error.end == 'number') {
this.updateStatus(error.error);
this.batch.forEach(entry => delete entry.testresult);
this.$refs.editor.markParseError(error);
return;
} else {
throw error;
}
}
for (let i = 0; i < promises.length; i++)
promises[i].then(result => {
this.batch[i].testresult = result;
evaluated++;
if (result.error)
errors++;
else if (result.result)
matches++;
this.updateStatus(`${matches}/${evaluated} match, ${errors} errors, ${((performance.now() - startTime) / evaluated).toFixed(2)} ms avg)`);
});
await Promise.all(promises);
this.markRanges();
},
setFile(event) {
if (event.target?.files?.length) {
this.file = event.target.files[0];
this.getBatch();
} else {
this.file = null;
}
},
async download() {
if (window.showSaveFilePicker) {
let handle = null;
try {
handle = await window.showSaveFilePicker({ suggestedName: "dump.json.gz" });
} catch (error) {
this.updateStatus(`Error opening file: ${error.message}`);
return;
}
if (handle)
this.shared.evaluator.createDownload(handle, /\.gz$/.test(handle.name));
} else {
let hidden = this.$refs.hiddenDownload;
let name = prompt("Filename", "dump.json.gz");
if (name !== null) {
hidden.download = name;
hidden.href = await this.shared.evaluator.createDownload(null, /\.gz$/.test(name));
hidden.click();
}
}
},
resize(event, target, dir) {
let start = dir == 'x' ?
target.clientWidth + event.clientX :
target.clientHeight + event.clientY;
let move = dir == 'x' ?
((event) => target.style.width = (start - event.clientX) + "px") :
((event) => target.style.height = (start - event.clientY) + "px");
let stop = () =>
document.body.removeEventListener("mousemove", move);
document.body.addEventListener("mousemove", move);
document.body.addEventListener("mouseup", stop, { once: true });
document.body.addEventListener("mouseleave", stop, { once: true });
}
},
template: `
<div class="fdb-wrapper" ref="wrapper">
<div class="fdb-first-col">
<div class="fdb-panel fdb-editor">
<editor ref="editor" :ace="ace" :wrap="wrap" @textchange="updateText"></editor>
</div>
<div class="fdb-panel">
<div class="fdb-status" ref="status">Waiting...</div>
</div>
<div class="fdb-panel fdb-controls" ref="controls">
<div>
<label><input type="checkbox" v-model="wrap"> Wrap</label>
<label><input type="checkbox" v-model="ace"> ACE</label>
<label><input type="checkbox" v-model="fullscreen"> FS</label>
<input type="text" size="4" v-model.lazy.trim="loadableFilter">
<select class="fdb-filter-revision" v-model="filterRevision">
<option value="">(cur)</option>
<option v-for="rev of filterRevisions" :value="rev.id">{{rev.timestamp}}</option>
</select>
<button @click="loadFilter">Load filter</button>
</div>
<div>
<select v-model="mode">
<option value="abuselog">Abuse log</option>
<option value="recentchanges">Recent changes</option>
<option value="revisions">Revisions</option>
<option value="file">Local file</option>
</select>
<button @click="getBatch">Fetch data</button>
<button @click="download" :disabled="mode == 'file' || !batch.length">Save...</button>
<a style="display:none;" download="dump.json.gz" ref="hiddenDownload"></a>
<span v-show="mode == 'recentchanges' || mode == 'revisions'">
<label><input type="checkbox" v-model="expensive"> Fetch slow vars</label>
</span>
<span v-show="mode == 'file'">
<label>File <input type="file" accept=".json,.json.gz" @change="setFile"></label>
</span>
</div>
<div>
<label>Limit <input type="text" size="5" placeholder="100" v-model.trim.lazy="limit"></label>
<span v-show="mode == 'abuselog'">
<label>Filters <input type="text" size="10" v-model.trim.lazy="filter"></label>
</span>
<span v-show="mode == 'recentchanges' || mode == 'revisions'">
<label>Namespace <input type="text" size="4" v-model.trim.lazy="namespace"></label>
<label>Tag <input type="text" size="10" v-model.trim.lazy="tag"></label>
</span>
</div>
<div>
<label>User <input type="text" size="12" v-model.trim.lazy="user"></label>
<label>Title <input type="text" size="12" v-model.trim.lazy="title"></label>
<span v-show="mode == 'abuselog'">
<label>Log ID <input type="text" size="9" v-model.trim.lazy="logid"></label>
</span>
<span v-show="mode == 'revisions'">
<label>Rev ID <input type="text" size="9" v-model.trim.lazy="revids"></label>
</span>
</div>
<div>
<label>After <input type="text" size="12" v-model.trim.lazy="end"></label>
<label>Before <input type="text" size="12" v-model.trim.lazy="start"></label>
<span v-show="mode == 'recentchanges' || mode == 'revisions'">
<label>Show <input type="text" size="7" v-model.trim.lazy="show"></label>
</span>
</div>
<div>
<label><input type="checkbox" v-model="showMatches"> Matches</label>
<label><input type="checkbox" v-model="showNonMatches"> Non-matches</label>
<label><input type="checkbox" v-model="showUndef"> Untested</label>
<label><input type="checkbox" v-model="showErrors"> Errors</label>
<label><input type="checkbox" v-model="markAll"> Mark all</label>
<a style="float: right;" v-if="!showAdvanced" @click="showAdvanced=true">[more]</a>
</div>
<div v-show="showAdvanced">
<label>Threads <input type="number" min="1" max="16" size="2" v-model="threads"></label>
<button @click="restart">Restart worker</button>
<button @click="clearCache">Clear cache</button>
<label><input type="checkbox" v-model="shortCircuit"> Quick eval</label>
<a style="float: right;" @click="showAdvanced=false">[less]</a>
</div>
</div>
</div>
<div class="fdb-column-resizer" @mousedown.prevent="resize($event, $refs.secondCol, 'x')"></div>
<div class="fdb-second-col" ref="secondCol">
<div class="fdb-panel fdb-selected-result" v-show="topSelect != 'none'">
<hit v-if="batch.length" :entry="batch[selectedHit]" :type="topSelect"></hit>
</div>
<div class="fdb-row-resizer" @mousedown.prevent="resize($event, $refs.batchPanel, 'y')"></div>
<div class="fdb-panel">
↑ <select class="fdb-result-select" v-model="topSelect">
<option value="none">(none)</option>
<option value="result">(result)</option>
<option value="matches">(matches)</option>
<option value="diff">(diff)</option>
<option value="vardump">(vardump)</option>
<option v-for="name of varnames" :value="'var-' + name">{{name}}</option>
</select>
↓ <select class="fdb-result-select" v-model="bottomSelect">
<option value="none">(none)</option>
<option value="result">(result)</option>
<option value="diff">(diff)</option>
<option value="matches">(matches)</option>
<option v-for="name of varnames" :value="'var-' + name">{{name}}</option>
</select>
</div>
<div class="fdb-row-resizer" @mousedown.prevent="resize($event, $refs.batchPanel, 'y')"></div>
<div class="fdb-panel fdb-batch-results" ref="batchPanel" :class="{'fdb-show-matches': showMatches, 'fdb-show-nonmatches': showNonMatches, 'fdb-show-errors': showErrors, 'fdb-show-undef': showUndef}" v-show="bottomSelect != 'none'">
<batch :batch="batch" :dategroups="dategroups" :type="bottomSelect" @selecthit="selectHit"></batch>
</div>
</div>
</div>
`
});
;// CONCATENATED MODULE: ./style/ui.css
const ui_namespaceObject = ".fdb-ace-marker {\n position: absolute;\n}\n.fdb-batch-results .fdb-hit {\n border-width: 0px 0px 1px 0px;\n border-style: solid;\n}\n.fdb-batch-results .fdb-hit:focus {\n outline: 2px inset black;\n border-style: none;\n}\n.fdb-match {\n background-color: #DDFFDD;\n}\n.fdb-match1 {\n background-color: #EEFFEE;\n}\n.fdb-nonmatch {\n background-color: #FFDDDD;\n}\n.fdb-undef {\n background-color: #CCCCCC;\n}\n.fdb-error {\n background-color: #FFBBFF;\n}\n.fdb-regexmatch {\n background-color: #AAFFAA;\n outline: 1px solid #00FF00;\n}\n\n.fdb-filter-revision {\n width: 15em;\n}\n\n.fdb-controls div {\n padding: 2px;\n}\n\n.fdb-batch-results .fdb-match, .fdb-batch-results .fdb-nonmatch, .fdb-batch-results .fdb-undef, .fdb-batch-results .fdb-error {\n padding-left: 25px;\n background-repeat: no-repeat;\n background-position: left center;\n}\n\n.fdb-batch-results .fdb-match {\n background-image: url(https://upload.wikimedia.org/wikipedia/en/thumb/f/fb/Yes_check.svg/18px-Yes_check.svg.png);\n}\n\n.fdb-batch-results .fdb-nonmatch {\n background-image: url(https://upload.wikimedia.org/wikipedia/commons/thumb/b/ba/Red_x.svg/18px-Red_x.svg.png);\n}\n\n.fdb-batch-results .fdb-undef {\n background-image: url(https://upload.wikimedia.org/wikipedia/en/thumb/e/e0/Symbol_question.svg/18px-Symbol_question.svg.png);\n}\n\n.fdb-batch-results .fdb-error {\n background-image: url(https://upload.wikimedia.org/wikipedia/en/thumb/b/b4/Ambox_important.svg/18px-Ambox_important.svg.png);\n}\n\n.fdb-matchedtext {\n font-weight: bold;\n background-color: #88FF88;\n}\n\n.fdb-parseerror, .fdb-parseerror {\n background-color: #FFBBFF;\n outline: 1px solid #FF00FF;\n}\n\n.fdb-outer {\n height: 95vh;\n width: 100%;\n}\n.fdb-wrapper {\n height: 100%;\n width: 100%;\n display: flex;\n background: #F8F8F8;\n\n}\n.fdb-first-col {\n display: flex;\n flex-direction: column;\n flex: 1;\n margin: 2px;\n}\n.fdb-column-resizer {\n display: flex;\n width: 0px;\n padding: 0.5em;\n margin: -0.5em;\n cursor: col-resize;\n z-index: 0;\n}\n.fdb-row-resizer {\n display: flex;\n height: 0px;\n padding: 0.5em;\n margin: -0.5em;\n cursor: row-resize;\n z-index: 0;\n}\n\n.fdb-second-col {\n display: flex;\n flex-direction: column;\n width: 45%;\n height: 100%;\n margin: 2px;\n}\n.fdb-panel {\n border: 1px solid black;\n background: white;\n padding: 2px;\n width: 100%;\n box-sizing: border-box;\n margin: 2px;\n}\n.fdb-selected-result {\n overflow: auto;\n flex: 1;\n word-wrap: break-word;\n font-family: monospace;\n white-space: pre-wrap;\n word-wrap: break-word;\n}\n.fdb-batch-results {\n overflow: auto;\n height: 75%;\n word-wrap: break-word;\n}\n.fdb-status {\n float: right;\n font-style: italic;\n}\n\n.fdb-result-select {\n display: inline;\n width: 40%;\n overflow: hidden;\n}\n.fdb-ace-editor, .fdb-textbox-editor {\n width: 100%;\n height: 100%;\n display: block;\n resize: none;\n}\n.fdb-editor {\n flex-basis: 20em;\n flex-grow: 1;\n}\ndiv.mw-abusefilter-editor {\n height: 100%;\n}\n.fdb-controls {\n flex-basis: content;\n}\n.fdb-filtersnippet {\n background: #DDD;\n}\n.fdb-matchresult {\n font-family: monospace;\n font-size: 12px;\n line-height: 17px;\n}\n.fdb-dateheader {\n position: sticky;\n top: 0px;\n font-weight: bold;\n background-color: #F0F0F0;\n border-width: 0px 0px 1px 0px;\n border-style: solid;\n border-color: black;\n}\n\n.fdb-diff {\n background: white;\n}\n.fdb-added {\n background: #D8ECFF;\n font-weight: bold;\n}\n.fdb-removed {\n background: #FEECC8;\n font-weight: bold;\n}\n\n@supports selector(.fdb-dateheader:has(~ .fdb-match)) {\n .fdb-dateheader {\n\tdisplay: none;\n }\n .fdb-show-matches .fdb-dateheader:has(~ .fdb-match) {\n\tdisplay: block;\n }\n .fdb-show-nonmatches .fdb-dateheader:has(~ .fdb-nonmatch) {\n\tdisplay: block;\n }\n .fdb-show-errors .fdb-dateheader:has(~ .fdb-error) {\n\tdisplay: block;\n }\n .fdb-show-undef .fdb-dateheader:has(~ .fdb-undef) {\n\tdisplay: block;\n }\n}\n\n.fdb-batch-results .fdb-match {\n display: none;\n}\n.fdb-batch-results .fdb-nonmatch {\n display: none;\n}\n.fdb-batch-results .fdb-error {\n display: none;\n}\n.fdb-batch-results .fdb-undef {\n display: none;\n}\n\n.fdb-show-matches .fdb-match {\n display: block;\n}\n.fdb-show-nonmatches .fdb-nonmatch {\n display: block;\n}\n.fdb-show-errors .fdb-error {\n display: block;\n}\n.fdb-show-undef .fdb-undef {\n display: block;\n}\n";
;// CONCATENATED MODULE: ./src/ui.js
/* globals mw, Vue */
function setup() {
mw.util.addCSS(ui_namespaceObject);
if (typeof Vue.configureCompat == 'function')
Vue.configureCompat({ MODE: 3 });
document.getElementById('firstHeading').innerText = document.title = "Debugging edit filter";
document.getElementById("mw-content-text").innerHTML = '<div class="fdb-outer"></div>';
Vue.createApp(Main).mount(".fdb-outer");
}
window.FilterDebugger = __webpack_exports__;
/******/ })()
;//</nowiki>