/*
* 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>