User:Bosco/Unsigned helper.js: Difference between revisions

From Test Wiki
Content deleted Content added
createTimestampWikitext: move timestamp formatting into wikitext = one usage of `const months` fewer
chooseTemplate & makeTemplate: add support for Template:Undated; searchFromIndex: make it easier to check found diff by showing it to the user; applySearcherResult: mention the used template in edit summary; findRevisionWhenTextAdded: add better protection against infinite recursion; createTimestampWikitext: use Unicode escapes instead of string concatenation for protection;
Line 22: Line 22:


const months = ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December'];
const months = ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December'];

const CONFIG = {
undated: 'Undated', // [[Template:Undated]]
unsignedLoggedIn: 'Unsigned', // [[Template:Unsigned]]
unsignedIp: 'Unsigned IP', // [[Template:Unsigned IP]]
};


info('Loading...');
info('Loading...');
Line 243: Line 249:
*/
*/
async function exponentialSearch(lower, upper, candidateIndex, testFunc) {
async function exponentialSearch(lower, upper, candidateIndex, testFunc) {
if (upper === null && lower === candidateIndex) {
throw new Error(`Wrong arguments for exponentialSearch (${lower}, ${upper}, ${candidateIndex}).`);
}
if (lower === upper && lower === candidateIndex) {
throw new Error("Cannot find it");
}
const progressMessage = `Examining [${lower}, ${upper ? upper : '...'}]. Current candidate: ${candidateIndex}`;
const progressMessage = `Examining [${lower}, ${upper ? upper : '...'}]. Current candidate: ${candidateIndex}`;
if (await testFunc(candidateIndex, progressMessage)) {
if (await testFunc(candidateIndex, progressMessage)) {
Line 275: Line 287:
setProgressCallback(progressCallback) {
setProgressCallback(progressCallback) {
this.#progressCallback = progressCallback;
this.#progressCallback = progressCallback;
}

async #findMaxIndex() {
return exponentialSearch(0, null, 1, async (candidateIndex, progressInfo) => {
this.#progressCallback(progressInfo + ' (max search)');
const candidateRevision = await this.#contentLoader.loadRevisionId(candidateIndex);
if (candidateRevision == undefined) {
return false;
}
return true;
});
}
}


Line 281: Line 304:
return new Promise(async (resolve, reject) => {
return new Promise(async (resolve, reject) => {
try {
try {
const startRevision = await this.#contentLoader.loadRevisionId(startIndex);
if (startIndex === 0) {
if (startRevision == undefined) {
const latestFullRevision = await this.#contentLoader.loadContent(startIndex);
if (latestFullRevision == undefined) {
if (startIndex === 0) {
reject("Cannot find the latest revision. Does this page exist?");
reject("Cannot find the latest revision. Does this page exist?");
return;
} else {
reject(`Cannot find the start revision (index=${startIndex}).`);
}
}
return;
}
if (startIndex === 0) {
const latestFullRevision = await this.#contentLoader.loadContent(startIndex);
if (!latestFullRevision.slots.main.content.includes(text)) {
if (!latestFullRevision.slots.main.content.includes(text)) {
reject("Cannot find text in the latest revision. Did you edit it?");
reject("Cannot find text in the latest revision. Did you edit it?");
Line 292: Line 320:
}
}
}
}
const foundIndex = await exponentialSearch(startIndex, null, startIndex + 10, async (candidateIndex, progressInfo) => {
const maxIndex = (startIndex === 0) ? null : (await this.#findMaxIndex());
const foundIndex = await exponentialSearch(startIndex, maxIndex, startIndex + 10, async (candidateIndex, progressInfo) => {
try {
try {
this.#progressCallback(progressInfo);
this.#progressCallback(progressInfo);
const candidateFullRevision = await this.#contentLoader.loadContent(candidateIndex);
const candidateFullRevision = await this.#contentLoader.loadContent(candidateIndex);
if (candidateFullRevision == undefined) {
if (candidateFullRevision == undefined) {
return false;
return undefined;
}
}
// debug('testFunc: checking text of revision:', candidateFullRevision, candidateFullRevision?.slots, candidateFullRevision?.slots?.main);
// debug('testFunc: checking text of revision:', candidateFullRevision, candidateFullRevision?.slots, candidateFullRevision?.slots?.main);
Line 305: Line 334:
}
}
});
});
if (foundIndex === undefined) {
reject("Cannot find this text.");
return;
}
const foundFullRevision = await this.#contentLoader.loadContent(foundIndex);
const foundFullRevision = await this.#contentLoader.loadContent(foundIndex);
resolve({
resolve({
Line 333: Line 366:
}
}


function chooseTemplateFromRevision(fullRevision) {
function chooseUnsignedTemplateFromRevision(fullRevision) {
if (typeof (fullRevision.anon) !== 'undefined') {
if (typeof (fullRevision.anon) !== 'undefined') {
return 'Unsigned IP';
return CONFIG.unsignedIp;
} else if (typeof (fullRevision.temp) !== 'undefined') {
} else if (typeof (fullRevision.temp) !== 'undefined') {
// Seems unlikely "temporary" users will have a user page, so this seems the better template for them for now.
// Seems unlikely "temporary" users will have a user page, so this seems the better template for them for now.
return 'Unsigned IP';
return CONFIG.unsignedIp;
} else {
} else {
return 'Unsigned';
return CONFIG.unsignedLoggedIn;
}
}
}
}

function chooseTemplate(selectedText, fullRevision) {
const user = fullRevision.user;
if (selectedText.includes(`[[User talk:${user}|`)) {
/*
* assume that presense of something that looks like a wikilink to the user's talk page
* means that the message is just undated, not unsigned
* NB: IP editors have `Special:Contributions` and `User talk` in their signature.
*/
return CONFIG.undated;
}
if (selectedText.includes(`[[User:${user}|`)) {
// some ancient undated signatures have only `[[User:` links
return CONFIG.undated;
}
return chooseUnsignedTemplateFromRevision(fullRevision);
}

function createTimestampWikitext(timestamp) {
function createTimestampWikitext(timestamp) {
/*
// https://www.mediawiki.org/wiki/Help:Extension:ParserFunctions##time_format_like_in_signatures
* Format is from https://www.mediawiki.org/wiki/Help:Extension:ParserFunctions##time_format_like_in_signatures
return '{{' + `subst:#time:H:i, j xg Y "(UTC)"|${timestamp}}}`;
*
* The unicode escapes are needed to avoid actual substitution, see
* https://en.wikipedia.org/w/index.php?title=User:Andrybak/Scripts/Unsigned_generator.js&diff=prev&oldid=1229098580
*/
return `\u007B\u007Bsubst:#time:H:i, j xg Y "(UTC)"|${timestamp}}}`;
}
}


function makeUnsignedTemplate(user, timestamp, template) {
function makeTemplate(user, timestamp, template) {
// <nowiki>
const formattedTimestamp = createTimestampWikitext(timestamp);
const formattedTimestamp = createTimestampWikitext(timestamp);
if (template == CONFIG.undated) {
return '{{subst:' + template + '|' + formattedTimestamp + '}}';
}
return '{{subst:' + template + '|' + user + '|' + formattedTimestamp + '}}';
return '{{subst:' + template + '|' + user + '|' + formattedTimestamp + '}}';
// </nowiki>
}
}


Line 433: Line 493:
function applySearcherResult(searcherResult) {
function applySearcherResult(searcherResult) {
const fullRevision = searcherResult.fullRevision;
const fullRevision = searcherResult.fullRevision;
const template = chooseTemplateFromRevision(fullRevision);
const template = chooseTemplate(txt, fullRevision);
const templateWikitext = makeUnsignedTemplate(
const templateWikitext = makeTemplate(
fullRevision.user,
fullRevision.user,
fullRevision.timestamp,
fullRevision.timestamp,
Line 442: Line 502:
wikitextEditor.value = newWikitextTillSelection + wikitextEditor.value.substr(pos);
wikitextEditor.value = newWikitextTillSelection + wikitextEditor.value.substr(pos);
$(wikitextEditor).textSelection('setSelection', { start: newWikitextTillSelection.length });
$(wikitextEditor).textSelection('setSelection', { start: newWikitextTillSelection.length });
appendToEditSummary(`mark unsigned [[Special:Diff/${fullRevision.revid}]]`);
appendToEditSummary(`mark [[Template:${template}|{{${template}}}]] [[Special:Diff/${fullRevision.revid}]]`);
mainDialog.dialog('close');
mainDialog.dialog('close');
}
}


function reportPossibleRevertToUser(searcherResult, useCb, keepLookingCb, cancelCb) {
function reportSearcherResultToUser(searcherResult, dialogTitle, useCb, keepLookingCb, cancelCb, createMainMessageDivFn) {
const fullRevision = searcherResult.fullRevision;
const fullRevision = searcherResult.fullRevision;
const revid = fullRevision.revid;
const revid = fullRevision.revid;
const comment = fullRevision.parsedcomment;
const comment = fullRevision.parsedcomment;
const suspicionDialog = $('<div>')
const questionDialog = createMainMessageDivFn()
.append(
"The ",
$('<a>').prop({
href: '/w/index.php?diff=prev&oldid=' + revid,
target: '_blank'
}).text(`found revision (index=${searcherResult.index})`),
" may be a revert: ",
comment
)
.dialog({
.dialog({
title: "Possible revert!",
title: dialogTitle,
modal: true,
modal: true,
buttons: {
buttons: {
"Use that revision": function () {
"Use that revision": function () {
suspicionDialog.dialog('close');
questionDialog.dialog('close');
useCb();
useCb();
},
},
"Keep looking": function () {
"Keep looking": function () {
suspicionDialog.dialog('close');
questionDialog.dialog('close');
keepLookingCb();
keepLookingCb();
},
},
"Cancel": function () {
"Cancel": function () {
suspicionDialog.dialog('close');
questionDialog.dialog('close');
cancelCb();
cancelCb();
},
},
}
}
});
}

function reportPossibleRevertToUser(searcherResult, useCb, keepLookingCb, cancelCb) {
const fullRevision = searcherResult.fullRevision;
const revid = fullRevision.revid;
const comment = fullRevision.parsedcomment;
reportSearcherResultToUser(searcherResult, "Possible revert!", useCb, keepLookingCb, cancelCb, () => {
return $('<div>').append(
"The ",
$('<a>').prop({
href: '/w/index.php?diff=prev&oldid=' + revid,
target: '_blank'
}).text(`found revision (index=${searcherResult.index})`),
" may be a revert: ",
comment
);
});
}

function reportNormalSearcherResultToUser(searcherResult, useCb, keepLookingCb, cancelCb) {
const fullRevision = searcherResult.fullRevision;
const revid = fullRevision.revid;
const comment = fullRevision.parsedcomment;
reportSearcherResultToUser(searcherResult, "Do you want to use this?", useCb, keepLookingCb, cancelCb, () => {
return $('<div>').append(
"Found a revision: ",
$('<a>').prop({
href: '/w/index.php?diff=prev&oldid=' + revid,
target: '_blank'
}).text(`[[Special:Diff/${revid}]] (index=${searcherResult.index})`),
".",
$('<br/>'),
"Comment: ",
comment
);
});
});
}
}
Line 487: Line 574:
}
}
info('Searcher found:', searcherResult);
info('Searcher found:', searcherResult);
const useCallback = () => { /* use */
applySearcherResult(searcherResult);
};
const keepLookingCallback = () => { /* keep looking */
// recursive call from a differfent index: `+1` is very important here
searchFromIndex(searcherResult.index + 1);
};
const cancelCallback = () => { /* cancel */
mainDialog.dialog('close');
};
if (isRevisionARevert(searcherResult.fullRevision)) {
if (isRevisionARevert(searcherResult.fullRevision)) {
reportPossibleRevertToUser(
reportPossibleRevertToUser(searcherResult, useCallback, keepLookingCallback, cancelCallback);
searcherResult,
() => { /* use */
applySearcherResult(searcherResult);
},
() => { /* keep looking */
// recursive call from a differfent index: `+1` is very important here
searchFromIndex(searcherResult.index + 1);
},
() => { /* cancel */
mainDialog.dialog('close');
}
);
return;
return;
}
}
applySearcherResult(searcherResult);
reportNormalSearcherResultToUser(searcherResult, useCallback, keepLookingCallback, cancelCallback);
}, rejection => {
}, rejection => {
error(`Searcher cannot find requested index=${index}. Got error:`, rejection);
error(`Searcher cannot find requested index=${index}. Got error:`, rejection);