User:Suffusion of Yellow/effp-helper-dev.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/effp-helper-dev. |
/*
* WP:EF/FP helper (development version)
*
* Do not import this directly. Use [[User:Suffusion of Yellow/effp-helper.js]] instead.
*/
// jshint esnext: false, esversion: 8
// <nowiki>
(function() {
'use strict';
let userMessages = {
"en": {
'effp-makeedit': "Make edit",
'effp-makeedit-more': "Perform edit on behalf of user",
'effp-pagedeleted': "Page deleted",
'effp-pagedeleted-more': "Page should be undeleted first, for attribution",
'effp-pagenevercreated': "Page does not exist",
'effp-pagemoved': "Page moved",
'effp-pagemoved-more': "Page moved from \"$1\" to \"$2\"",
'effp-sigsfound': "Contains signatures",
'effp-editmade': "Edit already made by $1",
'effp-editmadecurrent': "Edit already made by $1 and is current",
'effp-nolateredits': "No later edits",
'effp-somelateredits': "$1 later {{PLURAL:$1|edit|edits}}",
'effp-manylateredits': "At least $1 later edits",
'effp-editsby': "Edits by $1",
'effp-diff': "diff",
'effp-noconflict': "No edit conflict",
'effp-editconflict': "Edit conflict",
'effp-whatsthis': "What's this?"
}
};
let siteMessages = {
"en": {
'effp-spam' : "([[:en:User:Suffusion of Yellow/effp-helper|effp-helper]])",
'effp-sig-user': "[[User:$1|$1]] ([[User talk:$1#top|talk]])",
'effp-sig-ip': "[[Special:Contribs/$1|$1]] ([[User talk:$1#top|talk]])",
'effp-sig-timestamp' : "$1, $2 $3 $4 (UTC)",
'effp-sum': "Edit made on behalf of $1 because it was [[Special:AbuseLog/$2|disallowed]] by an edit filter $3",
'effp-sum-withorigsum': "Edit made on behalf of $1 because it was [[Special:AbuseLog/$2|disallowed]] by an edit filter. Original summary was \"$3\" $4",
}
};
function getLogId() {
switch (mw.config.get('wgCanonicalSpecialPageName')) {
case "AbuseLog":
let match = mw.config.get('wgTitle').match(/\/([0-9]+)$/);
return match ? match[1] : null;
case "AbuseFilter":
// Don't run when examining a recent change
let examine = mw.config.get('abuseFilterExamine');
return examine.type == "log" ? examine.id : null;
default:
return null;
}
}
async function SHA1(str) {
let hb = byte => byte.toString(16).padStart(2, "0");
let buf = await crypto.subtle.digest('SHA-1', new TextEncoder().encode(str));
return [...(new Uint8Array(buf))].map(hb).join("");
}
function formatTS(unixTime) {
return (new Date(unixTime * 1000)).toISOString().slice(0, 19) + "Z";
}
function formatTSForSig(unixTime) {
let d = new Date(unixTime * 1000);
return mw.msg('effp-sig-timestamp',
d.toISOString().slice(11, 16),
d.getUTCDate(),
mw.config.get('wgMonthNames')[d.getUTCMonth() + 1],
d.getUTCFullYear());
}
function formatSig(user) {
return mw.util.isIPAddress(user) ? mw.msg('effp-sig-ip', user) : mw.msg('effp-sig-user', user);
}
function buildSummary(user, id, origsum) {
if (!origsum.length)
return mw.msg('effp-sum', formatSig(user), id, mw.msg('effp-spam'));
let space = 500 - mw.msg('effp-sum-withorigsum', formatSig(user), id, "").length;
if (origsum.length > space)
origsum = origsum.slice(0, space - 3) + "...";
return mw.msg('effp-sum-withorigsum', formatSig(user), id, origsum, mw.msg('effp-spam'));
}
/*
* Attempt to sign comments with the user's name, not our own!
*
* Can't use action=parse directly, because that will substitue OUR sig.
* Can't just replace the ~~~~s because that won't catch nowiki, etc.
*
* Instead use action=parse to find all the places where ~~~~ ISN'T
* substed, and skip those when replacing the ~~~~s.
* Is there a less hacky way to do this?
*/
async function substTildes(text, user, unixTime) {
let munged = text.replace(/~{3,5}/g, (m, o) => ("<<" + o + ">>" + m));
let pst = (await (new mw.Api()).post({
action : "parse",
text : munged,
onlypst : 1
})).parse.text["*"];
let isFake = o => pst.includes("<<" + o + ">>~~~");
let sig = formatSig(user);
let ts = formatTSForSig(unixTime);
text = text.replace(/~~~~~/g, (m, o) => isFake(o) ? m : ts);
text = text.replace(/~~~~/g, (m, o) => isFake(o) ? m : sig + " " + ts);
text = text.replace(/~~~/g, (m, o) => isFake(o) ? m : sig);
return text;
}
/*
* Post an edit to web interface. Can't use the API, because the user
* might need to make fixes, or resolve an edit conflict.
*/
function postEdit(title, wikitext, summary,
revid, editTS, startTS,
newTab) {
let input = (name, value) =>
$('<input>', {type: "hidden", name, value});
let $form = $('<form></form>', {
style: 'display: none !important;',
action: mw.config.get('wgScript') + "?title="
+ mw.util.wikiUrlencode(title) + "&action=submit",
method: "POST"
});
if (newTab)
$form.attr("target", "_blank");
$('<textarea name="wpTextbox1"></textarea>').val(wikitext).appendTo($form);
input("wpSummary", summary).appendTo($form);
/* Don't show preview without opt-in, might contain NSFW content */
if (window.effpPreviewOnFirstClick)
input("wpPreview", "1").appendTo($form);
else
input("wpDiff", "1").appendTo($form);
input("wpEdittime", editTS.replace(/[^0-9]/g, "")).appendTo($form);
input("wpStarttime", startTS.replace(/[^0-9]/g, "")).appendTo($form);
if (revid) {
input("editRevId", revid).appendTo($form);
input("baseRevId", revid).appendTo($form);
input("parentRevId", revid).appendTo($form);
}
if (mw.user.options.get('watchdefault'))
input("wpWatchthis", "1").appendTo($form);
input("wpUltimateParam", "1").appendTo($form);
//No edit token needed for preview
$form.appendTo('body').submit();
}
/*
* Check if the edit will conflict. This depends on an undocumented internal
* feature, so an error here shouldn't cause any other problems.
*/
function checkEditConflict(title, text, revision) {
(new mw.Api()).postWithToken('csrf', {
action: "stashedit",
text: text,
title: title,
baserevid: revision,
contentmodel: "wikitext",
contentformat: "text/x-wiki"
}).catch().then(r => {
if (r && r.stashedit) {
if (r.stashedit.status == "stashed")
$('.effp-ecinfo').text(" (" + mw.msg('effp-noconflict') + ")");
else if (r.stashedit.status == "editconflict")
$('.effp-ecinfo').text(" (" + mw.msg('effp-editconflict') + ")");
}
}, () => null);
}
async function setupLink(vars, newer, older, text) {
let revisions = null, page, curTitle, curPageId;
let noLink = false, info = "", moreInfo = "";
let diff = null;
let baseRev, timestamp;
try {
curPageId = Object.keys(newer.query.pages)[0];
page = newer.query.pages[curPageId];
curTitle = page.title;
let oldRev = older.query.pages[curPageId].revisions || [];
let newRevs = page.revisions || [];
if (oldRev[0]) {
baseRev = oldRev[0].revid;
timestamp = oldRev[0].timestamp;
} else {
baseRev = null;
timestamp = formatTS(vars.timestamp);
}
if (oldRev[0] && newRevs[0] && oldRev[0].revid === newRevs[0].revid)
revisions = newRevs;
else
revisions = oldRev.concat(newRevs);
} catch(e) {
console.error("effp-helper: Bad response from API");
return;
}
if (page.missing !== undefined) {
curPageId = 0;
curTitle = vars.page_prefixedtitle;
}
// Let's not.
if (page.contentmodel != "wikitext" || page.ns == 8)
noLink = true;
if (curPageId === 0) {
if (vars.page_id == 0) {
info = mw.msg('effp-pagenevercreated');
} else {
info = mw.msg('effp-pagedeleted');
moreInfo = mw.msg('effp-pagedeleted-more');
noLink = true;
}
} else {
let sha1 = await SHA1(text);
let cnt = revisions.length;
let haveAll = (page.lastrevid == revisions[cnt - 1].revid);
let skip = (revisions[0].timestamp < formatTS(vars.timestamp));
let trueCnt = cnt - skip;
let editMadeBy = null, users = new Set();
for(let i = 0; i < cnt; i++) {
if (editMadeBy === null && revisions[i].sha1 == sha1) {
editMadeBy = revisions[i].user;
diff = revisions[i].revid;
}
if (i >= skip)
users.add(revisions[i].user);
}
if (editMadeBy !== null) {
if (haveAll && revisions[cnt - 1].sha1 == sha1)
info = mw.msg('effp-editmadecurrent', editMadeBy);
else
info = mw.msg('effp-editmade', editMadeBy);
} else if (trueCnt === 0) {
info = mw.msg('effp-nolateredits');
} else {
info = haveAll ?
mw.msg('effp-somelateredits', trueCnt) :
mw.msg('effp-manylateredits', trueCnt);
moreInfo = mw.msg('effp-editsby', [...users].join(", "));
checkEditConflict(curTitle, text, baseRev);
}
}
if (!vars.new_pst && vars.new_wikitext != text)
info = mw.msg('effp-sigsfound') + ") (" + info;
if (vars.page_prefixedtitle !== curTitle) {
info = mw.msg('effp-pagemoved') + ") (" + info;
moreInfo = mw.msg('effp-pagemoved-more', vars.page_prefixedtitle, curTitle) + ". " + moreInfo;
}
let $info = $('<span></span>', {
class: 'effp-info',
text: " (" + info + ")",
title: moreInfo
});
if (diff) {
$info.append(" (",
$('<a></a>', {
href: mw.config.get('wgScript') + "?diff=" + diff,
text: mw.msg('effp-diff')
}),
")");
noLink = true;
}
$('#firstHeading').append($info);
// Temporary, people won't know which script this is coming from
$('#firstHeading').append(" ",
$("<a></a>", {
style: "font-size: 50%;",
href: "https://en.wikipedia.org/wiki/User:Suffusion of Yellow/effp-helper",
text: mw.msg("effp-whatsthis")
})
);
if (noLink)
return;
$info.append('<span class="effp-ecinfo"></span>');
let link = mw.util.addPortletLink(
'p-cactions',
'#',
mw.msg('effp-makeedit'),
'ca-makeedit' ,
mw.msg('effp-makeedit-more')
);
let summary = buildSummary(vars.user_name, getLogId(), vars.summary);
$(link).on("click auxclick", (e) => {
if (e.button <= 1) {
e.preventDefault();
postEdit(curTitle,
text,
summary,
baseRev,
timestamp,
formatTS(vars.timestamp),
e.button == 1 || e.CtrlKey);
}
});
}
function startup() {
mw.messages.set(Object.assign(
{},
userMessages.en,
siteMessages.en,
userMessages[mw.config.get('wgUserLanguage')] || {},
siteMessages[mw.config.get('wgContentLanguage')] || {}
));
mw.util.addCSS(".effp-info { font-size: 75%; font-style: italic; }");
let vars = mw.config.get('wgAbuseFilterVariables');
// Private log entry and no permissions, or action != "edit"
if (!getLogId() || !vars ||
vars.page_id === undefined ||
vars.page_prefixedtitle === undefined ||
vars.new_wikitext === undefined ||
vars.timestamp === undefined)
return;
let newer = {
action: 'query',
prop: 'info|revisions',
rvprop: "ids|timestamp|user|sha1",
rvstart: formatTS(vars.timestamp),
rvdir: "newer",
rvlimit: 500
};
let older = {
action: 'query',
prop: 'revisions',
rvprop: "ids|timestamp|user|sha1",
rvstart: formatTS(vars.timestamp),
rvdir: "older",
rvlimit: 1
};
// Always use the page id if available, in case page moved
if (vars.page_id !== 0)
older.pageids = newer.pageids = vars.page_id;
else
older.titles = newer.titles = vars.page_prefixedtitle;
let textP;
/*
* Use new_pst if available, otherwise if the text looks like
* it contains a signature, attempt to subst it properly.
*/
if (vars.new_pst)
textP = vars.new_pst;
else if (!vars.new_wikitext.includes("~~~"))
textP = vars.new_wikitext;
else
textP = substTildes(vars.new_wikitext, vars.user_name, vars.timestamp);
let api = new mw.Api();
Promise.all([
api.get(newer),
api.get(older),
textP,
$.ready
]).then(r => setupLink(vars, r[0], r[1], r[2]));
}
if (mw.config.get('wgAbuseFilterVariables'))
mw.loader.using(['mediawiki.interface.helpers.styles',
'mediawiki.api',
'mediawiki.util']).then(startup);
})();
// </nowiki>